水無瀬のプログラミング日記

プログラムの勉強した時のメモ

ReactHooksざっくりまとめ

はじめに

Hooksを触り始めている今日この頃。
雰囲気で色々触っていたけど、だんだん辛くなってきたのでちゃんとまとめていこうと思う。
APIリファレンスを見ながら、簡単に使い方とサンプルをまとめておく。

TL;DR.

コード

useState

component内で使うステートフルな値と、それを更新するための関数を返す。
Class Componentのstateと同じ。

使い方

import { useState } from 'react';

// useStateに渡した値がstateの初期値になる
const [state, setState] = useState(initialState);
// stateの値を更新したいときはsetStateを使う
setState(updateState);
// 型指定したい場合、useStateの際に指定できる
const [stringState, setStringState] = useState<string>(initialState);
// 初期化を遅延させることもできる
const [lagyState, setLagyState] = useState(() => {
    // 初期値をなにかしら受け取る
    const initialState = getHoge();
    return initialState;
});

サンプル

import React, { useState } from 'react';

export const StateComponent: React.FC<any> = () => {
  // useStateを使う
  const [greetingMessage, setGreetingMessage] = useState<string>('hello');
  return (
    <>
      {/* stateの内容を表示 */}
      <div>{greetingMessage}</div>
      <form>
        {/* stateの内容を更新 */}
        <input type="text" onChange={text => setGreetingMessage(text.target.value)} />
      </form>
    </>
  )
};

useEffect

副作用を有する可能性のある命令型コードを受け付ける。
ClassComponentで言うところのcomponentDidMountcomponentDidUpdateの置き換え。
って言われているけど実際には近しい処理くらいが正しい気がする。
componentDidMountcomponentDidUpdateとは別物の認識。

使い方

// これだとレンダリングの度に毎回実行される
useEffect(() => {
    // 副作用がある処理を行う
  const subscription = props.source.subscribe();
    // クリーンアップ処理を行いたい場合は、useEffectに渡す関数の戻り値に関数を渡せば良い
  return () => {
    subscription.unsubscribe();
  };
});

// 副作用が依存している値を第2配列に指定する
// props.sourceに変更があったときのみ実行する
useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    subscription.unsubscribe();
    };
}, [props.source]);

// 一度だけ実行したい場合△
useEffect(() => {
    const subscription = props.source.subscribe();
  return () => {
    subscription.unsubscribe();
    };
// 第2引数に空配列を渡してあげれば良い
// 空配列を渡すことで、何にも依存しない(2回目が実行されることない)ことを伝える
// 処理としてはprops.sourceに依存するため、渡して上げるのが良い
}, []);

// 一度だけ実行したい場合◎
useEffect(() => {
  function doSomething() {
    console.log(someProp);
  }
  doSomething();
}, [someProp]);

サンプル

export const EffectComponent: React.FC<any> = () => {
  const [count, setCount] = useState(0);
  // useEffectを使う
  useEffect(() => {
    const interval = setInterval(() => setCount(count + 1), 1000);
    // intervalをリセットする
    return () => clearInterval(interval);
  // このeffectはcountに依存しているので、countを第二引数に渡す
  // →ここを渡さないとeffect無いではstateが初期値から変わらない
  }, [count]);
  return <div>{count}</div>
};

useContext

コンテクストオブジェクトを受け取り、そのコンテクストの現在値を返す。
コンテクストの現在値は、ツリー内でこのフックを呼んだコンポーネントの直近にある <MyContext.Provider>value の値によって決定される。

ざっくり孫コンポーネントなど階層が離れているコンポーネントに値を渡せるようになる認識。
(バケツリレーをしなくても良くなる)

いつ使うのが良いかは公式ドキュメントを読むのが良さそう。

使い方

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);
// 型を指定したければ下記のようにする
type ThemeColor = {
  foreground: string;
  background: string;
}
const ThemeContext = React.createContext<ThemeColor>(themes.light);

export const App = () => (
    // ここでcontextの現在値がdarkになる
  <ThemeContext.Provider value={themes.dark}>
    <Toolbar />
  </ThemeContext.Provider>
);
export const AppInitial = () => (
    // 何も渡さなければ初期値(themes.light)になる
  <Toolbar />
);

const Toolbar = props => (
    <div>
        <ThemedButton />
    </div>
);

const ThemedButton = () => {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

サンプル

import React, {useContext} from 'react';

// Contextを作成
const MemoContext = React.createContext<string>('initial text.');

export const ContextComponent = () => (
    // textの内容を変える
  <MemoContext.Provider value="memo text.">
    <MemoArea />
  </MemoContext.Provider>
);

const MemoArea = () => (
    <div>
        <Text />
    </div>
);

const Text = () => {
  const theme = useContext(MemoContext);
  return (
    <span>{theme}</span>
  );
}

useReducer

useStateの親戚。Reduxのreducerを記述できる。
(state, action) => newStateという型のreducer を受け取り、現在のstatedispatchメソッドとペアにして返す。

使い方

// initialState
const intialState = { count: 0 }
// Reducerを作成(stateとactionを受け取る関数)
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}
// useRedcerは第一引数にreducer、第二引数にinitialStateを渡してあげる
// stateとdispatchメソッドが返ってくる(どちらもReduxの使い方とほぼ同じ)
const [state, dispatch] = useReducer(reducer, initialState);

// 初期化を遅延させたい場合や特定の場合に初期値にさせたい場合、
// 第三引数に初期化関数を渡してあげる
const init(initialCount) => ({count: initialCount});
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
        case 'reset':
            return init(action.payload);
    default:
      throw new Error();
  }
}
const Counter = ({initialCount}) => {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
        </>
    );
}

