0

I am trying out state management with react hooks and the context API. I have implemented a reducer pattern following some code from a todo app, but now I want to starting fetching data regularly from an API (e.g. implementing an infinite scroll), and I'm not sure now where the best place in the code is to make these async-REST-api calls.

I'm used to using a redux middleware library like redux-observable, redux-thunk, etc. for asynchronous tasks. But now that I'm not using redux, it's not clear to me what the best way is to do async updates. I suppose I could use await-promise reducers, but that doesn't feel right.

Any suggestions? (Having implemented a reducer pattern, I'm tempted to just fall back to a full redux-with-redux-obnservable implementation, though I was hoping context would slim down all that boilerplate.)

Magnus
  • 3,086
  • 2
  • 29
  • 51
  • In the container component that needs the data you can use [useEffect](https://reactjs.org/docs/hooks-effect.html), You already use context so you can get dispatch from context and dispatch an event when data is fetching, fetched or failed. – HMR Sep 01 '19 at 16:59

3 Answers3

3

This is probably how I would implement it. I have a standard reducer. I will also create a helper functional component to help me set up the value for my context provider.

I also made some comments in the source code. I hope the following code snippet is simple enough to follow.

    import React, { useReducer, useEffect, createContext } from 'react';
    import FetchService from './util/FetchService'; // some helper functions


    const OrderInfoContext = createContext();

    const reducer = (state, action) => {
      switch (action.type) {
        case 'init':
          return {};
        case 'changeData':
          return action.payload;
        default:
          return state;
      }
    };

    const changeData = data => ({
      type: 'changeData',
      payload: data
    });

    /**
     * This is a helper component that generate the Provider wrapper
     */
    function OrderInfoProvider(props) {
      // We will persist API payload in the state so we can assign it to the Context
      const [orders, dispatch] = useReducer(reducer, {});

      // We use useEffect to make API calls.
      useEffect(() => {
        async function fetchData() {
          /**
           * This is just a helper to fetch data from endpoints. It can be done using
           * axios or similar libraries
           */
          const orders = await FetchService
            .get('/api/orders');
          dispatch(changeData(orders))
        }
        fetchData();
      }, []);

      /**
       * we create a global object that is available to every child components
       */
      return <OrderInfoContext.Provider value={[orders, dispatch]} {...props} />;
    } 

    // Helper function to get Context
    function useOrderInfo() {
      const context = useContext(OrderInfoContext);
      if (!context) {
        throw new Error('useOrderInfo must be used within a OrderInfoProvider');
      }
      return context;
    }

    export { OrderInfoProvider, useOrderInfo , changeData }; 


Andrew Zheng
  • 2,612
  • 1
  • 20
  • 25
  • This looks like it will load data initially, but after that you need to make separate fetch calls from within your components to change the data. But, ok, I think I see the pattern: instead of putting the fetch calls in the reducers (or in reducer-side-effect calls) you fetch the data from your component handlers and then feed that to your reducers. – Magnus Sep 01 '19 at 17:04
  • @Magnus I recently wrote an article about my solution to deal with the problem. Basically, I created action creators with reference to the dispatch function. After that, I pass down those actions creators as part of the context, so child components don't need to worry about the implementation details of those actions. I hope that answers you questions more clearly. https://medium.com/@xiangzheng/use-usereducer-as-usestate-replacement-to-handle-complicate-local-state-b107d7c3807e – Andrew Zheng Sep 18 '19 at 13:58
2

Here is an example that uses context and useReducer hook to set an app state and a context provider for state and dispatch.

The container uses useContext to get the state and the dispatch function, useEffect to do side effects like you'd use thunk, saga or middleware if you were using redux, useMemo to map state to props and useCallback to map each auto dispatched action to props (I assume you are familiar with react redux connect.

import React, {
  useEffect,
  useContext,
  useReducer,
  useCallback,
  useMemo,
} from 'react';

//store provider
const Store = React.createContext();
const initStoreProvider = (rootReducer, initialState) => ({
  children,
}) => {
  const [state, dispatch] = useReducer(
    rootReducer,
    initialState
  );
  return (
    <Store.Provider value={{ state, dispatch }}>
      {children}
    </Store.Provider>
  );
};
//container for component
const ComponentContainer = ({ id }) => {
  const { state, dispatch } = useContext(Store);
  const num = state.find((n, index) => index === id);
  //side effects, asynchonously add another one if num%5===0
  //this is your redux thunk
  const addAsync = num % 5 === 0;
  useEffect(() => {
    if (addAsync)
      Promise.resolve().then(dispatch({ type: 'add', id }));
  }, [addAsync, dispatch, id]);
  //use callback so function does not needlessly change and would
  //trigger render in Component. This is mapDispatch but only for
  //one function, if you have more than one then use 
  //useCallback for each one
  const add = useCallback(
    () => dispatch({ type: 'add', id }),
    [dispatch, id]
  );
  //This is your memoized mapStateToProps
  const props = useMemo(() => ({ counter: num, id }), [
    num,
    id,
  ]);

  return (
    <Component add={add} doNothing={dispatch} {...props} />
  );
};
//use React.memo(Component) to avoid unnecessary renders
const Component = React.memo(
  ({ id, add, doNothing, counter }) =>
    console.log('render in component', id) || (
      <div>
        <button onClick={add}>{counter}</button>
        <button onClick={doNothing}>do nothing</button>
      </div>
    )
);
//initialize the store provider with root reducer and initial state
const StoreProvider = initStoreProvider(
  (state, action) =>
    action.type === 'add'
      ? state.map((n, index) =>
          index === action.id ? n + 1 : n
        )
      : state,
  [1, 8]
);

//using the store provider
export default () => (
  <StoreProvider>
    <ComponentContainer id={0} />
    <ComponentContainer id={1} />
  </StoreProvider>
);

Example is here

HMR
  • 37,593
  • 24
  • 91
  • 160
0

https://resthooks.io/ uses the flux pattern just like you want, which allows things like middlwares, debuggability, etc. However, instead of having to write thousands of lines of state management, you just need a simple declarative data definition.

const getTodo = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
});

function TodoDetail({ id }: { id: number }) {
  const todo = useSuspense(getTodo, { id });
  return <div>{todo.title}</div>;
}
Nathaniel Tucker
  • 577
  • 1
  • 5
  • 17