1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[React]無駄なレンダリングを何とかしたい🤔

Last updated at Posted at 2022-03-20

環境

言語
TypeScript v4.4.2

React
React v17.0.2

僕のアプリ、レンダリング多くね???

例えば下記のようなカウンターアプリがあったとする。

Screen Shot 2022-03-20 at 10.59.00.png

// App.tsx

import React, { useState } from 'react';
import './App.css';

type IncrementProps = {
  onIncrement: () => void
}

// 加算ボタン
const Increment: React.FC<IncrementProps> = ({onIncrement}) => {
  console.log('Increment');
  
  return (
    <button onClick={onIncrement}>Increment</button>
  )
}

type DecrementProps = {
  onDecrement: () => void
}

// 減算ボタン
const Decrement: React.FC<DecrementProps> = ({onDecrement}) => {
  console.log('Decrement');
  
  return (
    <button onClick={onDecrement}>Decrement</button>
  )
}

type CounterProps = {
  count: number
  onIncrement: () => void
  onDecrement: () => void
}

// カウンター
const Counter: React.FC<CounterProps> = ({count, onIncrement, onDecrement}) => {
  console.log('Counter');

  return (
    <div className='counter-container'>
      <h2>{count}</h2>
      <div className='counter-actions'>
        <Increment onIncrement={onIncrement} />
        <Decrement onDecrement={onDecrement} />
      </div>
    </div>
  )
}

// 一番親のコンポーネント
const App: React.FC = () => {
  console.log('App');
  const [count, setCount] = useState<number>(0)
  
  const onIncrement = () => {
    setCount(count + 1)
  }

  const onDecrement = () => {
    setCount(count - 1)
  }

  return (
    <div className="app-container">
      <h1>My Counter</h1>
      <Counter count={count} onIncrement={onIncrement} onDecrement={onDecrement} />
    </div>
  );
}

export default App;

この状態でIncrementやDecrementをすると下記のように全てのコンポーネントがレンダリングされる。
AppとCounterがレンダリングされるのはまだ分かるが、IncrementボタンとDecrementボタンが同時にレンダリングされるのは由々しき事態である。
counter app 修正前.gif

React.memo、useCallback、useReducerを使おう!

結論から言うと、React.memoでPropsの状態を管理、useCallbackで関数の状態を管理、useReducerで加減算を管理すると無駄なレンダリングを回避できます。

何故Incrementボタンを押すとDecrementボタンもレンダリングされるかと言うと、useStateで管理しているcountの値が更新されるのが原因です。
IncrementボタンやDecrementボタンを押すと、setCountcountの値を更新しているので、新しいonIncrement関数とonDecrement関数が生成されているのです。

// 一番親のコンポーネント
const App: React.FC = () => {
  console.log('App');
  const [count, setCount] = useState<number>(0)
  
  const onIncrement = () => {
    // ここが良くない
    setCount(count + 1)
  }

  const onDecrement = () => {
    // ここが良くない
    setCount(count - 1)
  }

  return (
    <div className="app-container">
      <h1>My Counter</h1>
      <Counter count={count} onIncrement={onIncrement} onDecrement={onDecrement} />
    </div>
  );
}

useReducerを導入

useReducerを使うと、加減算をコンポーネントの外で行うことが出来るので、今回の問題を解決できます。
コードは以下のようになります。

// reducer.ts

// *** actions type *******************************************************

export const INCREMENT = "INCREMENT" as const;

export const DECREMENT = "DECREMENT" as const;

type IncrementActionType = {
  type: typeof INCREMENT;
  payload: {
    addCount: number;
  };
};

type DecrementActionType = {
  type: typeof DECREMENT;
  payload: {
    subtractionCount: number;
  };
};

export type CounterActionType = IncrementActionType | DecrementActionType;

// *** actions ************************************************************

export const IncrementAction = (addCount: number): CounterActionType => ({
  type: INCREMENT,
  payload: {
    addCount,
  },
});

export const DecrementAction = (subtractionCount: number): CounterActionType => ({
  type: DECREMENT,
  payload: {
    subtractionCount,
  },
});

// *** reducer ************************************************************

type State = {
  count: number;
};

export const initialState: State = {
  count: 0,
};

export const counterReducer = (
  state = initialState,
  action: CounterActionType
): State => {
  switch (action.type) {
    case INCREMENT: {
      const { addCount } = action.payload;
      return { ...state, count: state.count + addCount };
    }

    case DECREMENT: {
      const { subtractionCount } = action.payload;
      return { ...state, count: state.count - subtractionCount };
    }
  }
};

// App.tsx 変更分
import React, { useReducer } from 'react';
import './App.css';

import {counterReducer, initialState, INCREMENT, DECREMENT} from './reducer'

/**
 * 省略
 */

// 一番親のコンポーネント
const App: React.FC = () => {
  console.log('App');
  const [state, dispatch] = useReducer(counterReducer, initialState)
  
  const onIncrement = () => {
    dispatch({ type: INCREMENT, payload: { addCount: 1 } })
  }
  
  const onDecrement = () => {
    dispatch({ type: DECREMENT, payload: { subtractionCount: 1 } })
  }

  return (
    <div className="app-container">
      <h1>My Counter</h1>
      <Counter count={state.count} onIncrement={onIncrement} onDecrement={onDecrement} />
    </div>
  );
}

