React State

1. What is State?

In React, state is a way to manage data that can change over time, and it is specific to each component. Unlike props (which are immutable and passed down from parent to child), state allows a component to maintain and update its own data.

While props are used to pass data from a parent component to a child, state is used for data that needs to be modified or interacted with within the component itself. For example, when a user interacts with a component by clicking a button or entering text, the component’s state can change, and React will re-render the component to reflect the updated state.

Let's take a look at an example of how state works in a React component:

class Drink extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'Coke',
      price: 1000,
      quantity: 10
    };
  }

  render() {
    const { name, price, quantity } = this.state;
    return (
      <div>
        <h3>{name}</h3>
        <p>Price: {price}</p>
        <p>Quantity: {quantity}</p>
      </div>
    );
  }
}

In the example above, we define a state with three properties: name, price, and quantity. These are the initial values set when the Drink component is created. The state is accessed via this.state.

2. State and Components

Each React component has its own isolated state, which means that one component’s state will not affect the state of another component unless explicitly shared via props or callback functions. When state changes within a component, that component will re-render to reflect the new state.

Let’s consider a VendingMachine component where multiple Drink components are rendered:

class VendingMachine extends React.Component {    
  ...
		render() {
        return (
            <SegmentGroup size='small'>
                <Segment inverted color='blue' floated='left'>
                    <SegmentGroup vertical>
                        <SegmentGroup horizontal>
                            <Drink/>
                            <Drink/>
                            <Drink/>
                            <Drink/>
                        </SegmentGroup>

Here, each Drink component has its own state, and each of those components will update independently when their respective state changes, via setState(). If the quantity in one Drink component changes, it does not affect the other Drink components.

3. Re-rendering

React uses a special method called setState() to update the state of a component. When the state is updated, React will trigger a re-render of that component to display the updated data.

Let’s see how setState() works in the context of a button click:

class Drink extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'Coke',
      price: 1000,
      quantity: 10
    };
  }

  buySomeDrink = () => {
    this.setState({
      quantity: this.state.quantity - 1
    });
  };

  render() {
    const { name, price, quantity } = this.state;
    return (
      <Segment>
          <Image src={sodaImage} size='mini' centered />
          <Card.Content>
              <Card.Header textAlign='center'>{name}</Card.Header>
              <Card.Meta textAlign='center'>₩ {price}</Card.Meta>
          </Card.Content>
          <Card.Content extra>
              <Card.Header textAlign='center'>
                  <Icon name='cubes'/><Label content={quantity}/>
                  <Button onClick={this.buySomeDrink}>Buy</Button>
              </Card.Header>
          </Card.Content>
      </Segment>
    );
  }
}

The code above calls the buySomeDrink() method when the button's onClick event is triggered. When the buySomeDrink() function is called, it updates the quantity field in the component's state (decreasing it by 1) and triggers a re-render using setState(). As a result, when the button event occurs, the changes in the Drink component are reflected.

However, when re-rendering, the updated data should be applied afterward, but there is no guarantee that the state field value will be updated with the new data after a method is executed. Although we might visually see that the method is called in sequence and the state is updated followed by re-rendering, setState() is called asynchronously, so it doesn't wait for the value-changing task to finish. This means that if there is a method that takes time to execute, the component will re-render with the previous state value before the state has been updated. Therefore, if you call setState() in a method and immediately log console.log(this.state) right after, it will log the previous state values, not the updated ones, which could potentially lead to unexpected issues.

To ensure the changes to the state are applied and reflected after re-rendering, you should write the code like this:

this.setState({ quantity: this.state.quantity - 1 }, () => {
  console.log('State updated', this.state.quantity);
});

By writing the parameters of setState() in the form of ({state field to be changed: value}, function), the state field quantity in the Drink component is first updated, and then the function is called. This allows the re-rendering to happen with the updated state field values.

Next, let's explore how multiple setState() calls are executed in order. First, the setState() function is called asynchronously. Therefore, it is not possible for developers to know which change will be processed and reflected first. So, even though multiple setState() methods may be declared, they will be called in the order of the code, but they will not return or process in order.

Let's write the code as follows to call the setState() functions sequentially:

this.setState((state, props) => {
  return {
    quantity: this.state.quantity - 1
  }
});
this.setState((state, props) => {
  return {
    price: this.state.price + 300
  }
});
this.setState((state, props) => {
  return {
    name: 'Fanta'
  }
});

By writing the code in this way, the setState() methods are processed in the order they are written. The parameters of setState() are written as anonymous functions, and the parameters of the anonymous function are the state and props of the component that contains it. This way, the next setState() method will not be called until the anonymous function inside the current setState() returns its value. The reason for this is that each setState() method queues the function to be executed in order, and it will block the next setState() call until the function inside returns its value.

4. The setState() Method and State Merging

The setState() function handles merging in two ways: Shallow Merge and Deep Merge. Shallow merge overwrites the existing state with the changes. Deep merge only updates the fields that have changed, leaving the rest of the state intact. Here are examples of both types of merging:

constructor(props) {
    super(props);
    this.state = {
        name: {
            product: 'Coke',
            manufacturer: 'Coca_cola'
        },
        price: 1000,
        quantity: 10
    };
}

// Shallow Merge
modifyProduct = () => {
    this.setState({
        name: { product: 'Fanta' }
    });
};
/* The state will be merged as follows:
    name: {
        product: 'Fanta',
    },
    price: 1000,
    quantity: 10
*/

...

// Deep Merge
modifyProduct = () => {
    this.setState({
        name: {
            product: 'Fanta',
            manufacturer: 'Coca_cola'
        }
    });
};
/* The state will be merged as follows:
    name: {
        product: 'Fanta',
        manufacturer: 'Coca_cola'
    },
    price: 1000,
    quantity: 10
*/
...

By default, setState() performs a Shallow Merge. In a shallow merge, when you change an internal object in the state, it is overwritten by the object passed to setState(). Only the merging object is updated; all other properties remain the same. In contrast, a Deep Merge does not overwrite the entire state but only replaces the changed fields, leaving the rest of the state intact.

5. What data should be managed in the state?

In React, data can be managed using either props or state. State is dynamic, meaning it can change and trigger re-renders of the component. Therefore, data that needs to trigger a re-render should be stored in the state. If a field’s data changes but there is no visible change during re-rendering, that data does not need to be stored in the state.

Here are examples of data that are good candidates for being managed in the state:

  • Data entered by the user (e.g., in a textbox, form field)
  • The current or selected item (e.g., the currently active tab, selected row in a table)
  • Data received from a server (e.g., a list of objects)
  • Dynamic state (e.g., whether a modal is open/closed, whether a sidebar is open/closed)
React + MobX
SPA 라이브러리인 React를 MobX state 관리 라이브러리와 함께 배워봅시다.

Reference

https://github.com/namoosori/react-blog