What is the difference between initialization a useState inside a useEffect?

const [state, set_state] = useState(1);

vs

const [state, set_state] = useState();
useEffect(() => {
  set_state(1);
}, [])

Is there any real difference between the two in terms of in what cycle the update actually happens? Does using a useEffect cause an additional cycle, etc?

Upvotes: 0

Views: 241

Answers (1)

jsejcksn
jsejcksn

Reputation: 33691

You're probably looking for the lazy initial state functional argument:

Lazy initial state

The initialState argument is the state used during the initial render. In subsequent renders, it is disregarded. If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

But rather than just supply a link, let's examine the different behaviors of the code you showed (in addition to the lazy initial state functional argument).

A note about the code examples below:

They use a custom helper hook to record information about when component renders occur and when state values are updated. The outputs are serialized as JSON, so when a state value is undefined, it is represented as null in the JSON (because undefined is not a valid JSON type). Because of the restrictions of Stack Overflow's code snippet mechanism, there's a lot of repeated boilerplate — the code in focus is in the body of the App function.

Creating state initialized with a value:

const [state, setState] = useState(1);

In this case, the state is initialized once and never updated. The component is rendered one time:

<style>.json { background-color: hsla(0, 0%, 50%, 0.15); font-family: monospace; font-size: 1rem; padding: 0.5rem; }</style>
<div id="root"></div><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js"></script><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.development.js"></script><script src="https://cdn.jsdelivr.net/npm/@babel/[email protected]/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">

const { useCallback, useRef, useState } = React;

function VisualJson ({ data }) {
  const json = JSON.stringify(
    data,
    // undefined doesn't serialize, so null is used as a substitute:
    (_key, value) => typeof value === "undefined" ? null : value,
    2,
  );
  return (<pre className="json"><code>{ json }</code></pre>);
}

function useComponentUpdateMeta (state) {
  const ref = useRef({
    updates: [],
    get totalRenders () {
      return this.updates.filter(u => u.type === "render").length;
    },
    get totalStateUpdates () {
      return this.updates.filter(u => u.type === "state").length;
    },
  });

  ref.current.updates.push({ type: "render", time: performance.now(), state });

  const recordStateUpdate = useCallback(state => {
    ref.current.updates.push({ type: "state", time: performance.now(), state });
  }, [ref]);

  return [ref.current, recordStateUpdate];
}

function App () {
  const [state, setState] = useState(1);

  const [updateMeta, recordStateUpdate] = useComponentUpdateMeta(state);

  return (<VisualJson data={ updateMeta } />);
}

const reactRoot = ReactDOM.createRoot(document.getElementById("root"));

reactRoot.render(<App />);

</script>

Creating state uninitialized, then updating in a useEffect callback:

const [state, setState] = useState();

useEffect(() => {
  setState(1);
}, []);

In this case, the state is initialized with an implicit undefined value and the component is rendered. Then the useEffect hook's callback function is invoked and the state is updated, causing the component to render a second time:

<style>.json { background-color: hsla(0, 0%, 50%, 0.15); font-family: monospace; font-size: 1rem; padding: 0.5rem; }</style>
<div id="root"></div><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js"></script><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.development.js"></script><script src="https://cdn.jsdelivr.net/npm/@babel/[email protected]/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">

const { useCallback, useEffect, useRef, useState } = React;

function VisualJson ({ data }) {
  const json = JSON.stringify(
    data,
    // undefined doesn't serialize, so null is used as a substitute:
    (_key, value) => typeof value === "undefined" ? null : value,
    2,
  );
  return (<pre className="json"><code>{ json }</code></pre>);
}

function useComponentUpdateMeta (state) {
  const ref = useRef({
    updates: [],
    get totalRenders () {
      return this.updates.filter(u => u.type === "render").length;
    },
    get totalStateUpdates () {
      return this.updates.filter(u => u.type === "state").length;
    },
  });

  ref.current.updates.push({ type: "render", time: performance.now(), state });

  const recordStateUpdate = useCallback(state => {
    ref.current.updates.push({ type: "state", time: performance.now(), state });
  }, [ref]);

  return [ref.current, recordStateUpdate];
}

