Вообще говоря, стоит избегать запуска побочных эффектов внутри reducer. Именно поэтому reducers обычно не содержат async-функций, и так и должно оставаться.
Когда action меняет состояние reducer, можно сделать это состояние зависимостью чего-то вроде useEffect, а свои (async) побочные эффекты, включая вызовы fetch, выполнять там.
Также полезно явно перечислить возможные состояния компонента:
type model = Pending | Ready(Profile.t)
type event = ReplaceState(model)
let reduceState = (model, event) =>
switch event {
| ReplaceState(model) => model
}
@react.component
let make = () => {
let (model, sendAction) = React.useReducer(reduceState, Pending)
React.useEffect(() => {
fetch("")
->Promise.then(Response.json)
->Promise.thenResolve(payload => {
let record = JSON.Decode.object(payload)->Option.getOrThrow
let profile = Profile.fromJson(record)
sendAction(ReplaceState(Ready(profile)))
})
->Promise.ignore
None
}, [sendAction]
switch model {
| Ready(profile) => <div> {React.string("Profile ready")} </div>
| Pending => <div> {React.string("Waiting...")} </div>
}
}
При этом я сам часто прибегаю к приёму, который принудительно создаёт async-контекст, когда он нужен:
module AsyncRunner = {
let run = (createTask: unit => promise<unit>) =>
createTask()->Promise.ignore
}
let handleSync = () =>
AsyncRunner.run(async () => {
let payload = await fetch("")->Promise.then(Response.json)
let record = JSON.Decode.object(payload)->Option.getOrThrow
let profile = Profile.fromJson(record)
sendAction(ReplaceState(Ready(profile)))
})
Переведено с английского·Показать оригиналGenerally speaking, you ought to avoid starting side effects within a reducer. That's why reducers are normally free of async functions, and ought to stay like that.
When an action changes reducer state, you can make that state a dependency for something like useEffect, and perform your (async) side effects there, including fetch calls.
Listing your component's possible states explicitly can also help:
type model = Pending | Ready(Profile.t)
type event = ReplaceState(model)
let reduceState = (model, event) =>
switch event {
| ReplaceState(model) => model
}
@react.component
let make = () => {
let (model, sendAction) = React.useReducer(reduceState, Pending)
React.useEffect(() => {
fetch("")
->Promise.then(Response.json)
->Promise.thenResolve(payload => {
let record = JSON.Decode.object(payload)->Option.getOrThrow
let profile = Profile.fromJson(record)
sendAction(ReplaceState(Ready(profile)))
})
->Promise.ignore
None
}, [sendAction]
switch model {
| Ready(profile) => <div> {React.string("Profile ready")} </div>
| Pending => <div> {React.string("Waiting...")} </div>
}
}
Having said that, I frequently reach for a trick that forces an async context whenever needed:
module AsyncRunner = {
let run = (createTask: unit => promise<unit>) =>
createTask()->Promise.ignore
}
let handleSync = () =>
AsyncRunner.run(async () => {
let payload = await fetch("")->Promise.then(Response.json)
let record = JSON.Decode.object(payload)->Option.getOrThrow
let profile = Profile.fromJson(record)
sendAction(ReplaceState(Ready(profile)))
})