useReducer中第三个参数的作用是什么?

37

文档中可以得知:

[init,第三个参数]可让您将计算初始状态的逻辑从reducer中提取出来。这对于以后响应操作重置状态也非常方便。

而代码如下:

function init(initialCount) {
  return { count: initialCount };
}

function reducer(state, action) {
  switch (action.type) {
    ...
    case 'reset':
      return init(action.payload);
    ...
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  ...
}

相比于重复使用一个常量的 initialState ,为什么我要这样做呢?

const initialState = {
  count: 5,
};

function reducer(state, action) {
  switch (action.type) {
    ...
    case 'reset':
      return initialState;
    ...
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  ...
}

在我看来似乎更简练了。


如果你想知道为什么他们提供了一个看似无用的API,答案是懒加载。这只是一个很好的功能。而且既然useReducer钩子显然是从Redux中取出的,为什么不保持API的一致性呢? - hackape
是的,选择两种用法之一就取决于你了。我觉得文档写得很清楚,链接 - hackape
抱歉,我问的是,即使您设置了第三个参数,初始状态是否已设置?如果答案是否定的,我猜这就是您所说的惰性加载。也就是说,用户必须发送“重置”更新以设置初始计数,否则“计数”为null? - Paul Razvan Berg
1
你理解错了。情况1是useReducer(reducer, 0),那么初始计数器为0。情况2是useReducer(reducer, 7, n => 2 * n),那么初始计数器为14。你明白了吗? - hackape
我认为当你不确定时,最好建立一个代码库来测试这些事情。我有一个很好的资源可以给你。TNG-Hooks 阅读这个源代码。它将大大提高你对React Hooks的了解。 - hackape
显示剩余2条评论
4个回答

37

2020年7月修订: React文档现在对这个名为lazy initializer的参数有更好的解释。以其他方式使用此函数可能会导致未记录的影响而引起破坏性变化。下面的答案仍然有效。


根据我的实验,第三个参数作为init函数是initialState的转换器。

这意味着initialState不会被用作初始状态,而是用作init函数的参数。其返回值将成为真正的initialState。在useReducer初始化行中使用它可以避免使用大量参数。

/* Here is the magic. The `initialState` pass to 
 * `useReducer` as second argument will be hook
 * here to init the real `initialState` as return
 * of this function
 */
const countInitializer = initialState => {
  return {
    count: initialState,
    otherProp: 0
  };
};

const countReducer = state => state; // Dummy reducer

const App = () => {
  const [countState /*, countDispatch */] =
    React.useReducer(countReducer, 2, countInitializer);

  // Note the `countState` will be initialized state direct on first render
  return JSON.stringify(countState, null, 2);
}

ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


我认为这应该被标记为已接受。@developerKumar的陈述是错误的(虽然它确实可以工作,但是它是错误的),它不是一个初始操作,而是一个初始状态转换器。 - Alexander Kim
@AlexanderKim 如果您有兴趣,我已经升级了我的答案,以便更加明确。 - HollyPony

10

我的理解是,延迟初始化是为了特殊情况而设计的,即初始化状态的代码占用大量内存或CPU时间,因此开发人员希望将状态数据的范围限制在组件内部。

例如,如果您要设计一个PhotoPane组件,该组件保存用于编辑的高清晰度照片。

const PhotoPane = (props) => {
    const initialPixelData = loadPhoto(props.photoID);
    const [pixelData, dispatch] = useReducer(reducerFunc, initialPixelData);
    ...
}

上面的代码存在严重的性能问题,因为loadPhoto()被反复调用。如果您不想在每次组件渲染时重新加载照片,则直觉上的反应是将 loadPhoto(props.photoID) 移出组件。但这会导致另一个问题。您将不得不将所有照片加载到上下文或其他地方的内存中,这肯定会占用大量内存。

所以,现在是我们介绍懒惰初始化的时候了。请查看下面的代码。

const PhotoPane = (props) => {
    const init = (photoID) => loadPhoto(photoID);
    const [pixelData, dispatch] = useReducer(reducerFunc, props.photoID, init);
    ...
}

useReducer第一次调用时,init()函数仅会被执行一次。

实际上,useEffect()钩子也可以实现类似的结果。但是,懒加载仍然是最直接的解决方案。


7

useReducer接受一个可选的第三个参数initialAction。如果提供了,初始操作将在初始渲染期间应用。

例如:

function Counter({ initialCount }) {
  const [state, dispatch] = useReducer(reducer, initialState, {
    type: "reset",
    payload: initialCount
  });

正如您所看到的,第三个参数是指在初始渲染期间执行的初始操作。

例如:Codesandbox示例链接


1
我还是不明白,因为文档上说:它允许你在 reducer 之外提取计算初始状态的逻辑。这对于以后响应 action 重置状态也很方便。同时提供了一个函数作为示例。 - HollyPony
4
我认为这不是正确的。更多信息在此处 - https://reactjs.org/docs/hooks-reference.html#lazy-initialization - Niyas Nazar
这不是正确的,根据文档:您还可以懒惰地创建初始状态。为此,您可以将init函数作为第三个参数传递。初始状态将设置为init(initialArg)。https://reactjs.org/docs/hooks-reference.html#lazy-initialization - Brian Joseph Spinos

2

我认为理解useReducer的好方法是以useState作为例子,其中useState有一个初始值或惰性初始化器。

import { Dispatch, useReducer } from "react";
export function useStateUsingReducer<S>(initialState: S | (() => S)): [S, Dispatch<S>] {
  if (typeof initialState === "function") {
    return useReducer(
      (state: S, newState: S) => (Object.is(state, newState) ? state : newState),
      null as unknown as S,
      initialState as () => S
    );
  } else {
    return useReducer(
      (state: S, newState: S) => (equals(state, newState) ? state : newState),
      initialState
    );
  }
}

这个的更实用版本是使用深度比较,因为useState只能进行Object.is的比较。

import { equals } from "ramda";
import { Dispatch, useReducer } from "react";
export function useDeepState<S>(initialState: S | (() => S)): [S, Dispatch<S>] {
  if (typeof initialState === "function") {
    return useReducer(
      (state: S, newState: S) => (equals(state, newState) ? state : newState),
      null as unknown as S,
      initialState as () => S
    );
  } else {
    return useReducer(
      (state: S, newState: S) => (equals(state, newState) ? state : newState),
      initialState
    );
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接