Another Look at State Management
Remember Looking at this information when we talked about Props
now where ready to look at it again.
State is the most important concept in React, your app is "reactive" because you have state for data your UI depends on. As Apps get more complex, deciding how to handle state and where it should be housed can get quite daunting.
Here are some questions to use as a guide.
This Piece of State is Used in how many components?
- 0-1: It should be in the one component using it and nowhere else
- 2-5: It should be located in a parent all the components share but as low in the component tree as possible
- 5+: Time to consider Context
Lifting State
The concept of Lifting state occurs when siblings need to share state with each other. The lifting state pattern occurs of the following pattern.
- The state is housed in the parent of the two siblings
- The parent passes a function as props to the sender to alter the parents state
- The parent passes the state itself as a prop to the receiver to receive the updated state
// Component receive function as prop to update parents state
const SenderChild = props => {
return <button onClick={() => props.update("Goodbye")}>Click Me</button>
}
// Component receives parents state
const SeceiverChild = props => {
return <h1>{props.value}</h1>
}
// The parent who passes props to both children
const Parent = props => {
// The State
const [state, setState] = useState("Hello")
// Function to update state to send to child
const updateState = data => setState(data)
// we pass the function and the state as props to the children
return (
<div>
<ReceiverChild value={state} />
<SenderChild update={updateState} />
</div>
)
}
Prop Drilling
This is the inevitable tragedy that occurs when your components trees grow to several layers. Imagine a piece of state is in a component that is needed in a grandchild component... you'd have to do the following.
const GrandChild = props => <h1>{props.data}</h1>
const Child = props => <GrandChild data={cheese} />
const Parent = props => <Child cheese="gouda" />
This is prop drilling, the Parent passes cheese to child, who passes the same data as data to GrandChild. Imagine if it was a Great-Great-Grandchild... that's a lot of typing just so one component can receive a single piece of data.
There are several solutions to this.
- React Context
- React useReducer Hook
- The TaskRunner Pattern
- Redux
- And many more... (MobX, State Machines, ...)
Let's cover a few!
Context
What context allows us to do is to create an object that be passed directly to children of any level without having to pass them around as props. If props were like walking down several flights of stairs, Context is liking taking an elevator to where you need to go, faster and easier.
import { createContext, useContext } from "react"
//create the context object
const context = createContext(null)
const GrandChild = props => {
// consume the data from the provider in parent
const ctx = useContext(context)
return <h1>{ctx}</h1>
}
// notice... no props pass through child in this scenario
const Child = props => <GrandChild />
// the context provider determines what data the parent provides its children
const Parent = props => (
<context.Provider value={"cheese"}>
<Child />
</context.Provider>
)
So notice, because we used Context, the parent component was able to pass data directly to it's grandchild without having to pass any props. Context makes transporting data across your components much easier. The only downside is the direction of the data and where it is used will be a little less obvious to a random spectator.
The useReducer Hook
Before context many use Redux for state management. Not only did Redux allow you to store all your state in one place (the Redux Store) but also allowed you to house all your stateful logic in one place called the Reducer function.
The reducer function would normally be passed an "action" which is an object with two properties. This action was passed to the reducer calling a "dispatch" function.
- type: A string that is passed to a switch to determine how to update the state
- payload: Any data needed for the state update.
React eventually took the core Redux functionality and built it in to React as the useReducer hook. Below is a basic example of the useReducer hook.
import { createContext, useContext, useReducer } from "react"
//create the context object
const context = createContext(null)
const GrandChild = props => {
// consume the data from the provider in parent
const ctx = useContext(context)
// the h1 displays the state pulled from context
// the buttons call dispatch and pass the action to the reducer
return (
<>
<h1>{ctx.state}</h1>
<button onClick={() => ctx.dispatch({ type: "add", payload: null })}>
Add
</button>
<button onClick={() => ctx.dispatch({ type: "subtact", payload: null })}>
Subtract
</button>
</>
)
}
// notice... no props pass through child in this scenario
const Child = props => <GrandChild />
// the context provider determines what data the parent provides its children
const Parent = props => {
// the reducer with our stateful logic
const reducer = (state, action) => {
// get the type and payload from the action
const { type, payload } = action
switch (type) {
// determine how to update the state based on action type
case "add":
return state + 1
case "subtract":
return state - 1
default:
// if it doesn't match any type, keep the state as is
return state
}
}
// the initial value of the state
const initialState = 0
// create the state and the dispatch function
const [state, dispatch] = useReducer(reducer, initialState)
// pass the state and dispatch via context in an object
return (
<context.Provider value={{ state, dispatch }}>
<Child />
</context.Provider>
)
}