React useReducer异步数据获取

时间:2018-11-05 00:17:12

标签: reactjs react-hooks

我正在尝试使用新的react useReducer API来获取一些数据,并停留在需要异步获取的阶段。我只是不知道如何:/

如何将数据获取放置在switch语句中,或者这不是应该完成的方式?

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 }

我试图这样做,但是它不能与async一起使用;(

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

7 个答案:

答案 0 :(得分:7)

这是一个有趣的情况,useReducer示例没有涉及。我认为减速器不是异步加载的正确位置。来自Redux的心态,您通常会将数据加载到其他位置,例如以thunk,可观察(例如redux-observable)或仅在生命周期事件(如componentDidMount中)加载。使用新的useReducer,我们可以使用componentDidMount的{​​{1}}方法。您的效果可能类似于以下内容:

useEffect

另外,这里的工作示例:https://codesandbox.io/s/r4ml2x864m

如果您需要将prop或状态传递给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> ); } 函数,则可以通过将第二个参数调整为reloadProfile(示例中的空数组)来实现,以使其仅运行需要的时候。您需要对照先前的值进行检查或实施某种缓存,以避免在不必要时进行提取。

更新-从子级重新加载

如果您希望能够从子组件中重新加载,可以通过两种方法进行。第一种选择是将回调传递给子组件,该子组件将触发分派。这可以通过上下文提供程序或组件属性完成。由于您已经在使用上下文提供程序,因此下面是该方法的示例:

useEffect

如果您确实要使用分派功能而不是显式回调,则可以通过将分派包装在更高阶的函数中来进行处理,该函数处理由中间件处理的特殊操作在Redux世界中。这是一个例子。请注意,我们没有将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> ); } 直接传递给上下文提供者,而是传递了一种充当中间件的自定义变量,它拦截了reducer无关的特殊动作。

profileR

答案 1 :(得分:4)

good practice是保持减速器纯净的。这将使useReducer更具可预测性并简化可测试性。

dispatch之前获取数据(简单方法)

您可以用dispatch包装原始的asyncDispatch并通过上下文向下传递此功能:

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>

将中间件用于dispatch

要获得更大的灵活性和可重用性,可以使用middlewares增强dispatch。要么自己编写,要么使用redux-thunk之类的Redux生态系统中的文件。

比方说,您想在调用thunk之前使用dispatch来获取异步数据并进行一些日志记录:

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

然后,我们可以编写类似于applyMiddleware的编写器,该编写器创建一个链取数据→日志状态→分派。实际上,我在Redux存储库how this is done中进行了查找。

useMiddlewareReducer钩子

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

API与useReducer相同,您将中间件作为第一个参数传递。基本思想是,我们将中间状态存储在mutable refs中,因此每个中间件始终可以使用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>

找到外部库链接(按星数排序):react-usereact-hooks-global-statereact-enhanced-reducer-hook

答案 2 :(得分:3)

我对问题和可能的解决方案进行了非常详细的说明。 Dan Abramov建议了解决方案3。

注意:要点中的示例提供了有关文件操作的示例,但是可以采用相同的方法进行数据提取。

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

答案 3 :(得分:2)

我在调度方法上包裹了一层以解决异步动作问题。

这是初始状态。 loading键记录应用程序当前的加载状态。当应用程序从服务器获取数据时,要显示加载页面很方便。

{
  value: 0,
  loading: false
}

有四种动作。

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

假设有一个异步方法

async function asyncFetch(p) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(p);
    }, 1000);
  });
}

wrapperDispatch(dispatch)({
  type: "click_async",
  payload: asyncFetch(new Date().getTime())
});

完整的示例代码在这里:

  

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

答案 4 :(得分:2)

更新

我在下面的网络链接中添加了另一条评论。这是一个名为useAsyncReducer的自定义钩子,基于以下代码,该钩子使用与普通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>;
}

旧解决方案:

我刚刚发布了此回复here,并认为将其张贴在这里也可能会有所帮助。

我的解决方案是使用useReducer +异步函数来模拟useState

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' })} />
}

答案 5 :(得分:0)

您可以使用 useAsync 软件包:https://github.com/sonofjavascript/use-async,该软件包基本上是useReducer钩子的扩展,该钩子允许通过http请求管理应用程序状态上的异步操作。

通过ClientStore设置客户端代理(您可以使用自己的http客户端):

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

定义并使用您的操作:

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

答案 6 :(得分:0)

很简单 您可以在异步函数结果后更改 useEffect 中的状态

为获取结果定义useState

const [resultFetch, setResultFetch] = useState(null);

useEffect 用于收听 setResultFetch

获取异步 API 调用后 setResultFetch(result of response)

useEffect(() => {
if (resultFetch) {
  const user = resultFetch;
  dispatch({ type: AC_USER_LOGIN, userId: user.ID})

}}, [resultFetch])