React useReducer 异步数据获取

Posted

技术标签:

【中文标题】React useReducer 异步数据获取【英文标题】:React useReducer async data fetch 【发布时间】:2019-04-08 08:44:22 【问题描述】:

我正在尝试使用新的 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 

我试图这样做,但它不适用于异步;(

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

【问题讨论】:

我认为你希望你的减速器是同步的。也许您可以设置一个值,例如loadingtruereload 情况下,并且在您的组件中具有在 loading 更改时重新运行的效果,例如useEffect(() =&gt; if (loading) reloadProfile().then(...) , [loading]); 也许对遇到这个问题的人有用:robinwieruch.de/react-hooks-fetch-data 【参考方案1】:

这是useReducer 示例未涉及的有趣案例。我不认为减速器是异步加载的正确位置。来自 Redux 的思维模式,您通常会将数据加载到其他地方,或者在一个 thunk、一个 observable(例如 redux-observable)中,或者只是在像 componentDidMount 这样的生命周期事件中。使用新的useReducer,我们可以使用componentDidMount 方法使用useEffect。您的效果可能如下所示:

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

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

如果您需要将 prop 或状态传递给您的 reloadProfile 函数,您可以通过将第二个参数调整为 useEffect(示例中的空数组)来实现,以便它仅在需要时运行。您需要检查先前的值或实施某种缓存以避免在不必要时获取。

更新 - 从子项重新加载

如果您希望能够从子组件重新加载,有几种方法可以做到这一点。第一个选项是将回调传递给将触发调度的子组件。这可以通过上下文提供者或组件道具来完成。由于您已经在使用上下文提供程序,以下是该方法的示例:

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

如果您真的想要使用 dispatch 函数而不是显式回调,您可以通过将 dispatch 包装在更高阶的函数中来实现,该函数处理本来由中间件处理的特殊操作在 Redux 世界中。这是一个例子。请注意,我们没有将profileR 直接传递给上下文提供程序,而是传递了一个像中间件一样的自定义,它拦截了reducer 不关心的特殊操作。

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

【讨论】:

但是如何使用减速器开关从另一个组件重新加载我的配置文件?以前我已经传递了一个获取函数,该函数更改了顶层提供程序中的值。 我添加了一些示例,为子组件提供一种在父组件中重新加载数据的方法。这能回答你的问题吗? 是的,谢谢,当我添加休息时工作;重新加载案例! 你想避免使用useEffect(async () =&gt; )useEffect 中的第一个函数的return 语句用于清理,这总是会立即返回一个promise。这将在钩子生效时发出警告(并且可能是无操作)。 很好,内特!我忘记了清理功能。我更新了我的答案,在 useEffect 中不返回 Promise。【参考方案2】:

keep reducers pure 是一个很好的做法。它将使useReducer 更具可预测性并简化可测试性。随后的方法都将异步操作与纯 reducer 结合起来:

1。取dispatch之前的数据(简单)

asyncDispatch包裹原来的dispatch,让上下文向下传递这个函数:

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。为dispatch 使用中间件(通用)

dispatch 可以通过middlewares 增强,例如redux-thunk、redux-observable、redux-saga,以获得更大的灵活性和可重用性。或write your own 一个。

假设我们想要 1.) 使用 redux-thunk 获取异步数据 2.) 做一些日志记录 3.) 调用 dispatch 并得到最终结果。首先定义中间件:

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

然后编写一个自定义的useMiddlewareReducer Hook,您可以在这里看到useReducer 捆绑了额外的中间件,类似于 Redux applyMiddleware

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

中间件作为第一个参数传递,否则 API 与 useReducer 相同。对于实现,我们将 applyMiddleware source code 带到 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>

注意:我们将中间状态存储在 mutable refs - stateRef.current = reducer(...) 中,因此每个中间件都可以在使用 getState 调用时访问当前的最新状态。

要将确切 API 设为useReducer,您可以动态创建 Hook:

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>

更多信息 - 外部库: react-usereact-hooks-global-statereact-enhanced-reducer-hook

【讨论】:

在第一种方法中,由于dispatch 是异步的,它可能会在很久以后才完成动作。在我们开始获取数据之前,如何确保调度完成? @AdityaVerma 你不能不在这里增加更多的复杂性。但是为什么要降低用户的感知响应能力呢?通过 React 设计,异步处理阶段对开发人员是透明的。 dispatch 是按顺序执行的,所以你总是在 finished 之前得到 loading - 并且调度和纯 reducer 本身应该非常快,因为 .在最坏的情况下你看不到loading 如果您的 fetch 操作包含更新的状态,则解决方案 1 没有意义。赋予操作的状态将具有原始状态 - 因为更新过程正在异步执行。 研究这种方法我希望使用方法一,因为它是最简单的。我遇到的问题是 asyncDispatch 用于整个状态。如果您只想更新状态的一部分并需要执行异步获取但需要保留其他状态怎么办。【参考方案3】:

我对问题和可能的解决方案进行了非常详细的解释。 Dan Abramov 提出了解决方案 3。

注意:gist 中的示例提供了文件操作的示例,但可以为数据获取实现相同的方法。

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

【讨论】:

【参考方案4】:

我将 dispatch 方法包裹了一层来解决异步动作问题。

这里是初始状态。 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

【讨论】:

【参考方案5】:

更新:

我在下面的网络链接中添加了另一条评论。这是一个名为 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,并认为在这里发帖可能会很好,以防它对任何人有所帮助。

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

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

【讨论】:

对于什么是reducer存在误解。出于测试目的,它应该是一个没有副作用的纯函数。【参考方案6】:

这很简单 您可以在异步功能结果后更改 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])

【讨论】:

以上是关于React useReducer 异步数据获取的主要内容,如果未能解决你的问题,请参考以下文章

为啥 React 的 useReducer 在第一次在 useEffect 上获取数据时返回空状态?

useState & useReducer

在 React 中使用 useContext 和 useReducer 创建新的键值对

如何在 useReducer 钩子中发出异步服务器请求?

[react] 请说说什么是useReducer?

React中useReducer的理解与使用