How to use Redux/Toolkit: A Developer's first Interface with @reduxjs/toolkit
"Change is constant", that's something I used to hear a lot. What does that even mean? Is it an oxy... You know what? Never mind, I digress. In this article, what change means is, you'll be writing redux quite differently.
What is Redux?
Redux is a predictable state container, which is fancy lingo for saying Redux stores your application state. It is predictable because its behavior is consistent & nothing changes in your application state without your say-so.
Here's an article simplifying what Redux means, you can check that here
Moving on...
In this article, we'll be using reduxjs/toolkit which comes with nifty tools out of the box to build a responsive layout in our React app.
First, let's create a new react app. Open up your terminal & write the following command below:
npx create-react-app redux-tut
Next, let's install all the cool stuff we need. I will be using npm as my package manager, you can use yarn if you so please:
npm install @reduxjs/toolkit react-redux
After installing the packages we need, we're going to set up a folder structure for our application. Create two folders in your src
directory, redux
& components
. Our folder structure should look something like this:
Inside our redux
folder, create a store.js
file. This file is going to house our application state, the only way to change the state in our store is to dispatch an action on it.
#store.js
import { configureStore } from "@reduxjs/toolkit";
export default configureStore({
# some code we'll get to later
}
})
Ok, what the @#%! is going on?
In the past, configuring our Redux store would involve calling the createStore
API, passing a rootReducer
, combining reducers & using middleware but using reduxjs/toolkit, which comes with the configureStore
API, all of that is done for us by default.
Here, we have imported configureStore
from @reduxjs/toolkit
& we have also exported it for use throughout our application.
Next, we'll create a slice
folder inside our redux
folder then a windowDimensionSlice.js
file inside our slice
folder. Our folder structure will then look something like this:
Inside windowDimensionSlice.js
, let's write some code.
#windowDimensionSlice.js
import { createSlice } from '@reduxjs/toolkit';
export const windowDimensionSlice = createSlice({
name: 'windowDimension',
initialState: {
width: window.innerWidth,
},
reducers: {
resizeWidth: (state) => {
state.width = window.innerWidth;
},
},
});
export const { resizeWidth } = windowDimensionSlice.actions;
export default windowDimensionSlice.reducer;
Let me explain what's going on here.
First, we import createSlice
from @reduxjs/toolkit
. createSlice
is a function that accepts an initial state, an object of reducer functions that automatically generate actions (action creators & action types) that correspond to the reducers & state.
Secondly, we export windowDimensionSlice
for use throughout our application. The createSlice
object accepts a name
parameter, which is the slice name, in our case, it is 'windowDimension'
& an initialState
parameter which holds the default state value of the slice, in our case, it is width: window.innerWidth
Thirdly, we have a reducers
object that contains functions designed to handle action types (similar to a case statement in a switch) so you don't necessarily need to have a constants
folder with all the actions types you want to dispatch. In our case, we have named our reducer resizeWidth
, & it accepts a parameter state
, we then set the state of our application (from the initialState) by calling state.width
, we set that to window.innerWidth
.
Ok, before we take a break to look at cool cat photos on the internet, let's take a look at the last two lines here. We export { resizeWidth }
& set it to windowDimensionSlice.actions
. Remember our reducers
object contains functions that handles action types, so resizeWidth
is an action(type). We also export windowDimensionSlice.reducer
. If you also remember, the createSlice
API accepts an object of reducer functions. reduxjs/toolkit reduces the amount of logic we have to write by doing all the grunt work for us out of the box.
Cat Break!
We've learned a lot, I bet we feel as cool as these cats.
Diving back in...
Remember our store.js
file? Yes, let's update that.
#store.js
import { configureStore } from "@reduxjs/toolkit";
# updated code
import windowDimensionReducer from "./slice/windowDimensionSlice";
export default configureStore({
# updated code
reducer: {
windowDimension: windowDimensionReducer
}
})
Here, we have imported windowDimensionReducer
from our windowDimensionSlice.js
file then we went further to add it to the reducer
object inside our configureStore()
.
Whew! We have made a ton of progress but our application doesn't know what's going on yet, so let's tell it everything we have been up to. Navigate to your index.js
file.
Inside index.js
, let's update the code with the following imports & wrap our app component with all the cool Redux stuff we've been up to.
#index.js
import store from './redux/store';
import { Provider } from 'react-redux';
<Provider store={store}>
<App />
</Provider>
Ok, seriously?? What the @#!$ is this? Let me explain.
We imported our store
from store.js
& Provider
from react-redux
. Provider
- you guessed it - provides our application with every bit of state information it needs to function properly, we then pass it a store
prop which acts as the source of all that state information the Provider
gives our application.
Last Lap.
Inside our components
folder, let's create two files mobile.jsx
& desktop.jsx
. Our folder structure should now look like this:
Okay, great! Let's add some code to these files.
#mobile.jsx
const Mobile = () => <div>Mobile</div>;
export default Mobile;
#desktop.jsx
const Desktop = () => <div>Desktop</div>;
export default Desktop;
Now, to wrap it up. We go in our App.js
file & write a gazillion lines of code like this:
#App.js
import { useEffect } from 'react';
import { resizeWidth } from './redux/slice/windowDimensionSlice';
import { useDispatch, useSelector } from 'react-redux';
# components
import Desktop from './components/desktop';
import Mobile from './components/mobile';
const App = () => {
const updatedWidth = useSelector((state) => state.windowDimension.width);
const dispatch = useDispatch();
useEffect(() => {
const handleWindowResize = () => {
const resize = dispatch(resizeWidth({ width: window.innerWidth }));
console.log(resize);
};
window.addEventListener('resize', handleWindowResize);
return () => window.removeEventListener('resize', handleWindowResize);
}, [dispatch]);
return <> {updatedWidth <= 900 ? <Mobile /> : <Desktop />} </>;
};
export default App;
Sheesh! That's a lot of code. Let me walk you through what we've just done. We import useEffect
from react
, then we import resizeWidth
, which is the action we want to dispatch from windowDimensionSlice.js
, useDispatch
to well, dispatch the action & useSelector
to - you guessed it again - select & read the data from the store.
At the top level of the App
component, we declare a variable updatedWidth
& then call useSelector()
to grab the state we want to use from the store. We also declare a variable dispatch = useDispatch()
.
Inside our useEffect
hook, we create a function handleWindowResize
& inside that function, we dispatch the resizeWidth
action. We go further by adding a resize
EventListener for when our application mounts & we pass our handleWindowResize
function as a callback.
Finally, we clean up after ourselves by removing the EventListener return () => window.removeEventListener('resize', handleWindowResize);
then we pass dispatch
into the dependency array of the useEffect
hook so our application functions just right.
To test it all out, we use our updatedWidth
variable to check that the width of our browser window is less than or equal to 900 updatedWidth <= 900 ?
if it is, we render <Mobile />
, if it's not, we render <Desktop />
We should have something like this:
Disclaimer: You will typically have to do this sort of thing with React's ContextAPI but should your application become large, & will need Redux in the long run, you should consider using Redux for the responsive design of your application.
If you want to jump right into the code, the github repository is here