React useReducer async data fetch

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>
  );
}

It is a good practice to keep reducers pure. It will make useReducer more predictable and ease up testability. Subsequent approaches both combine async operations with pure reducers:

1. Fetch data before dispatch (simple)

Wrap the original dispatch with asyncDispatch and let context pass this function down:

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initState);
  const asyncDispatch = () => { // adjust args to your needs
    dispatch({ type: "loading" });
    fetchData().then(data => {
      dispatch({ type: "finished", payload: data });
    });
  };
  
  return (
    <AppContext.Provider value={{ state, dispatch: asyncDispatch }}>
      {children}
    </AppContext.Provider>
  );
  // Note: memoize the context value, if Provider gets re-rendered more often
};

const reducer = (state, { type, payload }) => {
  if (type === "loading") return { status: "loading" };
  if (type === "finished") return { status: "finished", data: payload };
  return state;
};

const initState = {
  status: "idle"
};

const AppContext = React.createContext();

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(reducer, initState);
  const asyncDispatch = () => { // adjust args to your needs
    dispatch({ type: "loading" });
    fetchData().then(data => {
      dispatch({ type: "finished", payload: data });
    });
  };

  return (
    <AppContext.Provider value={{ state, dispatch: asyncDispatch }}>
      {children}
    </AppContext.Provider>
  );
};

function App() {
  return (
    <AppContextProvider>
      <Child />
    </AppContextProvider>
  );
}

const Child = () => {
  const val = React.useContext(AppContext);
  const {
    state: { status, data },
    dispatch
  } = val;
  return (
    <div>
      <p>Status: {status}</p>
      <p>Data: {data || "-"}</p>
      <button onClick={dispatch}>Fetch data</button>
    </div>
  );
};

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(42);
    }, 2000);
  });
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

2. Use middleware for dispatch (generic)

dispatch might be enhanced with middlewares like redux-thunk, redux-observable, redux-saga for more flexibility and reusability. Or write your own one.

Let's say, we want to 1.) fetch async data with redux-thunk 2.) do some logging 3.) invoke dispatch with the final result. First define middlewares:

import thunk from "redux-thunk";
const middlewares = [thunk, logger]; // logger is our own implementation

Then write a custom useMiddlewareReducer Hook, which you can see here as useReducer bundled with additional middlewares, akin to Redux applyMiddleware:

const [state, dispatch] = useMiddlewareReducer(middlewares, reducer, initState);

Middlewares are passed as first argument, otherwise API is the same as useReducer. For the implementation, we take applyMiddleware source code and carry it over to React Hooks.

const middlewares = [ReduxThunk, logger];

const reducer = (state, { type, payload }) => {
  if (type === "loading") return { ...state, status: "loading" };
  if (type === "finished") return { status: "finished", data: payload };
  return state;
};

const initState = {
  status: "idle"
};

const AppContext = React.createContext();

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useMiddlewareReducer(
    middlewares,
    reducer,
    initState
  );
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};

function App() {
  return (
    <AppContextProvider>
      <Child />
    </AppContextProvider>
  );
}

const Child = () => {
  const val = React.useContext(AppContext);
  const {
    state: { status, data },
    dispatch
  } = val;
  return (
    <div>
      <p>Status: {status}</p>
      <p>Data: {data || "-"}</p>
      <button onClick={() => dispatch(fetchData())}>Fetch data</button>
    </div>
  );
};

function fetchData() {
  return (dispatch, getState) => {
    dispatch({ type: "loading" });
    setTimeout(() => {
      // fake async loading
      dispatch({ type: "finished", payload: (getState().data || 0) + 42 });
    }, 2000);
  };
}

function logger({ getState }) {
  return next => action => {
    console.log("state:", JSON.stringify(getState()), "action:", JSON.stringify(action));
    return next(action);
  };
}

// same API as useReducer, with middlewares as first argument
function useMiddlewareReducer(
  middlewares,
  reducer,
  initState,
  initializer = s => s
) {
  const [state, setState] = React.useState(initializer(initState));
  const stateRef = React.useRef(state); // stores most recent state
  const dispatch = React.useMemo(
    () =>
      enhanceDispatch({
        getState: () => stateRef.current, // access most recent state
        stateDispatch: action => {
          stateRef.current = reducer(stateRef.current, action); // makes getState() possible
          setState(stateRef.current); // trigger re-render
          return action;
        }
      })(...middlewares),
    [middlewares, reducer]
  );

  return [state, dispatch];
}

