sagantaf

メモレベルの技術記事を書くブログ。

React HooksのuseReducerとuseContextとカスタムフック

useReducer

useReducerの基本

Stateとdispatch(actionを送信する関数)を返すフック。

これを使うことでコンポーネント内でstate管理ができる。

書き方は以下。

const [state, dispatch] = useReducer(reducer, stateの初期値)

reducer にはstateの更新方法を記述する。

useReducerを生成して返ってくるdispatchはstateを更新ために利用する。dispatchにactionという更新内容を渡すことでstateを更新できる。

使い方は以下。

const action = {
    type: 'ADD_TODO',
    text: 'Learning React'
};

// reducerではactionをswitch文で分岐して書くことで可読性が上がる
function reducer(state, action) {
    switch(action.type) {
        case 'ADD_TODO':
            return [...state, [{ text: action.text, completed: false }]];
        default:
            return state;
    }
}

const [state, dispatch] = useReducter(reducer, {})
dispacth(action);

別の例。

// import省略
// 現在のstateとactionを受け取り、actionに応じて更新したstateを返す関数
function reducer(state, action){
    switch (action.type) {
        case 'INCRE':
            return {count: state.count + 1}
        case 'DECRE':
            return {count: state.count - 1}
        case 'RESET':
            return {count: 0}
        default:
            return state;
    }
}
export default App() {
    const [state, dispatch] = useReducter(reducer, { count:0 })
}

return(
    <>
        <p>count: {state.count}</p>
        <button onClick={dispatch(state, {type: 'INCRE'})}>+</button>
        <button onClick={dispatch(state, {type: 'DECRE'})}>-</button>
        <button onClick={dispatch(state, {type: 'RESET'})}>reset</button>
    </>
)

useReducerの使い所

複雑なstateを扱う時にはuseStateよりもuseReducerの方が良い。たとえば以下のような場合。

  • stateを更新するために他のstateを参照する
  • stateを更新したら別のstateも更新する必要がある
  • stateを更新するロジックが複雑である

useReducerを利用すると、

  • stateの更新ロジックをreducerに集約できる
  • stateの更新がdispatchの記述に統一される

というメリットがある。

また、dispatchは同一性が保たれるため、React.memoにdispatchを渡しても、親コンポーネントの再レンダーによる、子コンポーネントレンダリングを回避できる。useCallbackを使う必要はない。

useContext

useContextの基本

useContextはReactのContextと併用することで力をはっきする。

ReactのContextとは、Propsを利用せずに様々な階層のコンポーネントに値を共有できる機能。

共有したい値をContextオブジェクトとして生成し、Providerで使えるコンポーネント範囲を指定し、使いたいコンポーネントでuseContextを使って利用する、という流れ。Contextオブジェクトを利用するコンポーネントのことをConsumerと呼ぶ。

使い方は以下。

// import省略

// Contextオブジェクトの生成
const HogeContext = React.createContext();

function Child() {
    // コンポーネントでの利用
    const name = useContext(HogeContext);
    return <h1>Hello, {name}</h1>;
}

// Providerタグで各層のコンポーネントを囲むことで、囲まれたコンポーネントはContextを共有できるようになる
// 渡したいContextをvalueで渡す
export default function App() {
    const name = 'React'
    return (
        <HogeContext.Provider value={name}>
            <Child />
        </HogeContext.Provider>
    )
}

Contextのメリット

Contextを利用することで得られるメリットは

  • どのコンポーネントからもアクセスできるstateを作成できる
  • Prop drilling問題を解消できる(階層が深いとPropsを渡すのが煩わしくなる問題)

の2点。

たとえば、共通利用するデータをContextで一元化し、Contextの更新を全てのコンポーネントにもれなく反映させることができる(下記例)。

import React, { useState, useContext, createContext } from 'react';

const LangContext = createContext();

