It can be very annoying to manage component states
with the useState hook alone.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function Counter() {
const [count, setCount] = useState(0);
function handleIncrement() { setCount(count + 1); }
function handleDecrement() { setCount(count - 1); }
function handleReset() { setCount(0); }
return <>
<span>{count}</span>
<button onClick={handleIncrement}>+</button>
<button onClick={handleDecrement}>-</button>
<button onClick={handleReset}>Reset</button>
</>;
}
|
The Reducer Function#
A way to cetralize the state management
is to use the reducer function.
This function receive an action and a
state and return a new state.
The reducer is a pure function.
That means that it should not have
any side effects. That means
it should not change the state directly
but return a new state.
For a simple demo let’s define the state’s
data structure and the initial state:
const initialState = { value: 0 };
Let’s define a reducer just for the increment action:
function reducer(state, action) {
if (action.type === 'increment') return { value: state.value + 1 };
return state;
}
In order to use the reducer in react
we need to import the useReducer hook:
import { useReducer } from "react";
Then we will use it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import { useReducer } from "react";
const initialState = { value: 0 };
function reducer(state, action) {
if (action.type === 'increment') return { value: state.value + 1 };
return state;
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return <>
<span>{state.value}</span>
<button onClick={() => dispatch({ type: 'increment' })}>
+
</button>
</>;
}
|
Let’s add the decrement action:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { useReducer } from "react";
const initialState = { value: 0 };
function reducer(state, action) {
if (action.type === 'increment') return { value: state.value + 1 };
if (action.type === 'decrement') return { value: state.value - 1 };
return state;
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return <>
<span>{state.value}</span>
<button onClick={() => dispatch({ type: 'increment' })}>
+
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
-
</button>
</>;
}
|
And the reset action is going to be easier:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import { useReducer } from "react";
const initialState = { value: 0 };
function reducer(state, action) {
if (action.type === 'increment') return { value: state.value + 1 };
if (action.type === 'decrement') return { value: state.value - 1 };
if (action.type === 'reset') return initialState;
return state;
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return <>
<span>{state.value}</span>
<button onClick={() => dispatch({ type: 'increment' })}>
+
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
-
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
reset
</button>
</>;
}
|
There are many times when it’s not only the action type that determines the new state.
For example, when we want to increment the count by a given amount.
In this case we need to pass the amount as a payload in the action object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
import { useReducer } from "react";
const initialState = { value: 0 };
function reducer(state, action) {
if (action.type === 'increment') return { value: state.value + 1 };
if (action.type === 'decrement') return { value: state.value - 1 };
if (action.type === 'reset') return initialState;
if (action.type === 'increment-by') {
return { value: state.value + action.payload.amount }
}
return state;
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return <>
<span>{state.value}</span>
<button onClick={() => dispatch({ type: 'increment' })}>
+
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
-
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
reset
</button>
<button onClick={() => dispatch({
type: 'increment-by',
payload: { amount: 5 }
})}>
increment by 5
</button>
</>;
}
|
writing strings is annoying and prone to errors
Soloution: constants!
const ACTIONS = {
INCREMENT: "increment",
DECREMENT: "decrement",
RESET: "reset",
INCREMENT_BY: "change-count",
}
edit our reducer
1
2
3
4
5
6
7
8
9
|
function reducer(count, action) {
if (action.type === ACTIONS.INCREMENT) return { value: state.value + 1 };
if (action.type === ACTIONS.DECREMENT) return { value: state.value - 1 };
if (action.type === ACTIONS.RESET) return initialState;
if (action.type === ACTIONS.INCREMENT_BY) {
return { value: state.value + action.payload.amount }
}
return state;
}
|
edit our component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return <>
<span>{state.value}</span>
<button onClick={() => dispatch({ type: ACTIONS.INCREMENT })}>
+
</button>
<button onClick={() => dispatch({ type: ACTIONS.DECREMENT })}>
-
</button>
<button onClick={() => dispatch({ type: ACTIONS.RESET })}>
reset
</button>
<button onClick={() => dispatch({ type: ACTIONS.INCREMENT_BY,
payload: { amount: 5 }
})}>increment by 5</button>
</>;
}
|
also coding the object with types is a pain.
Solution: action creators!
function createIncrementAction() { return { type: ACTIONS.INCREMENT }; }
function createDecrementAction() { return { type: ACTIONS.DECREMENT }; }
function createResetAction() { return { type: ACTIONS.RESET }; }
function createIncrementByAction(amount) {
return { type: ACTIONS.INCREMENT_BY, amount };
}
use actions creators
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return <>
<span>{state.value}</span>
<button onClick={() => dispatch(createIncrementAction())}>
+
</button>
<button onClick={() => dispatch(createDecrementAction())}>
-
</button>
<button onClick={() => dispatch(createResetAction())}>
reset
</button>
<button onClick={() => dispatch(createIncrementByAction(5))}>
increment by 5
</button>
</>;
}
|
all together now
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
function createIncrementAction() { return { type: ACTIONS.INCREMENT }; }
function createDecrementAction() { return { type: ACTIONS.DECREMENT }; }
function createResetAction() { return { type: ACTIONS.RESET }; }
function createIncrementByAction(amount) {
return { type: ACTIONS.INCREMENT_BY, amount };
}
const initialState = { value: 0 };
function reducer(count, action) {
if (action.type === ACTIONS.INCREMENT) return { value: state.value + 1 };
if (action.type === ACTIONS.DECREMENT) return { value: state.value - 1 };
if (action.type === ACTIONS.RESET) return initialState;
if (action.type === ACTIONS.INCREMENT_BY) {
return { value: state.value + action.payload.amount }
}
return state;
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return <>
<span>{state.value}</span>
<button onClick={() => dispatch(createIncrementAction())}>
+
</button>
<button onClick={() => dispatch(createDecrementAction())}>
-
</button>
<button onClick={() => dispatch(createResetAction())}>
reset
</button>
<button onClick={() => dispatch(createIncrementByAction(5))}>
increment by 5
</button>
</>;
}
|