◆ 単純にデータを入れるだけのコンテキストが多いから自動作成するように
◆ API 扱うコンポーネントは API 呼び出しとその状態表示は受信データから内容表示と別コンポーネントに分けたほうが良さそう
◆ コンポーネントが API 呼び出しかつデータ受信時に子コンポーネントを再作成してると子コンポーネントが API を 2 回呼び出すことになってた
◆ setState のタイミングと useEffect の依存配列関係
◆ フックの呼び出しを分岐したい

いくつかの記事に分けてた React 関係の記事の最後です
他に比べて分けるほどでもない内容をこの記事にまとめてます

個別に分けた記事:
React で親から子コンポーネントを扱う方法
React で form と input の状態管理を楽にしたい
SPA にしない場合に create-react-app の開発サーバでダミーデータを使う
React で slot みたいなことしたい

Context を楽に作る

React の Context ってただのデータの入れ物としか使わないことが多くて ひとつひとつ定義するのが面倒になってきます

const FooContext = React.createContext()
export const FooProvider = FooContext.Provider
export const useFoo = () => useContext(FooContext)

みたいなのをコンテキストごとにコピペするのは手間なんです

ということでこんなのを作ってみました

plain-context.js
const contexts = {}

export const getProvider = id => {
if (!contexts[id]) {
contexts[id] = React.createContext()
}
return contexts[id].Provider
}

export const getHook = id => {
if (!contexts[id]) throw new Error(`context "${id}" is not created`)
return () => useContext(contexts[id])
}

取得時になければ自動で作るのでコピペで定義していく必要がなくなります

outside-module.js
import contexts from "./plain-context.js"
const FooProvider = contexts.getProvider("foo")

export default () => {
return (
<FooProvider value={...}>
<div>...</div>
</FooProvider>
)
}

inside-module.js
import contexts from "./plain-context.js"
const useFoo = contexts.getHook("foo")

export default () => {
const foo = useFoo()
return (
<div>{foo}</div>
)
}

これで簡単にいっぱい作れますね
そんなに頻繁に使うべきものじゃない気がしますけど……

API 呼び出しのロード表示

最近の React に追加された機能でロード中の表示みたいなのを聞いたのですが code splitting した場合のモジュールのロード関連の話みたいです
API 関係は自分で管理する必要があって 結構面倒なところなんですよね
API の呼び出しと結果の受け取りやロード画面を表示する部分や結果を使った画面表示を 1 つのページコンポーネントにまとめるのが問題だと思うので分けてみます

とりあえずこんな感じ でしょうか

const createApiComponent = (Component, request) => ({ query }) => {
const [api_data, setApiData] = useState()
const [error, setError] = useState()

useEffect(() => {
request(query).then(
data => setApiData(data),
err => setError(err)
)
}, [])

return (
api_data
? <Component {...api_data} />
: error
? <Error {...error} />
: <ApiLoader />
)
}

const UserInfoView = (props) => {
// API から受け取ったユーザ情報の表示
return ...
}

const UserInfo = createApiComponent(
UserInfoView
(id) =>
fetch("/api/user/" + id)
.then(x => x.json())
.catch(err => { throw new Error("ユーザの取得に失敗しました") })
)

const App = () => {
return <UserInfo query={1} />
}

UserInfoView がユーザ情報を表示する部分で UserInfo は API 呼び出しや結果を管理する部分です
UserInfo を動的に作らなくても

const ApiComponent = ({ component: Component, request, query }) => {
const [api_data, setApiData] = useState()
const [error, setError] = useState()

useEffect(() => {
request(query).then(
data => setApiData(data),
err => setError(err)
)
}, [])

return (
api_data
? <Component {...api_data} />
: error
? <Error {...error} />
: <ApiLoader />
)
}

const UserInfoView = () => {
return ...
}

const request = (id) => fetch("/api/user/" + id)
.then(x => x.json())
.catch(err => { throw new Error("ユーザの取得に失敗しました") })

const App = () => {
return <ApiComponent component={<UserInfoView/>} request={request} query={1} />
}