export default function App(){
    const [lang, setLang] = useState('JP')
    return (
        <>
            <button onClick={() => setLang('JP')}>日本語</button>
            <button onClick={() => setLang('EN')}>English</button>
            <LangContext.Provider value={lang}>
                <Header />
                <Body />
            </LangContext.Provider>
        <>
    );
}

function Header(){
    const lang = useContext(LangContext);
    const text = {
        JP: "Reactハンズオンラーニング",
        EN: "React Hands-on Learning"
    };
    return (
        <header>
            <h1>{text[lang]}</h1>
        </header>
    )
}

function Body() {
    const lang = useContext(LangContext);
    const text = {
        JP: "Reactの練習",
        EN: "React Training"
    };
    return <p>{text[lang]}</p>
}

Context利用時の注意

Contextオブジェクトを使うConsumerは、Providerのvalueが更新されるたびに再レンダーされてしまう。 これを防ぐ方法は、

  • Contextを分割する
  • React.memoを利用する
  • useMemoを利用する

の3パターンある。

  • Contextを分割する

Contextを細かく分割することで、関係あるContextの更新のみでConsumerが再レンダーされるようにする。

  • React.memoを利用する

useContextを直接利用したコンポーネントの再レンダーは避けられない。ただ、そのコンポーネントでさらにReact.memo化したコンポーネントを呼ぶ形で、再レンダーを少し減らすことはできる。

//import省略
const CountContext = createContext();

function countReducer(state, action) {
    switch (action.type) {
        case 'INCRE':
            return {count: state.count + 1}
        case 'DECRE':
            return {count: state.count - 1}
        case 'RESET':
            return {count: 0}
        default:
            return state;
    }
}

function CountProvider({ chileren }) {
    const [state, dispatch] = useReducer(countReducer, { count: 0 })
    const value = { state, dispacth }

    return (
        <CountContext.Provider value={value}>{children}</CountProvider>
    )
}

function Count() {
    const { state } = useContext(CountContext)
    return <h1>{state.count}</h1>
}

function Counter() {
    const {dispatch} = useContext(CountContext)
    return <DispatchButton dispatch={dispatch} />
}
// 上記コンポーネントでは DispatchButtonにdispatchを渡している
// valueの変更によるCounterの再レンダーは避けられないため、
// メモ化したDispatchButtonにdispatchを渡すことでDispatchButton自体の再レンダーを回避する対応を取っている

