3

Is there a way to update a component's state from props without useEffect (and without unmounting the component)?

If you google "update component state from props" you will find lots of articles that describe updating component state from props in a useEffect. I find this unsatisfying for two reasons:

  1. semantically, this does not seem like an "effect" to me
  2. practically, this causes the component to render twice as often

When I originally asked this question one of the answers was to remove the useEffect and instead set the key property to the value of the parent state. This indeed achieved my goal without the useEffect. I find this similarly unsatisfying because:

  1. semantically, this is not what keys are for... my components identity does not change when the parent's state changes, so its key should not change
  2. practically, this causes the component to be unmounted every time the parent state changes

Here is a sandbox forked from the original accepted answer to this question which demonstrates all these issues:

https://codesandbox.io/s/confident-microservice-2jktc2

Note that when you update the WithEffect component it renders 4 times instead of just once. That's at least twice as often as I would like :D

Note also that when you update the parent component, the WithKey component's render count drops back to 1 because it has been unmounted.


Original Question:

Often I will have an input made of two inputs. For example, a slider with a number input, or a colour picker with a hex input. These components need to announce a change of state whenever the user is done manipulating them, but they need to inform each other of every change. Each needs to track changes in the other with a finer granularity than the parent.

For example, if the user drags the slider then the number input should represent the value of the slider at all times. When the user types in the number input, the slider should jump around and stay in sync. When the user releases the slider, then an onChange callback should be fired from the component so that the parent can update the state.

For clarity: in the example below, if the user clicks "up" on the left input 10 times I would like to see each change reflected on the left input but only exactly 1 change in the parent, when the component looses focus.

Below is some code which implements this behaviour. This code does exactly what I want it to do. The behaviour is 100% correct for my use case. However, I do not want to have a useEffect in this component. That's my question "how can I remove this useEffect?"

import "./styles.css";
import { useEffect, useState } from "react";

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

  // this is called whenever the user is "done" manipulating the compound input
  const handleChange = (change) => {
    console.log("change", change);
    setState(change);
  };

  return (
    <div className="App">
      <CompoundInput value={state} onChange={handleChange} />
    </div>
  );
}

function CompoundInput(props) {
  const [internalState, setInternalState] = useState(props.value);

  // this is a _controlled_ component, so this internal state
  // must also track the parent state
  useEffect(() => {
    setInternalState(props.value);
  }, [props.value]);

  // each input updates to reflect the state of the other input
  // but does so without the parent knowing
  return (
    <>
      <input
        type="number"
        value={internalState}
        onChange={(e) => setInternalState(e.target.value)}
        onBlur={(e) => props.onChange(e.target.value)}
      />
      <input
        type="number"
        value={internalState}
        onChange={(e) => setInternalState(e.target.value)}
        onBlur={(e) => props.onChange(e.target.value)}
      />
    </>
  );
}

https://codesandbox.io/s/compassionate-sun-8zc7k9?file=/src/App.js

I find this implementation frustrating because of the useState. My feeling is that when following this pattern, eventually every component needs a little useEffect to track the internal state for controlled components. My feeling is that useEffect should be used to implement effects and that this is not really an effect.

