Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Can I avoid useEffect when siblings in a controlled component need to be kept in sync?

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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.

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

>Solution :

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.

Because 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 blue 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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading