2月の頭にReactのv16.8がリリースされ、hooks
が正式に使えるようになってから半年が過ぎました。
非常に強力なhooks
ですが、クラスコンポーネントに慣れた方の中にはhooks
を上手く使えないという方も多いのではないかと思います。
そこで今回は、クラスコンポーネントで行っていた様々な処理をhooks
を用いて関数コンポーネントで実現するための解説を行おうと思います。
本記事は、React hooksを知っていて、API自体はなんとなくわかっていることを前提としています。
もしhooks
が全くわからない場合は、公式リファレンスなどを参照ください。
また、本記事に記載しているコードはReact v16.9.0とTypeScript v3.5.31で動作確認をしており、GitHubでのコード公開と、Netlifyでのホスティングをしています。
クラスコンポーネントでやっていたあれこれを置き換える
早速、クラスコンポーネントで行っていた様々な処理をhooks
を使って関数コンポーネント(以下FC)に置き換えてみましょう。
componentDidMount
でAPIリクエスト
Reactのみでシンプルなアプリケーションを作る場合、componentDidMount
でAPIリクエストを行うこともあるかと思います。
そのような処理は、useState
とuseEffect
を使って以下のように書くことができます。
要件
- コンポーネントマウント時にAPIリクエストを行う
- APIリクエスト中にはローディング表示を行う
import React, {useEffect, useState} from 'react';
type MaybeLoading<T> = {data: T, isLoading: false} | {data: null, isLoading: true};
type SomeState = {id: string, name: string};
// APIリクエストのダミー
const request: () => Promise<SomeState> = () => {
return new Promise(resolve => {
setTimeout(() => resolve({id: '001', name: 'remew'}), 1000);
});
};
const ComponentDidMount = () => {
const [state, setState] = useState<MaybeLoading<SomeState>>({data: null, isLoading: true});
useEffect(() => {
request()
.then(json => setState({data: json, isLoading: false}));
}, []);
if (state.isLoading) {
return <p>Loading...</p>;
}
return (
<>
<p>id: {state.data.id}</p>
<p>name: {state.data.name}</p>
</>
);
};
export default ComponentDidMount;
useEffect
の第二引数に配列を指定すると、コンポーネントが再レンダリングされた際に配列の中身が変わっていないかを確認してもし変わっていれば第一引数に指定した関数が実行されます。
そのため、空配列を指定すると初回レンダリング時にのみ実行されるため、componentDidMount
相当の処理をすることができます。
componentDidMount
/componentDidUpdate
でAPIリクエスト
マウント時だけではなく、propsの特定の値が変わったときにAPIリクエストをしたいシチュエーションもあると思います。
これも同じくuseState
とuseEffect
を用いて実現することが可能です。
察しの良い方はわかると思いますが、useEffect
の第二引数に監視するpropsの値を渡せば良いです。
import React, {useEffect, useState} from 'react';
type MaybeLoading<T> = {data: T, isLoading: false} | {data: null, isLoading: true};
type SomeState = {id: string, name: string};
// APIリクエストのダミー
const request: (id: string) => Promise<SomeState> = (id) => {
return new Promise(resolve => {
setTimeout(() => resolve({id, name: 'remew' + id}), 1000);
});
};
// id指定用のラッパー
const Wrapper = () => {
const [id, setId] = useState('');
return (
<>
<label>id:<input onInput={(e: any) => setId(e.target.value)} style={{border: 'solid 1px #000'}} /></label>
<ComponentDidMountUpdate id={id} />
</>
);
};
const ComponentDidMountUpdate: React.FC<{id: string}> = props => {
const [state, setState] = useState<MaybeLoading<SomeState>>({data: null, isLoading: true});
useEffect(() => {
if (!props.id) {
return;
}
request(props.id)
.then(json => setState({data: json, isLoading: false}));
}, [props.id]);
if (!props.id) {
return <p>id is empty.</p>;
}
if (state.isLoading) {
return <p>Loading...</p>;
}
return (
<>
<p>id: {state.data.id}</p>
<p>name: {state.data.name}</p>
</>
);
};
export default Wrapper;
componentDidMount
とcomponentWillUnmount
で処理を行う
例えば、1秒ごとに数値が加算されるようなコンポーネントを考えてみましょう。
クラスコンポーネントであれば、componentDidMount
とcomponentWillUnmount
を組み合わせて実現することが可能だと思います。
対してFCでは、useEffect
を用いて以下のようなコードで実現することができます。
import React, {useEffect, useState} from 'react';
const Timer = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => {
console.log('clear interval'); // 確認用
clearInterval(timerId);
};
}, []);
return <div>{count}</div>;
};
export default Timer;
useEffect
の第二引数に空配列を指定することで、componentDidMount
相当の処理を行っています。
その中でsetInterval
を呼び出し、更にその中ではsetCount
を呼び出しています。
useEffect
の第一引数に渡した関数の返り値として返している関数は、コンポーネントのアンマウント時と、第一引数に渡した関数が再実行される直前に実行されます。
今回はコンポーネントがアンマウントされるときのみ実行されますが、第二引数に何らかの値を渡している場合、その値が変わって関数が再実行される際にも実行されます。
また、なぜsetCount
にアロー関数を渡しているのかわからない方もいるかもしれませんが、本記事では深くは解説しません。
簡単に言うと、setInterval
に渡した関数から見えるcount
変数は初回レンダリング時の値(今回は初期値0
)であるため、setCount(count + 1)
としても毎回1
がセットされてしまうためです。
詳しい説明は、React公式が提供しているQ&Aをご覧下さい。
Context
を使用する
ReactのContext APIは、複数のContextをクラスコンポーネントで使用する際は、render propsパターンを用いる必要がありました。
useContext
というhooksを用いることで、非常にシンプルにContextの値を使用することができます。
import React, {useContext, useState} from 'react';
const colorContext = React.createContext<string>('#000');
const Wrapper = () => {
const [color, setColor] = useState('#000');
return (
<>
<input value={color} type={'color'} onChange={e => setColor(e.target.value)} />
<colorContext.Provider value={color}>
<UseContext />
</colorContext.Provider>
</>
);
};
const UseContext = () => {
const color = useContext(colorContext);
return <p style={{color}}>{color}</p>;
};
export default Wrapper;
SomeContext.Provider
で値を渡す部分は変わりませんが、<SomeContext.Consumer>{value => ...}</SomeContext.Consumer>
とする必要があった部分が非常にシンプルになっていることがわかると思います。
PureComponent
/shouldComponentUpdate
React製アプリケーションのパフォーマンスチューニングの手段として、shouldComponentUpdate
を使用したり、React.PureComponent
を継承している人も多いと思います。
FCでもpropsが変化しない場合の再レンダリングを抑制するためには、React.memo
を使用する必要があります。
これはhooks
ではありませんが、FCを利用する際に必要な知識のためついでに説明しておきます。
import React, {useContext, useEffect, useState} from 'react';
const SomeComponent: React.FC<{id: string, name: string}> = props => {
console.log('re-rendering:', props.id);
if (!props.name) {
return <div>name is empty</div>;
}
return <div>name: {props.name}</div>;
};
const MemoizedSomeComponent = React.memo(SomeComponent);
const HighFrequencyUpdateComponent = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count => count + 1);
}, 100);
return () => clearInterval(intervalId);
}, []);
return (
<>
<label>name:<input onInput={(e: any) => setName(e.target.value)} style={{border: 'solid 1px #000'}} /></label>
<p>count: {count}</p>
<SomeComponent id={'not-memoized'} name={name} />
<MemoizedSomeComponent id={'memoized'} name={name} />
</>
);
};
export default HighFrequencyUpdateComponent;
上記コンポーネントを表示すると、コンソールにre-rendering: not-memoized
という表示が連続して表示されますが、re-rendering: memoized
は1度しか表示されないはずです(name
のinputを変更すると再表示されます)。
React.memo
に渡したコンポーネントはPureComponent
を継承したコンポーネントのように、propsが変更されたときにのみ再レンダリングが実行されるようになります。
また、React.memo
には第二引数として関数を渡すことができ、その関数がtrue
を返した際にはレンダリングがスキップされます(shouldComponentUpdate
と逆なので注意)。
import React, {useContext, useEffect, useState} from 'react';
const SomeComponent: React.FC<{obj: {id: string, name: string}}> = props => {
console.log('re-rendering:', props.obj.id);
if (!props.obj.name) {
return <div>name is empty</div>;
}
return <div>name: {props.obj.name}</div>;
};
const WrongMemoizedSomeComponent = React.memo(SomeComponent);
const MemoizedSomeComponent = React.memo(SomeComponent, (oldProps, newProps) => {
return oldProps.obj.id === newProps.obj.id && oldProps.obj.name === newProps.obj.name;
});
const HighFrequencyUpdateComponent = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count => count + 1);
}, 500);
return () => clearInterval(intervalId);
}, []);
return (
<>
<label>name:<input onInput={(e: any) => setName(e.target.value)} style={{border: 'solid 1px #000'}} /></label>
<p>count: {count}</p>
<SomeComponent obj={{id: 'not-memoized', name}} />
<WrongMemoizedSomeComponent obj={{id: 'wrong-memoized', name}} />
<MemoizedSomeComponent obj={{id: 'memoized', name}} />
</>
);
};
export default HighFrequencyUpdateComponent;
先程の例と違い、props
の型を{obj: {id: string, name: string}}
というように階層構造にしています。
WrongMemoizedSomeComponent
は、単純にReact.memo
でラップしているだけですが、props.obj
は毎回違うオブジェクトの参照になっているためレンダリングの抑制をすることができていません。
MemoizedSomeComponent
ではReact.memo
の第二引数に比較関数を渡しています。
props.obj.id
とprops.obj.name
をしっかり比較しているため、レンダリングの抑制ができています。
さいごに
以上で目的別hooks
の活用法の紹介を終わります。
他にも思いついたら追記したいと思います。
hooks
が登場した当初は「関数コンポーネントに状態が持てる!」「末端の関数コンポーネントで状態が必要になったときに修正が少なくて済む」などのような意見が多かったように思います。
しかし個人的には、hooks
は関数コンポーネントに状態を持たせるためだけに使うだけではなく、クラスコンポーネントを関数コンポーネントに置き換えるために使うことでより真価を発揮する機能だと思っています(あくまで新規コンポーネントを作成するときや、既存コンポーネントを修正するときの話です2)。
hooks
を駆使してはじめから関数コンポーネントベースで開発をすることで、開発効率が向上するのではないかと考えています。
みなさんが良いhooks
生活を送れることを祈っています。
参考リンク
- https://ja.reactjs.org/docs/react-api.html#hooks
- https://ja.reactjs.org/docs/hooks-faq.html
- https://qiita.com/uhyo/items/246fb1f30acfeb7699da
-
TypeScriptを使っているのは、TypeScriptと絡めた説明もするつもりだったためです。 ↩
-
Reactの公式ドキュメントではクラスコンポーネントを全部書き換える必要はないと言われており、既存のクラスをフックに書き換えることも推奨されていません。 ↩