function App () {
  const [state, setState] = useState();

  const [updateMeta, recordStateUpdate] = useComponentUpdateMeta(state);

  useEffect(() => {
    setState(1);
    recordStateUpdate(1);
  }, []);

  return (<VisualJson data={ updateMeta } />);
}

const reactRoot = ReactDOM.createRoot(document.getElementById("root"));

reactRoot.render(<App />);

</script>

Creating state initialized with a lazily-invoked function:

const [state, setState] = useState(() => 1);

Similar to the first example: in this case, the state is initialized once (using a function whose return value becomes the initial state value) and never updated. The component is rendered one time:

<style>.json { background-color: hsla(0, 0%, 50%, 0.15); font-family: monospace; font-size: 1rem; padding: 0.5rem; }</style>
<div id="root"></div><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js"></script><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.development.js"></script><script src="https://cdn.jsdelivr.net/npm/@babel/[email protected]/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">

const { useCallback, useRef, useState } = React;

function VisualJson ({ data }) {
  const json = JSON.stringify(
    data,
    // undefined doesn't serialize, so null is used as a substitute:
    (_key, value) => typeof value === "undefined" ? null : value,
    2,
  );
  return (<pre className="json"><code>{ json }</code></pre>);
}

function useComponentUpdateMeta (state) {
  const ref = useRef({
    updates: [],
    get totalRenders () {
      return this.updates.filter(u => u.type === "render").length;
    },
    get totalStateUpdates () {
      return this.updates.filter(u => u.type === "state").length;
    },
  });

  ref.current.updates.push({ type: "render", time: performance.now(), state });

  const recordStateUpdate = useCallback(state => {
    ref.current.updates.push({ type: "state", time: performance.now(), state });
  }, [ref]);

  return [ref.current, recordStateUpdate];
}

function App () {
  const [state, setState] = useState(() => 1);

  const [updateMeta, recordStateUpdate] = useComponentUpdateMeta(state);

  return (<VisualJson data={ updateMeta } />);
}

const reactRoot = ReactDOM.createRoot(document.getElementById("root"));

reactRoot.render(<App />);

</script>


One more consideration is this: when using Strict Mode in development, React will unmount and re-mount your entire app's tree of nodes, effectively causing an extra render.

References:

<style>.json { background-color: hsla(0, 0%, 50%, 0.15); font-family: monospace; font-size: 1rem; padding: 0.5rem; }</style>
<div id="root"></div><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js"></script><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.development.js"></script><script src="https://cdn.jsdelivr.net/npm/@babel/[email protected]/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">

const { StrictMode, useCallback, useEffect, useRef, useState } = React;

function VisualJson ({ data }) {
  const json = JSON.stringify(
    data,
    // undefined doesn't serialize, so null is used as a substitute:
    (_key, value) => typeof value === "undefined" ? null : value,
    2,
  );
  return (<pre className="json"><code>{ json }</code></pre>);
}

function useComponentUpdateMeta (state) {
  const ref = useRef({
    updates: [],
    get totalRenders () {
      return this.updates.filter(u => u.type === "render").length;
    },
    get totalStateUpdates () {
      return this.updates.filter(u => u.type === "state").length;
    },
  });

  ref.current.updates.push({ type: "render", time: performance.now(), state });

  const recordStateUpdate = useCallback(state => {
    ref.current.updates.push({ type: "state", time: performance.now(), state });
  }, [ref]);

  return [ref.current, recordStateUpdate];
}

function App () {
  const [state, setState] = useState();
  const [updateMeta, recordStateUpdate] = useComponentUpdateMeta(state);

  useEffect(() => {
    setState(1);
    recordStateUpdate(1);
  }, []);

  return (<VisualJson data={ updateMeta } />);
}

const reactRoot = ReactDOM.createRoot(document.getElementById("root"));

reactRoot.render(
  <StrictMode>
    <App />
  </StrictMode>
);

</script>

Upvotes: 1

Related Questions