React useReducer async data fetch

前端 未结 6 658
臣服心动
臣服心动 2020-11-30 22:36

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

相关标签:
6条回答
  • 2020-11-30 23:13

    You can use the useAsync package: https://github.com/sonofjavascript/use-async, which basically is an extension of the useReducer hook that allows managing asynchronous actions over application's state through http requests.

    Set the client agent (you can use your own http client) through the ClientStore:

    import React from 'react'
    
    import { ClientStore } from '@sonofjs/use-async'
    import axios from 'axios'
    
    import Component from './Component.jsx'
    
    const ViewContainer = () => (
      <ClientStore.Provider agent={axios}>
        <Component />
      </ClientStore.Provider>
    )
    
    export default ViewContainer
    

    Define and use your actions:

    import React, { useEffect }  from 'react'
    import useAsync from '@sonofjs/use-async'
    
    const actions = {
      FETCH_DATA: (state) => ({
        ...state,
        loading: true,
        request: {
          method: 'GET',
          url: '/api/data'
        }
      }),
      FETCH_DATA_SUCCESS: (state, response) => ({
        ...state,
        loading: false,
        data: response
      }),
      FETCH_DATA_ERROR: (state, error) => ({
        ...state,
        loading: false,
        error
      })
    }
    
    const initialState = {
      loading: false,
      data: {}
    }
    
    const Component = () => {
      const [state, dispatch] = useAsync(actions, initialState)
    
      useEffect(() => {
        dispatch({ type: 'DATA' })
      }, [])
    
      return (
        <>
          {state.loading ? <span>Loading...</span> : null}
          {<span>{JSON.stringify(state.data)}</span>}
          {state.error ? <span>Error: {JSON.stringify(state.error)}</span> : null}
        <>
      )
    }
    
    export default Component
    
    0 讨论(0)
  • 2020-11-30 23:15

    I wrapped the dispatch method with a layer to solve the asynchronous action problem.

    Here is initial state. The loading key record the application current loading status, It's convenient when you want to show loading page when the application is fetching data from server.

    {
      value: 0,
      loading: false
    }
    

    There are four kinds of actions.

    function reducer(state, action) {
      switch (action.type) {
        case "click_async":
        case "click_sync":
          return { ...state, value: action.payload };
        case "loading_start":
          return { ...state, loading: true };
        case "loading_end":
          return { ...state, loading: false };
        default:
          throw new Error();
      }
    }
    
    function isPromise(obj) {
      return (
        !!obj &&
        (typeof obj === "object" || typeof obj === "function") &&
        typeof obj.then === "function"
      );
    }
    
    function wrapperDispatch(dispatch) {
      return function(action) {
        if (isPromise(action.payload)) {
          dispatch({ type: "loading_start" });
          action.payload.then(v => {
            dispatch({ type: action.type, payload: v });
            dispatch({ type: "loading_end" });
          });
        } else {
          dispatch(action);
        }
      };
    }
    

    Suppose there is an asynchronous method

    async function asyncFetch(p) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(p);
        }, 1000);
      });
    }
    
    
    wrapperDispatch(dispatch)({
      type: "click_async",
      payload: asyncFetch(new Date().getTime())
    });
    

    The full example code is here:

    https://codesandbox.io/s/13qnv8ml7q

    0 讨论(0)
  • 2020-11-30 23:19

    Update:

    I’ve added another comment in the weblink below. It’s a custom hook called useAsyncReducer based on the code below that uses the exact same signature as a normal useReducer.

    function useAsyncReducer(reducer, initState) {
        const [state, setState] = useState(initState),
            dispatchState = async (action) => setState(await reducer(state, action));
        return [state, dispatchState];
    }
    
    async function reducer(state, action) {
        switch (action.type) {
            case 'switch1':
                // Do async code here
                return 'newState';
        }
    }
    
    function App() {
        const [state, dispatchState] = useAsyncReducer(reducer, 'initState');
        return <ExampleComponent dispatchState={dispatchState} />;
    }
    
    function ExampleComponent({ dispatchState }) {
        return <button onClick={() => dispatchState({ type: 'switch1' })}>button</button>;
    }
    

    Old solution:

    I just posted this reply here and thought it may be good to post here as well in case it helps anyone.

    My solution was to emulate useReducer using useState + an async function:

    async function updateFunction(action) {
        switch (action.type) {
            case 'switch1':
                // Do async code here (access current state with 'action.state')
                action.setState('newState');
                break;
        }
    }
    
    function App() {
        const [state, setState] = useState(),
            callUpdateFunction = (vars) => updateFunction({ ...vars, state, setState });
    
        return <ExampleComponent callUpdateFunction={callUpdateFunction} />;
    }
    
    function ExampleComponent({ callUpdateFunction }) {
        return <button onClick={() => callUpdateFunction({ type: 'switch1' })} />
    }
    
    0 讨论(0)
  • 2020-11-30 23:25

    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>
      );
    }
    
    0 讨论(0)
  • 2020-11-30 23:38

    I wrote a very detailed explanation of the problem and possible solutions. Dan Abramov suggested Solution 3.

    Note: The examples in the gist provide examples with file operations but the same approach could be implemented for data fetching.

    https://gist.github.com/astoilkov/013c513e33fe95fa8846348038d8fe42

    0 讨论(0)
  • 2020-11-30 23:38

    It is a good practice to keep reducers pure. It will make useReducer more predictable and ease up testability.

    Fetch data before dispatch (simple approach)

    You can wrap the original dispatch with asyncDispatch and pass this function down via context:

    const AppContextProvider = ({ children }) => {
      const [state, dispatch] = React.useReducer(reducer, initState);
      const asyncDispatch = () => {
        dispatch({ type: "loading" });
        fetchData().then(data => {
          dispatch({ type: "finished", payload: data });
        });
      };
    
      return (
        <AppContext.Provider value={{ state, dispatch: asyncDispatch }}>
          {children}
        </AppContext.Provider>
      );
    };
    

    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 = () => {
        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(fetchData())}>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>

    Use middleware for dispatch

    For more flexibility and reusability, you can enhance dispatch with middlewares. Either write your own or use the ones from Redux ecosystem like redux-thunk.

    Let's say, you want to fetch async data with thunk and do some logging, before dispatch is invoked:

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

    We can then write a composer similar to applyMiddleware, which creates a chain fetch data → log state → dispatch. In fact I looked up in the Redux repository, how this is done.

    useMiddlewareReducer Hook

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

    The API is the same as useReducer and you pass middlewares as first argument. Basic idea is, that we store intermediate state in mutable refs, so each middleware always can access the most recent state with getState.

    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>

    External library links found (ordered by star count): react-use, react-hooks-global-state, react-enhanced-reducer-hook

    0 讨论(0)
提交回复
热议问题