const DispatchButton = React.memo( ({ dispatch }) => {
    return (
        <>
            <button onClick={() => dispatch({ type: "DECRE" })>-</button>
            <button onClick={() => dispatch({ type: "INCRE" })>+</button>
        </>
    )
})

export default function App() {
    return (
        <CountProvider>
            <Count />
            <Counter />
        </CountProvider>
    )
}
  • useMemoを利用する

useContextを使ったコンポーネントのreturnでuseMemoを返す形にすることで、再レンダーを防ぐことができる。上記のReact.memoの例のCounterを下記のように変更することで実現される。

function Counter() {
    const { dispatch } = useContext(CountContext)
    return useMemo(() => {
        return (
            <>
                <button onClick={() => dispatch({ type: "DECRE" })>-</button>
                <button onClick={() => dispatch({ type: "INCRE" })>+</button>   
            </>
        )
    }, [dispatch])
}

つまり、Counterのレンダリング結果(return結果)をメモ化することで、dispatchの変更以外の要因でCounterが再レンダーされたとしてもreturn内容は再レンダーされないようになる。

Contextのデメリットと利用事例

デメリットは以下。

  • コンポーネントがContextに依存するため、再利用性が低下する
  • グローバルなstateなので、値がどこで更新されたり利用されたりしているのかが分かりづらくなる

上記デメリットを許容してでもContextを使いたい事例

  • 認証情報:ログインしているかどうかで要素の出し分けをするために利用
  • テーマ:選択しているテーマに応じて、コンポーネントに適用するスタイルを制御
  • 言語:選択している言語に応じて、表示する言語を切り替えるために利用する

カスタムフック

カスタムフックの基本

自作のフックのことをカスタムフックと呼ぶ。

カスタムフックのメリットは、

  • 独自のロジックをカスタムフックとして切り出すことで再利用ができる

→複数のフックを組み合わせた内容を再利用できるため便利。

  • 複雑なロジックをカスタムフックとして切り出すことで可読性が上がる

事例1

import React, { useState } from 'react';

// stateとstate更新ロジックを持つカスタムフックを定義
function useCounter(initialCount) {
    const [count, setCount] = useState(initialCount)
    const increment = () => {
        setCount(count + 1)
    }
    const decrement = () => {
        setCount(count -1)
    }
    return { count, increment, decrement }
}

function App() {
    const {count, increment, decrement} = useCounter(0);
    return (
        <div>
            <p>count: {count}</p>
            <button onClick={decrement}>-</button>
            <button onClick={increment}>+</button>
        </div>
    )
}

export default App;

事例2

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

カスタムフックの名前は必ずuseから始める必要がある。useを使うことで、linterプラグインが動作してバグ検知してくれる。

公開されているカスタムフックもあるので、便利なものがあるかもしれない。 GitHub - streamich/react-use: React Hooks — 👍 Collection of React Hooks useHooks - Easy to understand React Hook recipes

参考

基礎から学ぶ React/React Hooks

基礎から学ぶ React/React Hooks

Amazon

React.memoとReact HooksのuseCallbackとuseMemo

React.memo

React.memoの基本

処理した結果をキャッシュとして保持することを「メモ化」と呼ぶ。React.memoはそのメモ化をするための機能。

メモ化することでコンポーネントの再レンダーを回避し、パフォーマンスを上げることができる。レンダリングに時間がかかるコンポーネントや、親が頻繁に再レンダーされる子コンポーネントなどがある場合に効果的。

逆に上記のようなコンポーネント以外では使っても意味がない。

使い方

以下のように使うことで、コンポーネントの再レンダーを避けることができる。

const Hello = React.memo(({ name }) => {
    return <h1>Hello {name}>/h1>;
});

再レンダーの判断は、Propsが等価かどうかでチェックされる。上記の場合は、nameが変更されない限り、再レンダーされることはない。

以下の例ではChildコンポーネントに無駄なループをセットしているため、React.memoの効果を理解できる。

import React, { useState } from "react";

const Child = React.memo(({ count }) => {
  let i = 0;
  while (i < 300000000) i++; //無駄なループを設定しておく
  console.log('render Child')
  return <p>Child: {count}</p>
})

export default function App() {
  console.log('render App')

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  )
}

React.memoを定義せずに実行した場合は、count1が更新されたとしてもChildコンポーネントも再レンダーされ、count2には関係なくても毎回時間がかかることになってしまう。

上記の例ではChildコンポーネントをReact.memoで定義しているため、Childコンポーネントのprops(count2)が更新された時だけ再レンダーされる。count1を更新しても再レンダーされず、素早くレンダリングが完了させることができる。

React.memoではカバーできない部分

React.memoにコールバック関数を渡す場合は、React.memoを使っても再レンダーされてしまう。

// import 省略
// React.memoに関数を渡している
const Child = React.memo(({ handleClick }) => {
    console.log('render Child');
    return <button onClick={handleClick}>Child</button>
});

export default function App() {
    console.log('render App');
    const [count, setCount] = useState(0);
    const handleClick = () => {
        console.log('click');
    };

    return (
        <>
            <p>Counter: {count}</p>
            <button onClick=() => setCount(count + 1)}>Increment count</button>
            <Child handleClick={handoleClick} />
    );
}

これは、関数がAppコンポーネントの再レンダリングの度に再生成されるため。オブジェクトとしては前回と異なるものと判断され、Childコンポーネントも再レンダーされるという結果になる。

この問題を解消するためには、useCallbackを使って関数をメモ化する必要がある。

useCallback

useCallbackの基本

useCallbackは関数を受け取り、コンポーネントの再レンダーを防ぐ目的で利用される。React.memoでメモ化したコンポーネントに、useCallbackでメモ化したコールバック関数をPropsとして渡すことで、不要な再レンダーを回避できるようになる。

使い方

書き方は、const 変数 = useCallback(関数, 依存配列) となる。

一つ前の例を書き換えると以下のようになり、関数がメモ化され再レンダーされなくなる。

// import省略
const Child = React.memo(({ handleClick }) => {
    console.log('render Child');
    return <button onClick={handleClick}>Child</button>
});

export default function App() {
    console.log('render App');
    const [count, setCount] = useState(0);
    const handleClick = useCallback(() => {
        console.log('click');
    }, []);

    return (
        <>
            <p>Counter: {count}</p>
            <button onClick=() => setCount(count + 1)}>Increment count</button>
            <Child handleClick={handoleClick} />
    );
}

ただし、上記の場合は依存配列を空で設定しているためuseCallbackで生成した関数は初回しかレンダリングされない。

コールバック関数が他の状態に依存する場合は、依存配列に状態変数(下の例だとcount)を追加する必要がある。

const Button = React.memo((props) => {
  return <button onClick={props.onClick}>Click Me</button>
});

const App = () => {
  const [count, setCount] = useState(1);
  const onClick = useCallback(() => {
    alert(count);
    setCount(count+1);
  }, [count]);

  return <Button onClick={onClick}/>
};

また、React.memoしていないコンポーネントにuseCallbackでメモ化した関数を渡しても再レンダーのスキップはできないので注意。

useMemo

useMemoの基本

useMemoはメモ化された値を返すフック。

書き方は以下。

useMemo(() => 計算ロジック, 依存配列);

依存配列は計算ロジックが依存している値を格納することで、値が変わると再計算される。 また、計算ロジックだけではなくレンダー結果もメモ化できるため、React.memoと同じようにコンポーネントの再レンダーをスキップできる。

例は以下。

// import省略
export default function App() {
    console.log('render App')
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);

    const double = (count) => {
        let i = 0;
        while (i < 100000000) i++; // 無駄なループをセットしてわざと時間をかけている
        return count * 2;
    };

    const Counter = useMemo(() => {
        console.log('render Counter');
        const doubleCount = double(count2);
        return (
            <p>
                Counter: {count2}, {doubledCount}
            </p>
        );
    }, [count2]);
    // Counterはcount2にしか依存しないため、count1が更新され、Appコンポーネントが再レンダーされてもCounterは再レンダーされない。

    return (
        <>
            <h2>Increment count1</h2>
            <p>Counter: {count1}</p>
            <button onClick={() => setCount1(count1 + 1)}>count1 up</button>
            <h2>Increment count2</h2>
            {Counter}
            <button onClick={() => setCount2(count2 + 1)}>count2 up</button>
        </>
    );
}

