I have an AsyncContext
that allows me to start / stop any kind of async computation. Behind the scene, it manages a global loader and a snackbar.
export type Context = {
loading: boolean
start: () => void
stop: (message?: string) => void
}
const defaultContext: Context = {
loading: false,
start: noop,
stop: noop,
}
export const AsyncContext = createContext(defaultContext)
Here a consumer:
const MyChild: FC = () => {
const {start, stop} = useContext(AsyncContext)
async function fetchUser() {
try {
start()
const res = await axios.get('/user')
console.log(res.data)
stop()
} catch (e) {
stop('Error: ' + e.message)
}
}
return (
<button onClick={fetchData}>
Fetch data
</button>
)
}
As you can see, MyChild
does not care about loading
. But it is included in the context, so the component re-render 2 times for nothing.
To prevent this, my first attempt is to split my component in two, and use a memo
:
type Props = {
start: AsyncContext['start']
stop: AsyncContext['stop']
}
const MyChild: FC = () => {
const {start, stop} = useContext(AsyncContext)
return <MyChildMemo start={start} stop={stop} />
}
const MyChildMemo: FC<Props> = memo(props => {
const {start, stop} = props
async function fetchUser() {
try {
start()
const res = await axios.get('/user')
console.log(res.data)
stop()
} catch (e) {
stop('Error: ' + e.message)
}
}
return (
<button onClick={fetchData}>
Fetch data
</button>
)
})
It works, but I do not want to split all children that use AsyncContext
.
The second attempt is to use useMemo
directly on the JSX:
const MyChild: FC = () => {
const {start, stop} = useContext(AsyncContext)
async function fetchUser() {
try {
start()
const res = await axios.get('/user')
console.log(res.data)
stop()
} catch (e) {
stop('Error: ' + e.message)
}
}
return useMemo(() => (
<button onClick={fetchData}>
Fetch data
</button>
), [])
}
It works also, it is more condensed, but I'm not sure if this is a good practice.
Is any of my two approaches correct? If not, what would you advise?