🧐

re-exportを使うべきなのかを考えてみた

2024/03/25に公開
2

前提

これは私個人が調べて考えてみたことをまとめたものになります。
私自身ちゃんと理解ができていない点もあると思うので、実際にre-exportを使うのかはプロジェクトの特性やチームの方針を鑑みて判断をいただければと思います。

re-exportとはそもそも何?

下記のようにして、他のモジュールからエクスポートされた要素を、ある特定のモジュール(下記でいうindex.tsx)から再度エクスポートするものです。

├── Sample
│   ├── FugaFuga.tsx
│   ├── HogeHoge.tsx
│   └── index.tsx
// Sample/index.tsx
export { FugaFuga } from './FugaFuga';
export { HogeHoge } from './HogeHoge';

re-exportをすることによるメリットは何か?

自分が調べた限りだと大きく2つありそうでした。

外部機能として公開する

例えばなのですが、 bulletproof-reactProject Structureの一部を抜粋させていただきます。

※bulletproof-reactについては私もちゃんと理解をしているわけではありませんが、下記の記事でわかりやすく説明をいただいていますので、詳しくはこちらをご覧ください。

https://zenn.dev/manalink_dev/articles/bulletproof-react-is-best-architecture

src/features/awesome-feature
|
+-- api         # exported API request declarations and api hooks related to a specific feature
|
+-- assets      # assets folder can contain all the static files for a specific feature
|
+-- components  # components scoped to a specific feature
|
+-- hooks       # hooks scoped to a specific feature
|
+-- routes      # route components for a specific feature pages
|
+-- stores      # state stores for a specific feature
|
+-- types       # typescript types for TS specific feature domain
|
+-- utils       # utility functions for a specific feature
|
+-- index.ts    # entry point for the feature, it should serve as the public API of the given feature and exports everything that should be used outside the feature

こちらのindex.tsに書いている内容を和訳すると、

機能のエントリーポイントであり、指定された機能のパブリックAPIとして機能し、その機能の外部で使用されるすべてのものをエクスポートします。

と書いてあるようです。
つまりsrc/features/userのようなものがあった場合にこのsrc/features/user以外のところで使う場合にindex.tsからre-exportをして使うようです。
なので、このエントリーファイルとなる、src/features/user/index.tsのファイルを見ればuserのどのような機能が外部で使用可能なのかを管理することができそうです。

importをする時に特定のモジュールからシンプルにimportをできるようにする

下記のようにして、/components/ui-parts/Sample/index/tsxでimportをしてまとめてexportをさせることができます。

// こうせずに、
// import { FugaFuga } from '@/components/ui-parts/Sample/FugaFuga';
// import { HogeHoge } from '@/components/ui-parts/Sample/HogeHoge';

// こう出来る。、
import { FugaFuga, HogeHoge } from '@/components/ui-parts/Sample';

export default function sampleA() {
  return (
    <>
      <HogeHoge />;
      <FugaFuga />;
    </>
  );
}

ただ、正直自分の場合は何らかのモジュールをimportをする時はvscodeのAuto Importを使ってimportをしており、ファイルのトップ行をほぼ見ていないのであまりメリットとは思っていないです。

re-exportをすることによるデメリットは何か?

こちらは自分の中では下記の2つがあると思っています。

各所でre-exportをしだすと、毎回エントリーポイントとなるファイルを毎回作る必要がある。

これは個人的に思っていることで、re-exportを色んなディレクトリ(例えばrepository,
component, usecase, etc..)でやり出すと、毎回index.tsのようなエントリーポイントを作る必要がありそうです。
自分的にはこれを毎回考えるのも、毎回触るのも面倒だなと思ったりしています。

バンドルされるJavaScriptのサイズが上がる可能性がある

以前にNext.jsで検証をしていたときにこのような課題を感じていました。詳しくは下記を参照ください。

https://zenn.dev/gonta_ganbareyo/scraps/488ca8e9aae690

今回もう一度検証をしてみたく、Next.js(app router)でサンプルページを作ってデプロイをして検証をしてみました。

まずは前提として下記のような2つのコンポーネントを用意してみました。

src/components/ui-parts/Sample/HogeHoge.tsx
'use client';
import React from 'react';

console.log('HogeHogeコンポーネントを呼んでいるよ');

export const HogeHoge = () => {
  return (
    <div>
      <p>HogeHogeHogeHoge</p>
      <p>HogeHogeHogeHoge</p>
      <p>HogeHogeHogeHoge</p>
      <p>HogeHogeHogeHoge</p>
      .
      .
      .
    </div>
  );
};

src/components/ui-parts/Sample/FugaFuga.tsx
'use client';
import React from 'react';

console.log('FugaFugaコンポーネントを呼んでいるよ');

export const FugaFuga = () => {
  return (
    <div>
      <p>FugaFugaFugaFuga</p>
      <p>FugaFugaFugaFuga</p>
      <p>FugaFugaFugaFuga</p>
      <p>FugaFugaFugaFuga</p>
      .
      .
      .
    </div>
  );
};