React.memo / useCallback / useMemoの違い

  • React.memo

const コンポーネント = React.memo((props) => { return <p> props.hoge </p> })

メモ化する対象はコンポーネント

コンポーネントを定義するときに使い、そのコンポーネントの不要なレンダーを回避する目的で使われる。

再レンダーはpropsが更新された時だけ実行される。

  • useCallback

const 変数 = useCallback(() => {hoge}, [依存変数など])

メモ化する対象は関数。

React.memoのpropsが関数の場合に、そのコンポーネントの不要なレンダーを回避する目的で使われる。

関数を定義しているコンポーネントが再レンダーされると、関数は等価ではなくなるので、その関数をpropsとして受け取っている別コンポーネントはReact.memoを使っていても再レンダーされてしまう。

React.memoしているコンポーネントにuseCallbackでメモ化した関数を渡すことで、コンポーネントの不要なレンダーを回避できる。

ただ、React.memoとか関係なくなるべく使え、って話もある。

useCallbackはとにかく使え! 特にカスタムフックでは - uhyo/blog

  • useMemo

const 変数 = useMemo(() => {hoge}, [依存変数など])

メモ化する対象は計算結果の値(数値やレンダー結果)。

計算結果やレンダー結果をメモ化し、所属するコンポーネントが再レンダーされても、メモ化した変数内容が再計算されるのを回避する目的で使われる。他と違って必ずしも再レンダーの回避だけが目的ではない。

