[React] ユーザーの入力に対する自動保存機能の実装方法
概要
今回は実務でユーザーの入力に対する自動保存機能の実装を行なったため、どのように実装を行なったか記事として残しておこうと思います。
実際に実装した自動保存機能の内容については、googleドキュメントやnotionなどと同じように、ユーザーのテキスト入力の手が止まってから数秒間隔が空いたタイミングで保存してくれるようなものになります。
自分で実装するにあたって自動保存の記事をいくつか拝見しましたが、ライブラリを使用したものやカスタムフックを使用した少し複雑な実装のものが多くありました。ですが、思いのほか簡単に実装できる方法があったため、参考にしていただけると幸いです。
やりがちな実装
import { ChangeEvent, useCallback, useState } from 'react';
const AutoSave = () => {
const [text, setText] = useState('');
const handleTextChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setText(e.target.value);
setTimeout(() => {
console.log('何かしらデータを保存する処理');
}, 1000);
}, []);
return (
<div>
<h1>テキスト入力フォーム</h1>
<textarea onChange={ handleTextChange }>
{ text }
</textarea>
</div>
)
}
これもある意味自動保存と言っていいと思います。ユーザーの入力に対して、1秒の間隔を空けた上でログを出力してくれるものになります。
しかし、このような実装だと、ユーザーの入力毎(onChangeが発火する毎)にログが吐かれてしまいます。実際の実務だと、APIを絡めた処理が実行されることが想定されます。このような実装だと、余分にAPIを叩くことになるので、良くない実装だといえますね。
改善
import { ChangeEvent, useCallback, useState, useRef } from 'react';
const AutoSave = () => {
const [text, setText] = useState('');
const timer = useRef<NodeJS.Timeout | null>(null);
const handleTextChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setText(e.target.value);
if (timer.current) { clearTimeout(timer.current); }
timer.current = setTimeout(() => {
console.log('何かしらデータを保存する処理');
}, 1000);
}, []);
return (
<div>
<h1>テキスト入力フォーム</h1>
<textarea onChange={ handleTextChange }>
{ text }
</textarea>
</div>
)
}
このようにuseRefでsetTimeout自体を管理してあげることで、余分なログの出力を防ぐことができます。実装方法としてもシンプルで、useRefで管理している値が存在する時、clearTimeoutで発火されるはずだったsetTimeoutを解除し、新たなタイマーをスタートさせます。
setTimeoutが固有の数値を返すため、その値を取っ掛かりにタイマーの管理を行いました。思っていた以上に簡単に実装ができたため、嬉しい気持ちでいっぱいですね。
(「ここで一回止めた」→ログ、「改行」→ログ、「ここでも一回止めたよ」→ログ、の3回です)
余談
このような実装を行なっている記事があまり見られなかったため、少し邪道なやり方なのかな?と思っていましたが、MDN Web DocsでもsetTimeoutから返されるtimeoutIDと呼ばれる整数値から、clearTimeoutで解除するものを識別していると記載がありました。
そのため、やり方としてはそこまで悪い方法じゃないじゃん、と思っています。
Discussion