React hooks: How do I update state on a nested object with useSt

ghz 6months ago ⋅ 86 views

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:

  1. Correct Approach to Updating State:
    • Use the spread operator (...) to shallow copy the style object.
    • For the font object, also use spread to copy its existing properties before updating align.
setStyle({
  ...style,
  font: {
    ...style.font,
    align: event.target.value
  }
});

In this approach:

  • { ...style } spreads all properties of style at the top level into a new object.
  • font: { ...style.font, align: event.target.value } spreads all properties of style.font into a new object, and then updates align with the new value.

This way, React correctly updates the align property while preserving other properties in style.font.

  1. 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:
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.