react useReducer 异步数据获取

IT技术 reactjs react-hooks
2021-04-06 02:05:08

我正在尝试使用新的 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 }
    }
  }
}
6个回答

这是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 或 state 传递给您的reloadProfile函数,您可以通过将第二个参数调整为useEffect(示例中的空数组)来实现,以便它仅在需要时运行。您需要检查先前的值或实施某种缓存以避免在不必要时进行提取。

更新 - 从孩子重新加载

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

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>
  );
}
我添加了一些示例,为子组件提供了一种在父组件中重新加载数据的方法。这回答了你的问题了吗?
2021-06-01 02:05:08
是的,谢谢,当我添加休息时工作;重新加载案例!
2021-06-01 02:05:08
您想避免使用useEffect(async () => {})第一个函数的 return 语句useEffect用于清理,这将始终立即返回一个Promise。当钩子处于活动状态时,这将发出警告(并且可能是空操作)。
2021-06-09 02:05:08
好抓住内特!我忘记了清理功能。我更新了我的答案,不在 useEffect 中返回 Promise。
2021-06-09 02:05:08
但是如何使用减速器开关从另一个组件重新加载我的配置文件?以前我已经传递了一个获取函数,该函数在顶层更改了 provider 中的值。
2021-06-20 02:05:08

保持 reducers 纯净是一个很好的做法它将使useReducer更可预测并简化可测试性。后续方法都将异步操作与纯减速器相结合:

1.取数据之前dispatch(简单)

裹原来dispatchasyncDispatch,让背景下通过这个功能下降:

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

2. 使用中间件dispatch(泛型)

dispatch可以使用redux-thunkredux-observableredux-saga中间件进行增强,以获得更大的灵活性和可重用性。或者自己写一个。

比方说,我们想要 1.) 获取异步数据redux-thunk2.) 做一些日志记录 3.) 调用dispatch最终结果。首先定义中间件:

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

然后编写一个自定义useMiddlewareReducerHook,您可以在此处看到useReducer它与其他中间件捆绑在一起,类似于 Redux applyMiddleware

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

中间件作为第一个参数传递,否则 API 与useReducer. 对于实现,我们获取applyMiddleware 源代码并将其转移到 React Hooks。

注意:我们将中间状态存储在可变 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);
}

更多信息 - 外部库: react-use , react-hooks-global-state,react-enhanced-reducer-hook

@AdityaVerma 你不能不在这里增加更多的复杂性。但是为什么会降低用户的感知响应能力呢?通过 React 设计,异步处理阶段对开发人员是透明的。dispatch按顺序执行,所以你总能获得loadingfinished-和调度和纯减速本身应该是非常快的,因为。在最坏的情况下,您看不到loading.
2021-05-30 02:05:08
如果您的提取操作包含更新的状态,则解决方案 1 没有意义。赋予操作的状态将具有原始状态 - 因为更新过程正在异步执行。
2021-06-07 02:05:08
在第一种方法中,由于dispatch是异步的,它可能会在很晚之后完成操作。在我们开始获取数据之前,如何确保调度完成?
2021-06-17 02:05:08

我写了一个非常详细的问题解释和可能的解决方案。Dan Abramov 建议了解决方案 3。

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

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

我用一层包裹了dispatch方法来解决异步action的问题。

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

更新:

我在下面的网络链接中添加了另一条评论。这是一个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>;
}

旧解决方案:

我刚刚在此处发布了此回复并认为也可以在此处发布,以防它对任何人有所帮助。

我的解决方案是模拟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' })} />
}
关于什么是减速器存在误解。为了测试的目的,它应该是一个没有副作用的纯函数。
2021-06-08 02:05:08