3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

1人フロントエンドAdvent Calendar 2023

Day 17

Reactでpropsを扱う時に気をつける2つのケース

Posted at

はじめに

Reactはコンポーネント間の値をやり取りをpropsで行います。propsは親コンポーネントから子コンポーネントへと情報を渡す機能です。

const Parent: FC = () => {
  const user = useUser();

  return <Child id={user.id} name={user.name} />;
};

const Child: FC<{ id: number, name: string }> = ({
  id,
  name,
}) => {
  return <User id={id} name={name} />;
};

上記のコンポーネントではParentコンポーネントからChildコンポーネントへidnameを渡しています。このidnameがpropsと呼ばれています。
propsを渡されたコンポーネントはpropsが変化することで再レンダリングを起こします。

この記事ではそんなpropsを利用する中で気をつけるべき2つのケースを紹介します。

ある値に計算するために扱う

propsに基づいて特定の値を計算したい場合、useEffectを用いて以下のように記述されることがあります。

const Sample: FC<{ todos: Todo[] }> = ({ todos }) => {
  const [searchParams, setSearchParams] = useState<string>();
  const [displayTodos, setDisplayTodos] = useState<Todo[]>([]);

  useEffect(() => {
    setDisplayTodos(searchTodos(todos, searchParams));
  }, [todos, searchParams]);

  return ...;
};

この方法はマウント時や、todossearchParamsの変更がされた時に、レンダリングを行って画面へ反映した後に、エフェクトに記述したdisplayTodosの更新を行なって再度レンダリングと画面への反映を行うので非効率です。
そもそも、ReactではuseEffectをEscape Hatchをしており、React外のシステムとやりとりする時にのみ使用することを推奨しているのでした。
そのため、useEffectを使わずに書いてみます。todossearchParamsが変更されるたびにdisplayTodosを計算し直せば良いのでレンダー前の値を覚えておくようにして以下のように記述できます。

const Sample: FC<{ todos: Todo[] }> = ({ todos }) => {
  const [searchParams, setSearchParams] = useState<string>();
  const [displayTodos, setDisplayTodos] = useState<Todo[]>([]);

  const [prevTodos, setPrevTodos] = useState<Todo[]>(todos);
  const [prevSearchParams, setPrevSearchParams] = useState<string>(searchParams);

  if (prevSearchParams !== searchParams) {
    setPrevSearchParams(searchParams);
    setDisplayTodos(searchTodos(todos, searchParams));
  }

  if (!isSame(todos, prevTodos) {
    setPrevTodos(todos);
    setDisplayTodos(searchTodos(todos, searchParams));
  }

  return ...;
};

コンポーネントの計算を行うタイミングで状態の更新を行うのはそのコンポーネントで定義されたものでなければいけないことに注意してください。そして、if文がない場合や、オブジェクトが単純なObject.isによる比較で判定しようとした場合は無限ループに陥ることにも注意してください。
このコンポーネントはマウント時や、todossearchParamsが変更されるたびにレンダリングを行い、その計算中に状態の更新が行われるのでその子コンポーネントや画面の描画が起きる前に再レンダリングが引き起こされます。これは先ほどのuseEffectを用いて記述した方式よりもパフォーマンスが少し良いです。
この書き方は、複雑に入り組んだコードを書く必要もありますし、パフォーマンスにも改善の余地があります(レンダリングが一回無駄)。以下のように書くことでシンプルでパフォーマンス上の不利も無くせます。

const Sample: FC<{ todos: Todo[] }> = ({ todos }) => {
  const [searchParams, setSearchParams] = useState<string>();
  const displayTodos = searchTodos(todos, searchParams);

  return ...;
};

単純にpropsや状態を組み合わせて変数を作れば良いだけでした。この状態だとレンダリングのたびにsearchTodosの計算が行われるのでuseMemoを用いて最適化を用いると良いです(useMemoを使う目安は計算にかかる処理が1ms以上かかる場合がおすすめです参考)。
propsや状態を用いた新しい値を扱うときは組み合わせて表現できるか確認してください。もし、propsによって表される状態ではなく、propsの変化によってリセットや変更したい状態の場合は少し複雑にはなりますが2個目に紹介したようなやり方で記述してください。useEffectを用いた変更はしないようにしましょう。

初期値として扱う

propsをコンポーネントで扱うために以下のように新たな状態として作り直すような記法が見受けられます。

const Sample: FC<{ value: string }> = ({ value }) => {
  const [text, setText] = useState(value);

  const handleChange: ReactEventHandler<HTMLInputElement> = (e) => {
    setText(e.currentTarget.value);
  };

  return <input value={text} onChange={handleChange} />;
};

textを用いてコンポーネントの計算を行うように記述している場合はvalueの変化によって、コンポーネントの再レンダリングは引き起こされるものの、textの値はvalueの変化によって変化しないのでコンポーネントの計算結果は変わりません。
そして、textの変化もvalueを状態として扱う親コンポーネントには伝わらないことに注意してください。

valueをコンポーネントの計算でそのまま利用させるには、propsをそのまま計算へ使うようにさせましょう。

const Sample: FC<{ value: string }> = ({ value }) => {

  return <input value={value} onChange={??} />;
};

多くの場合先ほどのパターンに陥る理由はpropsを子コンポーネントで変化させたいからです。今回の変更はpropsを直接渡すようにしたことなので、それを更新するすべがなくonChangeに渡すものがなくなりました。そんな時は親コンポーネントからpropsとしてvalueを変化させるonChangeも合わせて渡すようにします。

const Sample: FC<{
  value: string,
  onChange: ReactEventHandler<HTMLInputElement>
}> = ({
  value,
  onChange,
}) => {

  return <input value={value} onChange={onChnage} />;
};

これで、親コンポーネントとつながりのある子コンポーネントして扱えるようになりました。
最初に見たpropsを新たな状態として扱う方法は、propsを初期値として扱う際は便利です。

const Sample: FC<{ defaultValue: string }> = ({ defaultValue }) => {
  const [text, setText] = useState(defaultValue);

  const handleChange: ReactEventHandler<HTMLInputElement> = (e) => {
    setText(e.currentTarget.value);
  };

  return <input value={text} onChange={handleChange} />;
};

初期値としてpropsを渡し、後の状態変化は子コンポーネントに託すようなケースでは上記のような書き方が適しています。その時はdefaultValueのようにdefaultを名前につけるのが慣習となっています。
適材適所でそれぞれ活用していきたいです。

3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?