モーダルを閉じる工作HardwareHub ロゴ画像

工作HardwareHubは、ロボット工作や電子工作に関する情報やモノが行き交うコミュニティサイトです。さらに詳しく

利用規約プライバシーポリシー に同意したうえでログインしてください。

React Hook のサンプルコード

モーダルを閉じる

ステッカーを選択してください

お支払い手続きへ
モーダルを閉じる

お支払い内容をご確認ください

購入商品
」ステッカーの表示権
メッセージ
料金
(税込)
決済方法
GooglePayマーク
決済プラットフォーム
確認事項

利用規約をご確認のうえお支払いください

※カード情報はGoogleアカウント内に保存されます。本サイトやStripeには保存されません

※記事の執筆者は購入者のユーザー名を知ることができます

※購入後のキャンセルはできません

作成日作成日
2020/06/03
最終更新最終更新
2022/09/21
記事区分記事区分
一般公開

目次

    フロントエンドエンジニア。React Hookが好きです!

    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;
    

    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;
    

    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;
    

    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;
    

    App.module.css

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

    関数の命名規則について

    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;
    

    処理結果のキャッシュ

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

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

    一つの 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();
    

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

    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;
    

    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;
    

    初回レンダリング

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

    なお、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;
    

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

    Likeボタン(off)0
    詳細設定を開く/閉じる
    アカウント プロフィール画像

    フロントエンドエンジニア。React Hookが好きです!

    記事の執筆者にステッカーを贈る

    有益な情報に対するお礼として、またはコメント欄における質問への返答に対するお礼として、 記事の読者は、執筆者に有料のステッカーを贈ることができます。

    >>さらに詳しくステッカーを贈る
    ステッカーを贈る コンセプト画像

    Feedbacks

    Feedbacks コンセプト画像

      ログインするとコメントを投稿できます。

      ログインする

      関連記事