State Within React Components

The Case for State

Up to now, we've learned enough that we could build what is effectively a static website out of small, cohesive building blocks.

That's pretty cool, but we aren't here to build websites that are static.

We want to build applications that are interactive and change over time.


this.state

React Components have a state object that's used to control how the component renders and behaves.

A Component can never change its props, but Components can change their state.

state is what allows us to create components with behavior.


A Counter

Let's build a basic counter component. It will:

  • Display the current total.
  • Allow the user to increment the total.
  • Allow the user to decrement the total.

What should the state look like for this component?


Hooks!

What number should our count start at?

Thats our default state.

We set our default state our component using the React.useState() hook, like so:

import React from 'react';

function App() {
  const [count, setCount] = React.useState(0)

  return (
    <main>TODO</main>
  );
}

export default App;

The first thing we notice is that React.useState() is giving us back an array containing whatever our initial state is (in this case 0) as well as a setter for that value.


Accessing State

You can use your state inside your Components by simply accessing the variable name.

Let's print out our count by grabbing it from our destructured array:

import React from 'react';

function App() {
  const [count, setCount] = React.useState(0)

  return (
    <main>
      <section className="counter-container">
        <h2 className="counter-container__count">{count}</h2>
      </section>
    </main>
  );
}

export default App;

setCount

We will now add an + button that adds 1 to count via the setCount() method.

import React from 'react';

function App() {
  const [count, setCount] = React.useState(0)

  const increment = () => {
    setCount(count + 1);
  }

  return (
    <main>
      <section className="counter-container">
        <h2 className="counter-container__count">{count}</h2>
        <button onClick={increment}>+</button>
      </section>
    </main>
  );
}

export default App;

Our button has an onClick attribute, similar to regular HTML, except instead of a string we have {increment}.

We are actually passing a reference to the increment function that's defined here.


Next, we decrement

The decrement functionality is very similar. Give it a shot on your own.


Separating State Management & Presentation

The app works, but taking a look at the App component thru the lens of the Single Responsibility Principle it seems to have multiple responsibilities:

import React from 'react';

function App() {
  const [count, setCount] = React.useState(0)

  const decrement = () => {
    setCount(count - 1);
  }

  const increment = () => {
    setCount(count + 1);
  }

  return (
    <main>
      <section className="counter-container">
        <h2 className="counter-container__count">{count}</h2>
        <button onClick={decrement}>-</button>
        <button onClick={increment}>+</button>
      </section>
    </main>
  );
}

export default App;

State Management & Presentation


Containers & Presentational Components

It's a common pattern to split your components into one of two categories:

  • Containers ("Smart" Components)

    • Concerned with how things work (behavior & state).
    • Have no control over how things look (JSX & CSS).
    • Hold the state
    • Define transition functions that operate on the state.
    • Pass state & transitions into child components via props.
  • Presentational Components ("Dumb" Components)A

    • Control how things look.
    • Have no state.
    • Receive everything they need (state & transitions) via props.

Further Reading


Pull State Up or Push Presentation Down

When you find you have a component that is responsible for both presentation and state, you have two options to split those responsibilities.

  • You can create a new parent component and "pull the state up" to it, or...
  • You can create a new child component and "push the presentation down" into the new component.

Pushing the Presentation Down

Let's try pushing the presentation down. Take a look at the render function:

return (
  <main>
    <section className="counter-container">
      <h2 className="counter-container__count">{count}</h2>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </section>
  </main>
);

I spy three things in there that are related to state and transitions here:

  • count: This is using state.
  • decrement: This is behavior that triggers a state transition.
  • increment: This is behavior that triggers a state transition.

When we push the presentation down, these three things should come from the parent component via props.

This allows the Container component to own the responsibility for state & transitions.


Presentation Component

Take a shot at extracting the presentation component. It should:

  • Be a React component named "Counter".
  • Use three props (decrement, count, increment).
  • Have no state itself.
  • Maintain the appearance of our app.

Extracted Presentation Component

// Counter.js
import React from 'react';

function Counter({count, decrement, increment}) {
  return (
    <main>
      <section className="counter-container">
        <h2 className="counter-container__count">{count}</h2>
        <button onClick={decrement}>-</button>
        <button onClick={increment}>+</button>
      </section>
    </main>
  );
}

export default Counter;

Notice that the component is only concerned with how things look, not how they work.


Refactoring the App to be a Container

Now that we have our presentational concerns isolated in the Counter component, we need to refactor the App class so that it uses this new component.

Give it a shot. You'll need to:

  • import the new Counter.
  • Render the Counter component.
  • Pass all the needed props into the Counter component.

The Refactored Container

Everything that's state related lives inside this container component, and none of the presentation is defined here.

That's a nice, clean, separation of concerns.

import React from 'react';
import Counter from './Counter';

function App() {
  const [count, setCount] = React.useState(0)

  const decrement = () => {
    setCount(count - 1);
  }

  const increment = () => {
    setCount(count + 1);
  }

  return (
    <Counter
      count={count}
      decrement={decrement}
      increment={this.increment}
    />
  );
}

export default App;

Thinking Functionally

It can be really helpful to take a few minutes and sketch out what your state object should look like before you start coding.

The current behavior and appearance of your UI is calculated by applying rendered content and changes to your state.


State Management: Redux, MobX, Microstates

As you continue to develop your React skills, you'll hit a point where managing state within a container component starts to get really difficult.

When you cross that line (you'll know it when you're there), learning Redux or another state management tool is a great idea.


BUT DON'T LEARN REDUX YET

You can do a lot with the state management that's built into React, and you'll do yourself a huge disservice if you head down this path too quickly. Learn from my mistake and cross that bridge when you need to, and no sooner.