でもよいかもしれません
UserInfo みたいなコンポーネントを作っても それをあちこちで使い回すわけでもないですし

再レンダリングと API 呼び出し

直接の親子でない離れた外側と内側のコンポーネントでそれぞれ API を呼び出しています
どちらもフックで取得する値の変化で再取得するようにしています

const foo = useFoo()

useEffect(() => {
resetComponent()
}, [foo])

なぜか毎回 2 回 API 呼び出しが行われると思って調べたら この作りが原因でした
変化の検知でそれぞれ再度 useEffect の処理が実行され API を呼び出しています
新しいデータを取得すると 内側の state をすべてクリアして初期化するために子コンポーネントを完全に入れ替えます
外側コンポーネントでこれが行われると内側コンポーネントは作り直されます
変化を検出して API の呼び出しを行ったのにすぐ破棄されて 新たな同じコンポーネントが API 呼び出しを行います
結果 内側コンポーネントが使う API は 2 回呼び出されます

外側コンポーネントが子コンポーネントを作り直すなら内側コンポーネントで API の再呼び出しがいらない気がしますが 完全に条件が一致してるわけでもないんですよね
内側コンポーネントの API 呼び出しが必要だけど 外側コンポーネントでは API 呼び出しが不要なケースがあったりです

コンポーネント内で API を呼び出すから発生する問題なので ページやアプリケーション単位で管理するほうがいいのかなと思ったり
これは React に限らず WebComponents でも発生する問題です
WebComponents で実装するならどうするか考えてみると コンポーネント内で API 呼び出しはまずやらないです
React だと useEffect とかちょうどよい機能があるので使ってますが 外部から受け取るほうが良いかもですね

フック関係

context を使うフックのデータに応じて setState したいときがあります

const Component = () => {
const [state, setState] = useState(0)
const foo = useFoo()

if (!foo) {
setState(0)
}

return (...)
}

これって正しい使い方なんでしたっけ……
以前どこかで読んだ気はするもののドキュメントなど探しても見当たりませんでした
コンポーネントの処理中に呼び出して問題があるならとりあえず useEffect にすればあとから実行できます

const Component = () => {
const [state, setState] = useState(0)
const foo = useFoo()

useEffect(() => {
if (!foo) {
setState(0)
}
})

return (...)
}

foo に変更があるたびに setState したい場合は依存配列に foo を入れるだけです

const Component = () => {
const [state, setState] = useState(0)
const foo = useFoo()

useEffect(() => {
setState(0)
}, [foo])

return (...)
}

この方法だと変化があったら毎回というだけで前回の値は取得できません

非同期処理でリセットする場合は少し複雑です
foo は props で受け取るものにしてみます

const Component = ({ foo }) => {
const [state, setState] = useState()

useEffect(() => {
if (state) {
setState()
}
getValueHeavy(foo).then(value => {
setState(value)
})
}, [foo])

return state ? <SubComponent foo={foo} value={state} /> : <Loader />
}

useEffect の中で state を使ってますが state を依存配列に入れてません
state を参照するのは直後のみで setState がどこかで行われても その後に参照することはないので問題はないはずです
とは言っても気になる部分です
単純に state を依存配列に入れると useEffect 内の setState のたびに useEffect が呼び出されることになって無限ループになります
foo だけが変わったことを検出して state の変更だった場合は何もしない処理が必要です

useMemo などで変更検出できますが 変更されたという情報を useEffect で使うならそれも依存配列に入って必要以上に複雑な感じがします

const Component = ({ foo }) => {
const [state, setState] = useState()
const sym1 = Symbol()
const sym2 = useMemo(() => sym1, [foo])
const foo_changed = sym1 === sym2

useEffect(() => {
if (!foo_changed) return
if (state) {
setState()
}
getValueHeavy(foo).then(value => {
setState(value)
})
}, [foo, foo_changed, state])
}

一応ドキュメントだと useEffect 内で使う値はすべて依存配列に書くべきみたいのがありますが 非同期的に実行される部分以外はなくていい気がしました

