Understanding React Components and StateUnderstanding React Components and State

Getting comfortable with these React concepts will help you on your journey to mastering this JavaScript library.

October 17, 2019

This article specifically focuses on how to deal with components and state because that seems to be the biggest hurdle to learning React after syntax. You can do it!

If you’re just getting started with React, make sure to check out my other resources first:

Why use components at all?

Why use a system to manage state and not just whatever I like?

  • They provide a sensible way to structure your app.
  • They solve collaboration with others (and your future self).

Remember when you were writing cool projects with jQuery, Flash or Framer Classic, just hooking up a few event handlers and things started to respond? Lots of fun! You quickly add a bunch of other ideas on top and…hmm isn’t this supposed to update on that click?

Ah, got it, easy fix, let’s add another few screens. Wow, more than a hundred lines of code already, I must be really having fun! But wait… where was that thing where I stored the value of the other thing? And I can’t seem to figure out why it’s doing some stuff sometimes, but not other times. It seems like the more I add, the more time I’m spending on figuring out bugs and simply looking at how I put everything together in the first place. I should maybe go back and restructure this whole thing… there goes the fun.

If this feels familiar, you were like me. After this, you likely came up with some way to add a ‘component structure’ to your project. Maybe you studied how classes worked. Or you went deep into functions, trying different things. And every approach was cool but had a bunch of less-than-ideal trade-offs that you eventually ran into.

This is what React components really solve. They give you a simple structure to divide every project into smaller pieces, or components. The rules are smart, simple, and powerful.

But the best part is that everyone uses the same structure. So now you can move components around any project, you can easily figure out how anything is built because even the largest app uses the same simple rules under the hood. The real power of components is a broad consensus.

It’s also worth noting that React’s component approach has been adopted by Apple (SwiftUI) and Google (Flutter). It’s more than a hip JavaScript thing. It’s a better way of thinking about interfaces that you will be able to use for decades to come.

‘Dumb’ Components

Most components are pretty simple. They just render a piece of interface like a header or a button. Basically just a bit of HTML and CSS with some input variables (like a title), nicely packaged up so that you can reuse them across projects or build them into a system. Their power is basically just in the packaging of structure and style.

function Header({ children }) {
const style = { fontSize: 22 };
return <h1 style={style}>{children}</h1>;
}
function Button({ title = "My Title", onClick }) {
const style = { background: "blue", color: "white" };
return <button style={style} onClick={onClick}>{title}</button>;
}
function App() {
return (
<div>
<Header>Warning: Test</Header>
<Button title="Cancel" onClick={() => alert("Cancel")} />
<Button title="OK" onClick={() => alert("OK")} />
</div>
);
}
COPY

CodeSandbox example ›

This shows the real power of components—they don’t rely on anything but themselves. They’re single, logical pieces that you can make once and keep using as often as you like. This simple structural agreement makes it easy to divide the biggest interfaces into smaller, manageable pieces.

It also makes it really easy to collaborate, and to read others’ or your own code in the future, because the structure is always clear—there is a single right way to do everything and it can do anything.

Even better, because React uses a very HTML-like language to compose components, any web designer should be able to start using your system in a matter of hours. And the markup looks really nice too, it’s easy to read what’s going on like in the App function (assuming you put the system together nicely, that’s up to you).

‘Smart’ Components

  • Some components need a “memory” to store “state”.
  • Example: date picker, toggle, etc. Things users will change.
  • Your app always needs some state (and your app is a component too!).
  • In these cases you can use the useState hook, it centralizes your state and updates the right components automatically when you change anything so your app is always up to date.

Some components need to “remember” things, though (example: a slider with the value next to it). The component needs to re-render the label as soon as you move the slider so that everything stays in sync. Similarly, a date picker needs to know the date you selected. The components need a brain and what’s in it is what we call state.

