はじめに
社内でReact Hooksとはなんぞやの勉強会が行われたので、議事録兼アウトプットとして記事を書きます。
React Hooksとは
React16.8で導入された機能であり、関数コンポーネントにおいて、ステート管理、ライフサイクルといった機能を使えるようにしてくれるもの
です。
React Hooksが生まれた背景
Reactにはクラスコンポーネント
と関数コンポーネント
の2種類の書き方が存在します。
以下にクラスコンポーネントの書き方でボタンで文言をtoggleさせるコード例を示します。
import React from "react";
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isLogin: true
}
}
toggleLoginStatus = () => {
this.setState({isLogin: !this.state.isLogin});
}
render() {
return (
<>
{this.state.isLogin ? "ログイン中" : "ログアウト中"}
<button onClick={this.toggleLoginStatus}>toggle</button>
</>
)
}
}
export default Hoge
ボタンを押す事でisLogin
の状態を変化させ、文言の出しわけを行っています。
ひと昔前は、クラスコンポーネントを使ってコードを記述するのが一般的だったそうです。
なぜかというと、クラスコンポーネントではステート管理、ライフサイクルといった機能が使えますが、関数コンポーネントではそれらを使うことができなかったからです。
そこで登場したのがReact Hooks
というものです。
React Hooksが登場したことにより、関数コンポーネントで状態管理やライフサイクル機能が使えるとようになり、可読性の高いコードがかけるようになりました。
以下に先ほどの例と同じものを関数コンポーネントで書いたバージョンを示します。
import { useState } from "react";
const Hoge = () => {
const [isLogin, setIsLogin] = useState(true);
const toggleLoginStatus = () => {
setIsLogin(isLogin => !isLogin);
}
return (
<>
{isLogin ? "ログイン中" : "ログアウト中"}
<button onClick={toggleLoginStatus}>toggle</button>
</>
);
}
export default Hoge
クラスコンポーネントに比べてより完結に同じ処理が記述ができることが見てわかるかと思います。
React Hooksの紹介
簡単にですが、React Hooksの紹介をします。
useState
useState
はステートを保持するために使われるフックで、React Hooksの中で一番よく使われます。
numberやboolean、mapなど様々な状態を保持することができます。
ボタンを押すとカウントが増えていくコード例を以下に示します。
import { useState } from "react";
const Hoge = () => {
const [count, setCount] = useState(0);
const countUp = () => {
setCount(count => count + 1);
}
return (
<>
<p>今のカウント:{count}</p>
<button onClick={countUp}>
count up!
</button>
</>
);
}
export default Hoge
useEffect
useEffect
はライフサイクル(クラスコンポーネントでいうところの、componentDidMount
とcomponentDidUpdate
, componentWillUnmount
)を実現させるためのフックで、レンダリング時やステートを更新するタイミングでuseEffect内の副作用関数を走らせることができます。
初回のレンダリング時のみ処理を実行させたいとき
useEffectの第二引数に空の依存配列
を指定します。
// レンダリングされた時にのみ実行(componentDidMount)
useEffect(() => {
console.log("component did mount");
}, []);
特定のステートが更新された時に処理を実行させたいとき
useEffectの第二引数にウォッチさせたいステート
を指定します。
// countが更新された時に実行(componentDidUpdate)
useEffect(() => {
console.log("component did update");
console.log(`current count is ${count}`);
}, [count])
コンポーネントを削除(アンマウント時)について
useEffectの中で関数をreturnすることで、マウント時に実行した副作用関数を削除します。
これを行うことで予期せぬバグを回避することができます。
returnする関数はクリーンアップ関数と呼ばれます。
// コンポーネントが削除される際に実行(componentWillUnmount)
useEffect(() => {
return () => {
console.log("component will unmount");
};
}, [])
以上をまとめたものがこちら
import { useState, useEffect } from "react";
const Hoge = () => {
const [count, setCount] = useState(0);
const countUp = () => {
setCount(count => count + 1);
}
useEffect(() => {
console.log("component did mount");
}, []);
useEffect(() => {
console.log("component did update");
console.log(`current count is ${count}`);
}, [count])
useEffect(() => {
return () => {
console.log("component will unmount");
};
}, [])
return (
<>
今のカウント:{count}
<button onClick={countUp}>
count up!
</button>
</>
);
}
useContext
親コンポーネントから子コンポーエントに値を渡したいときが多々あるかと思いますが、親コンポーネントから孫コンポーネントへ値を渡したいときも場合によってはあるかと思います。
そんな時に、親から子、子から孫へとpropsを渡すのではなく、useContext
を使えば、Contextに格納されている値をどの階層でも参照する
ことができます。
便利である一方、Contextを更新したら、そのContextを利用しているすべてのコンポーネントが再レンダリング
されてしまうので、注意が必要です。
親コンポーネントで定義したContextを孫コンポーネントで取得するコードを以下に示します。
import { useContext, createContext } from "react";
const user = {
name: "Taro",
age: 17
}
const UserContext = createContext(user);
const Hoge = () => {
return (
<UserContext.Provider value={user}>
<User />
</UserContext.Provider>
);
}
const User = () => {
return(
<UserProfile />
)
}
const UserProfile = () => {
const userInfo = useContext(UserContext);
return (
<>
<p>名前:{userInfo.name}</p>
<p>年齢:{userInfo.age}</p>
</>
);
}
export default Hoge
useReducer
useReducer
はuseStateと同じく、ステートを管理するフックで、複数の値や条件によってstateの更新を行いたいとき時に主に使われます。
(state, action) => newState
の型をもったreducerを受け取り、現時点でのstateとdispatchを返します。
actionとなる引数を持ったdispatchメソッドをonClick等で実行することによって、reducerが呼び出され、actionによってstateの更新が行われます。
あくまでnewStateをreturnするので値単体ではなく、オブジェクトで定義していたらオブジェクトとしてreturnしなければいけません。
import { useReducer } from "react";
const reducer = (state: any, action: any) => {
switch (action) {
case "increment":
return { count: state.count + 1 }
case "decrement":
return { count: state.count - 1 }
default:
throw new Error();
}
}
const Hoge = () => {
const [countState, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>今のカウント:{countState.count}</p>
<button onClick={() => dispatch("decrement")}>decrement</button>
<button onClick={() => dispatch("increment")}>increment</button>
</>
);
}
export default Hoge
useCallback
useCallback
を使うことで、関数をメモ化し、コールバック関数として返すことにより余計な再レンダリングを防ぐ
ことができます。
useCallbackはReact.memo
と使われることが多いです。
React.memoとは
React.memoとはメモ化したいコンポーネントをラップするときに用いられます。
メモ化とは、propsに差分がなければ再描画をスキップするために前回描画したコンポーネント結果をメモしておくことをいいます。
ただし、propsとしてコールバック関数を受け取る場合は、React.memoを使っていたときでも再描画されてしまいます。それは関数の内容は同じでも、コンポーネントが再描画されるたびに関数は生成されるので、前回と等価として扱われないためです。
そこでuseCallbackを使い、コールバック関数をメモ化させることで不要な再描画を防ぐことができます。
以下に、React.memoでメモ化したコンポーネントにuseCallbackでメモ化したコールバック関数を渡すコード例を示します。
この結果、カウントをインクリメント(ステートの更新)をすると、押されたほうだけのコンポーネントのみを再描画させられるようになります。
import { React.memo, useCallback } from "react";
const Button = React.memo(({text, handleClick}) => {
// 確認用
console.log(text);
return (
<button onClick={handleClick}>{text}</button>
);
});
const Hoge = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClick1 = useCallback(() => setCount1(count1 + 1), [count1]);
const handleClick2 = useCallback(() => setCount2(count2 + 1), [count2]);
return (
<>
<p>count1:{count1}</p>
<p>count2:{count2}</p>
<Button text={"count1:increment"} handleClick={handleClick1} />
<Button text={"count2:increment"} handleClick={handleClick2} />
</>
);
}
export default Hoge
useMemo
useMemoは関数の結果を保存しておくためのフックで、再描画の際に不要な再計算を行わないことで、パフォーマンス向上を図ることができます。
先ほどのuseCallBackは関数自体をメモ化し、useMemoは関数の結果をメモ化します。
useMemoを使って計算結果を保持しておき、依存するステートが更新されたときのみに再計算するコード例を以下に示します。
import { useMemo } form "react";
const Hoge = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const twiceCount1 = useMemo(() => {
return 2 * count1;
}, [count1]);
const handleClick1 = () => setCount1(count1 => count1 + 1);
const handleClick2 = () => setCount2(count2 => count2 + 1);
return (
<>
<p>count1:{count1}</p>
<p>count2:{count2}</p>
<button onClick={handleClick1}>count1:increment</button>
<button onClick={handleClick2}>count2:increment</button>
<p>count1:{twiceCount1}</p>
</>
);
}
export default Hoge
useRef
useRefはuseStateと同じく、コンポーネントの中で値を保持することができます。
useStateと異なる点は、値を更新するときに再描画を発生させないことです。
なので、ユースケースとしては描画に関係のない値を扱うときに使われます。
使用例として前回のステートを取得するコードを示します。 ※公式を参照
描画した後にuseEffectを用いて、prevCountに前回のステートを保持することができ、次回の再描画のタイミングでprevCountを表示します。
import { useRef } from "react";
const Hoge = () => {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0);
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return (
<>
<p>今のカウント:{count}</p>
<p>前回のカウント:{prevCount}</p>
<button onClick={() => setCount(count+1)}></button>
</>
)
}
export default Hoge
おわりに
React Hooksが生まれた経緯と簡単にですが公式Hooksの紹介をしました。
自分自身、useCallbackやuseMemo,React.memoをあまり意識して使えていなく、今回の議事録アウトプットで基本的な概念を再確認することができました。
今後の開発やリファクタリング等でパフォーマンスを意識したコードをかけるよう精進して参ります。