サンプル

import React, {useReducer} from 'react';

type State = {count: number};
const initialState: State = {count: 0};

const add = (num: number) => ({type: 'ADD', payload: num});
const subtract = (num: number) => ({type: 'SUBTRACT', payload: num});
type Action = ReturnType<typeof add | typeof subtract>;

const reducer = (state: State, action: Action) => {
  switch (action.type) {
    case 'ADD':
      return {count: state.count + action.payload};
    case 'SUBTRACT':
      return {count: state.count - action.payload};
    default:
      throw new Error();
  }
}

export const ReducerComponent = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch(subtract(1))}>-</button>
      <button onClick={() => dispatch(add(1))}>+</button>
    </>
  );
}

useCallback

メモ化されたコールバックを返す。
第一引数にコールバック関数、第二引数にコールバックが依存している値を配列で渡す。

使い方

const memoizedCallback = useCallback(
  // 第一引数に関数を指定
    () => doSomething(a, b),
    // 第一引数に渡した関数が依存している値(a, b)をリストで渡してあげる
  [a, b],
);

サンプル

import React, {useCallback, useState} from 'react';

export const CallbackComponent = () => {
  const [count, setCount] = useState(0);
    // コールバックはcountに依存しているので、第二引数にcountを渡してあげる
  // 渡さないと関数内のcountの値が更新されない 
  const buttonClick = useCallback(() => setCount(count + 1), [count]);
  return <ButtonComponent count={count} buttonClick={buttonClick}/>
}
const ButtonComponent = ({count, buttonClick}: {count: number, buttonClick: () => void}) => (
  <div>
    count: {count}
    <button onClick={buttonClick}>click</button>
  </div>
)

useMemo

メモ化された値を返す。
関数の結果を保持することができるので、同じ引数で関数を呼び出した時には中身の処理は実行せず結果だけ返す。
これによりレンダリングの度に重い処理が走らなくて済むようになる。

使い方

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

サンプル

あまり良い例を思いつかなかった。

import React, { useMemo } from 'react';

export const MemoComponent = () => {
  const count = 10000;
    // 10000回足し算行う
  const memoValue = useMemo(() => [...Array(count)].reduce((pure) => pure+1, 0), [count]);
  return <div>{memoValue}</div>
}

useRef

.currentプロパティがuseRefを呼び出した渡す値で初期化されたミュータブルなオブジェクトを返す。
返されるオブジェクトはコンポーネントが存在する限り、存在し続ける。

多分よく使われるのは、DOMにアクセスする場合にref={hogeRef}みたいな使い方だと思う。
useRefはref属性を扱うだけではなく、あらゆる書き換え可能な値を保持して多くためにも使える。

使い方

const refContainer = useRef(initialValue);

サンプル

import React, { useRef, RefObject } from 'react';

export const RefComponent: React.FC = () => {
  // Dom触るためのrefを作成
  const useRefWithDom: RefObject<HTMLInputElement> = useRef<HTMLInputElement>(null);
  // 値を保持しておくrefを作成
  const useRefWithValue = useRef('initial value');
  return (
      <div>
        <input type="text" ref={useRefWithDom} />
        <button onClick={() => {
          if (!useRefWithDom.current) {
            return;
          }
          // 変更前('initial value')
          console.log('useRefWithValue.current: ', useRefWithValue.current);
          // currentを上書きすることで変更できる
          useRefWithValue.current = useRefWithDom.current.value;
          // 変更後('hoge')
          console.log('useRefWithValue.current: ', useRefWithValue.current);
          
        }}>ボタン</button>
      </div>
  );
};

useImperativeHandle

refが使われた時に親コンポーネントに渡されるインスタンス値をカスタマイズするのに使う。
使う時にはforwardRefと一緒に使う。

使い方

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

サンプル

import React, { forwardRef, useRef, useImperativeHandle, RefObject } from 'react';

const FancyInput = (props: any, ref: any) => {
  // refを定義
  const inputRef: RefObject<HTMLInputElement> = useRef<HTMLInputElement>(null);
  // 受け取ったrefにfocus関数を追加する
  useImperativeHandle(ref, () => ({
    focus: () => inputRef?.current?.focus()
    
  }));
  return <input ref={inputRef} />;
}
// forwardRefに作った関数を渡す(変数に置かなくても問題なし)
const FancyInputRef = forwardRef(FancyInput);

export const ImperativeHandleComponent: React.FC = () => {
  // ここで使うrefを定義
  const ref: RefObject<HTMLInputElement> = useRef<HTMLInputElement>(null);
  return (<>
    {/* refを渡す(このrefにfocus関数が追加される) */}
    <FancyInputRef ref={ref} />
    {/* クリックした時にテキストボックスにフォーカスする */}
    <button onClick={() => ref?.current?.focus()}>clickでフォーカス</button>
  </>)

};

useLayoutEffect

基本的にはuseEffectと同じ。
違う点はDOM の変更があった後で同期的に副作用が呼び出されるところ。
DOMを操作して再描画する場合に使う。
最初はuseEffectを使って、問題があるときのみuseLayoutEffectを使う方が良い。

と、公式サイトに書いてある

useDebugValue

React DevTools でカスタムフックのラベルを表示することができる。

使い方

useDebugValue(value)

サンプル

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

  // DevToolのラベルが下記のように表示される
  // "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

まとめ

Hooksについてざっくりまとめた。
useStateとかuseEffectとか基本的なHooksについてはだいぶわかりやすいと思った。
メモ化ができるuseMemouseCallbackについては常に使えば良いってわけじゃなさそうなので、使っていくうちに使い所を見極めたい。

参考サイト