In this final post of the Redux series I will demonstrate how to use the React-Redux package to make state changes update React UI components. I will also show you how to use the Redux Toolkit package to simplify Redux development. So let's begin.
React-Redux
Let's begin with React-Redux first. As mentioned in the previous post, you create a React project with Redux support using:
npx create-react-app my-app --template redux
This automatically installs React-Redux into your project. There are two key differences in React-Redux projects versus plain Redux projects:
-
useSelector()
is used instead ofsubscribe()
andgetState()
. It returns the value of a state part. -
useDispatch()
is used instead ofdispatch()
. It returns a handle to thedispatch()
method.
In a React-Redux app, you don't access the Redux state directly, you use useSelector()
and useDispatch()
within components to read and write Redux state. Whenever Redux state is updated, it will automatically trigger a re-render of the React component that uses that state (via useSelector()
returning a different value).
State is dispatched the normal way, using the handle to the dispatch()
method returned by useDispatch()
:
export function SomeComponent {
const dispatch = useDispatch()
//...
return (
<button onClick={() => dispatch({type: "SomeAction"})}>Click me</button>
)
}
As for useSelector()
, that is invoked with a function passed as an argument that returns the relevant part of the state. So this works:
const selectCount = state => state.counter.value
const count = useSelector(selectCount)
By intuition, this also works:
const count = useSelector(state => state.counter.value)
Finally, you need to pass a Redux store to the main React component so it can propogate that store to child components for them to use. This is done by rendering a Provider
component above the main App
component:
import { Provider } from 'react-redux'
import { createStore } from 'redux'
// ...
ReactDOM.render(
<Provider store={configureStore({ reducer: someReducer })}>
<App />
</Provider>,
document.getElementById('root')
)
// Now the store is accessible from the <App> component and
// its child components
An example React-Redux component
The following component was taken from the Redux.js tutorial and is also available here. Key parts of the app are indicated with comments.
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
/*
* These are our action creator functions. They return action objects.
*/
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount
} from './counterSlice'
import styles from './Counter.module.css'
export function Counter() {
/*
* `useSelector()` calls `useState()` under the hood causing
* this component to update if the value returned from
* `useSelector()` changes.
*/
const count = useSelector(selectCount)
/*
* `useDispatch()` returns the dispatch object from the Redux
* store variable.
* Notice in the onClick prop of the <button> tags this
* dispatch function is called with the return value of the
* `increment` and `decrement` action creators (an action
* object)
*/
const dispatch = useDispatch()
const [incrementAmount, setIncrementAmount] = useState('2')
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
{/* omit additional rendering output here */}
</div>
)
}
Redux Toolkit
It's time to cover the second topic of this post, Redux Toolkit. Redux Toolkit exports configureStore()
and createSlice()
functions. If the first one sounds familiar it's because this function exists in Redux as well. But this function is exported from Redux Toolkit so you don't have to import anything from Redux directly. It is used the same way as the Redux configureStore()
.
As for createSlice()
, this is the convenience method that generates action creators I was talking about in the previous post. It generates actions in the form "name"/"reducer"
, where "name"
is the name of the action creator you give it (some people call "name" a namespace), and "reducer"
is the name of each reducer. Note that I said each reducer, createSlice()
accepts several reducers by means of keys in objects. (Please remember that actions are strings that can have any possible value.)
The input object should also contain an initialState
property that is an object with the initial state values.
createSlice()
returns what is called a slice which is just an action creator that has an actions
property that is an object of action creators, and a reducer
property that is the actual reducer function. It is a formal name the Redux community gave it as opposed to a subjective term like "feature" or "a state part".
This is the format of the object that is passed to createSlice()
:
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
// If we don't use a parameter we can omit it like
// we did to `action`.
increment: state => {
return state.value + 1
},
decrement: state => {
return state.value - 1
},
incrementByAmount: (state, action) => {
return state.value + action.payload
}
}
})
Here's a recap of the properties in the parameter object and returned object:
- Input object:
-
name
: name of the slice -
initialState
: Initial state of the slice -
reducers
: an object containing each reducer name and its associated function
-
- Output object
-
actions
: action creators returning actions of the form"name"/"<REDUCER-FUNCTION-NAME>"
. Each action can be accessed by its name, such asslice.actions.increment
. -
reducer
: The reducer function made from combining the reducers passed in the input.
-
A note about action creators: You can pass a single argument to the action creator to use as the payload
property. So you type incrementByAmount(5)
to set the action.payload
to 5. This works for the other actions to even though I didn't list an action parameter for it, the only reason I didn't list one is because the reducers don't use it.
You might be wondering at this point how can the reducers be combined into one. The answer here is by using a function that Redux exports called combineReducers()
. This function takes an object as input with each slice as a key and its associated reducer as a value. Here's an example:
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
})
In fact we can just pass the reducers
property of the input to this function to get our reducer. In practice though, you don't do this yourself, you let createSlice()
do this for you.
And we're done
This concludes the Redux series. I hope this series helped you become familiar with Redux development. If you see any errors in this post, please let me know so I can correct them.
Top comments (0)