But the best example is your app because your app is also a component. Every app needs to remember at least some things. A to-do list needs to remember your to-do items, a login screen needs to know if you’re logged in, etc.

To add state (a brain) to your component you can use a React Hook named useState. It uses some funky ES6 syntax but it’s actually really simple. It remembers a value and gives you a way to update the value. You need that special update function so that React can automatically figure out how to keep your interface in sync, and it will magically re-render all components relying on that state. Let me repeat that. React takes care of always keeping your interface in sync with your data. If you have built interfaces in jQuery or Flash, you know how big of a deal this is. No more manually telling the interface what to update when. It just works.

Let’s say we want to build that value slider with a label next to it as an example, maybe it’s like a volume setting for your app. It needs to keep track of the current volume and re-render as it changes.

function Volume() {
const [volume, setVolume] = React.useState(0);
return (
<div>
<input
type="range"
min="1"
max="100"
defaultValue={volume}
onChange={event => setVolume(event.target.value)}
/>
<span>Volume: {volume}</span>
</div>
);
}
COPY
CodeSandbox example ›

Before hooks, there were a few other ways to do this that you might run into across the internet, like class-based components that look like this:

class Volume extends React.Component {
// A ton of code using 'this' and 'setState'...
}
COPY

These still work and are fine to use, but if you are learning React today, don’t worry about them and just focus on hooks. It replaces everything class-based components can do.

React and State

  • State is local to a component by default (it cannot be modified from the outside).
  • State can only be passed down the component tree.
  • This is called unidirectional data flow, but it just means ‘one-way traffic’.

React has strict rules in place for how to deal with state, and that’s why people often get stuck here. Let me start by explaining why we want to have rules for state in the first place, instead of just a few variables that hold everything. What do you get back for using these rules?

If you try to categorically classify bugs in any app, the two largest causes are:

  1. Things are messing with your application state so it ends up with unexpected values, but it’s really hard to figure out what caused it because it can come from anywhere. For example, something in your app sets your login state to false (or null) but you don’t know what caused it.
  2. The state and the interface somehow became out of sync, and the interface does not reflect the actual application state. For example, you change a value with a slider, but nothing updates on the screen.

To solve the first one, React reverses the default rules for state modification. If a component creates state, only that same component can modify it, unless you explicitly allow other components to modify it too. That way state is way more ‘protected’ from unwanted changes, and if it changes, it’s easy to find out what caused it (and fix the bug). The rules allow you to define what can mess with your stuff.

The second one is just gone. The rules allow React to automatically keep track of what parts of the interface need to be updated so it takes care of that for you. Let me repeat: You will never have to write any interface-updating code, ever. It will always be in sync.

Now that you hopefully see the value of learning the rules, let’s dive in with some examples.

State Example

Here we have the App component-creating state that we want to use from the button. But by default, the MyButton cannot update the state, it has no way to access the count value or the setCount function.

function App() {
const [count, setCount] = React.useState(0)
return <div>The value is: {count} <MyButton /></div>
}
function MyButton() {
<button title="Increment" /> // I need setCount here.
}
COPY

To give it access, you can simply pass count and setCount directly to the component as props. This is the rule where you “explicitly give another component access”. You pass it through the props. One big thing to note here is that because React works like a tree of components, you can only pass state down the tree. That means that if two components need to share state, it needs to live higher up in the hierarchy in a component that is a parent for both. That component is often your App component.

function App() {
const [count, setCount] = React.useState(0);
return (
<div>
The value is: {count} <MyButton count={count} setCount={setCount} />
</div>
);
}
function MyButton({ count, setCount }) {
function add() {
setCount(count + 1);
}
<button title="Increment" onClick={add} />;
}
COPY

That works! But it’s kind of meh (and a lot of typing) to pass all the state as props. So there is a slightly better way here. Instead of passing down the count and setCount props, we are going to pass the function add that modifies it.

