🙆

持続可能なReactを書くための簡易チェックリスト✅

2024/09/21に公開

どんな人が読むと良い記事か?

Reactで実装はできるけど、もう一段階良いコードを書きたいと思っている方向けです。

扱わない内容

  • フックについて
    ESLintの設定で概ねルール化できるかつ、React公式でわかりやすく解説されているので、チェックリストには入れませんでした。

https://ja.react.dev/reference/rules/rules-of-hooks

  • useMemo、useCallback、memoなどのメモ化について
    メモ化することによってのパフォーマンス上の影響はそこまで大きくなく、優先的に実施するものではないと思ったので、チェックリストには入れませんでした。

持続可能なReactの定義

「持続可能なReact」は、コンポーネントやフックが何をしているのかをコードだけを見て理解できるメンテナンス性の高いソースコードと定義します。

そのために、React公式の思想を取り入れる必要があります。

僕の意見ですが、状態に基づいて動的に変化するUIライブラリという原則を重視することが重要だと思います。

入力が同じであれば、出力が常に同じであるという関数型プログラミングの冪等性の思想を取り入れています。
Reactだと、propsやstateが同じであれば、UIは常に同じであるということです。

公式ドキュメントは、純度が最も高い情報なので熟読することをおすすめします。
https://ja.react.dev/reference

チェックリスト

  • letを使ってないか?
  • 不要なstateが定義されていないか?
  • useEffectは外部システムと連携しているか?
  • propsを書き換えていないか?
  • stateを書き換えていないか?

letを使ってないか?

letはミュータブルな値として宣言するため、値を追いにくく、バグの原因になりやすいです。
Reactは宣言的UIという特徴を持っているため、処理も宣言的に書きたいです。

letを使った命令的なスタイルを2つ紹介します。

その1
const numbers = [1, 2, 3, 4, 5];
let doubled = [];

for (let i = 0; i < numbers.length; i++) {
  doubled.push(numbers[i] * 2);
}

console.log(doubled); // [2, 4, 6, 8, 10]

その2
let message;

if (new Date().getHours() < 12) {
  message = "Good morning!";
} else {
  message = "Good afternoon!";
}

🙆‍♂ constを使う

例えば、その1であればJavaScriptのメソッドを使って宣言的に書くことができます。

その1
const numbers = [1, 2, 3, 4, 5];

// mapメソッドを使って配列の要素を2倍にする
const doubled = numbers.map((num) => num * 2);

console.log(doubled); // [2, 4, 6, 8, 10]

その2であれば、三項演算子で書いたほうがより可読性が高く、関数型っぽくかけるなと思います。

その2
const message = new Date().getHours() < 12 ? "Good morning!" : "Good afternoon!";

不要なstateが定義されていないか?

stateの数は少なければ少ないほど可読性、パフォーマンスが良いです。

以下は、①ユーザ一覧、②ユーザが存在するのか、③ソートしたユーザ一覧をstateに持っています。

不要なstateの例
import React, { useState, useEffect } from 'react';

const UnnecessarySetStateExample = () => {
  const [users, setUsers] = useState([]);
  const [isUsers, setIsUsers] = useState(false);
  const [sortedUsers, setSortedUsers] = useState([]);

  useEffect(() => {
    // APIからデータを取得
    fetch("https://sample.com/users")
      .then(response => response.json())
      .then(data => {
        setUsers(data);
        setIsUsers(true) // これはいらない
      });
  }, []);

  useEffect(() => {
    if (users.length > 0) {
      const sorted = [...users].sort((a, b) => a.name.localeCompare(b.name));
      setSortedUsers(sorted); // これもいらない
    }
  }, [users]);

  return (
    <div>
      <h3>Sorted Users List:</h3>
      <ul>
        {sortedUsers.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
        {isUsers && <span>ユーザが存在</span>}
      </ul>
    </div>
  );
};

export default UnnecessarySetStateExample;

🙆‍♂ stateを加工する

import React, { useState, useEffect } from 'react';

const UnnecessarySetStateExample = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    // APIからデータを取得
    fetch("https://sample.com/users")
      .then(response => response.json())
      .then(data => {
        setUsers(data);
      });
  }, []);

  // ユーザーをソートする
  const sortedUsers = [...users].sort((a, b) => a.name.localeCompare(b.name));
  const isUsers = users.length > o

  return (
    <div>
      <h3>Sorted Users List:</h3>
      <ul>
        {sortedUsers.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
        {isUsers && <span>ユーザが存在</span>}
      </ul>
    </div>
  );
};

export default UnnecessarySetStateExample;

ユーザ一覧のstateから、ユーザが存在するのか、ソートしたユーザ一覧は導き出せるはずです。
このようなケース以外にも不要なstateを使っている事あると思うので、stateが必要かどうかは考えると良いと思います。