re-exportをしない場合

re-exportをせずに<HogeHoge />コンポーネントをimportしてみました。

src/app/sample-b/page.tsx
import { HogeHoge } from '@/components/ui-parts/Sample/HogeHoge';
import Link from 'next/link';
export function generateMetadata() {
  return {
    title: `sample-b | cm-component-app`,
  };
}

export default function sampleB() {
  return (
    <>
      <Link href={'/sample-a'}>to sample-a</Link>
      <p>import @/components/ui-parts/Sample/HogeHoge</p>
      <HogeHoge />
    </>
  );
}

結果は下記のようになります。

当然ではあるのですが、<HogeHoge />コンポーネントを直接importをしているときに<FugaFuga />コンポーネントに関するコードがバンドルされたファイルには含まれていません。

re-exportをする場合

このようにしてre-exportをした<HogeHoge />をimportする

src/app/sample-a/page.tsx
import { HogeHoge } from '@/components/ui-parts/Sample';
import Link from 'next/link';
export function generateMetadata() {
  return {
    title: `sample-a | cm-component-app`,
  };
}

export default function sampleA() {
  return (
    <>
      <Link href={'/sample-b'}>to sample-b</Link>
      <p>import @/components/ui-parts/Sample</p>
      <HogeHoge />;
    </>
  );
}

結果は下記のようになりました。

一見すると、sample-a/page.tsxで呼び出しているのは<HogeHoge />コンポーネントだけですが
<FugaFuga />に関する実装にこのsample-a/page.tsxは関心がないのに読み込まれてしまっています。これは人によっては想像をしていない挙動なのかもしれません。

原因としては、下記のre-exportをしているエントリーファイルで両方のファイルがimportされており、そのsrc/components/ui-parts/Sample/index.tsxをpageコンポーネントでimportしたことにより、結果<FugaFuga />のコンポーネントまでimportをされてしまっているようです。

src/components/ui-parts/Sample/index.tsx
export { FugaFuga } from './FugaFuga';
export { HogeHoge } from './HogeHoge';

今回は<HogeHoge /><FugaFuga />の2つのモジュールで検証をしていますが、モジュール内部の具体の実装や、re-exportをするモジュールの数が増えるとバンドルされるコードは更に多くなることが想像できそうです。

re-exportを使うのか?

チームで何かしらの理由で使うという選択をするのであればその限りではありません、基本的には私は使わないことを選択します。
このあたりはバンドラーをうまく使いこなせば、tree-shakingをさせることによって今回のような事象はもしかしたら起きえないのかもしれません。ただ、私としては、

  • Next.js周りや周辺バンドラー調整にそこまで詳しくない。
  • バンドラーを調整してまでre-exportをするメリットをあまり感じていない。

という理由から、そこまでしてre-exportをさせるのもどうかな?と思っている次第です。

追記

こちらでコメントをいただきまして、index.tsでexportをまとめたファイルのことは、Barrel Filesと呼ばれています。呼ばれるそうです。自分も最近まで知りませんでしたので皆様の参考になりますように。

https://zenn.dev/link/comments/ee3f39afed8314

最後に

ここまで書いた内容は私が思うre-exportに関するメリットとデメリットを紹介させていただきました。
基本的には私はre-exportを使わない選択をするかと思うのですが、チームによっては今書いたデメリットを許容する、もしくは解決する方法を持ち合わせている上で、re-exportをさせることのメリットを感じているのであれば、このような実装をするのも良いかと思います。

色々と書いたのですが、最後にこの記事が少しでも面白かったり、参考になればライクボタンを押してもらえると執筆の励みになり嬉しいです。
最後まで読んでいただきありがとうございました!

過去記事紹介

たまに記事を書いていまして、よければ下記の記事も見ていただけると嬉しいです😀

https://zenn.dev/tacoms/articles/09ff8e5481480f

https://zenn.dev/tacoms/articles/ef6a13f55ec014

Discussion

unvalleyunvalley

こんにちは。FYI程度ですが、このindex.tsでexportをまとめたファイルのことは、Barrel Filesと呼ばれています。PreactのメンバーであるMarvin Hagemeisterの記事などでも近しいデメリットが述べられていました。
Speeding up the JavaScript ecosystem - The barrel file debacle

余談ですが、僕が最近関わっているBiomeでは、次のバージョンでBarrel Filesに対するlintを含む予定です。よければチェックしてみてください。

gontagonta

コメントありがとうございます!

Barrel Filesと呼ばれています。

こちらの記事を恥ずかしながら、まとめているタイミングでその存在を知りました...!
自分のように知らない人もいそうなので追記させていただきました!

また参考リンクありがとうございます!
記事を通して改めてBarrel Filesについて考えるきっかけになりました。

Barrel Filesに対するlintを含む予定

Biomeはまだ手元で触れていなかったので時間がある時にみてみます!

諸々ありがとうございます🙇