36

From the docs:

[init, the 3d argument] lets you extract the logic for calculating the initial state outside the reducer. This is also handy for resetting the state later in response to an action.

And the code:

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

Why would I do that over reusing a constant 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);
  ...
}

Looks less verbose to me.

Paul Razvan Berg
  • 16,949
  • 9
  • 76
  • 114
  • If you are asking why they provide a seemingly useless API, the answer is lazy initialization. It’s just a nice-to-have feature. And since the useReducer hook is apparently taken from Redux, why wouldn’t they keep API consistent. – hackape Nov 25 '19 at 23:16
  • Gotcha. But isn't the state initialised with the default values anyway? `initialCount` is still present as the 2nd argument. – Paul Razvan Berg Nov 25 '19 at 23:19
  • Yep it’s up to you to pick either one of 2 usages. I think the doc is pretty clear, [link](https://reactjs.org/docs/hooks-reference.html#usereducer) – hackape Nov 25 '19 at 23:21
  • Sorry, what I asked is whether the initial state is set even if you set the 3rd argument? If the answer is no, I guess this is what you mean by lazy loading. that is, the user must dispatch a `reset` update for the initial count to be set, otherwise the `count` is null? – Paul Razvan Berg Nov 25 '19 at 23:29
  • 1
    You get it wrong. Case 1, `useReducer(reducer, 0)` then init count is 0. Case 2 `useReducer(reducer, 7, n => 2 * n)`, then init count is 14. Is that clear? – hackape Nov 25 '19 at 23:40
  • I think you should probably setup a code base to test these sorts of things out when unsure. I got a good source for you. [TNG-Hooks](https://github.com/getify/TNG-Hooks/blob/master/src/tng-hooks.src.js) read this source code. It’ll greatly improve you knowledge about react hooks. – hackape Nov 25 '19 at 23:49
  • Thanks @hackape the example with `n => 2 * n` clarified it – Paul Razvan Berg Nov 26 '19 at 00:20

4 Answers4

36

EDIT july 2020: React documentation has now better explainations on this arg called lazy initializer. Using this function in another way could result to breaking changes due to undocumented effect. Following answer remain valid.


As far as I can experiment, the init function as third arg is a transformer of the initialState.

It means that initialState will not be used a initial state, but as arg for init function. The return of this one will be the true initialState. It could be usefull to avoid huge param during the useReducer initialization line.

/* 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>
Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89
HollyPony
  • 817
  • 9
  • 15
  • This should be marked as accepted, imo. @developerKumar's statement is wrong (it does work, but it's wrong), it's not an initial action, but an initial state transformer, indeed. – Alexander Kim May 03 '20 at 08:22
  • @AlexanderKim if you're interested I've upgrade my answer in order to be more explicit – HollyPony May 12 '20 at 10:30
10

My understanding is that the lazy initialization is designed for special situations that the code initializing the state is memory-intensive or CPU-intensive so the developer wants to keep the scope of the state data inside the component.

For example, if you are going to design a PhotoPane component which holds a high definition photo for editing.

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

Above code has a serious performance issue because loadPhoto() is repeatedly called. If you don't want to load the photo again every time when the component renders, the intuitive reaction is to move the loadPhoto(props.photoID) out of the component. But it will cause another problem. You will have to load all photos into memory in Context or somewhere else and it will definitely create a memory hog.

So this is the time for us to introduce lazy initialization. Please check out the code below.

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

The init() function is executed exactly only one time when the useReducer is first called.

Actually useEffect() hook can achieve a similar result. But lazy initialization is still the most direct solution.

George Lei
  • 467
  • 5
  • 5
7

useReducer accepts an optional third argument, initialAction. If provided, the initial action is applied during the initial render.

For example:

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

As you can see, the third parameter is an initial action to be performed, applied during the initial render.

For Example: Codesandbox Example Link

Artur Carvalho
  • 6,901
  • 10
  • 76
  • 105
developerKumar
  • 1,706
  • 10
  • 14
  • 1
    I still don't understand since the doc says : `It lets you extract the logic for calculating the initial state outside the reducer. This is also handy for resetting the state later in response to an action` . With a function as example. – HollyPony Apr 16 '20 at 07:59
  • 4
    I don't think this is right.More info here - https://reactjs.org/docs/hooks-reference.html#lazy-initialization – Niyas Nazar Jul 14 '20 at 10:41
  • This is not correct, from documentation: You can also create the initial state lazily. To do this, you can pass an init function as the third argument. The initial state will be set to init(initialArg). https://reactjs.org/docs/hooks-reference.html#lazy-initialization – Brian Joseph Spinos Mar 05 '22 at 03:09
2

I think a good way of understanding useReducer is to useState as an example where useState has an initial value or lazy initializer.

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

And a more practical version of this one is to do deep equals as useState only goes as far as 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
    );
  }
}
Archimedes Trajano
  • 35,625
  • 19
  • 175
  • 265