Ziggy
  • 21,845
  • 28
  • 75
  • 104
  • 1
    passing setState down to the child component, and call them whenever you onChange fired. then state will trigger re-render your component so you dont have to have useEffect. ,is this satisfy for your case? – KaraX_X Jan 12 '23 at 00:27
  • No. Maybe my question was not clear :/ the parent should _only_ update when `onBlur` is called. The `onChange` callback is used to keep the left and right inputs in sync while the user is updating them. If the user types 123 in the left input and then clicks outside, the right input will take the values 1, then 12, then 123... but the `onChange` callback will be called just once with `123`. Does that make sense? – Ziggy Jan 12 '23 at 00:29
  • I am not sure what you mean by `will take the values 1, then 12, then 123...` but here is what I tried https://codesandbox.io/s/pedantic-bash-7o4jwi – KaraX_X Jan 12 '23 at 00:39
  • ^^^ this is exactly the opposite behaviour to what I want :D please understand that the code in my question works _exactly_ the way I want it to. My entire question is "can I remove the useEffect and, if so, how?" Thanks for your effort though! – Ziggy Jan 12 '23 at 00:41
  • 1
    I don't see a need for the effect hook at all. If you change the parent `state`, the child should re-render because its props changed ~ https://codesandbox.io/s/dreamy-ellis-1086v1?file=/src/App.js – Phil Jan 12 '23 at 00:44
  • I don't want the parent state to change every time the child state changes, I want the parent state to change only and exactly when the use clicks outside or otherwise "stops editing". Sorry if this is not clear from my question, I'll try to update it! – Ziggy Jan 12 '23 at 00:44
  • 1
    @Ziggy isn't that exactly what happens in the link I added? Note that number inputs don't trigger the blur event when you use the arrow buttons – Phil Jan 12 '23 at 00:45
  • I'm just playing with it now, you might be right! – Ziggy Jan 12 '23 at 00:45
  • 1
    To directly answer you question... yes. Just remove the effect hook, it is redundant. I'm not sure if that's worthy of a full answer though – Phil Jan 12 '23 at 00:47
  • you can just delete it. this useEffect is not necessary in your logic. So, when you are fire onBlur your call `setState` and this will trigger re-render of your component because your component pass state as a props. – KaraX_X Jan 12 '23 at 00:50
  • I think you should undelete your answer and add this codesandbox link. What I've learned today is that I can get a `useState` to "re-initialize" to a new value if I use a `key`. I didn't know that, my understanding was that `useState` accepts an initial value and then further changes to that initial value are not reflected in `useState`. I'll read more about this, but I will accept and upvote your answer. – Ziggy Jan 12 '23 at 00:50

1 Answers1

1

To address some more recent updates to the question...

  1. semantically, this does not seem like an "effect" to me

Why not? The hook is handling the side-effect of the component props changing.

  1. practically, this causes the component to render twice as often

That's just <StrictMode>. See Why is useEffect running twice?

Edit immutable-pine-jr0bkm


Original answer

Is there a way to implement this component without a useEffect?

Absolutely, just omit it and use a key prop to track parent state changes.

If your component has the state value as a key, it will re-render if the parent changes that state.

Internally, it will maintain its own state and only communicate that change back up when the blur event triggers the onChange prop function.

This example demonstrates it more clearly

import { useState } from "react";

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

  const handleChange = (change) => {
    console.log("change", change);
    setState(change);
  };

  return (
    <div className="App">
      <fieldset>
        <legend>App state</legend>
        <input
          type="number"
          onChange={(e) => handleChange(e.target.value)}
          value={state}
        />
        <pre>state = {state}</pre>
      </fieldset>
      <fieldset>
        <legend>CompoundInput</legend>
        <CompoundInput value={state} onChange={handleChange} key={state} />
      </fieldset>
    </div>
  );
}

function CompoundInput({ value, onChange }) {
  const [internalState, setInternalState] = useState(value);

  return (
    <>
      <input
        type="number"
        value={internalState}
        onChange={(e) => setInternalState(e.target.value)}
        onBlur={(e) => onChange(e.target.value)}
      />
      <pre>internalState = {internalState}</pre>
    </>
  );
}

Edit dreamy-ellis-1086v1

See https://reactjs.org/docs/lists-and-keys.html#keys

Phil
  • 157,677
  • 23
  • 242
  • 245
  • Am I right that this works because react is not just re-rendering but actually re-mounting the entire component whenever the key changes? I think this is probably not what I want in the long-run, despite being a correct answer to my question as stated. It's a cool thing to know though so thanks for the answer! – Ziggy Jan 12 '23 at 04:53
  • I'm aware of strict mode. My understanding is that a use effect calling setState will cause the component to render again. First the component renders, putting the effect callback on the effect queue. Then the effect queue resolves, updating the state. Then the component renders again (because state changed) and this time the effect's deps have not changed so that's the end of it. Is this wrong? – Ziggy Jan 13 '23 at 16:06
  • According to the wikipedia article you linked: "In computer science, an operation, function or expression is said to have a side effect if it _modifies_ some state variable value(s) _outside_ its local environment". That matches my understanding of side-effect. Under that definition responding to change in props by mutating own state isn't a side effect, unless you suggesting that state is "outside" a component's local environment, so that any change to state is an effect? – Ziggy Jan 13 '23 at 16:10
  • (P.S. I really appreciate your time and attention! This kind of back and forth is really helpful. I'm sorry if some of the stuff I'm stuck on seems obvious to you) – Ziggy Jan 13 '23 at 16:12