Reactのメモ化は最終手段にしたほうが良いという話



# memo化について
Reactでは親コンポーネントが再レンダリングされると、子のコンポーネントも再レンダリングされてしまいます。

例えば、以下のような例だと親コンポーネントでcount upボタンを押すたびに、
子の`ExpensiveComponent`も再レンダリングされてしまいます。

`ExpensiveComponent`が非常に重い処理が含まれていると、count upボタンを押すたびに重い処理が走ってしまい、パフォーマンス悪化につながってしまいます。


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

export function App() {
  [count,setCount]= useState(0);
  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <p>{count}</p>
      <button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>

      <ExpensiveComponent/>
    </div>
  );
}

function ExpensiveComponent() {
  console.log("rendering");
  //なんらかの重い処理
  for (let i = 0; i <= 100000;i++){
    i= i+1;
  }
  return <p>expensive</p>
}
```
![not_memo_rendering.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255598/aa1382e6-dde2-bb29-877c-fe66afc38660.gif)



この挙動を防ぐためにmemo化を行います。

以下のように子コンポーネントをmemo化することで、親コンポーネントが再レンダリングされたときでも、子コンポーネントが再レンダリングされることはなくなります。

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

export function App() {
  [count,setCount]= useState(0);
  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <p>{count}</p>
      <button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>

      <ExpensiveComponentMemo/>
    </div>
  );
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);

function ExpensiveComponent() {
  console.log("rendering");
  //なんらかの重い処理
  for (let i = 0; i <= 100000;i++){
    i= i+1;
  }
  return <p>expensive</p>
}
```

![momo_render.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255598/6f408f3e-195f-2694-3a6b-67a911d6c152.gif)




# メモ化の問題点

memo化を用いた場合、将来的に開発を行なっていく中で、実装上のちょっとした変更で想定通り動かなくなってしまうという懸念点が挙げられます。

## propsを渡すパターン

例えば、次のようにExpensiveComponentにstyleをpropsで渡すようにしたらどうでしょうか？

一見ExpensiveComponentに渡されるものは常に同一なので、想定通り動きそうに思えますが、親コンポーネントのレンダリングに合わせて子コンポーネントもレンダリングされてしまい、memo化が機能しないコードになってしまいました。


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

export function App() {
  [count,setCount]= useState(0);
  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <p>{count}</p>
      <button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>

      <ExpensiveComponentMemo style={{"color":"red"}}/>
    </div>
  );
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);

function ExpensiveComponent({style}) {
  console.log("rendering");
  //なんらかの重い処理
  for (let i = 0; i <= 100000;i++){
    i= i+1;
  }
  return <p style={style}>expensive</p>
}
```

![props_rendering.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255598/93b0f42c-af85-9820-935f-6a4c0f5a3d56.gif)

これはReactのPropsの差分は`Object.is`で同一であれば差分なしでレンダリングしない、同一でなければ差分ありと判断されレンダリングされるためです。(https://ja.react.dev/reference/react/memo#minimizing-props-changes)

`Object.is`では両方の値がメモリー内の同じオブジェクトを参照している場合のみ`true`になるので
下記のような場合`false`になってしまいます。(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/is)

```javascript
// falseになってしまう！！
Object.is({"color":"red"},{"color":"red"})
```

この挙動によって`object`の中身は実際には変わっていないにもかかわらず、Reactの内部的には変わってしまったものと判断され再レンダリングが走ってしまいます。

これを解決するためには`useMemo`を使います。
これにより`memoStyle`は`Object.is`での比較でも同一のものと判断され
子コンポーネントが再レンダリングされることはなくなります。


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

export function App() {
  [count,setCount]= useState(0);
  const memoStyle = React.useMemo(()=>({"color":"red"}),[])
  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <p>{count}</p>
      <button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>

      <ExpensiveComponentMemo style={memoStyle}/>
    </div>
  );
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);

function ExpensiveComponent({style}) {
  console.log("rendering");
  //なんらかの重い処理
  for (let i = 0; i <= 100000;i++){
    i= i+1;
  }
  return <p style={style}>expensive</p>
}
```

![rendering_props_memo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255598/813910bb-219f-5b96-bd14-56d2e9e2724d.gif)



しかし、渡すpropsが増えた際に全ての開発者がこの挙動を予期してちゃんと`useMemo`を使用できるかというと怪しい部分があり、いざパフォーマンスの低下が検知されて初めて実装ミスに気づくといったこともあると思います。
このような罠に気づかずハマるおそれがあるというのが、memo化をなるべく避ける理由の一つです。