これでonIncementとonDecrementの関数は常に同じになったので無駄なレンダリングが発生しないはず!!!
...と思ったら大間違いです。
確かに関数は常に同じになりましたが、ボタンを押す度にuseReducerのstateが更新されるので、Appコンポーネントがレンダリングされ、その都度onIncrement関数とonDecrement関数は新たに生成されてしまい、無駄なレンダリングが発生してしまうのです。
なんてこった...😇

React.memoとuseCallbackを導入

そこで登場するのがReact.memoとuseCallbackです!
React.memoはコンポーネントのpropsの状態を管理してくれていて、状態に変化がなければレンダリングをスキップしてくれます。
useCallbackは関数の状態を管理してくれていて、関数に変化がなければ再生成せず、キャッシュしてる状態を渡してくれます。

要するに、何の変化が無ければレンダリングをスキップしてくれる優れもの!(但し、キャッシュを返すか否かをその都度再計算しているので、無闇やたらに使えば良いという訳では無い。場合によっては再計算がネックになる。)

コードは以下のようになります。

// App.tsx

/**
 * 省略
 */

// 加算ボタン
const Increment: React.FC<IncrementProps> = React.memo(({onIncrement}) => {
  console.log('Increment');
  
  return (
    <button onClick={onIncrement}>Increment</button>
  )
})

/**
 * 省略
 */

// 減算ボタン
const Decrement: React.FC<DecrementProps> = React.memo(({onDecrement}) => {
  console.log('Decrement');
  
  return (
    <button onClick={onDecrement}>Decrement</button>
  )
})

/**
 * 省略
 */

// 一番親のコンポーネント
const App: React.FC = () => {
  console.log('App');
  const [state, dispatch] = useReducer(counterReducer, initialState)
  
  const onIncrement = useCallback(() => {
    dispatch({ type: INCREMENT, payload: { addCount: 1 } })
  },[])
  
  const onDecrement = useCallback(() => {
    dispatch({ type: DECREMENT, payload: { subtractionCount: 1 } })
  },[])

  return (
    <div className="app-container">
      <h1>My Counter</h1>
      <Counter count={state.count} onIncrement={onIncrement} onDecrement={onDecrement} />
    </div>
  );
}

counter app 修正後.gif

これで無駄なレンダリングを防ぐことが出来ました!🙌
しかし待ってください...よく考えたらAppコンポーネントがレンダリングされるのはおかしいのでは???

何でもかんでも親から渡さない

僕は再利用性を高めようと、何でもかんでも親から渡すことをよくやりがちなのですが、不要なstate upは止めた方が良いです。
今回のカウンター機能はAppコンポーネントではなく、Counterコンポーネントで行うのが適切です。

コードは以下のようになります。(完成形なので、App.tsxを全てのせます)

import React, { useReducer, useCallback } from 'react';
import './App.css';

import {counterReducer, initialState, INCREMENT, DECREMENT} from './reducer'

type IncrementProps = {
  onIncrement: () => void
}

// 加算ボタン
const Increment: React.FC<IncrementProps> = React.memo(({onIncrement}) => {
  console.log('Increment');
  
  return (
    <button onClick={onIncrement}>Increment</button>
  )
})

type DecrementProps = {
  onDecrement: () => void
}

// 減算ボタン
const Decrement: React.FC<DecrementProps> = React.memo(({onDecrement}) => {
  console.log('Decrement');
  
  return (
    <button onClick={onDecrement}>Decrement</button>
  )
})

// カウンター
const Counter: React.FC = () => {
  console.log('Counter');
  const [state, dispatch] = useReducer(counterReducer, initialState)
  
  const onIncrement = useCallback(() => {
    dispatch({ type: INCREMENT, payload: { addCount: 1 } })
  },[])
  
  const onDecrement = useCallback(() => {
    dispatch({ type: DECREMENT, payload: { subtractionCount: 1 } })
  },[])

  return (
    <div className='counter-container'>
      <h2>{state.count}</h2>
      <div className='counter-actions'>
        <Increment onIncrement={onIncrement} />
        <Decrement onDecrement={onDecrement} />
      </div>
    </div>
  )
}

// 一番親のコンポーネント
const App: React.FC = () => {
  console.log('App');

  return (
    <div className="app-container">
      <h1>My Counter</h1>
      <Counter />
    </div>
  );
}

export default App;

counter app 完成.gif

これでAppコンポーネントのレンダリングがされなくなり、Counterコンポーネントだけがレンダリングされるようになりました!
パフォーマンスの最適化完了です!😎

あとがき

パフォーマンスを意識してコードが書けるようになると、React使ってる感が出てきて気分上がりますよね😎
やはりReactは楽しい...!!!

今回のアプリをローカルで動かしたい方は僕のGitHubからクローンしてください。
(いつまであるかは分かりませんが...)

他にもパフォーマンスを最適化するのにuseMemoというReact Hooksがありますが、これもまた今度書こうと思います🙆‍♂️

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?