React useReducer async data fetch

ZiiMakc picture ZiiMakc · Nov 5, 2018 · Viewed 39.9k times · Source

I'am trying to fetch some data with new react useReducer API and stuck on stage where i need to fetch it async. I just don't know how :/

How to place data fetching in switch statement or it's not a way how it's should be done?

import React from 'react'

const ProfileContext = React.createContext()

const initialState = {
  data: false
}

let reducer = async (state, action) => {
  switch (action.type) {
    case 'unload':
      return initialState
    case 'reload':
      return { data: reloadProfile() } //how to do it???
  }
}


const reloadProfile = async () => {
  try {
    let profileData = await fetch('/profile')
    profileData = await profileData.json()

    return profileData
  } catch (error) {
    console.log(error)
  }
}

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState)

  return (
    <ProfileContext.Provider value={{ profile, profileR }}>
      {props.children}
    </ProfileContext.Provider>
  )
}

export { ProfileContext, ProfileContextProvider }

I was trying to do it like this, but it's not working with async ;(

let reducer = async (state, action) => {
  switch (action.type) {
    case 'unload':
      return initialState
    case 'reload': {
      return await { data: 2 }
    }
  }
}

Answer

Tyler picture Tyler · Nov 5, 2018

This is an interesting case that the useReducer examples don't touch on. I don't think the reducer is the right place to load asynchronously. Coming from a Redux mindset, you would typically load the data elsewhere, either in a thunk, an observable (ex. redux-observable), or just in a lifecycle event like componentDidMount. With the new useReducer we could use the componentDidMount approach using useEffect. Your effect can be something like the following:

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState);

  useEffect(() => {
    reloadProfile().then((profileData) => {
      profileR({
        type: "profileReady",
        payload: profileData
      });
    });
  }, []); // The empty array causes this effect to only run on mount

  return (
    <ProfileContext.Provider value={{ profile, profileR }}>
      {props.children}
    </ProfileContext.Provider>
  );
}

Also, working example here: https://codesandbox.io/s/r4ml2x864m.

If you need to pass a prop or state through to your reloadProfile function, you could do so by adjusting the second argument to useEffect (the empty array in the example) so that it runs only when needed. You would need to either check against the previous value or implement some sort of cache to avoid fetching when unnecessary.

Update - Reload from child

If you want to be able to reload from a child component, there are a couple of ways you can do that. The first option is passing a callback to the child component that will trigger the dispatch. This can be done through the context provider or a component prop. Since you are using context provider already, here is an example of that method:

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState);

  const onReloadNeeded = useCallback(async () => {
    const profileData = await reloadProfile();
    profileR({
      type: "profileReady",
      payload: profileData
    });
  }, []); // The empty array causes this callback to only be created once per component instance

  useEffect(() => {
    onReloadNeeded();
  }, []); // The empty array causes this effect to only run on mount

  return (
    <ProfileContext.Provider value={{ onReloadNeeded, profile }}>
      {props.children}
    </ProfileContext.Provider>
  );
}

If you really want to use the dispatch function instead of an explicit callback, you can do so by wrapping the dispatch in a higher order function that handles the special actions that would have been handled by middleware in the Redux world. Here is an example of that. Notice that instead of passing profileR directly into the context provider, we pass the custom one that acts like a middleware, intercepting special actions that the reducer doesn't care about.

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState);

  const customDispatch= useCallback(async (action) => {
    switch (action.type) {
      case "reload": {
        const profileData = await reloadProfile();
        profileR({
          type: "profileReady",
          payload: profileData
        });
        break;
      }
      default:
        // Not a special case, dispatch the action
        profileR(action);
    }
  }, []); // The empty array causes this callback to only be created once per component instance

  return (
    <ProfileContext.Provider value={{ profile, profileR: customDispatch }}>
      {props.children}
    </ProfileContext.Provider>
  );
}