■Reactの再レンダリングの仕組みと問題点
import { useState } from 'react';
import './App.css';
import { ChildArea } from './ChildArea';
function App() {
console.log('Appがレンダリングされました');
const [text, setText] = useState('');
const [opened, setOpened] = useState(false);
const onChangeText = (e) => setText(e.target.value);
const onClickOpen = () => setOpened(!opened);
return (
<>
<div className="App">
<input type="text" placeholder="Type something..." value={text} onChange={onChangeText} />
<br />
<button onClick={onClickOpen}>表示</button>
<ChildArea opened={opened} />
</div>
</>
);
}
export default App;
const style = {
width: '100%',
height: '100px',
backgroundColor: 'lightblue',
};
export const ChildArea = (props) => {
console.log('ChildAreaがレンダリングされました');
const data = [...Array(2000).keys()];
data.forEach(() => {
console.log('重い処理');
});
return (
props.opened && (
<div style={style}>
<p>子コンポーネント</p>
</div>
)
);
};
Reactの再レンダリングの条件は、以下になります。
- stateが更新されると、stateを使っているコンポーネントは再レンダリングされる
- propsが変更されると、コンポーネントは再レンダリングされる
- 再レンダリングされたコンポーネント配下の子コンポーネントは再レンダリングされる
そのため、上のサンプルコードで<input type="text"フォームに値を入力すると、onChangeText関数が実行されて、textというステートが更新されます。
そうすると、Appコンポーネントは再レンダリングされるので、ChildAreaコンポーネントも再レンダリングされます。
もし、ChildAreaコンポーネントに重い処理があると、ChildAreaコンポーネントに関係のないtextというステートが更新されるたびに重い処理が実行されて、レンダリングコストがかかります。
このような無駄なレンダリングコストをなくすために使うのが、memoとuseCallbackです。
■memoを使ったコンポーネントのメモ化
import { useState } from 'react';
import './App.css';
import { ChildArea } from './ChildArea';
function App() {
console.log('Appがレンダリングされました');
const [text, setText] = useState('');
const [opened, setOpened] = useState(false);
const onChangeText = (e) => setText(e.target.value);
const onClickOpen = () => setOpened(!opened);
return (
<>
<div className="App">
<input type="text" placeholder="Type something..." value={text} onChange={onChangeText} />
<br />
<button onClick={onClickOpen}>表示</button>
<ChildArea opened={opened} />
</div>
</>
);
}
export default App;
import { memo } from 'react';
const style = {
width: '100%',
height: '100px',
backgroundColor: 'lightblue',
};
export const ChildArea = (props) => memo({
console.log('ChildAreaがレンダリングされました');
const data = [...Array(2000).keys()];
data.forEach(() => {
console.log('重い処理');
});
return (
props.opened && (
<div style={style}>
<p>子コンポーネント</p>
</div>
)
);
});
先程のコードとの違いはmemo()関数でChildAreaコンポーネントのロジック部分を囲っているところです。
このようにすると、引数で受け取っているpropsの値が変わらない限り、ChildAreaコンポーネントが再レンダリングされなくなります。
しかし、memoはObject.isを用いた浅い比較で新旧の比較を行います。
そのため、propsに渡ってくる値がプリミティブ値(string, number, boolean など)の場合は、
値が同じなら「等しい」と判定され、再レンダリングされません。
オブジェクト・配列・関数など、非プリミティブ値の場合は、毎回新しい参照として生成されるため「異なる」と判定され、再レンダリングされます。
■useCallbackを使った関数のキャッシュ
import { useState } from 'react';
import './App.css';
import { ChildArea } from './ChildArea';
function App() {
console.log('Appがレンダリングされました');
const [text, setText] = useState('');
const [opened, setOpened] = useState(false);
const onChangeText = (e) => setText(e.target.value);
const onClickOpen = () => setOpened(!opened);
const onClickClose = () => setOpened(false);
return (
<>
<div className="App">
<input type="text" placeholder="Type something..." value={text} onChange={onChangeText} />
<br />
<button onClick={onClickOpen}>表示</button>
<ChildArea opened={opened} onClickClose={onClickClose} />
</div>
</>
);
}
export default App;
import { memo } from 'react';
const style = {
width: '100%',
height: '100px',
backgroundColor: 'lightblue',
};
export const ChildArea = memo((props) => {
console.log('ChildAreaがレンダリングされました');
const data = [...Array(2000).keys()];
data.forEach(() => {
console.log('重い処理');
});
return (
props.opened && (
<div style={style}>
<p>子コンポーネント</p>
<button onClick={props.onClickClose}>閉じる</button>
</div>
)
);
});
AppコンポーネントでonClickClose関数を定義しました。
この関数をChildAreaコンポーネントにpropsで渡しています。
この状態で、<input type="text"に値を入力するとChildAreaコンポーネントをmemo化していても、ChildAreaコンポーネントが再レンダリングされます。
理由は、memo化はObject.isを用いた浅い比較をしているためです。
[Object.is](http://object.is/)(prevProps.onClickClose, nextProps.onClickClose) が false になり、
ChildArea は毎回再レンダリングされます。
このような場合に、useCallbackを使って関数をキャッシュします。
import { useState, useCallback, useMemo } from 'react';
import './App.css';
import { ChildArea } from './ChildArea';
function App() {
console.log('Appがレンダリングされました');
const [text, setText] = useState('');
const [opened, setOpened] = useState(false);
const onChangeText = (e) => setText(e.target.value);
const onClickOpen = () => setOpened(!opened);
const onClickClose = useCallback(() => setOpened(false), []);
return (
<>
<div className="App">
<input type="text" placeholder="Type something..." value={text} onChange={onChangeText} />
<br />
<button onClick={onClickOpen}>表示</button>
<ChildArea opened={opened} onClickClose={onClickClose} />
</div>
</>
);
}
export default App;
import { memo } from 'react';
const style = {
width: '100%',
height: '100px',
backgroundColor: 'lightblue',
};
export const ChildArea = memo((props) => {
console.log('ChildAreaがレンダリングされました');
const data = [...Array(2000).keys()];
data.forEach(() => {
console.log('重い処理');
});
return (
props.opened && (
<div style={style}>
<p>子コンポーネント</p>
<button onClick={props.onClickClose}>閉じる</button>
</div>
)
);
});
useCallback(() => setOpened(false), []);と関数をuseCallbackで囲うと、初回レンダリング時に() => setOpened(false)関数が作成されて、その後は関数がメモ化されるの、再生成を防ぐ事ができます。
関数が再生成されないので、[Object.is](http://object.is/)(prevProps.onClickClose, nextProps.onClickClose)の
結果がtrueになって、再レンダリングされないという仕組みになります。
第2引数の入れるにstateを含めて、特定のstateが更新されたときだけ関数を再生成して再レンダリングさせるということもできます。
■useMemoを使ったオブジェクトや配列のメモ化
import { useState, useCallback } from 'react';
import './App.css';
import { ChildArea } from './ChildArea';
function App() {
console.log('Appがレンダリングされました');
const [text, setText] = useState('');
const [opened, setOpened] = useState(false);
const onChangeText = (e) => setText(e.target.value);
const onClickOpen = () => setOpened(!opened);
const onClickClose = useCallback(() => setOpened(false), []);
// { width: '100%', height: '100px', backgroundColor: 'lightblue' }が代入されている
const style = {
width: '100%',
height: '100px',
backgroundColor: 'lightblue',
};
return (
<>
<div className="App">
<input type="text" placeholder="Type something..." value={text} onChange={onChangeText} />
<br />
<button onClick={onClickOpen}>表示</button>
<ChildArea opened={opened} onClickClose={onClickClose} style={style} />
</div>
</>
);
}
export default App;
import { memo } from 'react';
export const ChildArea = memo((props) => {
console.log('ChildAreaがレンダリングされました');
const data = [...Array(2000).keys()];
data.forEach(() => {
console.log('重い処理');
});
return (
props.opened && (
<div style={props.style}>
<p>子コンポーネント</p>
<button onClick={props.onClickClose}>閉じる</button>
</div>
)
);
});
次にstyleオブジェクトをChildAreaコンポーネントにpropsで渡してみます。
この場合、<input type="text"に値を入力すると、ChildAreaコンポーネントも再レンダリングされます。
関数と同じようにObject.isで比較するとfalseと判断されるためです。
このような場合、オブジェクトや配列をメモ化するためにuseMemoを使います。
import { useState, useCallback, useMemo } from 'react';
import './App.css';
import { ChildArea } from './ChildArea';
function App() {
console.log('Appがレンダリングされました');
const [text, setText] = useState('');
const [opened, setOpened] = useState(false);
const onChangeText = (e) => setText(e.target.value);
const onClickOpen = () => setOpened(!opened);
const onClickClose = useCallback(() => setOpened(false), []);
const style = useMemo(
() => ({
width: '100%',
height: '100px',
backgroundColor: 'lightblue',
}),
[]
);
return (
<>
<div className="App">
<input type="text" placeholder="Type something..." value={text} onChange={onChangeText} />
<br />
<button onClick={onClickOpen}>表示</button>
<ChildArea opened={opened} onClickClose={onClickClose} style={style} />
</div>
</>
);
}
export default App;
import { memo } from 'react';
export const ChildArea = memo((props) => {
console.log('ChildAreaがレンダリングされました');
const data = [...Array(2000).keys()];
data.forEach(() => {
console.log('重い処理');
});
return (
props.opened && (
<div style={props.style}>
<p>子コンポーネント</p>
<button onClick={props.onClickClose}>閉じる</button>
</div>
)
);
});
useMemo関数にオブジェクトを渡して実行することで、初回レンダリング時に作成されたオブジェクトが再レンダリング時に再定義されずに、無駄なレンダリングを防ぐことがで切るようになります。