React State Management Basics

CodeJourney.net - React state management basics

If I were to point out a single concept that is critical to understand in React development, it would be state management.

So what is React state management? How is it different from managing data in C# or Java? Let’s find out! 🙂

State

If you are a backend or desktop developer, you may have never come across the term of state. At least, I never used it before learning frontend web development. Surely not in the context it is used in frameworks like React. The term more familiar to me at that time was data binding known from WPF or WinForms.

This is what WPF documentation says:

Data binding is the process that establishes a connection between the app UI and the data it displays. If the binding has the correct settings and the data provides the proper notifications, when the data changes its value, the elements that are bound to the data reflect changes automatically

In case of WPF, the crucial part to me is If the binding has the correct settings and the data provides the proper notifications (…). If you have ever worked with WPF, you know how frustrating can these notifications be (any INotifyPropertyChanged fans here?).

WPF’s state management it not that bad. However, React has something better 😉

React introduces us to a concept of state. In React components, you do not set anything directly, like calling some code to update an input’s value (this is possible, we’re still in JavaScript world, but it’s not a React way of doing stuff). You don’t directly bind your UI controls with data like you would in WPF or WinForms. Instead, you keep the data in memory, which is a current state of the UI (the whole app or a piece of it).

When the state changes, your UI is updated by React. In other words – in React, UI is a function of state:

UI = fn(state)

It means that your React app (or its individual pieces called components) take some state and output the actual, rendered UI.

This is React state management in a nutshell. Let’s explore it a bit more 😉

Small disclaimer: I will focus on state in React functional componets. I consider class components somewhat legacy. I think this meme summarizes it best 😀:

The Naive Way

Let’s try to approach React state management as C# developer. In React, the basic piece of UI we create is a component. Let’s add one:

import Button from "react-bootstrap/Button";

export const NaiveComponent = () => {
return (
<>
<div>I am a naive component</div>
<Button variant="success">Click me!</Button>
</>
);
};
NaiveComponent.tsx

For now, we render some text and a button which does nothing. What we want is to have some data displayed, like a number, which is incremented on every button click. Let’s try to implement that naively:

export const NaiveComponent = () => {
let myCount = 0;

return (
<>
<div>I am a naive component</div>
<div>Current count: {myCount}</div>
<Button
variant="success"
onClick={() => {
myCount++;
}}
>
Click me!
</Button>
</>
);
};
NaiveComponent.tsx – the naive counter incrementing

Now, let’s check if it works:

It looks nothing happens 🥹 To investigate that, we can add a console.log (the most common debugging technique in JavaScript world 😅) to the onClick event:

onClick={() => {
myCount++;
console.log(myCount);
}}
onClick with console.log

How does that look in the console now?

