105

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

6 回答 6

83

这是一个有趣的案例,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 。

如果您需要将道具或状态传递给您的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>
  );
}
于 2018-11-05T00:48:55.657 回答
21

保持 reducer 纯净是一个好习惯。它将useReducer提高可预测性并简化可测试性。随后的方法都将异步操作与纯 reducer 结合起来:

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

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可以使用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。

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>

注意:我们将中间状态存储在可变 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-use , react-hooks-global-state,react-enhanced-reducer-hook

于 2020-05-20T21:31:53.640 回答
20

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

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

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

于 2019-03-12T10:19:44.557 回答
6

更新:

我在下面的网络链接中添加了另一条评论。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' })} />
}
于 2020-06-24T12:20:24.880 回答
5

我用层包裹了 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

于 2019-03-18T12:56:48.623 回答
0

非常简单,您可以在异步功能结果后更改 useEffect 中的状态

定义useState获取结果

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

useEffectsetResultFetch

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

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

}}, [resultFetch])
于 2021-04-24T11:23:25.407 回答