こんにちは。フロントエンドエンジニアの渡邉です。 普段はReactとTypeScriptを書いています。 今回は自分がコンポーネントを実装する際に意識していることについていくつか紹介できればなとおもいます。
※ スタイリングに関して話すときはstyled-componentsを使用しています。
目次
- はじめに
- 再利用性の高いコンポーネントを実装するために意識していること
- 共通のコンポーネントを作成する際は汎用性を意識する
- コンポーネントが知らなくてもいい情報を持たない(コンポーネント・構成編)
- コンポーネントが知らなくていい情報をもたない(コンポーネント・スタイル編)
- 無駄な描画を減らすために意識していること
- 状態に関係ないコンポーネントを混ぜない
- さいごに
はじめに
今まで自分がReactを書いてきて、再利用性が低いコンポーネントを実装してしまったり、コンポーネントの設計自体が無駄な再描画を起こしてしまうことがあったので、その過ちを起こさないためにも実装する際に自分が意識していることを悪い例・良い例と比べながら紹介します。 この記事を読んだ後に得られる知見としては以下の2つです。
- 再利用性の高いコンポーネントが実装できる
- 無駄な再描画を可能な限り減らせたコンポーネントの実装(memo化などを使わずに)
再利用性の高いコンポーネントを実装するために意識していること
共通のコンポーネントを作成する際は汎用性を意識する
共通のコンポーネントの例としてButtonコンポーネントを作るとします。
ここで意識しているのは、共通のコンポーネント(子コンポーネント)に、呼び出し側のコンポーネント(親コンポーネント)を依存させることです。
悪い例
interface ButtonInterface { title: string getUserData: () => void } export const Button = ({title, getUserData}: ButtonInterface) => { return ( // getUserDataを実行するだけのボタンになってしまっている // ただ、getUserDataというpropsで違う振る舞い(会社情報を取得)をすることも可能だが、このような使い方は負債の原因となる <StyledButton onClick={getUserData}>{title}</StyledButton> ) } const StyledButton = styled.button` background-color: red; ` //... // 親コンポーネント const User = () => { const getUserData = () => { //... } const getComapanyData = () => { //... } return ( <div> <Button title='ユーザー情報取得' getUserData={getUserData} /> {/* これでもちゃんと動きますが、上記で説明したとおり負債の原因となる */} <Button title='ユーザー情報取得' getUserData={getComapanyData} /> <h1>ユーザー情報</h1> // ... </div> ) }
上記だとユーザー情報を取得するためだけのボタンになってしまっています。
良い例
interface ButtonInterface { title: string onClick: (e: React.MouseEvent<HTMLButtonElement>) => void } export const Button = ({title, onClick}: ButtonInterface) => { return ( <StyledButton onClick={onClick}>{title}</StyledButton> ) } const StyledButton = styled.button` background-color: red; ` //... // 親コンポーネント const User = () => { const getUserData = () => { //... } const getComapanyData = () => { //... } return ( <div> <Button title='ユーザー情報取得' onClick={getUserData} /> <Button title='ユーザー情報取得' onClick={getComapanyData} /> <h1>ユーザー情報</h1> // ... </div> ) }
ボタンが押されたときの振る舞いを実行するだけです。
まとめ
- 共通のコンポーネントを作成する際は、親コンポーネントに依存したコンポーネントを作らないようにします
- 親コンポーネントに共通コンポーネントに依存させます
- 親に依存した時点で依存元のコンポーネントで作成します
- 何にも依存していない場合 :
components/common/Button.tsx
- ユーザーに依存している場合:
components/user/Button.tsx
- 何にも依存していない場合 :
コンポーネントが知らなくてもいい情報を持たない(コンポーネント・構成編)
親コンポーネントは子コンポーネントのことをしるべきではないです。 逆も然りで子コンポーネントは親コンポーネントを知るべきではないです。 知ってしまった時点で再利用性は低くなります。
悪い例
const TodoList = () => { return ( <ul> {todo.map(item => ( <Item key={item.id} item={item} /> ))} </ul> ) } const Item = ({ item }) => { return ( <li> <p>{item.title}</p> </li> ) }
最終的に表示されるのはul
の中にtodoの個数分li
が表示されます。
これの何が悪いのかというと、TodoListコンポーネントはItemコンポーネントがliを返すことをしっているから、ulの中に含めることができています。
つまり、ItemコンポーネントはTodoList専用のコンポーネントになります。
もしItemコンポーネントを他の場所かつ単体で使いたい場合は以下のようになり、<div><li></li></div>
というよろしくない構成になってしまいます。
const AnotherComponent = () => { return ( <div> <h1>AnotherComponent</h1> <Item item={item} /> </div> ) }
そのため、コンポーネントが知らなくてもいい情報を持たないのが大事です。 下記が適用したコードになります。
良い例
const TodoList = () => { return ( <ul> {todo.map(item => ( <li key={item.id}> <Item item={item} /> </li> ))} </ul> ) } const Item = ({ item }) => { return ( <div> <p>{item.title}</p> </div> ) }
これで親コンポーネントと子コンポーネントはお互いのことを知らなくなりました。
コンポーネントが知らなくていい情報をもたない(コンポーネント・スタイル編)
スタイルに関しても知らなくてもいい情報を知ってしまうと、再利用性が低くなってしまいます。
例えば、アイコンがいくつか並んだコンポーネントがあるとします。
悪い例
const Icon = ({src}) => { return ( <Img src={src} /> ) } const Img = styled.img` margin: 0 10px; `
アイコンのコンポーネントは上記のように定義されてあり、他の箇所でこのコンポーネントを使いたくなったとします。 今回は左右に20px必要です。このときにどのように解決すればよいでしょうか。 コンポーネント内に条件を追加してスタイリングをするなど様々な解決方法がありますが、、知らなくていい情報を持つことによって分岐が増えて可読性が下がります。
良い例
const Icon = ({src}) => { return ( <img src={src} /> ) } const IconList = () => { return ( <IconWrapper> <Icon src={//...} /> <Icon src={//...} /> <Icon src={//...} /> </IconWrapper> ) } const IconWrapper = styled.div` // アイコンのレイアウト記載 `
子コンポーネントはどのように配置されるかを知らないようにします。 親がどのように配置するかを考えます。
まとめ
- 基本的にコンポーネントのトップでmarginを持たせないようにします
- 子コンポーネントは親のレイアウトを知るべきではないです
- 親も子の見た目について知らないようにします
無駄な描画を減らすために意識していること
状態に関係ないコンポーネントを混ぜない
状態に関係ないコンポーネントを混ぜてしまうことによって、無駄な再描画が起きてしまいます。 React.memo()でも防げますが、React.memo()をしないで防ぐのがベストだと思います。 状態の管理をReact.useStateを使っている場合と、Reduxで管理している場合の2つのパターンで紹介します。
悪い例
const Hoge = () => { const [count, setCount] = useState(0) return ( <div> <Counter count={count} setCount={setCount} /> <AnotherComponent /> </div> ) }
AnotherComponent
コンポーネントはcount
という状態に関係ないのにも関わらずcountに変更があるたびに再描画されてします。
良い例
const Hoge = () => { return ( <div> <Counter /> <AnotherComponent /> </div> ) } const Counter = () => { const [count, setCount] = useState(0) // ... }
正しい箇所で状態を管理します。
Reduxを使っている場合 悪い例
const Hoge = () => { const count = useSelector(state => state.count) return ( <div> <Counter count={count} /> <AnotherComponent /> </div> ) }
countはCounterコンポーネントには必要だが、AnotherComponent
には関係のない状態です。
良い例
const Hoge = () => { return ( <div> <Counter /> <AnotherComponent /> </div> ) } const Counter = () => { const count = useSelector(state => state.count) // ... }
ただ、一つ問題点があり、このCounterコンポーネントがpropsのcountのみを表示する共通コンポーネントの場合です。 そのような場合は以下のようにしています。
良い例2
const Hoge = () => { return ( <div> <HogeCounter /> <AnotherComponent /> </div> ) } // Hoge専用のCounter const HogeCounter = () => { const count = useSelector(state => state,count) return <Counter count={count} /> } interface CounterInterface { count: number } const Counter = ({ count }: CounterInterface) => { return ( <span>{count}</span> ) }
まとめ
- 状態に関係のないコンポーネントが見つかった場合は状態が使われているコンポーネントを新たに切り出します
- Reduxを使っている場合はより意識します
- storeで状態を管理しているため、コンポーネント外から対象の状態(上記でいうと、state.count)に変更を加える可能性があるため
さいごに
この記事で説明したことを少しでも意識し始めたことによって自分はかなり再利用性の高いコンポーネントが実装できたと感じているので、参考にしていただければなと思います。 時にはこのケースに当てはまらない場合もあるとは思いのますが、その時は新たな観点で考えて貰えれば幅もより良い実装になっていくと思います。
株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひエンジニア採用サイトをご覧ください。