LoginSignup
227
182

More than 1 year has passed since last update.

Reactのベストプラクティスを模索する

Last updated at Posted at 2022-12-21

はじめに

Reactを触って早くも2年が経ちました。エンジニア歴も2年が経ってしまいました。
時が経つのは本当に早いですね。

最近は人に教えたり、新規プロジェクトの立ち上げメンバーとして任される機会が増えてきました。

そういう中で、自分でベストプラクティスを考えて、コードや設計に落とし込む必要があるとひしひし感じる今日この頃です。

これまで二年間色々なコードを見たり書いたりして、最近やっと自分の中で少しずつベストプラクティスが見えてきた気がするので、色々な記事の力を借りつつ言語化してみようと思います。

書く内容は自分がReactの案件にアサインされた時に知りたかった内容を意識しています。
主に考え方の話になるので、具体的なコードは書きません。
自由に書いている為、各レイヤーもずれていますので(前半は原則の話、後半はメモ化の話)、その辺りはご了承ください。

意識するべき原則の話

良いコードや設計は、全てよくある原則に沿っているなと感じます。
逆にメンテナンス性が悪いコードは、原則に反していることが多いです。

ということで、Reactを想像しながらまとめてみます。

DRY

Don't Repeat Youtself
「同じことを書くな」

有名な原則ですね。
ロジックを書いていて、あのロジックと似ているな、同じこと書いているなと気付いたら、一つにまとめようという話ですね。

よくある例として、バグが発生した時に同じコードが散乱しているより、一箇所にまとまっていた方がすぐに直すことができます。DRYはメンテナンス性に優れていると言えます。

KISS

Keep It Simple, Stupid
「できるだけシンプルにしろ、アホ!」

やたら複雑化して書くのは避けたいです。
複雑な機能をいかにわかりやすくシンプルなコードで書けるかという意識は大切ですよね。

どんなエンジニアレベルでも、仕様の理解が浅い途中参加のメンバーにもわかりやすいコードは、優れていると言えます。

個人的にかなり好きな原則です。シンプルが一番です。複雑は辛いです。

SOLID

SOLID原則とは、オブジェクト指向プログラミングで使用される原則で、拡張性とメンテナンス性をより簡単にするガイドラインです。

こちらは実は最近知りました。
同僚がやたらSOLID原則がいいぞ!とずっと言っていたので、調べたらReactでも活かせる設計方針でした。

Reactに置き換えている良い記事がありましたので、こちらを元にまとめいます。
具体的なコードが見たくなるのと思うので、その時は参考記事をご覧ください。

【参考記事】
SOLID Principles in React
SOLID原則で考えるReact設計

SOLIDは5つの原則があります。

S — Single Responsibility:単一責任の原則
O — Open-Closed:開放閉鎖の原則
L — Liskov Substitution:リスコフの置換原則
I — Interface Segregation:インタフェース分離の原則
D — Dependency Inversion:依存性逆転の原則

S — Single Responsibility:単一責任の原則

Every function/class/component should do exactly one thing.

関数やクラス、コンポーネントは全て1つのことに集中しましょう

責務の分離ですね。

これはわかりやすいかなと思います。
一つのコンポーネントの中で様々なUIとロジックが入っていたら巨大なファイルになります。
一つのコンポーネントは、あらゆる責務を負ってしまいます。

なので、コンポーネントをできるだけ細分化します。
カスタムhooksを活用しUIとロジックもきちんと責務を分けます。
そうやって、それぞれは単一のことに集中させましょうということです。

O — Open-Closed:開放閉鎖の原則

Make big components from lots of smaller ones

小さいパーツで大きなコンポーネントを作りましょう

これについては、日本語の記事の方がわかりやすいです。
英語の記事の方は、まあ普通にコンポーネント作ってたら大丈夫っしょという感じで捉えました。

日本語の記事に、

「コンポーネントや関数の拡張に対しては開いて、変更に対しては閉じているべき」

という言葉がありました。

要は、拡張に強いコンポーネントを作っておいて、変更する時は狭い範囲で対応できるようにするのが理想ということです。

記事の例では、props.childrenを使っている大元のコンポーネントがいて、それらを使って新しいコンポーネントABCを作っています。
大元はかなり拡張性があります。ABCは一つ一つ固有のコンポーネントですが、それゆえ変更時にはその一つのコンポーネントを変更するだけで済みます。

L — Liskov Substitution:リスコフの置換原則

Make classes substitutable for subclasses

クラスは、サブクラスで使える機能にしましょう

今はReactをクラスで書くことがないと思うので、今後他の言語を書くときに活かせる内容かなと思います。

親クラスで定義した内容はサブクラスでも同じように使いましょう。
親クラスで定義した内容をサブクラスで変更する必要がある時、親クラスの定義をそもそも見直す必要があります。

