Mark Skelton
Published on

Conquering Derived State

A couple weeks ago, my fellow front-end developers at Widen had a conversation regarding one of our shared React components which we were struggling to implement in one of our applications. While most React components are fully controlled (via props) or fully uncontrolled (internal state), this component used what is known as "derived state" which is internal state that is controlled to some degree by props. Although derived state might seem like a good solution, it often causes many more problems than it attempts to solve.

My goal in this article is not to convince you that derived state should be avoided. React has an excellent blog post on derived state which does a great job of explaining why derived state is an anti-pattern. The focus of this article will be about how to recognize derived state in your components and several alternatives to derived state.

Recognizing Derived State

Before I go any further, let me provide a few examples of derived state to help you start recognizing derived state patterns. I'll begin with class based components as they are easier to recognize than function based components.

class PartiallyControlledInput extends React.Component {
  state = {
    value: this.props.value,
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.value !== this.props.value) {
      this.setState({ value: nextProps.value })
    }
  }

  render() {
    return (
      <input
        onChange={(e) => this.setState({ value: e.target.value })}
        value={this.state.value}
      />
    )
  }
}

In this example, we have a component which is a partially controlled input field. The component manages its state internally but it also allows the parent component to update the state by changing the value prop. The key part of this example is componentWillReceiveProps which is the red flag indicating the component derives its state from props.

Because componentWillReceiveProps is being deprecated, React has a newer lifecycle method named getDerivedStateFromProps. This method almost needs no example as its name speaks for itself, but a quick example won't hurt.

class PartiallyControlledInput extends React.Component {
  state = {
    value: this.props.value,
    prevFormId: this.props.formId,
  }

  static getDerivedStateFromProps(props, state) {
    if (props.formId !== state.prevFormId) {
      return {
        prevFormId: props.formId,
        value: props.value,
      }
    }

    return null
  }

  render() {
    return (
      <input
        onChange={(e) => this.setState({ value: e.target.value })}
        value={this.state.value}
      />
    )
  }
}

As you can see, getDerivedStateFromProps gets even more complex as we are required to store a prop value in state so when that prop changes we can reset state based on props.

As we move on to function based components, derived state is a bit tougher to spot as you have to look for more than just componentWillReceiveProps or getDerivedStateFromProps.

function PartiallyControlledInput(props) {
  const [value, setValue] = useState(props.value)

  useEffect(() => {
    setValue(props.value)
  }, [props.value])

  return <input onChange={(e) => setValue(e.target.value)} value={value} />
}

As you can see, the previous example is a more subtle use of derived state. When props.value changes, an effect will run which will set the value of the input with the new prop value. This is essentially the same as the first example we showed above and suffers from the exact same problems. Just because it is written with hooks doesn't mean it is magically better than its class based equivalent!

Alternatives to Derived State

Now that we've seen a few examples of derived state, let's look at some alternative solutions.

Fully uncontrolled component with a key

The first option would be to make the component fully uncontrolled and use React's special key prop which will create a new instance of the component rather than updating the existing one. This includes re-initializing component state with the initial values provided.

function FullyUncontrolledInput({ initialValue }) {
  const [value, setValue] = useState(initialValue)

  return <input onChange={(e) => setValue(e.target.value)} value={value} />
}

To use the component, we simply need to provide an initialValue prop as well as a key prop. To re-initialize the internal state of the input, we simply need to change the key. In the following code block, we use a variable named formId which we can change when the form needs to be reset.

function Form() {
  return <FullyUncontrolledInput key={formId} initialValue="Initial" />
}

Fully controlled component The next option is to make the input fully controlled and let the parent component manage its state. This option will require us to add an onChange prop to the input component so the parent can listen to changes and update state accordingly.

function FullyControlledInput({ value, onChange }) {
  return <input onChange={(e) => onChange(e.target.value)} value={value} />
}

With our fully controlled input component, we now can add a simple useState hook to our form component which will manage the state of our input.

function Form() {
  const [value, setValue] = useState("Initial")

  return <FullyControlledInput onChange={setValue} value={value} />
}

Custom hooks!

While the previous examples sound like easy solutions, many real-world components are much more complex than those simple examples. In these situations, creating a custom hook might just be what you need to achieve a high level of customization without large boilerplate.

For example, consider a date picker component which accepts props for the currently selected day and month as well as event handlers when the user changes the day or month. Rather than making our form component manage all the state and event handlers, we can wrap that logic in a custom useDatePicker hook!

function useDatePicker(initialValues = { day: 1, month: "Jan" }) {
  const [month, setMonth] = useState(initialValues.month)
  const [day, setDay] = useState(initialValues.day)

  return {
    datePickerProps: {
      day,
      month,
      onDayChange: setDay,
      onMonthChange: setMonth,
    },
    day,
    month,
    setDay,
    setMonth,
  }
}

Now, we can very easily call the useDatePicker hook inside our form component and pass datePickerProps to the date picker component which will add all the necessary props for managing the state of the component.

function Form() {
  const { datePickerProps, day, month } = useDatePicker()

  useEffect(() => {
    // Do something when day or month changes
  }, [day, month])

  return <DatePicker {...datePickerProps} theme="dark" />
}

Because the hook isn't directly tied to the component, we can achieve a high level of customization. For example, if we want to trigger some side effect when the day changes, we can simply add a useEffect hook with a dependency on the day variable. Or, if we needed to customize the onDayChange logic, we could override the prop returned in datePickerProps with a custom onDayChange prop. We could even create a reset button in the form component which when clicked uses the setDay and setMonth functions to reset the date picker to the current day. The possibilities are endless, so use your imagination and come up with something great!