React state management - naive way (like C# dev), not working

Woooow, something is really going wrong here! The data is updated in memory, but not reflected in the UI 🤔

Why does that happen? Because myCount is not a state. It’s just some variable we created hoping that it will work fine as part of the UI. Remember what we have discussed in the previous section – we need to start thinking in React. This is The React way, not Java way or C# way anymore.

The React Way

So, how to use React state management properly in our example? We need to make myCount be a part of component’s state. The simplest way to do that looks as follows:

export const TheReactWayComponent = () => {
const [myCount, setMyCount] = useState(0);

return (
<>
<div>I am a React Way component</div>
<div>Current count: {myCount}</div>
<Button
variant="success"
onClick={() => {
setMyCount(myCount + 1);
}}
>
Click me!
</Button>
</>
);
};
TheReactWayComponent.tsx – The React Way

First, instead of declaring a “normal” variable for storing our UI-related data, we used the useState hook. As you can see, it returns an array with two things: our state variable (myCount) and a function to update its value (setMyCount). We also set the initial value to 0. A good practice is to always use a construct like [myCount, setMyCount] to destructure what useState returns into named, separate objects.

Now we are managing our component’s state The React Way. Does it work? Let’s see:

React state management - The React Way

Great! We have just learned how the basic form of state management works in React. That was easy, wasn’t it? 😉

Now, let us discuss one more aspect related to the state itself.

Reference vs value

We know the React Way for state management now. We feel confident. However, we are also used to how data is managed in C# or Java, especially in terms of handling reference and value types. This knowledge will come very handy now 🙂

Let’s try to create a bit more complex component:

type Person = {
id: number;
name: string;
age: number;
};

export const ObjectHolderComponent = () => {
const johnInitialValue: Person = {
id: 1,
name: "John Doe",
age: 30,
};

const [john, setJohn] = useState<Person>(johnInitialValue);

return (
<>
<div>ID: {john.id}</div>
<div>Name: {john.name}</div>
<div>Age: {john.age}</div>
<Button
variant="success"
onClick={() => {
const currentJohnObject = john;
currentJohnObject.age = currentJohnObject.age + 1;
console.log(currentJohnObject);
setJohn(currentJohnObject);
}}
>
Make John older
</Button>
</>
);
};
ObjectHolderComponent.tsx

Now, instead of storing a single value in our component’s state, we store a whole object of type Person. The object has a few properties: id, name and age.

There is also a button. We want this button to take the current john object, increment the value of age on it and update the state. We do it The React Way using setJohn function returned by useState. Just to be sure, we also have the console.log(john) just before updating the state. Does that work as we expected?

Changing an object's (reference) internal variable does not trigger a re-render, even we use useState

Eh, what’s happening here this time? We can clearly see that age is updated on the object on each click, but why isn’t the UI re-rendered? 🤔

As I mentioned before, it’s critical to understand the difference between reference and value types here. If you don’t know it, go and make up for it now.

The reason it does not work is that we haven’t updated the actual content of john state variable by calling setJohn(currentJohnObject). What is really being hold in john variable is the reference (pointer, address in memory) to the actual object. And this is what React tracks. We didn’t really update it, because we assigned the same instance of the object to the state. It doesn’t matter that we updated its internal property age – the reference itself wasn’t updated.

How do we solve that? Well, in current solution we’d need to create a copy of john object before using setJohn to alter the state. In effect, we’ll get a new instance of the object in memory and john variable will hold a new reference (address) to it.

So, if we only change our onClick implementation to this one:

onClick={() => {
const johnCopy = { ...john };
johnCopy.age = john.age + 1;
setJohn(johnCopy);
}}
ObjectHolderComponent.tsx – onClick implementation with copying the original object

Everything starts to work as expected:

Changing a reference state by creating a copy of it works fine in React

I think you now understand how React state management works. I also hope that you see the issues managing state in such a way can bring. Even in this trivial example, we create a new instance of an object on every button click. Apart from memory usage considerations, this just looks ugly. Imagine manually creating copies of much more complex objects, like classes storing arrays or dictionaries. This gets pretty wild 🤪

We will see how this issue can be addressed by the end of this article. But first, let’s discuss props – another concept critical to really understand React state management.

Props

Apart from state, we also use props to control the state of our React components. The name is a short for properties, which quite well describes its purpose. Props are the data input provided to a component from outside (from a parent component). They are immutable and cannot be changed inside the component which receives them. Let’s see some examples.

Sharing state between components

The most common usage for props is sharing state between components. Imagine that you have an input, where the user manually enters some data. You want to use the current value of this input in two other components. This is where you need to share your component’s state (the value of the input) with these two other components. We often call it lifting the state up – you keep your state higher in the components tree, so the child components can use it.

Let’s try to see an example of connecting state with props:

export const PersonData = () => {
const minimumAge = 5;
const maximumAge = 100;
const [age, setAge] = useState<number>(minimumAge);

const handleAgeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const enteredAge = parseInt(event.target.value);
if (enteredAge >= minimumAge && enteredAge <= maximumAge) {
setAge(enteredAge);
}
};

return (
<>
<Form>
<Form.Group controlId="age">
<Form.Label>Your age</Form.Label>
<Form.Control
type="number"
value={age}
onChange={handleAgeChange}
min={minimumAge}
max={maximumAge}
/>
</Form.Group>
</Form>
<DaysLiving currentAge={age} />
<YearsUntilCentenarian currentAge={age} />
</>
);
};
PersonData.tsx – state and props in practice

As you can see, at lines 4 and 9 we manage the state variable age. This is the old stuff we have already seen.