あと よく考えるとこのケースだとそもそも if 文いらなかったです
if 文がないと最初に必ず setState されますが setState は前回と同じ値なら何も起きません
今回みたいな undefined が初期値なら同じになるので気にする必要がないです
初期値がオブジェクトだと 別の値になって再レンダリングされます
それでも foo しか依存配列にないので useEffect は実行されず getValueHeavy がもう一度呼び出されることにはなりません

このあたりは React ならではの考え方なので 未だに苦労することがあります

フックの使用の有無を分岐したい

フックは Reactの制限上 if 文の中に書いて呼び出す出さないを分岐できません

const Component = ({ need_foo }) => {
const foo = need_foo ? useFoo() : null

useEffect(() => {
if (!need_foo) return
something(foo)
}, [foo])
}
<Component need_foo={true}/>
<Component need_foo={false}/>

こういうことはできないです

コンポーネントを分ける必要があります
使う側がこれと同じような使い方にしたいならこうなります

const ComponentNeedFoo = () => {
const foo = useFoo()

useEffect(() => {
something(foo)
}, [foo])

// ...
}

const ComponentUnneedFoo = () => {
// ...
}

const Component = ({ need_foo, ...props }) => {
return need_foo
? <ComponentNeedFoo {...props} />
: <ComponentUnneedFoo {...props} />
}

ですが実際には props によって使い分けたいことはそうありません
useFoo の値を使わない場合も useFoo を呼び出して結果を無視すれば良いです

ただ 動的にコンポーネントを作る場合はそうではありません
例えば こういう関数があるとします

const createComponent = (fn) => (props) => {
useEffect(() => {
fn()
}, [])

return ...
}

関数を受け取り 関数コンポーネントを返します
受け取った関数を useEffect の中で実行します
ここで渡す関数では 物によっては特定のフックで得られるデータがほしいです

const Component1 = createComponent(() => {
// ここで useFoo() のデータを使いたい
})

ここに直接 useFoo と書いても この関数が呼び出されるのは useEffect の中なのでフックは使えません
createComponent を

const createComponent = (fn) => (props) => {
const foo = useFoo()
useEffect(() => {
fn(foo)
}, [foo])

return ...
}

とすればできますが useFoo の値は createComponent で作るコンポーネント全てで必要ではありません
コンポーネントによってはコンテキストの範囲外なので useFoo を呼びだすと例外が起きる可能性もあります
また useFoo ではなく useBar を使いたいコンポーネントもあるかもしれません
使う可能性のあるフックを全部実行するのもどうかと思います

これで困ったのですが コンポーネントを作る段階でフックを渡せばできそうです

const createComponent = (fn, useHooks) => (props) => {
const value = useHooks?.()
useEffect(() => {
fn(value)
}, [value])
}

const Component1 = createComponent(
foo => {
console.log(foo)
},
useFoo,
)

const Component2 = createComponent(
({ foo, bar }) => {
console.log(foo, bar)
},
() => {
const foo = useFoo()
const bar = useBar()
return { foo, bar }
}
)

const Component3 = createComponent(
() => {
console.log("no hook")
}
)

props ではなくコンポーネントを作る段階で受け取ったフックを使います
コンポーネント内で呼び出すフックは常に同じものになります
複数のフックの結果がほしいなら Component2 のように useHooks に渡すフック内で複数のフックを使ってそれらの結果をマージして返せば良いです
フックを使わないコンポーネントなら Component3 のように渡さなければ良いです
「useHooks?.()」 で呼び出しているので if 文みたいになってますが 同じコンポーネント内で呼び出されるかどうかは変わらないので問題なしです

単純にフックだけなら createComponent を使わず個別に作って共通部分の関数化でも良さそうです

const common = () => {
...
}

const Component1 = () => {
const foo = useFoo()
useEffect(() => {
console.log(foo)
}, [foo])
return common()
}

const Component2 = () => {
return common()
}

common の部分で useState とかフックを使った処理をするなら common を useCommon とフックとして扱う関数名にします
フックの結果をそのまま返すコンポーネントは少し変な感じがありますが特に問題はないはずです