We've been building a hooks based REST API wrapper.
A specific use case discussed in this gist is – how do you close a modal after updating a remote resource completes succesfully?
Which of the following options seems best or is there a better approach altogether?
function EditNoteModal ({ id, onClose }) {
const { patch } = useResource('api/notes')
const [formError, setFormError] = useState()
async function onSubmit () {
try {
await patch(id, { note: 'bla' })
onClose()
} catch (err) {
setFormError(err)
}
}
return (
<Modal>
<Form onSubmit={onSubmit} error={formError}>
</Modal>
)
}
This is not great, because if the modal is unrendered while request is in flight, you're setting state on unmounted component or calling onClose when it's already closed. Async/await takes away the control from the hooks/React in an uncontrollable/uninteruptable way.
function EditNoteModal ({ id, onClose }) {
const { error, patching, patched, patch } = usePatch('api/notes')
function onSubmit () {
patch(id, { note: 'bla' })
}
useEffect(() => {
if (patched) {
onClose()
}
}, [patched])
return (
<Modal>
<Form onSubmit={onSubmit} error={error}>
</Modal>
)
}
This doesn't have the shortcomings of the previous approach. If modal is closed mid flight, the effect won't be executed and modal will not be closed twice. Also error handling is easier. I'm not sure this handles multiple patches well (say if you're interacting with a list that allows user to modify multiple resources easily).
function EditNoteModal ({ id, onClose }) {
const { patch } = useResource('api/notes')
const [formError, setFormError] = useState()
function onSubmit () {
patch(id, { note: 'bla' }, (err) => {
// this is an effect!
// it's not gonna be called if
// this component is unmounted
if (err) setFormError(err)
else onClose()
})
}
return (
<Modal>
<Form onSubmit={onSubmit} error={formError}>
</Modal>
)
}
This is quite cool. The only odd thing is error handling reminiscent of Node's callbacks. But that seems to be the way effects in React currently work. I think this handles lists of items better than the previous approach.
function EditNoteModal ({ id, onClose }) {
// errors is an array of errors that happened as part of the
// operations that were performed, e.g. [{ type: 'patch', id, error }]
const { patch, errors } = useResource('api/notes')
function onSubmit () {
patch(id, { note: 'bla' }, (err) => {
// this is an effect!
// it's not gonna be called if
// this component is unmounted
if (!err) onClose()
})
}
return (
<Modal>
<Form onSubmit={onSubmit} error={errors && errors[0].error}>
</Modal>
)
}
This is similar, but errors are being populated by the original hook in a way that supports many operations, e.g. if you're patching 2 items and removing 1 (say all triggered as part of the user interacting with a list), all those errors would get populated into the errors array.
import { useAtom } from 'tiny-atom'
function MyModal ({ id, onClose }) {
const notes = useResource('api/notes')
const [state, actions] = useAtom(() => ({
actions: {
close () {
onClose()
},
async patch({ set, actions }) {
try {
await notes.patch(id, { note: 'bla' })
// this will not be dispatched if component
// is already unmounted, i.e. further effects
// are cancelled
actions.close()
} catch (error) {
// this will update atom, but will not
// trigger a rerender if component is
// already unmounted
set({ error })
}
}
}
}))
function onSubmit () {
actions.patch(id, { note: 'bla' })
}
return (
<Modal>
<Form onSubmit={onSubmit} error={state.error}>
</Modal>
)
}
This is a more verbose approach compared to the previous ones, but it's the most flexible. You get to control how your state looks like, how errors are handled, what happens when you perform multiple operations, etc. And it allows using async/await without the drawbacks of the first approach, since useAtom hook will handle components unmounting for you. (Note: this API is experimental and not released yet).
- Does Suspense help in this scenario or is Suspense only useful when reading data?
- How does Apollo useMutation handle this case? I.e. how do you close the modal there after mutation succeeds?
- Is this "Dancing between state and effects" facebook/react#15240?
- Do you know any good and simple hooks based REST wrappers?