背景
reactでこんなことを実現したいとします。
まあよくある、ボタンを押すと数字がインクリメントされるやつです。
日本ではvueが盛り上がっていますが、reactも盛り上がって欲しいところ...!
あまり日本語の文献がないので書いていきます。
react16.8より追加されたHookを使って作っていきましょう。
typescriptで書いていきます。
だめな例1
// Sample.tsx
import React, { useState } from "react";
const Sample = () => {
const [count, setCount] = useState(0);
return (
<div>
<button
onClick={() => {
setCount(count + 1);
}}
>
click here!
</button>
<p>count: {count}</p>
</div>
);
};
export default Sample;
これをすると、tslintから以下のような警告が出てしまいます。
Lambdas are forbidden in JSX attributes due to their rendering performance impact (jsx-no-lambda)
レンダリングのときのパフォーマンスがよろしくないから、jsx(拡張子は.tsxですが)のなかでlambdaを書かないでね、と言っています。
このように書くと、レンダリングのたびにこの関数が新しく宣言されてしまうので、無駄が発生してしまいます。
だめな例2
すこし書き換えてみましょう。
import React, { useState } from "react";
const Sample = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<button onClick={handleClick}>click here!</button>
<p>count: {count}</p>
</div>
);
};
export default Sample;
onClick
の処理の部分を、handleClick
として切り出してみました。
これでtslintからは怒られなくなります。
ですが、これでもやはりhandleClick
がレンダリングのたびに宣言されてしまいます。
この記事や、このstackoverflowの回答に書いてあります。
いいけど面倒な例→だめでした
なるほど、では更に書き換えてみましょう。
import React, { useState } from "react";
const handleClick = (
count: number,
setCount: React.Dispatch<React.SetStateAction<number>>
) => () => {
setCount(count + 1);
};
const Sample = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={handleClick(count, setCount)}>click here!</button>
<p>count: {count}</p>
</div>
);
};
export default Sample;
こんどはhandleClick
の関数宣言を、Sampleの外に出してみました。
これでレンダリングのたびに宣言される問題は解決します。 →解決しないらしいです...
ですがなんでしょうか、このoverkill感。
typescriptというのも相まって、単にインクリメントさせたいだけなのに記述量が半端ない...泣
ちなみに筆者はずっとこれで書いていました、上記でも紹介したこの記事に出会うまでは。
useCallbackを一緒に使う→まだだめ
外に出すと宣言が面倒です、できれば関数の中に書いてスコープの恩恵を受けつつ、レンダリング時に再定義しないようにしたい。。。
そんな事できないと思ってました。
全然できました。
useCallbackというhookを使いましょう。
import React, { useState, useCallback } from "react";
const Sample = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<button onClick={handleClick}>click here!</button>
<p>count: {count}</p>
</div>
);
};
export default Sample;
ただ、このままだとcountが更新されるたびに定義されてしまいます
useCallbackを一緒に使う→OKバージョン
import React, { useState, useCallback } from "react";
const Sample = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<div>
<button onClick={handleClick}>click here!</button>
<p>count: {count}</p>
</div>
);
};
export default Sample;
最高ですね、めちゃくちゃスッキリ。
もっと早く知っていたかった...独学のつらみです。
ちなみに、 useCallback
と似たようなものに useMemo
があります。
これらの違いに関してはこの記事がわかりやすいです。
だれかの参考になれば幸いです。