React Hook のサンプルコード
[履歴] [最終更新] (2020/06/17 01:41:48)
最近の投稿
注目の記事

概要

React 16.8 で導入された Hook のサンプルコードを記載します。

コンポーネントの機能を共有するための手法であった Render PropsHigher-Order Components を利用する必要がなくなります。複数コンポーネントでの状態共有も簡単になります。

componentDidMountcomponentWillUnmount といったライフサイクルフックに相当する処理の記述も簡単になります。

useState (this.state、this.setState に相当する機能)

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;

Uploaded Image

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;

Uploaded Image

useEffect (componentDidMount、componentDidUpdate、componentWillUnmount に相当する機能)

ライフサイクルイベントにおける componentDidMountcomponentDidUpdate に相当する処理を 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;

Uploaded Image

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;

Uploaded Image

実際には 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;

Uploaded Image

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;

App.module.css

.grayBox {
    width: 100px;
    height: 100px;
    background: gray;
}

Uploaded Image

関数の命名規則について

React Hook を利用する際には「関数」と「関数コンポーネント」を区別する必要があります。以下では前者を小文字で始めて、後者を大文字で始めるようにして区別しています。更に「関数」には状態をもつものと持たないものがあり、状態を持つ場合は「use」で始めるようにしています。

以下の例では

  • App は関数コンポーネントです。
  • useSiteStatus は状態を持つ関数です。
  • sleep は状態を持たない関数です。

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 のバージョンを利用することでインストールされます。

一つ前の state を保存しておく

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;

Uploaded Image

処理結果のキャッシュ

既定では、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;

state 群の更新処理を関数化する (useReducer)

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;

Uploaded Image

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
});

Uploaded Image

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();
  }
}

Uploaded Image

一つの state を非同期処理を含む複数の処理で扱う場合

以下の例では 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 されるため、正確に一秒毎にインクリメントが行われなくなります。

上位コンポーネントの state を下位コンポーネントから更新

ContextuseContext、更に簡単のため 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();

Uploaded Image

以下のように 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();

state に応じてふるまいを変える関数のキャッシュ useCallback

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;

Uploaded Image

DOM のサイズを取得

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;

Uploaded Image

state の初期値が重いオブジェクトの場合

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;

初回レンダリング

Uploaded Image

count がインクリメントされる度にレンダリング

Uploaded Image

なお、development build の場合は関数を useState の引数に指定した場合であっても二回評価されます

D3 による SVG の生成

こちらのページに記載した 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;

Uploaded Image

SVG で利用できる要素や属性についてはこちらを参照します。アニメーションが必要な場合は react-spring併用することができます

関連ページ