In my previous article, I mentioned the potential upsides of using the Redux library in your React.js apps. I also explained the key concepts for beginners, including stores and a unified reducer for state management. In this article, I'll be explaining the setup of Redux in your React codebase and also the conversion of contexts and reducers to Redux.
What we'll be building
We'll be building a really simple, plain webpage that changes colour when a toggle is clicked. There will also be a textbox, in which the user will enter text. Once the user submits the text, the text on the webpage will change. Although we can simply use useState()
to accomplish this, I'll be showing you how this might be implemented using useContext()
and useReducer()
and then I'll move on to Redux.
Setup
If you're eager to learn Redux, I expect you to be familiar with React.js. Set up a React project by running the following command in a folder you wish to make your project folder:
npx create-react-app .
Install the following library which has a pre-made UI component (a switch) for us:
npm install @radix-ui/react-switch
Once done, delete the unnecessary boilerplate code and add the following code to App.js.
import InputBox from './InputBox'
import * as Switch from '@radix-ui/react-switch'
import './App.css'
function App() {
return (
<div className='App'>
<label className="Label" htmlFor="change-color" style={{ paddingRight: 15 }}>
Change color
</label>
<Switch.Root className="SwitchRoot" id="change-color">
<Switch.Thumb className="SwitchThumb" />
</Switch.Root>
<InputBox inputText={"Replace text here"} />
</div>
)
}
export default App
As you can see, we are importing the readymade switch component and the InputBox
component over here. However, we haven't created the InputBox
component yet. Let's make the App.css file first and then create the required component.
button {
all: unset;
}
.SwitchRoot {
width: 42px;
height: 25px;
background-color: blue;
border-radius: 9999px;
position: relative;
box-shadow: 0 2px 10px var(--black-a7);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.SwitchRoot:focus {
box-shadow: 0 0 0 2px black;
}
.SwitchThumb {
display: block;
width: 21px;
height: 21px;
background-color: white;
border-radius: 9999px;
box-shadow: 0 2px 2px var(--black-a7);
transition: transform 100ms;
transform: translateX(2px);
will-change: transform;
}
.SwitchThumb[data-state='checked'] {
transform: translateX(19px);
}
.Label {
font-size: 15px;
line-height: 1;
}
export default function InputBox({inputText}) {
function handleBtnClick() {
console.log('Clicked')
}
return (
<div>
<textarea>
{inputText}
</textarea>
<button onClick={handleBtnClick}>Submit</button>
</div>
)
}
In the InputBox
component we're simply triggering a console.log
when the user presses submit.
Using context and reducer
To use context and reducer, add the following code in a new file called context.jsx:
import { createContext, useReducer } from 'react'
function createAction(type, payload) { return {type, payload} }
export const AppContext = createContext({
isToggleChecked: false,
setIsToggleChecked: () => {},
userDisplayText: '',
setUserDisplayText: () => {}
})
export const AppActionTypes = {
SET_IS_TOGGLE_CHECKED: 'SET_IS_TOGGLE',
SET_USER_DISPLAY_TEXT: 'SET_USER_DISPLAY_TEXT'
}
function AppReducer(state, action) {
const {type, payload} = action
switch (type) {
case AppActionTypes.SET_IS_TOGGLE_CHECKED:
return {
...state,
isToggleChecked: payload
}
case AppActionTypes.SET_USER_DISPLAY_TEXT:
return {
...state,
userDisplayText: payload
}
default:
throw new Error(`Unhandled type of ${type} in AppReducer`)
}
}
const initialState = {
isToggleChecked: false,
userDisplayText: ''
}
export const AppContextProvider = ({ children }) => {
const [{isToggleChecked, userDisplayText}, dispatch] = useReducer(AppReducer, initialState)
function setIsToggleChecked(bool) {
dispatch(createAction(AppActionTypes.SET_IS_TOGGLE_CHECKED, bool))
}
function setUserDisplayText(str) {
dispatch(createAction(AppActionTypes.SET_USER_DISPLAY_TEXT, str))
}
const value = {isToggleChecked, setIsToggleChecked, userDisplayText, setUserDisplayText}
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}
This is the standard template that I use for implementing context in my React apps. Let me explain the code:
createAction()
is used to modularize the creation of actions inside dispatches. This is a very nitpicky optimization, so it's not necessary. It's being used inside theAppContextProvider
function when dispatching actions.AppActionTypes
defines the limited number of action types that can be used for dispatching actions. This minimizes errors we make while typing the types out ourselves.AppReducer
is the main reducer for the context and appropriately modifies the state with the payload depending on the action type.In
AppContextProvider
we're returning the provider for the context we created. So this function behaves as a provider itself. It also creates a reducer usinguseReducer()
. If you're unaware of reducers or wish to revise, this article I found on Hashnode is a must-read!
How do we use the context.jsx file in our application?
Change the code of index.jsx to the following:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import {AppContextProvider} from './context'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AppContextProvider>
<App />
</AppContextProvider>
</React.StrictMode>
)
We're importing AppContextProvider
and using it as a component and wrapping the App
component inside it. Let's use the context in App.jsx now:
import InputBox from './InputBox'
import * as Switch from '@radix-ui/react-switch'
import './App.css'
import {useContext} from 'react'
import { AppContext } from './context'
function App() {
let {isToggleChecked, setIsToggleChecked, userDisplayText} = useContext(AppContext)
let backgroundColor = isToggleChecked ? 'black' : 'white'
let textColor = isToggleChecked ? 'white' : 'black'
return (
<div className='App' style={{backgroundColor: backgroundColor, color: textColor}}>
<p>{userDisplayText}</p>
<label className="Label" htmlFor="change-color" style={{ paddingRight: 15 }}>
Change color
</label>
<Switch.Root className="SwitchRoot" id="change-color" checked={isToggleChecked} onCheckedChange={() => setIsToggleChecked(!isToggleChecked)}>
<Switch.Thumb className="SwitchThumb" />
</Switch.Root>
<InputBox inputText={"Replace text here"} />
</div>
)
}
export default App
We're importing and using the context using useContext()
. We're using the value of isToggleChecked
to determine the background color and the text color too. For our Switch.Root
, we're setting its checked
property to equal the isToggleChecked
and its onCheckedChange
property to trigger a function that toggles the boolean in isToggleChecked
. We also need to be able to set the userDisplayText
in InputBox.jsx to appropriately change the value in App.jsx. Put the following code in InputBox.jsx:
import {useContext, useRef} from 'react'
import {AppContext} from './context'
export default function InputBox({inputText}) {
let {setUserDisplayText} = useContext(AppContext)
let inputRef = useRef(null)
function handleBtnClick() {
setUserDisplayText(inputRef.current.value)
}
return (
<div>
<textarea ref={inputRef}>
{inputText}
</textarea>
<button onClick={handleBtnClick} style={{border: '1px solid black'}}>Submit</button>
</div>
)
}
We're using useRef()
to reference the value present in the textarea
element. Once the user clicks the submit button, we're triggering handleBtnClick
which sets the userDisplayText
to be inputRef.current.value
. This blog by Refine on Hashnode is a great place to get started with useRef()
if you've never used it before.
Using Redux patterns
There is a lot of templating that goes into utilizing Redux in our app. First, install redux, react-redux and redux-logger. Redux-logger is a tool that enables us to understand our state changes visually in the console.
npm install redux react-redux redux-logger
Create a folder called store inside of src. Then create the following files in the following structure.
That's a lot of files! You might start to think it's better to just stick to contexts and reducers. I do agree with that, we could've even used useState()
for such a simple application! However, I aim to explain to you the core Redux concepts, which are easier done with a smaller application.
In store.js, add the following code:
import { compose, legacy_createStore as createStore, applyMiddleware } from "redux"
import logger from 'redux-logger'
import { rootReducer } from './root-reducer'
const middleWares = [process.env.NODE_ENV !== 'production' && logger].filter(Boolean)
const composedEnhancers = compose(applyMiddleware(...middleWares))
export const store = createStore(rootReducer, undefined, composedEnhancers)
This is almost always how your store.js file will start out. If you are using more Redux libraries this file will look longer and more different. We're importing a bunch of functions from redux and also logger
from redux-logger. We're importing rootReducer
from our root-reducer.js file, to which we haven't added any code in yet.
middleWares
contains the logger only if the process.env.NODE_ENV
(the variable stating the environment in which the application is running) is not production. composedEnhancers
uses the compose
function and the applyMiddleware
function to spread out middleWares
. store
is created using createStore()
and takes in rootReducer
for its main compiled reducer, undefined
for its initial state and composedEnhancers
for the store enhancers.
We now need to create the rootReducer
in root-reducer.js. Let's do it now.
import { combineReducers } from "redux"
import { appReducer } from './reducer'
export const rootReducer = combineReducers({
app: appReducer
})
combineReducers()
is a function that combines the various reducers in our application (ours only has one since it's so small). The reducer for the app
in our state
will be appReducer
from reducer.js. Let's change the code in reducer.js now:
import { actionTypes } from "./types"
export const initialState = {
isToggleChecked: false,
userDisplayText: ''
}
export function appReducer(state = initialState, action = {}) {
const { type, payload } = action
switch (type) {
case actionTypes.SET_IS_TOGGLE_CHECKED:
return {
...state,
isToggleChecked: payload
}
case actionTypes.SET_USER_DISPLAY_TEXT:
return {
...state,
userDisplayText: payload
}
default:
return state
}
}
As you can see, a lot of our code is similar to the code in context.jsx. This is the benefit of using Redux: if you understand the context and reducer pattern this becomes hella easier. We haven't created actionTypes
yet, so let's do that in types.js:
export const actionTypes = {
SET_IS_TOGGLE_CHECKED: 'SET_IS_TOGGLE',
SET_USER_DISPLAY_TEXT: 'SET_USER_DISPLAY_TEXT'
}
This is exactly the same as the default types in types.js. Now we need action creators, like setIsToggleChecked
and setUserDisplayText
that we had in context.jsx. Add the following code in action.js:
import { actionTypes } from './types'
function createAction(type, payload) { return {type, payload} }
export function setIsToggleChecked(bool) {
return createAction(actionTypes.SET_IS_TOGGLE_CHECKED, bool)
}
export function setUserDisplayText(str) {
return createAction(actionTypes.SET_USER_DISPLAY_TEXT, str)
}
We're again importing actionTypes
and creating the handy createAction
function. Then instead of dispatching the actions in setIsToggleChecked
and setUserDisplayText
, we're simply returning the created actions. Dispatching of these actions will be done inside App.jsx and InputBox.jsx.
To be able to use our newly created store, change the code in index.jsx:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { store } from './store/store'
import { Provider } from 'react-redux'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)
We're using the Provider
component to expose our store
and its values to the entire application. Provider
wraps App
instead of our context provider.
We can't simply get the values present in our store inside App.jsx and InputBox.jsx. To accomplish this, we need selectors. They select the values in our store and provide them to our components. Let's modify selector.js now:
export const selectIsToggleChecked = (state) => state.app.isToggleChecked
export const selectUserDisplayText = (state) => state.app.userDisplayText
These selectors get the state object and extract the appropriate values out of it to keep our code clean and modular.
Now let's modify App.jsx to use our newly created selectors:
import InputBox from './InputBox'
import * as Switch from '@radix-ui/react-switch'
import './App.css'
import { useSelector, useDispatch } from 'react-redux'
import { selectIsToggleChecked, selectUserDisplayText } from './store/selector'
import { setIsToggleChecked } from './store/action'
function App() {
const dispatch = useDispatch()
const isToggleChecked = useSelector(selectIsToggleChecked)
const userDisplayText = useSelector(selectUserDisplayText)
let backgroundColor = isToggleChecked ? 'black' : 'white'
let textColor = isToggleChecked ? 'white' : 'black'
function onCheckedChangeHandler() {
dispatch(setIsToggleChecked(!isToggleChecked))
}
return (
<div className='App' style={{backgroundColor: backgroundColor, color: textColor}}>
<p>{userDisplayText}</p>
<label className="Label" htmlFor="change-color" style={{ paddingRight: 15 }}>
Change color
</label>
<Switch.Root className="SwitchRoot" id="change-color" checked={isToggleChecked} onCheckedChange={onCheckedChangeHandler}>
<Switch.Thumb className="SwitchThumb" />
</Switch.Root>
<InputBox inputText={"Replace text here"} />
</div>
)
}
export default App
We're creating dispatch
which uses react-redux's useDispatch()
to be able to dispatch actions such as setIsToggleChecked
. useSelector()
is enabling us to use our selectors to extract values from the store. If you have any questions in the above code, ask in the comments, and I'll definitely reply!
Now for InputBox.jsx:
import {useRef} from 'react'
import { useDispatch } from 'react-redux'
import { setUserDisplayText } from './store/action'
export default function InputBox({inputText}) {
const dispatch = useDispatch()
let inputRef = useRef(null)
function handleBtnClick() {
dispatch(setUserDisplayText(inputRef.current.value))
}
return (
<div>
<textarea ref={inputRef}>
{inputText}
</textarea>
<button onClick={handleBtnClick} style={{border: '1px solid black'}}>Submit</button>
</div>
)
}
Again, we're dispatching setUserDisplayText()
in our event handler.
We're finally done!
Conclusion
Redux has a lot of setup, but if you have the hang of contexts and reducers, Redux will come to you with practice. I highly recommend comparing the code in context.jsx to the files in the store folder to understand the key differences between these two state management patterns.
Thanks for reading! I'm sure you learnt a lot and have improved as a frontend software engineer. If you'd like to learn more about JavaScript and coding, check out my blog page. To make sure you don't miss out, subscribe to my email newsletter or follow me. I highly appreciate any feedback in the comments :)