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 Component
s have a state
object that's used to control how the component renders and behaves.
A Component
can never change its props
, but Component
s 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 Component
s 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
.
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 newCounter
.- 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.