React 16.8 で導入された Hook のサンプルコードを記載します。
コンポーネントの機能を共有するための手法であった Render Props や Higher-Order Components を利用する必要がなくなります。複数コンポーネントでの状態共有も簡単になります。
componentDidMount
や componentWillUnmount
といったライフサイクルフックに相当する処理の記述も簡単になります。
useState
の引数に初期値を設定します。class で利用していた this.state
は登場しません。
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default App;
Hook とは関係ありませんが <>
と </>
を用いると DOM の階層を一つ浅くすることができます。参照: Fragments
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
);
}
export default App;
ライフサイクルイベントにおける componentDidMount
と componentDidUpdate
に相当する処理を useEffect に記載できます。
例えば document.title
に値を設定するためには仮想 DOM ツリーが完成している必要があります。以下の例では componentDidMount
に相当する処理を目的として useEffect を利用しています。
import React, { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default App;
useEffect
に記載した処理は componentDidUpdate
に相当する処理としても利用されます。つまり、仮想 DOM が完成したタイミングに加えて、コンポーネントの props または state が変更されたタイミングでも実行されます。
useEffect
の第二引数に props または state を配列で指定することで、変更を監視する props と state を制限できます。
import React, { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
console.log(count);
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default App;
実際には state の場合は、第二引数に指定しない場合であっても document.title
の値が適切に変更されていきます。ただし、これは useEffect
の処理が実行されたからではなく、例えば以下の例における console.log(count)
は実行されません。
import React, { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
console.log(count);
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default App;
useEffect
内で関数を return することで componentWillUnmount
相当の処理を記載できます。以下はマウスイベントの例です。window のイベントリスナを、仮想 DOM が完成したタイミングで登録しています。各要素に対するイベントハンドラの登録と区別します。利用できるイベント一覧はこちらです。
App.jsx
import React, { useState, useEffect } from 'react';
import styles from './App.module.css';
function App() {
const [position, setPosition] = useState({ left: null, top: null });
const [position2, setPosition2] = useState({ left: null, top: null });
useEffect(() => {
function handleWindowMouseMove(e) {
setPosition({ left: e.clientX, top: e.clientY });
}
window.addEventListener('mousemove', handleWindowMouseMove);
return () => {
window.removeEventListener('mousemove', handleWindowMouseMove);
};
}, []);
return (
<>
<p>position: {position.left}, {position.top}</p>
<p>position2: {position2.left}, {position2.top}</p>
<br />
<div className={styles.grayBox} onMouseMove={(e) => setPosition2({ left: e.clientX, top: e.clientY })}>
</div>
</>
);
}
export default App;
.grayBox {
width: 100px;
height: 100px;
background: gray;
}
React Hook を利用する際には「関数」と「関数コンポーネント」を区別する必要があります。以下では前者を小文字で始めて、後者を大文字で始めるようにして区別しています。更に「関数」には状態をもつものと持たないものがあり、状態を持つ場合は「use」で始めるようにしています。
以下の例では
HTTP クライアントを利用するために以下の package を利用します。
npm install axios --save
App.jsx
useSiteStatus の引数に count を利用することで、count の変更が siteStatus に反映されます。
import React, { useState } from 'react';
import useSiteStatus from './useSiteStatus';
function App() {
const [count, setCount] = useState(0);
const path = count === 0 ? '/' : `/${count}`;
const siteStatus = useSiteStatus(path);
return (
<>
<p>HTTP GET {path}: {siteStatus === 200 ? 'ok' : 'not ok'}</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
);
}
export default App;
useSiteStatus.jsx
useEffect 内で必要になる関数は useEffect 内で定義すると綺麗になります。
import { useState, useEffect } from 'react';
import axios from 'axios';
import sleep from './sleep';
function useSiteStatus(url) {
const [status, setStatus] = useState(null);
useEffect(() => {
let enabled = true;
async function monitor() {
while(true) {
let response = null;
try {
response = await axios.get(url, { timeout: 1000 });
}
catch(error) {
response = error.response;
}
finally {
if (!enabled) {
break;
}
console.log(`${url}: ${response.status}`);
setStatus(response.status);
}
await sleep(1000);
}
}
monitor(url);
return () => {
console.log(`stopped monitoring for ${url}`);
enabled = false;
};
}, [url]);
return status;
}
export default useSiteStatus;
sleep.jsx
function sleep(msec) {
return new Promise(resolve => setTimeout(resolve, msec));
}
export default sleep;
props または state が変更されるタイミングで毎回 useEffect に記載した処理を実行することがパフォーマンスとして問題になる場合、上記例のように特定の props または state が変更されたタイミングでのみ実行させます。これは今後の React の更新で自動的に検知されるようになる可能性があります。
}, [url]);
useSiteStatus は状態を持つ関数であり、独自に定義した Hookです。Hook に関連する文法チェックには eslint-plugin-react-hooks を利用できます。Create React App を利用している場合は react-scripts >= 3
のバージョンを利用することでインストールされます。
useRef はクラスのインスタンス変数のように利用できます。
App.jsx
import React, { useState } from 'react';
import usePrevious from './usePrevious';
function App() {
const [count, setCount] = useState(0);
const count2 = count + 10;
const prevCount2 = usePrevious(count2);
const prevPrevCount2 = usePrevious(prevCount2);
return (
<>
<p>count: {count}</p>
<p>count2: {count2}</p>
<p>prevCount2: {prevCount2}</p>
<p>prevPrevCount2: {prevPrevCount2}</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
);
}
export default App;
usePrevious.jsx
import { useRef, useEffect } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
export default usePrevious;
既定では、state または props が変更される度に関数コンポーネント内に記載されている処理はレンダリングを含めてすべて実行されます。これがパフォーマンスとして問題になる場合は useMemo を利用して、指定した state または props の変更時以外は再実行しないように設定できます。useEffect との違いに注意します。document.title
の設定やイベントリスナの登録は useEffect で行います。
以下の例では memoizedDate は初回レンダリング時に設定されたものが count の変更によらず利用され続けます。
import React, { useState, useMemo } from 'react';
function App() {
const [count, setCount] = useState(0);
const memoizedDate = useMemo(() => {
return (new Date()).getTime();
}, []);
return (
<>
<p>count: {count}</p>
<p>memoizedDate: {memoizedDate}</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
);
}
export default App;
useReducer を用いると、複数の state の更新処理が複雑な場合に、その処理を関数化できるため便利です。
import React, { useReducer } from 'react';
function App() {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<>
<button onClick={() => dispatch({ type: 'add', text: `mytask-${todos.length + 1}` })}>
Add Task
</button>
<button onClick={() => dispatch({ type: 'reset', payload: [] })}>
Reset Tasks
</button><br />
{todos.map((todo, i) => {
return <div key={i}>
<p>{todo.text}</p>
</div>
})}
</>
);
}
function todosReducer(state, action) {
switch(action.type) {
case 'add':
return [...state, {
text: action.text
}];
case 'reset':
return action.payload;
default:
throw new Error();
}
}
export default App;
useReducer の実装は以下のようになっており、内部的に useState が利用されています。
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
Hook とは関係ありませんが ...
は JavaScript におけるスプレッド構文です。他の言語における flatten と似ています。以下のように記載するとマージ処理が行えます。
const hoge = {
aaa: 123,
bbb: 999
};
console.log(hoge);
console.log({
...hoge,
aaa: 888
});
useReducer の第三引数に関数を渡すと、「第二引数」を「第三引数に指定した関数の引数」としたときの返り値によって、状態を初期化できます。
App.jsx
import React, { useReducer } from 'react';
import { todosReducer, initTodosReducer } from './todosReducer';
function App() {
const initialTodo = 'say hello';
const [todos, dispatch] = useReducer(todosReducer, initialTodo, initTodosReducer);
return (
<>
<button onClick={() => dispatch({ type: 'add', text: `mytask-${todos.length + 1}` })}>
Add Task
</button>
<button onClick={() => dispatch({ type: 'reset', payload: initialTodo })}>
Reset Tasks
</button><br />
{todos.map((todo, i) => {
return <div key={i}>
<p>{todo.text}</p>
</div>
})}
</>
);
}
export default App;
todosReducer.jsx
export function initTodosReducer(initialTodo) {
return [{ text: initialTodo}];
}
export function todosReducer(state, action) {
switch(action.type) {
case 'add':
return [...state, {
text: action.text
}];
case 'reset':
return initTodosReducer(action.payload);
default:
throw new Error();
}
}
以下の例では count が「非同期処理の setInterval によるインクリメント」と「同期処理のクリックによるデクリメント」の二つの処理によって更新されていきます。
import React, { useEffect, useReducer } from 'react';
function App() {
const [count, dispatch] = useReducer(myReducer, 0);
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'increment' });
}, 1000);
return () => clearInterval(id);
}, []);
return (
<>
<button onClick={() => dispatch({ type: 'decrement' })}>
Click me
</button>
<p>{count}</p>
</>
);
}
function myReducer(state, action) {
switch(action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
throw new Error();
}
}
export default App;
useReducer の dispatch を用いずに useEffect 内で count のインクリメントを行う場合、useEffect の第二引数に [count]
を指定しなければならなくなり、クリックによって count の値が変更される度に setInterval が clear されるため、正確に一秒毎にインクリメントが行われなくなります。
Context と useContext、更に簡単のため useReducer を用いると、state を複数コンポーネントで共有する処理が簡単に記述できます。useReducer の利用は必須ではありません。
Hook を用いない場合は、上位コンポーネントの state を更新するコールバック関数を下位コンポーネントに渡す必要がありましたが、その必要がなくなります。大規模なものでない限り、標準ライブラリでない Redux を利用する必要もありません。
App.jsx
import React, { useReducer } from 'react';
import DeepChild from './DeepChild';
import { countReducer, CountDispatch } from './countReducer';
function App() {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<>
<CountDispatch.Provider value={dispatch}>
<DeepChild count={count} />
</CountDispatch.Provider>
<p>{count}</p>
</>
);
}
export default App;
DeepChild.jsx
import React, { useContext } from 'react';
import { CountDispatch } from './countReducer';
function DeepChild(props) {
const dispatch = useContext(CountDispatch);
return (
<>
<button onClick={() => dispatch({ type: 'increment' })}>
increment {props.count}
</button>
</>
);
}
export default DeepChild;
countReducer.jsx
import React from 'react';
export function countReducer(state, action) {
switch(action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
throw new Error();
}
}
export const CountDispatch = React.createContext();
以下のように state を props ではなく context で渡すこともできます。
App.jsx
import React, { useReducer } from 'react';
import DeepChild from './DeepChild';
import { countReducer, CountDispatch } from './countReducer';
function App() {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<>
<CountDispatch.Provider value={[count, dispatch]}>
<DeepChild />
</CountDispatch.Provider>
<p>{count}</p>
</>
);
}
export default App;
DeepChild.jsx
import React, { useContext } from 'react';
import { CountDispatch } from './countReducer';
function DeepChild(props) {
const [count, dispatch] = useContext(CountDispatch);
return (
<>
<button onClick={() => dispatch({ type: 'increment' })}>
increment {count}
</button>
</>
);
}
export default DeepChild;
countReducer.jsx
import React from 'react';
export function countReducer(state, action) {
switch(action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
throw new Error();
}
}
export const CountDispatch = React.createContext();
useCallback を用いると、例えば上位コンポーネントの state に依存してふるまいが変わる関数を下位コンポーネントに渡すことができます。
import React, { useState, useEffect, useCallback } from 'react';
function App() {
const [count, setCount] = useState(0);
const countCallback = useCallback((num) => {
return num + count;
}, [count]);
return (
<>
<Child countCallback={countCallback} />
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
);
}
function Child(props) {
const countCallback = props.countCallback;
const [sum, setSum] = useState(0);
useEffect(() => {
setSum(countCallback(100));
}, [countCallback]);
return (
<p>{sum}</p>
);
}
export default App;
DOM のサイズを取得するためには DOM が完成するのを待つ必要があります。useEffect を利用することもできますが、React の Callback Refs 機能を利用すると簡単です。
DOM が完成したタイミングで、指定した DOM が引数として渡されます。DOM が unmount されるタイミングでは null が引数として渡されて実行されます。コールバック関数内では Element.getBoundingClientRect を利用しています。
import React, { useState, useCallback } from 'react';
function App() {
const [rect, ref] = useClientRect();
const height = rect === null ? null : rect.height;
return (
<>
<h1 ref={ref}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
function useClientRect() {
const [rect, setRect] = useState(null);
const ref = useCallback(node => {
if(node !== null) {
setRect(node.getBoundingClientRect());
}
}, []);
return [rect, ref];
}
export default App;
useState の引数には state の初期値を指定しますが、関数を指定すると、その返り値によって初期値を設定できます。初期値を直接指定する場合はレンダリングの度に初期値が評価されるため、重いオブジェクトを指定する場合は後者を利用して回避します。useMemo による処理結果のキャッシュと区別します。
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
// 初回レンダリングでのみ heavyComputation が実行されます。
const [data, setData] = useState(() => {
return heavyComputation(count);
});
// 毎回 heavyComputation2 が実行されます。
const [data2, setData2] = useState(heavyComputation2(count));
function heavyComputation(count) {
const res = `heavyComputation result at count = ${count}`;
console.log(res);
return res;
}
function heavyComputation2(count) {
const res = `heavyComputation2 result at count = ${count}`;
console.log(res);
return res;
}
return (
<>
<p>count: {count}</p>
<p>data: {data}</p>
<p>data2: {data2}</p>
<button onClick={() => {
setCount(count + 1);
setData(`new data at count = ${count}`);
setData2(`new data at count = ${count}`);
}}>
Click me
</button>
</>
);
}
export default App;
初回レンダリング
count がインクリメントされる度にレンダリング
なお、development build の場合は関数を useState の引数に指定した場合であっても二回評価されます。
こちらのページに記載した D3 を React から扱う場合は、D3 の手続的な記述を避け、React によって宣言的に記述するように注意すると React との統合が簡単になります。React コンポーネントは SVG (Scalable Vector Graphics) を返すことができるため、SVG の生成は React で行い、D3 はデータから幾何情報を計算するために利用します。
計算のために必要となる D3 のライブラリを選択的に利用します。
npm install d3-scale --save
npm install d3-array --save
App.jsx
import React from 'react';
import { scaleLinear } from 'd3-scale';
import { extent } from 'd3-array'
import RandomData from './RandomData';
import AxisLeft from './AxisLeft';
import AxisBottom from './AxisBottom';
function App() {
// 実際には API などで取得するデータです。
const data = RandomData();
// SVG 全体のサイズです。
const w = 600;
const h = 600;
// SVG 内に padding を設けてみます。
const margin = {
top: 40,
bottom: 40,
left: 40,
right: 40,
};
const width = w - margin.right - margin.left;
const height = h - margin.top - margin.bottom;
// D3 の機能を利用します。データから幾何情報を計算する関数です。
const xScale = scaleLinear()
.domain(extent(data, d => d.x))
.range([0, width]);
const yScale = scaleLinear()
.domain(extent(data, d => d.y))
.range([height, 0]);
// React の機能を利用して、宣言的に SVG を生成します。
const circles = data.map((d, i) => {
return <circle key={i} r={5} cx={xScale(d.x)} cy={yScale(d.y)} style={{ fill: 'lightblue' }} />
});
return (
<>
<svg width={w} height={h}>
<g transform={`translate(${margin.left},${margin.top})`}>
<AxisLeft yScale={yScale} width={width} />
<AxisBottom xScale={xScale} height={height} />
{circles}
</g>
</svg>
</>
);
}
export default App;
AxisLeft.jsx
import React from 'react';
function AxisLeft(props) {
const textPadding = -20;
const axis = props.yScale.ticks(5).map((d, i) => (
<g key={i} className='y-tick'>
<line
style={{ stroke: '#e4e5eb' }}
y1={props.yScale(d)}
y2={props.yScale(d)}
x1={0}
x2={props.width}
/>
<text
style={{ fontSize: 12 }}
x={textPadding}
dy='.32em'
y={props.yScale(d)}
>
{d}
</text>
</g>
));
return <>{axis}</>;
}
export default AxisLeft;
AxisBottom.jsx
import React from 'react';
function AxisBottom(props) {
const textPadding = 10;
const axis = props.xScale.ticks(10).map((d, i) => (
<g className='x-tick' key={i}>
<line
style={{ stroke: '#e4e5eb' }}
y1={0}
y2={props.height}
x1={props.xScale(d)}
x2={props.xScale(d)}
/>
<text
style={{ textAnchor: 'middle', fontSize: 12 }}
dy='.71em'
x={props.xScale(d)}
y={props.height + textPadding}
>
{d}
</text>
</g>
));
return <>{axis}</>;
}
export default AxisBottom;
SVG で利用できる要素や属性についてはこちらを参照します。アニメーションが必要な場合は react-spring を併用することができます。