简体   繁体   中英

Can I update component state from props without useEffect?

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.

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?

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

编辑 dreamy-ellis-1086v1

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM