React hooks: How do I update state on a nested object with useState()?
I have a component that receives a prop that looks like this:
const styles = {
font: {
size: {
value: '22',
unit: 'px'
},
weight: 'bold',
color: '#663300',
family: 'arial',
align: 'center'
}
};
I'm trying to update the align
property, but when I try to update the object, I wind up replacing the whole object with just the align
property.
this is how I'm updating it:
const { ...styling } = styles;
const [style, setStyle] = useState(styling);
return (
<RadioButtonGroup
onChange={(event) => {
setStyle({ ...style, font: { align: event.target.value } });
console.log(style);
}}
/>);
When I console.log style
I just get {"font":{"align":"left"}}
back. I expected to see the whole object with the updated value for align
. I'm new to destructuring so what am I doing wrong here?
Answers
The issue you're encountering is due to how you're updating the state in React and managing nested objects. Let's break down the problem and how to solve it.
Problem Analysis
In your component, you have a state style
which contains an object font
nested within it. You want to update the align
property of the font
object when RadioButtonGroup
onChange event occurs. However, your current approach:
setStyle({ ...style, font: { align: event.target.value } });
leads to the entire font
object being replaced with { align: event.target.value }
, losing the other properties like size
, weight
, color
, and family
that were originally in styles.font
.
Solution
To update nested state correctly in React, especially when dealing with nested objects like styles.font
, you need to ensure you update state immutably, preserving the existing structure. Here's how you can achieve that:
- Correct Approach to Updating State:
- Use the spread operator (
...
) to shallow copy thestyle
object. - For the
font
object, also use spread to copy its existing properties before updatingalign
.
- Use the spread operator (
setStyle({
...style,
font: {
...style.font,
align: event.target.value
}
});
In this approach:
{ ...style }
spreads all properties ofstyle
at the top level into a new object.font: { ...style.font, align: event.target.value }
spreads all properties ofstyle.font
into a new object, and then updatesalign
with the new value.
This way, React correctly updates the align
property while preserving other properties in style.font
.
- Using Functional Updates (Optional):
- When updating state that depends on the previous state, it's recommended to use functional updates with
prevState
to avoid potential issues with stale state:
- When updating state that depends on the previous state, it's recommended to use functional updates with
setStyle(prevStyle => ({
...prevStyle,
font: {
...prevStyle.font,
align: event.target.value
}
}));
Using prevState
ensures you're always working with the latest state when updating asynchronously.
Full Example
Here’s how your component might look with these changes:
import React, { useState } from 'react';
const styles = {
font: {
size: {
value: '22',
unit: 'px'
},
weight: 'bold',
color: '#663300',
family: 'arial',
align: 'center'
}
};
const MyComponent = () => {
const { ...styling } = styles;
const [style, setStyle] = useState(styling);
return (
<RadioButtonGroup
onChange={(event) => {
setStyle(prevStyle => ({
...prevStyle,
font: {
...prevStyle.font,
align: event.target.value
}
}));
}}
/>
);
};
export default MyComponent;
Summary
To update nested objects in React state immutably, always spread the previous state and nested objects before updating specific properties. This approach ensures that React's state management remains predictable and avoids unintended side effects.