function App() {
const [count, setCount] = React.useState(0);
function add() {
setCount(count + 1);
}
return (
<div>
The value is: {count} <MyButton onClick={add} />
</div>
);
}
function MyButton({ onClick }) {
<button title="Increment" onClick={onClick} />;
}
COPY

And that is how I would actually build this. All the state is neatly organized in your app component, it’s super easy to see what can update it, and it’s always in sync. Pretty neat.

Communication Between Components

One big parent component

The simplest version is a single big component (like your App component) that just uses smaller ones. Notice how all the Button components can just access the setCount directly because it’s all in the scope of the same larger component.

I know that counter examples are not so useful for real work but they are simple to grasp. Realize that the useState hook can take everything like a piece of text (useState("Hello")) to an animation state (useState({opacity: 1, width: 200})).

function Button({ title, onClick }) {
return <button onClick={onClick}>{title}</button>;
}
function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<div>Count: {count}</div>
<Button title="Add One" onClick={() => setCount(count + 1)} />
<Button title="Add Two" onClick={() => setCount(count + 2)} />
<Button title="Add Three" onClick={() => setCount(count + 3)} />
</div>
);
}
COPY
CodeSandbox example ›

Just pass it in the props

Another way of doing the same is to pass the actual count value and setCount function around to components. Props can just be functions like anything else. Here is the exact version of the above built like that. It might be less useful for this specific case, (you would definitely prefer the above) but it should help illustrate your options.

function AddOne({ count, setCount }) {
return <button onClick={() => setCount(count + 1)}>Add One</button>;
}
function AddTwo({ count, setCount }) {
return <button onClick={() => setCount(count + 2)}>Add Two</button>;
}
function AddThree({ count, setCount }) {
return <button onClick={() => setCount(count + 3)}>Add Three</button>;
}
function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<div>Count: {count}</div>
<AddOne count={count} setCount={setCount} />
<AddTwo count={count} setCount={setCount} />
<AddThree count={count} setCount={setCount} />
</div>
);
}
COPY
CodeSandbox example ›

Shared state via hooks (no props)

Your last good prototyping option is to go around props entirely where that makes sense, using a small utility where you can directly share state between components without having to pass it around. In this example I use my own createStore utility, which allows you to create your own shared hooks that you can then immediately use in any component to pass data around. It still magically knows exactly which components to update when you change the data.

import { createStore } from "./store";
// Create your own "shared hook" to use in any component
const useStore = createStore({ count: 0 });
function Button({ title, add = 1 }) {
// Use it like any other hook
const [store, setStore] = useStore();
function onClick() {
setStore({ count: store.count + add });
}
return <button onClick={onClick}>{title}</button>;
}
function App() {
const [store, setStore] = useStore();
return (
<div>
<div>Count: {store.count}</div>
<Button title="Add One" add={1} />
<Button title="Add Two" add={2} />
<Button title="Add Three" add={3} />
</div>
);
}
COPY
CodeSandbox Example ›

So you might now ask yourself, if the above works, why go through the hoops with props? The answer is almost spiritual: the purity of your component. If you take a look at your button component before this example, you could see that it was not dependent on anything but the props you passed in. You can safely copy and paste it around even between projects and it would work exactly as advertised, anywhere. That is the hallmark of a great component.

But with the createStore shortcut, it now depends on an extra library and the store being set up correctly within your project. Because it depends on things outside of just itself, it is less robust and less “pure”. This is totally fine for prototyping though as long as you understand the difference.

Now that we’re here, there are actually a ton more state management options for React like contexts all the way to Redux. They each have different trade-offs, for example, some give you undo for free but can be harder to use. For prototyping, I wouldn’t really bother with them (I make all my prototypes using the above), but now that you have all the basics down, they are fun to study because state management is often the most important part of building great production apps.


Did you like this post? Check out our other resources on React:

Like this article? Spread the word.

Want more design articles?

Join our newsletter and get resources, curated content, and design inspiration delivered straight to your inbox.