◆ useReducer を使ったときの dispatch がリスナ設定に向いてない
◆ useReducer をラップしたフックを作る
◆ 「onChange={dispatcher.change("name")}」 形式で書けるように

React で useReducer を使うときの dispatch 関数ってリスナに設定するのにあまり向いてないんですよね
onClick などにアロー関数を作ることになってコードが長くなります

<button onClick={() => dispatch({ type: "clear" })}>Clear</button>

<input
title={form.title}
onChange={(event) =>
dispatch({ type: "change", name: "title", value: event.target.value })
}
/>

ボタンが 1 つ 2 つ程度のページだと気にするほどでもないのですが input が 10 以上は並ぶフォームだと面倒です
コピペにしても時々ミスが出てきます
イベントが起きたらではなくリスナセット時に呼び出してしまって無限ループとかよくあります

以前書いたような 1 つ 1 つに dispatch を書かなくていいようにする方法もありますが 今回はリスナをそれぞれにセットする前提で dispatch を楽に使えるようにしようと思います

普段のやり方だとこういう dispatch ラッパー関数を作っています

const Component = () => {
const [form, dispatch] = useReducer(reducer, { title: "" })
const dispatchChange = name => event => {
dispatch({ type: "change", name, value: evnet.target.value })
}

return (
<input
value={form.title}
onChange={dispatchChange("title")}
/>
)
}

onChange の中がシンプルで見やすくなってます

ただ コンポーネントが増えてきたときに 全部のコンポーネントに同じ dispatchChange を貼り付けるのって面倒です
外部の共通関数にしたくてもコンポーネント内で作られる dispatch 関数を呼び出せないといけません
useReducer をラップしたフックを作るのが一番楽そうだったので useReducer をラップします

上の例の dispatchChange は { type: "change" } 専用でしたが いろいろなタイプに対応させたいです
引数のオブジェクトの中に書いてもいいですが もう少し見やすくしたかったので Proxy を使って動的にメソッドを作ってメソッド名を type にするようにしました

const useCustomReducer = (...a) => {
const [state, dispatch] = useReducer(...a)

const dispatcher = useMemo(
() => new Proxy({}, {
get: (_, name) => (value) => event => {
dispatch({ type: name, value, event })
}
}),
[]
)

return [state, dispatcher]
}

const Component = () => {
const [state, dispatcher] = useCustomReducer(reducer, { title: "" })

return (
<>
<button onClick={dispatcher.clear()}>Clear</button>

<input
value={state.title}
onChange={dispatcher.change("title")}
/>
</>
)
}

dipatcher.clear なら { type: "clear" } という風に対応します
メソッドの引数は value プロパティに入ります

動く例はこんな感じです

<!DOCTYPE html>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useReducer, useMemo } = React

const useCustomReducer = (...a) => {
const [state, dispatch] = useReducer(...a)

const dispatcher = useMemo(
() => new Proxy({}, {
get: (_, name) => (value) => event => {
dispatch({ type: name, value, event })
}
}),
[]
)

return [state, dispatcher]
}

const createInitData = () => {
return {
foo: "foo",
bar: "bar",
baz: "baz",
}
}

const appReducer = (state, action) => {
switch(action.type) {
case "change": {
return {
...state,
[action.value]: action.event.target.value,
}
}
case "clear": {
return createInitData()
}
}
}

const App = () => {
const [form, dispatcher] = useCustomReducer(appReducer, null, createInitData)

return (
<div>
<div style={{ display: "flex", gap: "10px" }}>
<input
value={form.foo}
onChange={dispatcher.change("foo")}
/>
<input
value={form.bar}
onChange={dispatcher.change("bar")}
/>
<input
value={form.baz}
onChange={dispatcher.change("baz")}
/>
<button onClick={dispatcher.clear()}>Clear</button>
</div>
<div>
{form.foo} / {form.bar} / {form.baz}
</div>
</div>
)
}

ReactDOM.render(
<App/>,
document.getElementById("root")
)
</script>
<div id="root"></div>

これならリスナをいっぱい書くのも苦じゃないです