paint-brush
What's The Right Way to Fetch Data in React Hooks? [A Deep Dive]by@Mathew
14,437 reads
14,437 reads

What's The Right Way to Fetch Data in React Hooks? [A Deep Dive]

by JacobApril 22nd, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The React Hooks docs has a FAQ entry: “How can I do data fetching with Hooks?” where they link to this well written and detailed article by Robin Wieruch. There is however a shortcoming in that approach which affects correctness is some important situations. Using useEffect() to sync the state to a prop is problematic. The browser may briefly display out of sync data to the user before it updates to ‘loading…’ But there is a bigger issue. Bad data is passed to child components, but will get fixed before the paint.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - What's The Right Way to Fetch Data in React Hooks? [A Deep Dive]
Jacob HackerNoon profile picture

The React Hooks docs has a FAQ entry: “How can I do data fetching with Hooks?” where they link to this well written and detailed article by Robin Wieruch. There is however a shortcoming in that approach which affects correctness is some important situations.

As far as I can tell, there isn’t a widely available description of a more robust solution. There is nuance in getting it completely right so it seems worthwhile to fill this gap. I attempt to do that here.

TL;DR

You need something like 

getDerivedStateFromProps
 from Class Components. Use the 
useDerivedState()
 custom hook provided below.

Data fetching means we probably need to keep state in sync with props.

Consider a component that fetching something from the server to display to the user or base further action on. You probably want to store the result of the data fetching as state in the component and the call to the server probably depends on some prop that is passed to the component.

This means that when the prop changes, we have to update the state correctly. That is, we need to keep the state in sync with the props.

For example, consider a social app where a user has a list of friends. You are building a component call 

FriendList
 that displays the user’s name along with a list of his/her friends .

The component is passed in the current user as prop and has to fetch the list of friends from the server, which is stored as state. When the user changes, the list of friends needs to be fetched from the server again.

Keeping state in sync with props though useEffect() is problematic.

The approach taken by Robin Wieruch in the article linked above is to use 

useEffect()
 to sync the state to the prop. Here is some code that implements
FriendList
 using this approach.

type User = {name: String; id: string}
type State = {loading: true} | {loading: false; friends: User[]}

function FriendList(user: User) {
    const [state, setState] = React.useState<State>({loading: true})

    React.useEffect(() => {
        setState({loading: true})
        serverCallToGetFriendList(user, friends =>
            setState({loading: false, friends})
        )
    }, [user])

    return (
        <>
            <h1>{user.name}, your friends are:</h1>
            {state.loading ? (
                'Loading...'
            ) : (
                <ul> {state.friends.map(friend => ( <li> {friend.name} </li> ))} </ul>
            )}
        </>
    )
}

When the user prop changes, the 

useEffect()
 is triggered, which updates the state to 
{loading:true}
 and makes the call to the server. When the server call returns the state is updated to 
{loading:false, friends}
. Where 
friends
 is the list fetched from the server.

This approach has a problem. Effects are only run after the rendering of the whole component tree is completed, and after the browser completes painting and after the effects of child components are run. This means when the prop changes, the state is out of sync for all these operations.

The lesser issue here, which may be acceptable in some cases, is that the browser will actually briefly display out of sync data to the user. In our example, the browser may briefly display the previous user’s friends as belonging to the new user before it updates to ‘loading…’.

But there is a bigger issue. Given that the rendering was completed with out of sync data, child components that were passed the prop and state will see out of sync data. They might have scheduled effects based on that bad data. 

These effects will run before the state is corrected and will likely be doing bad things. 

The child components may also have set callbacks that will keep the bad data in their closures, and if triggered before the effect is run, will probably do something bad as well.

Even if the above issues don’t materialize in specific use cases, having out of sync data surface in this manner is intrinsically problematic and should be avoided.

Out of sync data should not be visible to child components. It makes reasoning about the program much harder when you have bad data moving around.

useLayoutEffect() won’t solve the problem.

Since the problem with using 

useEffect()
 to sync the state to a prop is that it runs too late, we might consider 
useLayoutEffect()
 which runs earlier. 
useLayoutEffect()
 guarantees that the effect is run before the browser paints. This is better. Bad data is passed to child components, but will get fixed before the paint, so is not visible to the user

But what about effects scheduled by children based on the bad data? This is a tricky part of the semantics of 

useLayoutEffect()
. It turns out that if you call 
setState()
 inside 
useLayoutEffect()
 it forces the effects to run before the state is updated and before the paint. (Note, this is the reverse of what happens normally. Usually effects run after the paint.) This is clarified by Brian Vaughn from the React team here.

As part of this second, synchronous render [triggered by a setState() call in a layout effect], React also flushes the remaining/pending effects (including the passive ones- which would otherwise have not been flushed until a later frame).

So 

useLayoutEffect()
 is not a satisfactory solution.

Changing the key works, but it is a blunt tool.

A perhaps non-obvious solution to the issue of keeping state in sync with props is to simply get React to destroy the underlying instance (and therefore state) of the component and create a new one when the prop changes. We can do this by changing the key of the React component whenever the prop changes. In our example, we could set the key of the 

FriendList
 component to 
user.id
. This effectively makes the prop a constant over the lifetime of the component instance, so we never have to worry about it changing.