再計算のコストとuseMemoの実行コストを比較検討して、使うかどうかを判断する。

  • まとめ

useCallbackはuseCallback自身がpropsとして設定されたコンポーネントの再レンダーを回避する目的で使うのに対し、useMemoはuseMemo自身に設定した関数の再実行を回避する目的で使う。

参考

基礎から学ぶ React/React Hooks

基礎から学ぶ React/React Hooks

Amazon

React HooksのuseEffectとuseRef

useEffect

useEffectの基本

  • useEffectとは何か?

コンポーネントのレンダー後か、アンマウント後に処理が稼働するフック。

「レンダー後」に稼働するため、DOMを操作できる。また、アンマウントとはコンポーネントをDOMから削除して破棄することを指す。

【React】マウントとレンダリングとその違い | Milestones ReactのuseEffectとレンダリング|NOBORI Tech|note

  • 何ができるのか?

DOMの変更、APIとの通信、非同期処理などに使われる。

最新版のReact17ではいくつか破壊的変更があったらしい。

React17におけるuseEffectの破壊的変更を理解する

  • どんな使い方をするか?

以下の書き方。

useEffect(処理, 依存配列);

依存配列に格納された変数などが変更されたタイミングで処理が実行される。

下記の場合はmessageが更新されるたびに処理が実行される。

useEffect(() => {
    console.log(message);
}, [message]}

依存配列を省略すると、レンダリングの度に処理が実行される。

useEffect(() => {
    console.log(document.getElementById('texthook').innerText);
}}

なお上記は<p id='texthook'>hogehoge</p>の場合、コンソールにhogehogeと表示される。

空の配列を指定すると、最初の1回だけ処理が実行される。

useEffect(() => {
    console.log(message);
}, []}

処理内で関数を返すと、初回は何も実行されず、依存配列によって再度処理実行タイミングが来た時、もしくはアンマウントされた時に処理が実行される。 これは「クリーンアップ関数」と呼ばれる。

useEffect(() => {
    return () => {
        console.log('cleanup');
    }
}

何かのステータスを変えたり、変数定義をクリアにしたいなどに使われる。

ただし、注意点としてクリーンアップ関数は、アンマウントというタイミング上、useEffectの再レンダリングの前にも実行されてしまう。

useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  }, [props.friend.id]);

依存配列の書き忘れ防止などのためにLintツールもある(eslint-plugin-react-hooks パッケージの exhaustive-deps ルール)。

useEffectはDOM更新の後に実行したいことがある場合に利用する。たとえば、APIの実行、ログ記録など。

クラスで書く場合は、componentDidMountcomponentDidUpdateの2つのタイミングで処理を書く必要がある。もし同じ処理を実行させたい場合は2回書く必要がある。

一方で、useEffectの場合はコンポーネントのマウント直後か更新後かは区別しない。レンダリング後に実行する、という仕組みがあるだけなので、1回useEffectを書くだけで同じ挙動を実現できる。

また、クリーンアップについても、クラスの場合はcomponentWillUnmountを使って定義する必要があったが、フックの場合はreturnに処理を書くだけで実現できる。

  • DOM操作の例

レンダリング後でないとDOM上に要素が存在しない(取得しようとするとnullになる)ため、useEffectを使わないとDOM要素を取得できない。

