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