I — Interface Segregation:インタフェース分離の原則

Only pass a component props it needs

必要なpropsだけ渡しましょう

これもわかりやすいですね。コンポーネントに不要な情報は渡すなということです。

ありがちなのが、APIデータをそのまま流してしまう例です。

TypeScriptであれば、propsに型を指定するので余計恩恵があるのではないでしょうか。

UserList.tsx
  type UserListProps = {
    id: number
    name: string
    email: string
  }

APIのデータと実際にコンポーネントで受け取っているデータの構造が変わった時、きちんとコンパイル時にエラーを吐いてくれるので予期せぬバグも防ぐことができますね。

コンポーネントはコンポーネント内で必要な情報だけ受け取るべきです。

D — Dependency Inversion:依存性逆転の原則

Always have high-level code interface with an abstraction, rather than an implementation detail

高水準なコードは常に抽象化と連動し、具体的な内容に依存するべきではない
(ちょっと日本後訳が怪しい)

こちらを読んだだけでは英語も日本語もピンと来ませんでしたが、詳細を読み進めていくとOpen-Closedの内容と関連していることがわかります。

どちらもフェッチ方法を例としていますが、フェッチ方法が変わった時にそのコード自体が抽象化されていれば、具体的なコード(細かい部分)には影響なく変更ができ、メンテナンス性に優れているという解釈です。

英語の記事では抽象の基準についても触れています。
API通信を担当する関数と、正規表現のマッチングを担当する関数は、別次元のレイヤーなので分けています。

そこまでは良いのですが、正規表現のマッチングまでも、細かいレイヤーに分けることができています。
これについては、人それぞれの意見があり、丁度良いと思うかもしれないし、やりすぎだと思うかもしれません。
その辺りは、プログラミングの経験と知識が活かされそうですね。

This means we should slowly abstract away be extracting out functionality, until we reach a low enough level. This is a somewhat vague definition, and relies on the experience of a programmer to know how high-level each concept is.

そう言う意味でも、抽象度を自分の中で考える必要があり、個人的にはこの中で一番難しい原則かと想像します。

AHA

Avoid Hasty Abstractions
「早まった抽象化は避けよう」

恐らく有名とかではないのですが、個人的に最近見つけて共感でしかなかったので、今後意識していこうと思った原則です。

KentC.Doddsさん(JS,Reactの教育者/登壇者/ライブラリ開発者)の記事です。
AHA Programming

先ほど出てきたDRYの原則と、WET("Write Everything Twice.")という原則がありますが、両者は正反対で究極的です。
AHAは、良い塩梅の考えだなと思います。

大事になのは、元々の記事でも書いてある

prefer duplication over the wrong abstraction

間違った抽象化をするなら、重複のがまだマシという考えです。

なぜなら、最初の時点で汎用的なコンポーネントだったり抽象化された関数を作ったとしても将来はわかりません。
大規模な仕様変更があったり、大規模な機能追加があるわけです。

それに備えて、最初から「間違った抽象化」をして条件分岐だらけにしてしまうと、後続のエンジニアは辛いわけです。
後続のエンジニアは変なバグを起こしたくないので、修正箇所を最低限にしたいと思います。
そうすると既に存在している抽象化の関数やコンポーネントを使用し続け、条件分岐やパラメーターを追加していくのです(他には一切害のないように)

これが間違った抽象化という考えです。
抽象化は素晴らしいが、間違った抽象化をしてしまうなら、重複コードがいくつか発生して必要だと思ったときに抽象化すれば良いのです。

もしすでに間違った抽象化があるのであれば、将来どんどん辛くなるので早めに直す勇気も必要かもしれません。

元々の記事で、ではどういうときに間違った抽象化ということを判断ができるか?が書いてあります。

If you find yourself passing parameters and adding conditional paths through shared code, the abstraction is incorrect.

条件分岐やパラメータを足さなければ使えないと分かったとき、それは間違った抽象化です。

ディレクトリ設計

最初にジョインしたプロジェクトではAtomicDesignを採用していました。
それ以外でも共通して言えるのでは、全て種類ごとにディレクトリが分かれているパターンです。
一番メジャーですよね。

/components
/hooks
/util
/pages
/stories

AtomicDesignはそこまで悪くもなかったですが、やはり分け方が曖昧でコンポーネントの設計的には少し壊れてしまった記憶があります。

AtomicDesignどうこうの前に、種類ごとに分かれているディレクトリ設計は大規模な案件では徐々に辛くなることがわかりました。
影響範囲がすぐにわからず、ファイルが散らかっているので、メンテナンスが大変です。

まあそんなものかと思っていましたが、より良い設計があったので今回初めて新規プロジェクトでこちらを採用しています。

ファイルの種類ごとに分けるのでなく、機能ごとに分けるというものです。

src/
├── pages/
│   ├── index.tsx
│   ├── articles/
│       ├── index.tsx
│       ├── [id].tsx
├── features/
│   ├── articles/
│   │   ├── components/
│   │   │   ├── articleList.tsx
│   │   │   ├── articleCard.tsx
│   │   ├── api/
│   │   ├── hooks/
│      │      ├── types/  
│   │   ├── utils/
│      │      ├── test/
│      │      ├── index.ts
│   ├── favorites/
├── components/
│   ├── elements/
│   │   ├── button/
│   │   │   ├── button.tsx
│   │   │   ├── index.ts
│   │   │   ├── stories/
│   ├── layouts
│   │   ├── header/
│   │   ├── modal/
├── functional/  *見た目を伴わない系(providerにする時など)
│      ├── authenticator.tsx
│   ├── error.tsx
│   ├── test/
├── hooks/
├── utils/ 
├── constants/
├── rtk/
├── lib/
└── theme/

※Next.js ver12を使っているので、pagesディレクトリが存在しています

会社では大規模な新規案件で、ReduxとRTKQueryを使うことが多いのでそれらのディレクトリも存在していたりslice用のディレクトリが追加されますが、それ以外はよくある機能毎の分け方と一緒です。
命名は好みかと思います。

/featuresの中に、componentsやhooks、apiを閉じ込めます。
そうすることで、依存関係を最小限に抑えることができ、メンテナンス性や拡張性に優れています。
featureAとfeatureBは独立しているからです。

どこかの記事で、ソースコード見た時に「あーこれはReactのプロジェクトか」とわかるのではなく、「〇〇のサービスを作っているプロジェクト」とわかるソースコードが良いのではという意見がありました。

それを叶えるのが、Group by featureでした。
小さいアプリでは、ファイルの種類毎に分けることで問題ないと思いますが、大規模な案件ではこちらが最適ではないかと考えています。

また、基本的にディレクトリごとにindex.tsを活用します。

【参考記事】
Evolution of a React folder structure and why to group by features right away
How To Structure React Projects From Beginner To Advanced
私の推しフロントエンドディレクトリ構成と気をつけたいポイント

カスタムhooksについて

カスタムフックは便利ですよね。
UIとロジックが分離できるし、ロジックの再利用も可能で、テストもしやすい。

今までのプロジェクトでカスタムフックをいっぱい使ってきましたが、この使い方で合ってるのかな?と疑問を持つことがありました。

何でもかんでもカスタムフック化して良いのか?
再利用性がない場合はカスタムフック化しない方が良いのか?などです。

そういう疑問を解消したくて、カスタムフックについてかなり調べてもう一度公式をじっくり読みました。
※beta版の方が具体的且つ詳細に説明してくれています
Reusing Logic with Custom Hooks

最後のRecapを見てみるとこう書いてあります。

・Custom Hooks let you share logic between components.
・Custom Hooks must be named starting with use followed by a capital letter.
・Custom Hooks only share stateful logic, not state itself.
・You can pass reactive values from one Hook to another, and they stay up-to-date.
・All Hooks re-run every time your component re-renders
・The code of your custom Hooks should be pure, like your component’s code.
・Wrap event handlers received by custom Hooks into Effect Events.
・Don’t create custom Hooks like useMount. Keep their purpose specific.
・It’s up to you how and where to choose the boundaries of your code.

  • カスタムフックは、コンポーネント間でロジックをシェアできます
  • カスタムフックは、useから始まりキャピタルレターで命名しましょう
  • カスタムフックは、ステートそのものではなく、ステートフルなロジックをシェアします
  • 他のフックから他のフックに値をリアクティブに、最新の状態で渡すことができます
  • 全てのフックは、コンポーネントの再レンダリングの度に実行されます
  • コンポーネントのコードのように、カスタムフックは純粋である必要があります
  • カスタムフックによって受け取ったイベントハンドラーは、EffectEventsでラップしてください
  • useMountのようなカスタムフックは作らないで下さい。具体的な目的のために作ってください
  • コードの境界線をどこにするかはあなた次第です

※下の部分は私的翻訳です

上記の通り、カスタムフックのルールや挙動を正しく理解することはとても大切です。

そして、何でもかんでもカスタムフック化して良いのか?という疑問ですが、下記が答えになると理解しています。

Keep your custom Hooks focused on concrete high-level use cases

カスタムフックは、具体的な抽象レベルのケースにフォーカスしましょう

具体的な抽象レベルとは、コンポーネント自身のロジックに依存していないことです。

再利用を目的に作ってるケースもあると思いますが、実際に再利用されるかはフックによリますよね。
しかし、抽象度の高いカスタムフックの場合、コンポーネントに依存していないはずです。
なのでコンポーネントに依存しないロジックというのが、カスタムフック化するべきなのだということでした。

