Hi!
I was working on my Phase-4 project for Flatiron, and ran into a very annoying problem.
The project requirements were to build a rails server with three associated models, and a react front end with full CRUD that would also update state.
I only want a user to see their own data, and therefore sent all of a user's data in one large user object. This made updating state based on CRUD actions very difficult and confusing.
The solution to this problem was to use a state management system: I chose to use Redux.
Redux works by storing state in a separate component known as a store, and allowing all components access to the store. This changes state into a global variable. No more passing down endless props! Any component that needs access to state or updates state can access it directly from the redux store.
1. Install React-Redux and Redux Toolkit
I used Redux Toolkit to minimize the amount of code I needed to write. Install redux toolkit and react-redux by running
nnpm install @reduxjs/toolkit react-redux
2. Wrap App with Provider
This is my index.js, where I am importing a Provider from react-redux and using it to wrap my entire app. I am then importing my store and passing it in as props to the Provider. This gives my entire app direct access to the state stored in the redux store.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from "react-redux";
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './components/App';
import reportWebVitals from './reportWebVitals';
import store from "./redux/store"
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
3. Create a Store
Next, create a store to hold your state. The simplest way to create a store is to use redux toolkit's configureStore(). The store contains two pieces of state, my user and a list of recipients.
import { configureStore } from "@reduxjs/toolkit";
import { recipientsSlice } from "./recipientsSlice";
import { usersSlice } from "./usersSlice";
const store = configureStore({
reducer: {
recipients: recipientsSlice.reducer,
users: usersSlice.reducer
},
});
export default store;
4. Set Up Slices
A slice is made up of actions and reducers. The actions are methods to change state, while the reducer applies the change to the current state.
The reducers job is to combine two pieces of the app - the current state and an action. It is a method that takes in the current state and the action we want to do to update state and returns the updated state.
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const fetchRecipients = createAsyncThunk('recipients/getRecipients', async () => {
try{
const getRecipients = await axios.get('/recipients');
const res = await getRecipients.data;
if (getRecipients.status === 200) {
return res;
} else {
return res.error;
}
} catch(e) {
return e.error;
}
});
export const postRecipient = createAsyncThunk('recipients/postRecipient', async (recipient) => {
const newRecipient = await axios.post('/recipients', recipient);
const res = await newRecipient.data;
return res;
});
export const recipientsSlice = createSlice({
name: "recipients",
initialState: {
recipients: [],
isFetching: false,
isSuccess: false,
isError: false,
errorMessage: "",
},
extraReducers: {
[postRecipient.fulfilled]: (state, { payload }) => {
state.isFetching = false;
state.isSuccess = true;
state.recipient = [...state.recipient, payload]
},
[postRecipient.pending]: (state) => {
state.isFetching = true;
},
[postRecipient.rejected]: (state, { payload }) => {
state.isFetching = false;
state.isError = true;
state.errorMessage = payload.error;
},
[fetchRecipients.fulfilled]: (state, { payload }) => {
state.recipient = payload;
state.isFetching = false;
state.isSuccess = true;
return state;
},
[fetchRecipients.rejected]: (state, { payload }) => {
state.isFetching = false;
state.isError = true;
state.errorMessage = payload.error;
},
[fetchRecipients.pending]: (state) => {
state.isFetching = true;
},
},
})
5. Access State in React Components
Finally, we need to access state in a react component, as well as tell state when to update from the component. To do this there are two tools to use: useSelector and useDispatch. Both are imported into a component from "react-redux", and can be called in any component.
UseSelector is used to specify which values from state should be displayed.
useDispatch is used to send an action to the reducer.
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from "react-redux";
import { userSelector, clearState, loginUser } from '../redux/usersSlice';
const LoginForm = () => {
const { isError, errorMessage } = useSelector(userSelector)
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [errors, setErrors] = useState(false)
const dispatch = useDispatch()
useEffect(() => {
if (isError) {
setErrors(errorMessage)
dispatch(clearState())
}
}, [isError, dispatch, errorMessage])
function handleSubmit(e) {
e.preventDefault()
const user = {
username,
password
}
dispatch(loginUser(user))
}
return (
<div className="row container valign-wrapper">
<form onSubmit={handleSubmit} className="col s8">
<div className="row">
<div className="input-field col s8">
<label>Username </label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
></input>
</div>
<div className="input-field col s8">
<label>Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
></input>
</div>
<div className="input-field col s8">
<input className="btn" type="submit" value="Log In" />
</div>
</div>
</form>
{ errors ? <h5>{errors}</h5> : null }
</div>
)
}
export default LoginForm
And there it is! State is now stored in a redux store, making it a global variable. No more passing down confusing props in order to update state in so many places.
Thanks for reading!
Top comments (0)