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 article 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!