re-exportを使うべきなのかを考えてみた
前提
これは私個人が調べて考えてみたことをまとめたものになります。
私自身ちゃんと理解ができていない点もあると思うので、実際に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-reactのProject Structureの一部を抜粋させていただきます。
※bulletproof-reactについては私もちゃんと理解をしているわけではありませんが、下記の記事でわかりやすく説明をいただいていますので、詳しくはこちらをご覧ください。
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で検証をしていたときにこのような課題を感じていました。詳しくは下記を参照ください。
今回もう一度検証をしてみたく、Next.js(app router)でサンプルページを作ってデプロイをして検証をしてみました。
まずは前提として下記のような2つのコンポーネントを用意してみました。
'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>
);
};
'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してみました。
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する
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をされてしまっているようです。
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と呼ばれています。呼ばれるそうです。自分も最近まで知りませんでしたので皆様の参考になりますように。
最後に
ここまで書いた内容は私が思うre-exportに関するメリットとデメリットを紹介させていただきました。
基本的には私はre-exportを使わない選択をするかと思うのですが、チームによっては今書いたデメリットを許容する、もしくは解決する方法を持ち合わせている上で、re-exportをさせることのメリットを感じているのであれば、このような実装をするのも良いかと思います。
色々と書いたのですが、最後にこの記事が少しでも面白かったり、参考になればライクボタンを押してもらえると執筆の励みになり嬉しいです。
最後まで読んでいただきありがとうございました!
過去記事紹介
たまに記事を書いていまして、よければ下記の記事も見ていただけると嬉しいです😀
Discussion
こんにちは。FYI程度ですが、このindex.tsでexportをまとめたファイルのことは、Barrel Filesと呼ばれています。PreactのメンバーであるMarvin Hagemeisterの記事などでも近しいデメリットが述べられていました。
Speeding up the JavaScript ecosystem - The barrel file debacle
余談ですが、僕が最近関わっているBiomeでは、次のバージョンでBarrel Filesに対するlintを含む予定です。よければチェックしてみてください。
コメントありがとうございます!
こちらの記事を恥ずかしながら、まとめているタイミングでその存在を知りました...!
自分のように知らない人もいそうなので追記させていただきました!
また参考リンクありがとうございます!
記事を通して改めてBarrel Filesについて考えるきっかけになりました。
Biomeはまだ手元で触れていなかったので時間がある時にみてみます!
諸々ありがとうございます🙇