そうは言ってもコンポーネントの巨大化を防ぐためにカスタムフック化したくなることもしばしばあります。
しかし、それ自体のカスタムフックを抽象度を高くすることができるかもしれないので、こういう意識を持つことが大切と感じました。

また、数行程度の軽いロジックを毎回カスタムフック化する必要もありません。

You don’t need to extract a custom Hook for every little duplicated bit of code. Some duplication is fine.

カスタムフックは、React界隈の開発者達が良いフックを作っているリストがあります。
(最近知りました)

なので、抽象度や切り分け方などは、こういったベストプラクティスを参考にしながら作っていくのが良いのかなと思います。
どのプロジェクトでも使えるようなカスタムフックなので、それ自体を使うというのも良さそうです。

Collection of React Hooks
useHooks(🐠)
react-use →テストも書いてあるので、テストまで参考にできます

メモ化するタイミング

メモ化いつしてますか・・?

React.memo = コンポーネントのメモ化 
useMemo = 値のメモ化 
useCallback = 関数のメモ化 

なんとなく再レンダリングがすごい時。重そうな処理で負荷が高い時。
みたいに、私はあまり明確にこういう時という答えがありませんでした。

しかし最近気づきました。メモ化するという判断は人それぞれだなと。

面白いと思ったのが、下記二つの記事を読み比べた時に両者は別の意見を持っていました。

どちらも私が勝手に崇拝している方々です

①TypeScript入門著者のuhyoさんの記事です。
useCallbackはとにかく使え! 特にカスタムフックでは

結論は、カスタムフックが関数を返すなら常にuseCallbackで囲めです。

まとめるとこういうことです。

  • オーバーヘッドなんてないのだから、カスタムhooksで返す関数は全てメモ化しておこうよ
  • オーバーヘッドを気にするより、責務の分離とカプセル化を考慮した設計重視

「使う側の都合で初めて使われる側を修正するのは設計に反している」という考えは、とても納得ですよね。

もう一方ですが、

②先ほどのAHA Programmingでご紹介したKentさんの記事です。
When to useMemo and useCallback

The point is this:
Performance optimizations are not free. They ALWAYS come with a cost but do NOT always come with a benefit to offset that cost.

まとめるとこういうことです。

  • よくuseCallbackはパフォーマンスを向上するから使えと言うけど、全てのコードには代償がかかっているんだ
  • メモ化というのはメモリを消費しているんだ。しかもやたら複雑にさせるんだ
  • メモ化で得るベネフィットは極小で、それより他のことに頭を使った方が良い
  • じゃあいつ使えば良いのか?というと下記2パターンだ
  1. Referential equality
  2. Computationally expensive calculations
  • 参照型の一致
    • useEffectの依存配列内に、オブジェクトや配列を指定する時
    • 重いコンポーネントをReact.memoするが、関数が再生成されてしまう時
  • 計算が重い処理
    • レンダリングの度に重い計算をしたくない時

(最後のパターンのまとめ方が下手な気がしますので、詳細は元々の記事を読んでください)

そういうわけで、メモ化には色々な意見があるということがわかりました。

個人的な現状の結論は、設計も大事ですがオーバーヘッドになってしまう可能性を考慮して、Kentさんの考えに賛同しています。

必要な時に必要なメモ化で対応するということです。

再レンダリングは悪だと当初は思っていたのですが、再レンダリングは仮想DOMまでの話なので実際のDOMに影響はしていません。
そう考えるとそこまで再レンダリングがパフォーマンスに影響する可能性は少ないはずです。
なので必要な時にメモ化はしますが、その状況は少なそうです。

下記動画の「Should You Use React.Memo?」辺りでそういう話をしています☟

まとめ

なんだか色々書いていたら、予想以上に長くなってしまいました。

最初に紹介した原則の中にある「SOLID」や「AHA」なんかは最近知リまして、書きながら「あーあれリファクタしたいな」とか思っていました。

2年たったこのタイミングで、自分の中でこういうのが恐らくベストプラクティスかなという考えをきちんと言語化することは、今後のためにも大事かなと思い書きました。

タイトル通り、何が良い設計やコードなのか?というのは模索し続けています。
ベストプラクティスなんて人によっては違うわけで定義するのが難しいですが、とりあえず自分で信念と正義を持つことが大切なのかなと思ってきたこの頃です。

将来、自分のコードを見直した時に「なんやねんこのコード」と思えることは成長している証とポジティブに捉えています。

また半年後、2年後に振り返った時、もしかしたら考えが変わっているかもしれません。
そもそもReactを使っていないかもしれません。

より良いベストプラクティスを自分の中で見つけられるように、引き続きスキルアップしていこうと思います。

227
182
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
227
182