## childrenを渡しているパターン

`children`を渡しているパターンも同様に考えてみます。

この場合も、一見`ExpensiveComponent`の`children`は同一なので想定通り動きそうに思えますが、親コンポーネントのレンダリングに合わせて子コンポーネントもレンダリングされてしまい、memo化が機能しないコードになってしまいました。

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

export function App() {
  [count,setCount]= useState(0);
  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <p>{count}</p>
      <button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>

      <ExpensiveComponentMemo>
        <p>children</p>
      </ExpensiveComponentMemo>
    </div>
  );
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);

function ExpensiveComponent({children}) {
  console.log("rendering");
  //なんらかの重い処理
  for (let i = 0; i <= 1000;i++){
    i= i+1;
  }
  return <div>expensive{children}</div>
}
```

![render_children.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255598/a6163edf-67be-fed1-91cb-6cd85c275650.gif)



jsx記法は`React.createElement`の糖衣構文であり、実際は新しいobjectが作成されてしまっているので、先ほどの場合と同様にこの事象が起こってしまっています。

こちらの場合も`useMemo`を用いることでレンダリングを防ぐことは可能ですが、
可読性や、変更時の柔軟性を考えるとバグを生みやすいコードになってしまいます。

# memoに変わる代替案
ここまで、記述した通りmemo化は思わぬところに落とし穴が多く、複数人で開発し引き継ぎを行っていくようなプロジェクトだと気づかずにバグを混入させてしまう可能性が高く、あまり使用しないことを推奨したいです。
しかし、パフォーマンス上の懸念からどうしても再レンダリングを避けたい時の解決策を紹介します。

## stateを用いているコンポーネントを分離させる

`count`の`state`を使っているのは一部だけなのでその部分を別のコンポーネントに切り出すというのが一つです。

これによって、`count`によって変化を受けるコンポーネントは
`CountComponent`だけになるので`count`の変化によって`ExpensiveComponent`が再レンダリングを避けることができます。

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

export function App() {
  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <CountComponent/>
      

      <ExpensiveComponent/>
    </div>
  );
}

function CountComponent(){
 [count,setCount]= useState(0);
 return <p>{count}</p>
}


function ExpensiveComponent(props) {
  console.log("rendering");
  //なんらかの重い処理
  for (let i = 0; i <= 100000;i++){
    i= i+1;
  }
  return (
  <>
  <p>expensive</p>
  <button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
  </>
  )
}
```

## childrenを用いてコンポーネントを分離させる

次のようなDOM構造を維持したい場合、単純に`state`を用いているコンポーネントを分離させることが難しいように思えます。

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

export function App() {
  [count,setCount]= useState(0);
  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <div className='count'>{count}</div>
      <div>
          <button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
          <ExpensiveComponent/>
      </div>
    </div>
  );
}

function ExpensiveComponent() {
  console.log("rendering");
  //なんらかの重い処理
  for (let i = 0; i <= 100000;i++){
    i= i+1;
  }
  return <p>expensive</p>
}
```

この場合でも、下記のように`children`として重い処理をしている子コンポーネントを渡し`CountComponent`で表示することにより、`count`の変化による`ExpensiveComponent`の再レンダリングを避けることができます。

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

export function App() {
  return (
    <div className='App'>
      <h1>Hello React.</h1>
      <CountComponent>
          <ExpensiveComponent/>
      </CountComponent>
    </div>
  );
}

dunction CountComponent({children}){
    [count,setCount]= useState(0);
    return (
       <>
          <div className='count'>{count}</div>
          <div>
              <button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
              {children}
          </div>
        </>
      );
}

function ExpensiveComponent() {
  console.log("rendering");
  //なんらかの重い処理
  for (let i = 0; i <= 100000;i++){
    i= i+1;
  }
  return <p>expensive</p>
}
```

# まとめ

コンポーネントのメモ化は、実装した際にはパフォーマンスを向上できるものの、その後の保守運用フェーズで気付かぬうちにバグを生み出してしまう可能性も高いです。
今回の代替案でご紹介したものなどmemo化しないで実装できないかをまず考えることが大事だと思いました。



# 参考文献
https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2

https://tkdodo.eu/blog/the-uphill-battle-of-memoization