const Message = () => {
  // ここだとレンダリング前なのでタイミングによっては
  // DOMで要素を取得できない可能性がある
  const elem = document.querySelector('#message')
  console.log(elem); // null

  // useEffectを使うとレンダリング後に実行されるので
  // 確実に取得できる
  useEffect(() => {
    const elem = document.querySelector('message');
    console.log(elem); // HTMLDivElement
    elem.innerText = 'Hello!'
  );

  return <div id='mesage'></iv>
};
  • クリーンアップ関数の例 const Timer = () => { const [time, setTime] = useState(0);

    // クリーンアップ関数を登録(return)する
    useEffect(() => {
      const timerId = setInterval(() => setTime(new Date().getTime()), 1000);
      return () => clearInterval(timerId);
    });
    
    return <div>{time}</div>
    

    };

APIからデータを取得する例

import React, { useState, useEffect } from "react";
import axios from 'axios';

export default function App() {
  const [items, setItems] = useState([]);
  const [inputValue, setInputValue] = useState("react");
  const [query, setQuery] = useState(inputValue);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);

      const result = await axios(
        `https://hn.algolia.com/api/v1/search?query=${query}`
      )

      setItems(result.data.hits);
      setIsLoading(false);
    }

    fetchData();
  }, [query]);

  return (
    <>
      <form onSubmit={(event) => {
        event.preventDefault();
        setQuery(inputValue);
      }}>
        <input type='text' value={inputValue} onChange={(event) => setInputValue(event.target.value)} />
        <button type='submit'>検索</button>
      </form>
      {isLoading ? (
        <p>Loading</p>
      ) : (
        <ul>
          {items.map((item) => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </>
  )
}

useEffectの依存配列としてqueryを指定しているため、検索ワードが変わらないと処理は実行されない。つまり、同じ検索ワードのまま連打しても処理は1回だけ実行されることになる。

event.preventDefault();については以下を参照。

【JavaScript】event.preventDefault()が何をするのか - Qiita

useEffectを同期的に実行したい場合は

useEffectは非同期で処理されるため、ブラウザの動きをブロックしない。もし、同期させて処理させたい場合(レイアウトによって分岐させるなど)は、useLayoutEffect という別のフックが使える。機能はuseEffectと同じで、実行タイミングが同期的になり、レンダリング→useLayoutEffect→ブラウザ表示 の順に実行される。 useEffectはレンダリングの後にブラウザ表示・useEffectが同時実行されるイメージ。


useRef

useRefの基本

  • useRefとは何か

useStateと同じく値を保存できるが、値を更新してもレンダリングが発生しない違いがある。

  • 何ができるのか

DOMを参照する、もしくは前回の値を参照する時などに利用される。

  • どんな使い方をするのか

useRef(初期値)

currentプロパティで現在の値を参照できる。

const count = useRef(0);
console.log(count.current); // 0
count.current = count.current + 1
console.log(count.current); // 1

DOM参照の例

useRefで参照を作り、それをJSXに渡しておくことで、useEffectで参照できる。

const Message = () => {
  const divRef = useRef(null);

  useEffect(() => {
    // ref.currentで現在の参照の値を取得できる
    // ここではdiv要素のDOM
    divRef.current.innerText = ‘Hello!’;
  }, []);

  // refに渡しておく
  return <div ref={divRef}></div>
};

前回の値を参照する例

import React, { useState, useEffect, useRef } from "react";

export default function App() {
  const [count, setCount] = useState(10);
  const prevCountRef = useRef(0);

  useEffect(() => {
    prevCountRef.current = count;
  });

  return (
    <>
      <p>
        現在のcount: {count}, 前回のcount: {prevCountRef.current}
      </p>
      <p>前回のcountより{prevCountRef.current > count ? "小さい" : "大きい"}</p>
      <button onClick={() => setCount(Math.floor(Math.random() * 11))}>update</button>
    </>
  )
}

上記のようにすることで、useRefで定義されたオブジェクトが更新されても、再レンダーは実行されず、useEffect内の処理も稼働しない。

currentの値が更新された時のみ再レンダーが稼働し、useEffect内の処理も稼働する。

もしuseRefを使わずに let prevCountRef = 0;のようにして実装しようとすると、再レンダー時に毎回0に初期化されてしまう。

もしuseRefを使わずにuseStateを使ってしまうと、countが更新され、再レンダー→useEffect稼働となり、prevCountRefがcurrentと同じ値になった後に、画面が描画される。つまり、常に同じ値が表示されてしまい、意図とは異なる挙動になってしまう。

参考

基礎から学ぶ React/React Hooks

基礎から学ぶ React/React Hooks

Amazon

React Hooksの基本とuseState

Hooksとは

HooksはReactの16.8.0バージョンから追加された新しい機能。

Hooksを使うことで、ReactのComponentを関数で定義することできる。従来はクラスによる定義のみであったが、関数でも定義できるようになったことで、

  • 同等の機能を実装する場合、クラスで実装するよりもコード量が少なくなる
  • ロジックを分離できるため再利用やテストがしやすい

といったメリットを得られる。

仕組み

フックに関するよくある質問 – React

  • Reactはどのコンポーネントが現在レンダー中なのかを把握しており、メモリとして保持している。
  • また、Reactはフックが呼ばれる順番をメモリとして保持している。そのため、フックとコンポーネントを紐付けて管理できている。

注意点

  • フックはトップレベルでのみ定義すべき。上記の通り、フックが呼ばれる順番をReactは覚えるため、ループやif文の中で定義すると条件によっては順番が変わってしまい、エラーやおかしな挙動の原因になる。もし必要ならが、useEffectの中でループやif文を実行する、という回避策をとる。
  • Reactの関数コンポーネントの中でのみ定義すべき。

上記を2点をチェックできるlinterツールがあるので使う。create react appにはデフォルトで含まれている。インストールする場合は npm install eslint-plugin-react-hooks —-save-devを実行する。 eslint-plugin-react-hooks - npm

ESLint confファイルの事例

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

useState

useStateの基本

書き方は、const [変数名, 更新関数名] = useState(初期値)

const [hogeNum, setHogeNum] = useState(0);
const [gehoStr, setGehoStr] = useState("hoge");

setHogeNum(10) // hogeNumに 10 に更新される
setGehoStr('geho') // gehoStrが 'geho' に更新される

更新関数に値を渡す → stateが渡された値に更新される

更新関数に関数を渡す → その関数の引数にstateの現在の値が入り、returnで渡された値が新たなstateになる。以下事例。currentCountに現在のcountの中身が格納される。

import React, { useState } from "react";

export default function App() {
  const [count, setCount] = useState(10);
  const decrement = () => {
    setCount((currentCount) => currentCount - 1);
  }
  const increment = () => {
    setCount((currentCount) => currentCount + 1);
  }

  return (
    <>
      <p>Count: {count}</p>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </>
  )
}

setCount((currentCount) => currentCount - 1); ではなく、setCount(count - 1);でも同等の結果になる。しかし、関数を渡した方が、stateを更新するロジックが外部に依存しない形になる。

こうすることで、更新するロジックが長くなって読みにくくなった場合でも、関数ごと外出しすることで読みやすさを保つことができる。

オブジェクトやリストもstateとして扱える

数値や文字列だけではなく、オブジェクト型やリスト型も利用できる。

  • オブジェクト型の例

更新メソッドを利用するときには、スプレッド演算子を使って更新する必要がある(後述の「いつ再レンダーされるか」を参照)。

import React, { useState } from "react";

export default function App() {
  const [count, setCount] = useState({ men: 0, women: 0 })

  const comingMen = () => {
    setCount((currentCount) => ({ ...currentCount, men: currentCount.men + 1 }));
  }
  const comingWomen = () => {
    setCount((currentCount) => ({ ...currentCount, women: currentCount.women + 1 }))
  }

  return (
    <>
      <p>Men: {count.men}</p>
      <p>Women: {count.women}</p>
      <button onClick={comingMen}>Men</button>
      <button onClick={comingWomen}>Women</button>
    </>
  )
}
  • リスト型の例

追加はオブジェクト型と同じくスプレッド構文を使って更新できるが、削除は色々方法がある。ここではfilterを利用している。

import React, { useState } from "react";

export default function App() {
  const [years, setYears] = useState([2019, 2020, 2021])

  const addYear = () => {
    setYears((current) => ([...current, current[current.length - 1] + 1]));
  }

  const deleteYear = () => {
    setYears(years.filter((_, i) => i != years.length - 1))
  }

  return (
    <>
      <p>Years: {years.map((year) => year + ',')}</p>
      <button onClick={addYear}>add year</button><button onClick={deleteYear}>del year</button>
    </>
  )
}

再レンダーされるタイミング

オブジェクト型のstateを更新するときに、以下のような書き方をしてしまうと再レンダーされない(上記の例の一部を抜粋)。

const comingMen = () => {
  count.men = count.men + 1;
  setCount(count);
}

公式ページ(フック API リファレンス – React)にあるように、ReactではObject.jsによる比較アルゴリズムを利用している。 たとえば

const hoge = { num: 1 };
const geho = { num: 1 };
console.log(Object.is(hoge, hoge)); // trueになるため同一とみなされる
console.log(Object.is(hoge, geho)): // 値は同じでもfalseになる

つまり、オブジェクトが同じであればtrueになり、別であればfalseになる。

count.men = count.men + 1としただけでは、countオブジェクトは同一であり何も変わっていないとみなされるため、再レンダーされない。 newCount = { ...count, men: count.men + 1 }のようにスプレッド演算子を利用して新しいオブジェクトを生成すれば別のオブジェクトとしてみなされ、再レンダーが実行される。

リスト型の場合でも、list.push()list.splice()を使うとオブジェクトが変化していないと判断されてしまうため注意。

参考

基礎から学ぶ React/React Hooks

基礎から学ぶ React/React Hooks

Amazon

文章を書くときのチェックリスト

書く前にやること

  • 記事を書く目的を明確にする
  • 読み手を意識する
  • 構成を考え、要素を箇条書きにする(結論、理由、具体例、結論など定型を考える)
  • 「である調」「ですます調」のどちらにするか決める →「である」は強い印象を与えるが端的に書ける。「ですます」は丁寧な印象を与えるが冗長になりがち。
  • 見出しを考える(具体的な内容にする)

書くときに意識すること

見た目/読みやすさ

  • リズムが良い、声を出して呼吸する場所で読点(、)を打っているか
  • 5行以内で改行ができているか
  • 適度な余白があるか
  • 漢字とひらがなのバランスが良いか
  • 同じ言葉の重複がないか(省略するか、言い換える)
  • 話し言葉が混ざっていないか

理解しやすさ

  • 1文の長さは60文字以内か
  • ワンセンテンス、ワンメッセージになっているか
  • 主語と述語が遠くなっていないか
  • 修飾する語と修飾される語を近くに置いているか(修飾語が多く読みにくいときは文章を分ける)
  • 見出しは具体的か、本文と合っているか
  • 単純接続の「が」を使っていないか →「XXについてですが、どう思いますか?」という文章は単純接続の「が」の使い方。分かりにくくなるため、文章を分けるか、表現を変える。「が」は逆説の時だけ使うようにする。

その他

  • 比喩、例え話を積極的に使っているか
  • 体験談が入っているか(説得力を高めたい内容の場合)

書き終わった後にやること

  • 上記チェックポイントを再度チェックする
  • 削れる言葉を削る →接続詞、主語、副詞(とても、すごく)、指示語など。ただし、削るか残すか迷ったときは残すようにする。
  • 時間をおいて読み直す
  • 声に出して読み直す
  • 誰かに読んでもらう

参考