However, at lines 27 and 28 we are passing the age state variable into DaysLiving and YearsUntilCentenarian children components as currentAge prop. This is how the DaysLiving component is implemented:

type DaysLivingProps = {
currentAge: number;
};

export const DaysLiving = (props: DaysLivingProps) => {
const daysLived = props.currentAge * 365;

return (
<div>
<p>You have lived for {daysLived} days.</p>
</div>
);
};
DaysLiving.tsx

Notice that props is simply a typed object you pass to a function component. To make things easier, you can also use object destructuring and accept the props in the following way:

export const DaysLiving = ({currentAge}: DaysLivingProps) => {
const daysLived = currentAge * 365;
/// ... rest of the code
DaysLiving.tsx – props with object destructuring

That way, you don’t need to write props.currentAge every time you want to use this property inside the component.

Does changing props always trigger a re-render?

Well, generally the answer to this question is yes. Every time a prop of a component changes, this component will get re-rendered. However, this is not entirely true 🤪

To make this belief true, we need to assume that this prop is changed in a React way. So, to take things literally, consider this component we saw before:

export const DaysLiving = ({currentAge}: DaysLivingProps) => {
const daysLived = currentAge * 365;

return (
<div>
<p>You have lived for {daysLived} days.</p>
</div>
);
};
DaysLiving.tsx

From its perspective only, it accepts a currentAge property. For this component to re-render, it’s not enough that the currentAge property changes in any way. Its value must be changed by mutating the state in the parent component.

So, if the value provided as currentAge to DaysLiving component changes in the parent component by properly mutating the state (with setAge mutation callback, in our case):

export const PersonData = () => {
// ...
const [age, setAge] = useState<number>(minimumAge);

const handleAgeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// ...
setAge(enteredAge);
}
};

return (
<>
// ...
<DaysLiving currentAge={age} />
// ...
</>
);
};
PersonData.tsx – mutating the state for children components

the child component (DaysLiving) will get re-rendered.

This is because when a parent component changes, React by default re-renders the parent component itself and all of its children.

If you modified the age variable in the parent compont directly, without using setAge (which we already know it’s incorrect), the currentAge prop would technically change, but the DaysLiving component will not get re-rendered.

state vs props

This is a question that people oftern ask: what’s the difference between props and state in React? 🤔

I would start with addressing this differently: what do props and state have in common?

As you already know, both are used for React state management. Both of them influence when the components will be re-rendered by React. Props and state are the absolute foundations of React.

The main difference is that state is used internally in a component, while props are used to pass information from parent to children components. State is also naturally mutable within a component where it’s defined, but props are immutable and cannot be changed in a component that receives and uses them.

React state management libraries

That’s basically everything you need to know to be able to comfortably work with React state management. However, I mentioned before that managing state using only built-in React mechanisms actually sucks 🤪 That’s why I’d like to make a short point on state management libraries.

Maybe you heard about Redux, Zustand or MobX. All of them are state management libraires for React. But why would you even need one? And should you learn one now?

In my opinion, you can easily start working with React using the built-in state management mechanisms we discussed today. It’s good to know how it works in practice. However, the truth is that as your application grows and you want to keep it scalable and maintenable, you will need a state management library at some point. As soon as your components tree gets bigger, sharing data and state will not be as easy as “passing props to a children component”. You will need to share state between multiple components in the tree, sometimes not only between parent and children, but also with another components defined on the same level or even in a totally different place of the tree.

This is where React state management libraries come very handy. They solve many of the React’s issues that come to the surface as your state gets complex. But even if you use one, you will still always need state and props. That’s why it’s critical to understand these two basic concepts first and play with them for some time.

If you’re starting out, it’s good to know that state management libraries exist and that one day you will have to get your hands on one (or more 😉) of them. I hope to share my thoughts on state management libraries in a separate article one day.

Summary

That’s it! Now you should know how React manages state of its components. Of course, we didn’t cover everything related to state management in React, but I hope you got the basics 😉

You can find all of the source code used in this article here.

How do you manage state in your React apps? Do you use a state management library? If yes, which one and why? Let me know in the comments below!

.NET full stack web developer & digital nomad
4.3 3 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments