일반적으로 reducer 안에서 부수 효과를 시작하는 건 피하는 게 좋아요. 그래서 reducer에는 보통 async 함수가 없고, 그렇게 유지해야 해요.
action이 reducer 상태를 바꾸면, 그 상태를 useEffect 같은 것의 의존성으로 넣고 그 안에서 fetch 호출 같은 (async) 부수 효과를 수행할 수 있어요.
컴포넌트가 가질 수 있는 상태를 명시적으로 나열하는 것도 도움이 돼요:
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)))
})