This weeks in my React
adventure I've been looking at how to untangle the spaghetti of passing around states
using Redux, and react-redux, while I was there I looked at storing the Redux store in localStorage.
Benefits
Let's imagine a scenario where we have 2 components, a to do list to display items and offer some interaction, such as delete, and an entry form that allows you to add items, not an uncommon scenario.
You'd probably make a wrapper component that had a state containing the list and some functions to edit the state which we can pass down to our components using props.
That seems simple enough but now let's say we have another page that is a filtered down list, let's say it only shows items marked as complete, how would you get access to that original state? You'd have to store the state even higher so it can be passed down to all components that need it. The functions would need to be passed down too. The more places you need the data the more convoluted this approach becomes.
Redux, however, creates a store which we can access, or even edit, from any component. You need to check the list in some obscure settings panel of your app? No problem just go to the store and get it. Isn't that simpler? Redux does have a fair bit of code before you can get started but, honestly, when it's all in it's easy to add new items and function to the store.
The boiler plate
Let's get all the boiler plate out the way, I make 3 folders in src
each containing an index.js
. These are reducers
, store
and actions
.
reducers
This is where we create the logic behind our store. We will need a file for each store item. I want to make our list store so I'll show you the adding items logic. We'll call this file list.js
// We pass in a state, which is empty by default
// and an action which we will learn about in the
// actions file
const listReducer = (state = {}, action) => {
// Clone state object
const newState = Object.assign({}, state);
// Look for type set in the actions file
// these types should be as unique as possible
switch (action.type) {
case "LISTITEM_ADD":
// Generate random key and populate with default object.
// Payload is set in the actions file
newState[
Math.random()
.toString(36)
.replace(/[^a-z]+/g, "")
] = {
complete: false,
label: action.payload
};
break;
default:
break;
}
// return the modified state
return newState;
};
export default listReducer;
Now let's look at the index file. The aim of the index file is to merge all of out reducers into one easy to manage reducer. Redux has a function called combineReducers
for this very purpose.
import listReducer from "./list";
import { combineReducers } from "redux";
// The key of this object will be the name of the store
const rootReducers = combineReducers({ list: listReducer });
export default rootReducers;
store
This is where the localStorage
magic happens. Simply by adding these 2 functions we get to store all our data between sessions.
import { createStore } from "redux";
import rootReducers from "../reducers";
// convert object to string and store in localStorage
function saveToLocalStorage(state) {
try {
const serialisedState = JSON.stringify(state);
localStorage.setItem("persistantState", serialisedState);
} catch (e) {
console.warn(e);
}
}
// load string from localStarage and convert into an Object
// invalid output must be undefined
function loadFromLocalStorage() {
try {
const serialisedState = localStorage.getItem("persistantState");
if (serialisedState === null) return undefined;
return JSON.parse(serialisedState);
} catch (e) {
console.warn(e);
return undefined;
}
}
// create our store from our rootReducers and use loadFromLocalStorage
// to overwrite any values that we already have saved
const store = createStore(rootReducers, loadFromLocalStorage());
// listen for store changes and use saveToLocalStorage to
// save them to localStorage
store.subscribe(() => saveToLocalStorage(store.getState()));
export default store;
If you don't want to store the data you will need to remove the saveToLocalStorage
and loadFromLocalStorage
functions also you'll need to remove loadFromLocalStorage
from createStore
and the whole store.subscribe
line.
actions
This is where we will store our "functions", I call them function but they're super simplistic. The function simply returns an object with a type and a payload, payload is just the word we use for the parameters we pass in.
export const addItem = payload => {
return {
type: "LISTITEM_ADD",
payload
};
};
Using provider
Provider is given to us by react-redux
. It's a wrapper component we put in our React's index file. It should look a little something like this.
import React from "react";
import ReactDOM from "react-dom";
import store from "./store";
import { Provider } from "react-redux";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
Using the store
I said at the top of this article that there was a lot of boiler plate and we're finally through it, we can finally start using the store.
useSelector
useSelector
is redux-react
's way of reading data from the store and it's very simple to use. You must import it and then you can use it like so.
import { useSelector } from "react-redux";
// As you see we're getting the whole state
// but we're only returning list which is the
// name we gave it in the reducers file
const list = useSelector(state => state.list);
We can now use list
in our component as we like.
useDispatch
useDispatch
is another redux-react
thing. It allows you to dispatch a function to the store. Again it's quite simple to use as all the boiler plate from earlier does the heavy lifting. We need to import the function we want to use from actions
and useDispatch
.
import { addItem } from "../actions";
import { useDispatch } from "react-redux";
// This stores the dispatch function for using in the component
const dispatch = useDispatch();
// we run the dispatch function containing the addItem function
// As you remember addItem takes a payload and returns an object
// It will now run the reducer
dispatch(addItem(value));
Closing thoughts
Once all the boiler plate is out of the way this makes using accessing data across components so much easier, I can see it really helping me with projects down the line. It also has the added benefit of making cross session saving super easy!
It was a bit of a long one this week but we got there. Thank you for reading. If you have any questions or corrections feel free to post them down below.
Thanks again 🦄🦄💕❤️🧡💛💚🤓🧠