useReducer
を使う
コメント消すツールに置換機能を追加しようと思ったときに、絡み合ったuseState
よりも1つのuseReducer
を使った方がいいということに思い至ったので、使い所の参考として使用例を記録しておきます。
コメント消すツールはこちら:
これまでのuseState
置換機能の追加以前は3つのuseStateを使っていました:
function App() {
// 左側の入力用のエリアの値を保持
const [srcValue, setSrc] = useState("#some code here");
// 右側の出力用のエリアの値を保持
const [resultValue, setResult] = useState("result here");
// マッチに使う正規表現の配列(RegExp[])を保持
const [regexes, setRegexes] = useState(initialRegexes);
// ...
}
設定を保持する意図で使っていて、setSrc
はonChange
で、setRegexes
は設定画面の保存ボタンを押したときに動作するようにしていて、setResult
はsetSrc
の直後に使っていました。このように:
function App() {
// ...
return (
<textarea id="source" onChange={e => {
setSrc(e.target.value);
let resultstr = e.target.value;
for (const re of regexes) {
resultstr = resultstr.replaceAll(re, '');
}
// 加工した結果をセット
setResult(resultstr);
}} value={srcValue}></textarea>
);
}
この段階ではマッチ部分は空文字に置換するようにしていました。そして置換後にsetResult
を使用していました。
このように使っていると、入力用のテキストエリアに文字列を打ち込むことでしかsetResult
を走らせることができません。
この仕様は置換機能を追加するときに動作の不自然さを感じるような気がするので修正する必要があります。(正直にいうと、現状でも正規表現を編集してもすぐに反映されないという問題はあります。)
ということで設定変更の反映に一旦入力が必要になる問題を修正します。
単純にstateでは不自然
まずはうまくいっていない例から。(お急ぎの場合はスキップしてください。目次からの移動が便利です。)
最初は、置換に使う文字列用のstate
を保持して、replaceAll
でその文字列を使用するようにしていました。このように:
function App(){
// ...
// 置換文字列用のstateを追加
const [substitutionString, setSubstitutionString] = useState(storedSubstitutionString);
// ...
}
そして置換文字列はinput[type=text]
な要素から入力し、onChange
で随時state
で保持することにしていました:
function App() {
// ...
return (
<input type="text" id="substitution-str" placeholder="置換する場合はここに置換先テキストを入力…"
onChange={e => {
setSubstitutionString(e.target.value);
}}
value={substitutionString}
/>
);
}
当然このままでは置換文字列を入力しただけでは出力の文字列は更新されないので、setSubstitutionString()
のあとにreplaceAll
してsetResult()
を実行したくなります。しかしこのままでは置換文字列が即時には反映されません。これは、state
へのset
は非同期的に実行されるため、setSubstitutionString()
してもreplaceAll
するときの値は、直前のsetSubstitutionString()
がまだ反映されておらず、前回の値のままであることが原因です。そのため1つ前の状態の置換文字列が出力に反映されてしまいます。
一応stateでも何とかなる
useState
のままでもなんとかなる方法も書いておきたいです。お急ぎなら読み飛ばして下さい。
useState
ではobject
も保持できるので、このように、先程の4つのstate
を1つのオブジェクトとして保持すれば一応は何とかなります:
function App() {
const [state, setState] = useState({
src: 'initialSrcText',
result: 'initialResultHere',
regexes: initialRegexes,
substitutionString: 'initialSubstitutionString'
});
// ...
}
しかし、更新時の処理がコンポーネントのレンダーの中で何度も現れてしまうのは好ましくありません。
それぞれのonChange
やonClick
で似たような微妙に異なる出力の生成を何度も行うのは、記述が長くなり直観的に分かりづらいと思います:
function App() {
// ...
return (
<textarea id="source" onChange={e => {
// updateResultは別で必要になる。
const result = updateResult(e.target.value, state.regexes, state.substitutionString);
setState({...state, src: e.target.value, result: result});
}} value={state.src}></textarea>
);
}
このsrc
の部分や、updateResult()
に渡す引数が微妙に入れ替わるだけなのに、レンダー部分に頻出してくるのは非常に見辛いと思うので今回は使用しませんでした。
今回の場合ではuseState
でオブジェクトを持つのは相応しくないですが、そういうこともできるというのは知っておくと便利そうです。
useReducerを使う
useReducer(reducer, initialState)
は呼び出すと、state
とdispatch
が返ってきて、state
の更新をdispatch()
で行える関数です。
dispatch
にはオブジェクトを渡すことができ、dispatch(obj)
すると渡したオブジェクトはreducer
に渡ります。
reducer(state, obj)
は自分で定義する関数で、先ほどの渡されたオブジェクトをもとに新しいstate
を生成して返すことでstate
の更新を行えます。
reducer
関数に更新処理をまとめられるので、関連性の高いstate
の更新処理の見通しが良くなると思います。
コンポーネントのonChange
などもdispatch
を呼び出すだけなので、レンダー部分が簡潔になります。
こんな感じで使用します:
// ...
const initialState = {
src: "#some code here\nprint('HELLO WORLD')",
result: 'result here',
regexes: initialRegexes,
substitutionString: ''
};
function reducer(state, action) {
// actionに応じた新しいstateを返す
const updateResult = (src, regexes, substitutionString) => {
let resultstr = src;
for (const re of regexes) {
resultstr = resultstr.replaceAll(re, substitutionString);
}
return resultstr;
};
switch (action.type) {
case 'changeSrc':
return {
...state,
src: action.src,
result: updateResult(action.src, state.regexes, state.substitutionString),
};
case 'changeRegexes':
return {
...state,
regexes: action.regexes,
result: updateResult(state.src, action.regexes, state.substitutionString),
};
case 'changeSubstitutionString':
return {
...state,
substitutionString: action.substitutionString,
result: updateResult(state.src, state.regexes, action.substitutionString),
};
// ...
default:
throw new Error('undefined action type was assigned');
}
}
function App() {
const [editor, dispatch] = useReducer(reducer, initialState);
// ...
return (
// ...
<textarea id="source"
onChange={e => dispatch({ type: "changeSrc", src: e.target.value })}
value={editor.src}></textarea>
);
}
reducer
の定義は必要ですが、コンポーネントのレンダーにてonChange
がとてもすっきりするのがわかります。先ほどのと同じくらいの記述量で出力結果の更新を行えます。
dispatch
に種類と必要な値を渡すだけなのでここまで簡潔にできます。
reducer
でのstate
の更新では、更新しない値もreturn
に含めるのを忘れないようにしましょう。...state
を最初に含めて、後続に更新するものを追記して上書きするのが良い方法だそうです。
おわり
state
が絡み合ってきたらオブジェクトを利用するか、useReducer
を使うか、というようなことも考えておきたいと思います。
レンダー部分に書く処理が長くなるようならuseReducer()
を使うように心掛けるようにしたいです。一方で2つセットで扱うくらいのもの(座標など)ならオブジェクトを利用していきたいと思います。
結構公式(下記参照)のと被ってしまうことを書いてしまったけど、使い分けについては考えることができたのでよしとしたいです。
コードのハイライトが一部不正確になっているのを修正できなくて残念です… ...state
とかを入れているせいかな。
以上です。
Amazonアソシエイト
コメント