This works. Child components are never rendered with bad data. But it is a blunt tool. It may not be desirable to destroy the instance for a number of reasons. You will lose all the other state from the instance, for example.

Call setState() directly during render.

Let us step back and consider what we are trying to do. We are trying to keep state in sync with props. There is an idiomatic to do this with Class Components and it is 

getDerivedStateFromProps
. We have to implement something similar in Hooks. There is a FAQ entry addressing exactly this. The suggestion is to call 
setState()
 directly during render when a prop change is detected to keep them in sync. Calling 
setState() 
during render has the following semantics:

[Y]ou can update the state right during rendering [a functional component]. React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.

The key to this is that render will re-run immediately. In particular, before any child component renders, so child components never see out of sync state.

Applying this to 

FriendList
 we have the following:

type User = {name: String; id: string}
type State = {loading: true} | {loading: false; friends: User[]}

function FriendList(user: User) {
    const [state, setState] = React.useState<State>({loading: true})

    // Save a local copy of the prop so we know when it changes.
    const [localUser, setLocalUser] = React.useState(user)

    // When the prop changes, update the local copy and the state that needs.
    // to be in sync with the prop.
    if (user !== localUser) {
        setLocalUser(user)
        setState({loading: true})
    }

    React.useEffect(() => {
        if(!state.loading) return
        serverCallToGetFriendList(user, friends =>
            setState({loading: false, friends})
        )
    }, [user, state])

    return (
        <>
            <h1>{user.name}, your friends are:</h1>
            {state.loading ? (
                'Loading...'
            ) : (
                <ul> {state.friends.map(friend => ( <li> {friend.name} </li> ))} </ul>
            )}
        </>
    )
}

We keep a local copy of the prop to check for changes. When a change is detected we update the local copy and the state to keep it in sync with the prop. Now when the prop changes the state is updated as soon as the render completes, and the render re-runs with the updated state.

Child components never see bad data. Note that we still have the 

useEffect()
 , but it is only used to make the server call, and not to update the state on prop change.

Note however that since 

setState()
 is asynchronous, the state is fixed only after the function returns. This means that you still have to deal with out of sync state for the rest of the function. What if you schedule effects locally based on the bad data? Do we have to be careful about that? Turns out we do not.

There is no official documentation, but testing reveals that effects scheduled during the render are discarded if 

setState()
 is called during that render. It also does not get counted as being run for the dependency list change calculations. It short, it is a no-op.

We can actually do even better though and not have to worry about out of sync state even inside the render function. There some bookkeeping to take care of and this is a good time to extract the details to a custom hook.

useDerivedState()

Here is code for a custom hook 

useDerivedState
 (alluding to the Class Component equivalent):

export function useDerivedState<State>(
    onDepChange: () => State,
    depList: any[]
): [State, (newState: State | ((state: State) => State)) => void] {
    const [localState, setLocalState] = React.useState<
        | {init: false}
        | {
              init: true
              publicState: State
              depList: any[]
          }
    >({init: false})

    let currPublicState: State
    if (
        !localState.init ||
        depList.length !== localState.depList.length ||
        !localState.depList.every((x, i) => depList[i] === x)
    ) {
        currPublicState = onDepChange()
        setLocalState({
            init: true,
            publicState: currPublicState,
            depList
        })
    } else {
        currPublicState = localState.publicState
    }

    const publicSetState = React.useCallback(
        (newState: State | ((state: State) => State)) => {
            setLocalState(localState => {
                if (!localState.init) throw new Error()
                const publicState =
                    typeof newState === 'function'
                        ? (newState as any)(localState.publicState)
                        : newState
                return {...localState, publicState}
            })
        },
        []
    )
    return [currPublicState, publicSetState]
}

useDerivedState()
 is intended to be used similarly to 
useState()
. It returns a
[state, setState]
 tuple just like 
setState()
. The difference is that instead of taking an initial state like 
setState()
 , 
useDerivedState()
 takes in a function to generate the state, and a dependency list. Whenever anything in the dependency list changes 
useDerivedState()
 recalculates the state from the function and returns the new state synchronously.

FriendList
 now becomes:

function FriendList(user: User) {
    const [state, setState] = useDerivedState<State>(() => {
        return {loading: true}
    }, [user])

    React.useEffect(() => {
        if(!state.loading) return
        serverCallToGetFriendList(user, friends =>
            setState({loading: false, friends})
        )
    }, [state, user])

    return (
        <>
            <h1>{user.name}, your friends are:</h1>
            {state.loading ? (
                'Loading...'
            ) : (
                <ul> {state.friends.map(friend => ( <li> {friend.name} </li> ))} </ul>
            )}
        </>
    )
}

Whenever the 

user
 prop changes, the state is reset to 
{loading:true}
 synchronously which keeps the relationship between them easy to reason about.

Conclusion

That hopefully gives you a new idiomatic way for data fetching in Hooks, and more generally, keeping state in sync with props. It is unfortunate that the official docs don’t provide more support for this and in fact suggests using 

useEffect()
which can lead to serious errors. Strangely the docs actively discourage the right approach by going so far as to saying of calling 
setState()
directly while rendering: “You probably don’t need it, in rare cases that you do”. As we can see, it is needed for the correct approach to data fetching, which is pretty widespread use case if there ever was one.

About Me

I'm building Qurika -- the future of eBooks.

Image Credit: ccPixs.com