はじめに
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
コンポーネントへid
とname
を渡しています。このid
やname
が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 ...;
};
この方法はマウント時や、todos
やsearchParams
の変更がされた時に、レンダリングを行って画面へ反映した後に、エフェクトに記述したdisplayTodos
の更新を行なって再度レンダリングと画面への反映を行うので非効率です。
そもそも、ReactではuseEffect
をEscape Hatchをしており、React外のシステムとやりとりする時にのみ使用することを推奨しているのでした。
そのため、useEffect
を使わずに書いてみます。todos
とsearchParams
が変更されるたびに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
による比較で判定しようとした場合は無限ループに陥ることにも注意してください。
このコンポーネントはマウント時や、todos
とsearchParams
が変更されるたびにレンダリングを行い、その計算中に状態の更新が行われるのでその子コンポーネントや画面の描画が起きる前に再レンダリングが引き起こされます。これは先ほどの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
を名前につけるのが慣習となっています。
適材適所でそれぞれ活用していきたいです。