//                                                         |  dispatch fn  |
// A middleware has type (dispatch, getState) => nextMw => action => action
function enhanceDispatch({ getState, stateDispatch }) {
  return (...middlewares) => {
    let dispatch;
    const middlewareAPI = {
      getState,
      dispatch: action => dispatch(action)
    };
    dispatch = middlewares
      .map(m => m(middlewareAPI))
      .reduceRight((next, mw) => mw(next), stateDispatch);
    return dispatch;
  };
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js" integrity="sha256-2xw5MpPcdu82/nmW2XQ6Ise9hKxziLWV2GupkS9knuw=" crossorigin="anonymous"></script>
<script>var ReduxThunk = window.ReduxThunk.default</script>

Note: we store intermediate state in mutable refs - stateRef.current = reducer(...), so each middleware can access current, most recent state at the time of its invocation with getState.

To have the exact API as useReducer, you can create the Hook dynamically:

const useMiddlewareReducer = createUseMiddlewareReducer(middlewares); //init Hook
const MyComp = () => { // later on in several components
  // ...
  const [state, dispatch] = useMiddlewareReducer(reducer, initState);
}

const middlewares = [ReduxThunk, logger];

const reducer = (state, { type, payload }) => {
  if (type === "loading") return { ...state, status: "loading" };
  if (type === "finished") return { status: "finished", data: payload };
  return state;
};

const initState = {
  status: "idle"
};

const AppContext = React.createContext();

const useMiddlewareReducer = createUseMiddlewareReducer(middlewares);

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useMiddlewareReducer(
    reducer,
    initState
  );
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};

function App() {
  return (
    <AppContextProvider>
      <Child />
    </AppContextProvider>
  );
}

const Child = () => {
  const val = React.useContext(AppContext);
  const {
    state: { status, data },
    dispatch
  } = val;
  return (
    <div>
      <p>Status: {status}</p>
      <p>Data: {data || "-"}</p>
      <button onClick={() => dispatch(fetchData())}>Fetch data</button>
    </div>
  );
};

function fetchData() {
  return (dispatch, getState) => {
    dispatch({ type: "loading" });
    setTimeout(() => {
      // fake async loading
      dispatch({ type: "finished", payload: (getState().data || 0) + 42 });
    }, 2000);
  };
}

function logger({ getState }) {
  return next => action => {
    console.log("state:", JSON.stringify(getState()), "action:", JSON.stringify(action));
    return next(action);
  };
}

function createUseMiddlewareReducer(middlewares) {
  return (reducer, initState, initializer = s => s) => {
    const [state, setState] = React.useState(initializer(initState));
    const stateRef = React.useRef(state); // stores most recent state
    const dispatch = React.useMemo(
      () =>
        enhanceDispatch({
          getState: () => stateRef.current, // access most recent state
          stateDispatch: action => {
            stateRef.current = reducer(stateRef.current, action); // makes getState() possible
            setState(stateRef.current); // trigger re-render
            return action;
          }
        })(...middlewares),
      [middlewares, reducer]
    );
    return [state, dispatch];
  }
}

//                                                         |  dispatch fn  |
// A middleware has type (dispatch, getState) => nextMw => action => action
function enhanceDispatch({ getState, stateDispatch }) {
  return (...middlewares) => {
    let dispatch;
    const middlewareAPI = {
      getState,
      dispatch: action => dispatch(action)
    };
    dispatch = middlewares
      .map(m => m(middlewareAPI))
      .reduceRight((next, mw) => mw(next), stateDispatch);
    return dispatch;
  };
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js" integrity="sha256-2xw5MpPcdu82/nmW2XQ6Ise9hKxziLWV2GupkS9knuw=" crossorigin="anonymous"></script>
<script>var ReduxThunk = window.ReduxThunk.default</script>

More infos - external libraries: react-use, react-hooks-global-state, react-enhanced-reducer-hook