useEffectは外部システムと連携しているか?

コンポーネントは純粋であるべきですが、外部システムと連携する場合、副作用を伴います。
useEffectは外部システムと連携させるために使います。

以下の例では、countが更新されたときにmessageの表示のstateを操作しています。

import React, { useEffect, useState } from "react";

export const Counter = () => {
  const [count, setCount] = useState(0);
  const [isShowMessage, setIsShowMessage] = useState(false);

  const handleClick = () => {
    setCount(count + 1);
  };

  useEffect(() => {
    if (count > 5) {
      setIsShowMessage(true);
    } else {
      setIsShowMessage(false);
    }
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      {isShowMessage && <p>5以上です</p>}
    </div>
  );
};

🙆‍♂ 外部システムと連携していないならuseEffectは不要(ユーザーイベントはいらない)

stateの更新など、外部システムが関わってこない処理であれば、useEffectは不要です。

import React, { useState } from "react";

export const Counter = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  // `count`に基づいてメッセージを直接計算
  const isShowMessage = count % 3 === 0 && count !== 0;

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      {isShowMessage && <p>3の倍数です</p>}
    </div>
  );
};

一方で、冪等性を損なうタイマーのような実装だとuseEffectが必要になります。
例えば、以下のClockコンポーネントで表示されている時間は、常に変わり続けます。冪等性というのは、入力が一定であれば、出力は常に一定であるということでした。

import { useState, useEffect } from 'react';

const useTimeHook = () => {
  const [currentTime, setCurrentTime] = useState(() => new Date());

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCurrentTime(new Date());
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);

  return currentTime;
};

const Clock = () => {
  const time = useTimeHook();
  return <span>{time.toLocaleString()}</span>; // この値は常に変わる
};

export default Clock;

非冪等な値をReactで使えないというわけではなく、レンダー中に使用できないということです。
非冪等な関数の呼び出しをエフェクトでラップすることで、その計算をレンダーの外側に移動させています。

propsを書き換えていないか?

propsはイミュータブルであるべきです。
propsは、コンポーネントの外側にあるデータです。それが変わってしまうと、コンポーネントの純粋性が損なわれてしまい、予期せぬバグの発生リスクが上がります。

Reactは、ローカル変数のミューテーションはOKですが、コンポーネント外部の値に対して変更を加えることはNGです。

let externalValue = 0; // これはコンポーネント外なので編集してはいけない

function BadComponent() {
  const handleClick = () => {
    externalValue += 1; // 直接外部の値を更新
    console.log(externalValue);
  };

  return <button onClick={handleClick}>Update External Value</button>;
}

このように考えると、コンポーネント外のpropsを変更することもNGと想像できると思います。

props変更してる
import React from 'react';

type BadComponentProps = {
  title: string;
};

const BadComponent: React.FC<BadComponentProps> = (props) => {
  props.title = "強制的に書き換えられたタイトル";

  return (
    <div>
      <h1>{props.title}</h1>
    </div>
  );
};

const App: React.FC = () => {
  return (
    <BadComponent title="正しいタイトル" />
  );
};

export default App;

🙆‍♂️ ESLintをいれる、readonblyにする、コピーしたオブジェクトをいれる、変数定義をする

readonlyにする場合は、以下のような感じです。

type BadComponentProps = {
  readonly title: string;
};

readonlyにするのであれば、ESLintを入れたほうが良さそうです。

どうしても書き換えたい場合は以下のようにスプレッド構文でコピーするのが良いと思います。

const copyProps = { ...props }
copyProps.title = "強制的に書き換えられたタイトル";

propsの値を使って加工したい場合は、変数定義しましょう。

const originalString = "HELLO, WORLD!";
const lowerCaseString = originalString.toLowerCase();

stateを書き換えていないか?

stateもイミュータブルな値として扱うべきです。
stateを直接書き換えても再レンダリングは発生せず、コンポーネントが更新されません。つまり、古いUI が表示されたままになります。

import React, { useState } from 'react';

const BadComponent: React.FC = () => {
  // カウンタの状態を管理
  const [count, setCount] = useState(0);

  const handleClick = () => {
    count = count + 1; // この処理をしてもUIは変わらない
  };

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={handleClick}>カウントを増やす</button>
    </div>
  );
};

🙆‍♂️ セッター関数を使う

import React, { useState } from 'react';

const GoodComponent: React.FC = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);  // setCount を使って state を変更する
  };

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={handleClick}>カウントを増やす</button>
    </div>
  );
};

export default GoodComponent;

最後に

Reactは他にも考えることはたくさんあると思いますが、最低限このチェックリストを抑えると、良いコードに近づくと思います。
React公式が分かりやすいので、熟読して良いコードを世の中に創出していきたいですね。

最後まで読んでいただきありがとうございました!

エックスポイントワン技術ブログ

Discussion