useStateで状態管理を行う
それでは、 useState の使い方について解説します。まずは前回と同様に、状態を初期化して、そのまま表示してみましょう(リスト3)。
export default function App() {
const [name] = useState("Sara"); // (1)
return (
<View>
<Welcome name={name} />
</View>
);
};
function Welcome(props) {
return <Text>Hello, {props.name}</Text>;
}
useState は引数を1つ取って、配列を返す関数です。(1)の右辺で読み込まれた "Sara" という初期値が、左辺では配列の最初の要素である name に代入されています。クラスコンポーネントでは this.state を通じて状態にアクセスしていましたが、このサンプルでは useState から出てきた値をそのまま使っています。
次は、状態を更新してみましょう。こちらも前回と同じ仕様のカウンターアプリを再実装してみます(リスト4)。
export default function App() {
const [count, setCount/*(1)*/] = useState(0);
const onPressMinusOne = () => {
setCount(count - 1); // (2)
};
const onPressPlusOne = () => {
setCount(count + 1); // (3)
};
return (
<View style={styles.container}>
<Button title="-1" onPress={onPressMinusOne} />
<Text>{count}</Text>
<Button title="+1" onPress={onPressPlusOne} />
</View>
);
};
状態を更新したいので、クラスコンポーネントでいうところの setState 相当の関数が必要になります。 useState の場合は、(1)の setCount がそれにあたります。使い方としては、(2)や(3)のように、コールバック内などで呼び出すだけです。 setState とは違って、オブジェクトをマージする機能がないことには注意が必要ですが、基本的には同じような使い方をします。
useStateの戻り値は配列
さて、ご覧の通り、 useState の戻り値は、最初の要素が「状態」、その次の要素が「状態を更新するための関数」という配列で提供されています。図1のように、左辺で分割代入して使うのが一般的です。
見慣れない形なので、初めは抵抗があるかも知れませんが、この形のAPIになっていることには大きな利点があります。配列で提供されていることによって、状態や、それを更新する関数の名前を、自分で決められるのです。例えば、リスト5のように、カウント対象が複数ある場合には、 useState で別々に初期化することができます。
const [countA, setCountA] = useState(0); const [countB, setCountB] = useState(0);
更新用の関数の名前を別々に定義できるので、状態を更新するときに、明示的にどの状態を変えようとしているのかを意識しやすくなります。
状態の更新=関数の再実行
クラスコンポーネントで setState を実行した場合には、 render メソッドが再度呼び出されることで再描画が行われていました。一方、関数コンポーネントで useState の更新関数(今回は setCount )を呼び出した場合は、関数コンポーネントそのものが再実行されます。
再実行ということは、 useState(0) からやり直しになるので、 count がずっとゼロのままになるのではないかと心配になりますが、実際にはそうはなりません。フックは、自分が実行されているコンポーネントがマウントされてからアンマウントされるまで、内部で状態を保持することができるので、関数としては再実行でも、引き続き最新の状態を保ったまま動作するのです(図2)。
しばらくは奇妙に感じるかも知れませんが、慣れると使いやすいAPIなので、ぜひ使ってみてください。
[コラム]React Hooksはビューツリーを「フック」している
同じ関数を呼んでいるのに、コンポーネントごとに別々のデータが保持されて、しかも関数が再実行されてもデータが継続する、というのは、なかなか仕組みが想像しづらい魔法のような機能です。
しかし、これは魔法ではなく、ReactがJSXから生み出した仮想的なビューツリー(いわゆるVirtual DOM)を管理するための機構を間借りして実現されています。状態の更新を受けて、Reactがビューツリーを再評価する度に、各ノード(ツリーの節)に対して「このコンポーネントにはこのデータがひもづいていたはずだから、次に useState が呼ばれたらこのデータを返してあげよう」という処理が行われています。ビューツリーの処理にフック(割り込み)して実現された機構なので、Hooksという名前になったわけですね。
useEffectで副作用を扱う
続いて、 useEffect の使い方を解説します。 useEffect は副作用(Side Effect)を扱うためのフックです。 useState とは違い、戻り値を持たず、関数を登録するAPIになっています。まずは最も簡単な使い方として、アラートを表示してみましょう(リスト6)。
export default function App() {
useEffect(() => {
alert('hello!'); // (1)
});
return (
<View>
<Text>Hello!</Text>
</View>
);
};
実行すると、画面が表示された直後に副作用の関数が実行され、(1)のアラートが表示されます(図3)。
この例では、 useEffect はクラスコンポーネントの componentDidMount に近い挙動をしていますが、 useEffect の独特な点として、 componentDidUpdate 相当のタイミングにも動作します。何らかの理由で関数コンポーネントが実行された後には、毎回呼び出されると思っておくとよいでしょう。
なお, useEffect はDOMへの描画よりも後で実行されるため、 componentDidMount / componentDidUpdate の実行タイミングとは厳密には違います。とはいえ、実用上は問題にならないケースが多いでしょう。
条件付きで副作用を実行する
useEffect には、パラメータを監視して、渡された関数の実行を制限する機能があります。これを利用するには、第二引数にパラメータの配列を登録します。例えば、リスト7のような、propsで渡されたIDを元に、表示するデータをフェッチする処理について考えてみます。
const [data, setData] = useState(null); // (3)
useEffect(() => {
fetch(`https://example.com/api/data?id=${props.dataId}`)
.then(res => res.json())
.then(_data => setData(_data)); // (2)
}, [props.dataId]); // (1)
(1)で props.dataId だけを監視しているので、それ以外のpropsやstateの変更で関数コンポーネントが再実行された場合でも、 useEffect の関数は実行されません。 props.dataId が更新されると、 useEffect の関数が実行され、フェッチが成功すると(2)の setData が実行されます。あとは既に解説した通り、(3)で useState の更新処理が発生して、 data を元に画面の描画が行われます。このように、 useEffect の処理は useState と組み合わせて行われることがあります。
副作用のクリーンアップ
副作用の中で実施したいものの中には、後始末が必要なものがあります。例えば、 setInterval や、websocket経由の継続的なデータ購読などです。そういった後始末を行うための機能として、 useEffect にはクリーンアップ関数を渡す機能があります(リスト8)。
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe(); // (1)
};
});
リスト8は条件をつけていないので、コンポーネントが更新される度に副作用も実行されます。その直前に、(1)のクリーンアップ関数が実行されます。これによって、後始末を適切に行って、メモリリークを防ぐことができます。
もう少し複雑な例として、前回作成した時計アプリをフックで再実装してみましょう(リスト9)。
const Clock = () => {
const [date, setDate] = useState(() => new Date()); // (3)
useEffect(() => {
const timerID = setInterval(() => { // (1)
setDate(new Date());
}, 1000);
return () => { // アンマウント時に呼ばれる
clearInterval(timerID); // (2)
};
}, []); // マウント後に1回だけ呼ばれる
return (
<View>
<Text>現在時刻</Text>
<Text>{date.toLocaleTimeString()}</Text>
</View>
);
};
(1)で setInterval を実行したときに入手した timerID を使って、(2)でアンマウントされる際に clearInterval を行うようにしてみました。クラスコンポーネントでは this.timerID のようにクラスのプロパティで保持していましたが、この例では(1)で定義した変数をそのままクリーンアップ関数に持ち込んでいるので、見通しが非常に良くなりました。
余談ですが、(3)のように useState の初期値の設定に関数を使うこともできます。初回以外は無視されるのに、毎回 new Date() が実行されるのは、少しだけコストが高そうに思われたため、この書き方を選択しました。
