DEV Community

CluelessTurtle
CluelessTurtle

Posted on • Edited on

CRUD with Redux

As react applications grow and become bigger apps the state and props that are shared between them become more and more harder to manage. Eventually, the state and props become a tangled mess. This can lead to unpredictable behaviors and bugs. That is where Redux comes in! Imagine taking all the props and states and storing them in one place where you can call upon whatever state you need in any component. In this blog, I am going to go over how to create, read, update, and delete (CRUD) state with redux.

Initial Setup

Before we start getting into creating state we first need to setup our react app with redux.

First, we create our React app and cd into it.

npx create-react-app my-app
cd my-app
Enter fullscreen mode Exit fullscreen mode

Second, we need to install the Redux package and with it, we will also install Redux toolkit which will be useful for us later. We can do both by running the following command in the terminal.

npm install @reduxjs/toolkit
Enter fullscreen mode Exit fullscreen mode

One of the perks of installing the Redux toolkit is there is a lot less setup we have to do. As I mentioned before we are going to be holding all our state in one place. This place is called the store and to the core, it is a simple javascript object.

Redux Store Setup

Thanks to our tool kit creating a store is a pretty simple process. first, we need to create a new javascript file in our source folder let's call it store.js.

Within our store.js we are going to need to import configureStore from our toolkit and create a variable to represent our store.

import { configureStore } from "@reduxjs/toolkit"; 

const store = configureStore({
    reducer: {

   }
})

export default store;
Enter fullscreen mode Exit fullscreen mode

This store is looking good! Now the last step to setting up our store object is providing that state within it to our App component the best way to do that is in our index.js file. We do this by importing the Provider component from react-redux and wrapping it around our entire App like so.

import App from "./App"
import { Provider } from "react-redux"
import store from "./store"

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}/>
      <App/>
  </Provider>
)
Enter fullscreen mode Exit fullscreen mode

Awesome! Now with that, we have our store set up and ready to go! There isn't much in our store right now so let's change that.

Reducers

You may be wondering what that reducer key is in our store object. Well to put it simply a reducer is a function that takes in two arguments the current state and an action. With these arguments, it reduces them to one output which will be put into our store.

Let's start by creating our own reducer for puppies! So first we are going to create two folders one folder called features and inside that folder we have one called dogs. Within our dogs folder, we are going to create a javascript file called dogsSlice.js

In the dogsSlice.js file we are going to import createSlice from our tool kit to create our reducer. We are also going to equal it to a variable so we can export it to our store.

import { createSlice } from "@reduxjs/toolkit" 

const dogsSlice = createSlice({
     name: "dogs",
     initialState: {
         dogs: []    
     },
     reducers: {

     }
})

export default dogsSlice.reducer
Enter fullscreen mode Exit fullscreen mode

Here we are importing createSlice from our tool kit to create our new reducer. We gave our reducer the name of dogs and setting its initial state to an object with a key called dogs that points to an empty array. After we set up our reducers which are currently blank because we haven't created any we then export all of it.

So now we can link our new reducer to our store like so.

import { configureStore } from "@reduxjs/toolkit"; 
import dogsReducer from "./features/dogs/dogsSlice"

const store = configureStore({
    reducer: {
       dogs: dogsReducer
   }
})

export default store;
Enter fullscreen mode Exit fullscreen mode

BAM! We created our first reducer which will handle the state logic and our store which will handle storing all of our state. There is one more thing we are going to need to understand before we start creating our new state

Actions

We are so close to creating our new state but before we do we need to tell our reducers which one is going to run. What that means is that we are going to create a reducer for creating, reading, updating, and deleting our state so how does our dogsSlice know which one to run?

While creating our reducer I mentioned that they take two arguments the current state and an action. To be more specific the reducer actually takes the action.payload which it uses to maipulate the state. The action object actually looks like this.

const action = {type: "dog/add", payload: {}}
Enter fullscreen mode Exit fullscreen mode

the action.type tells our dogsSlice which reducer to run and the payload is what we use to manipulate the state. That's great we just have one issue how are we going to populate the payload with state?

We are going to create a function that takes state as an argument and returns an action.

function dogAdded(newDog) {
   return {
      type: "dog/add",
      payload: newDog
   }
}
Enter fullscreen mode Exit fullscreen mode

This function we created is called an action creator because it creates our action instead of us creating it manually. The most important part is that it takes state as an argument and sets it to payload.

The Redux tool kit already takes care of this for us. For example, let's say we want to create our dogAdded function we would write the following code at the bottom of our dogsSlice.

// this will auto generate our action creators
export const { dogAdded } = reviewsSlice.actions
Enter fullscreen mode Exit fullscreen mode

Create

Finally! We can start talking about adding some state! Let's start by creating a function called you guessed it dogAdded that takes in two arguments state and an action.

import { createSlice } from "@reduxjs/toolkit" 

const dogsSlice = createSlice({
     name: "dogs",
     initialState: {
         dogs: []    
     },
     reducers: {
         dogAdded(state, action) {

         }
     }
})

export const { dogAdded } = reviewsSlice.actions

export default dogsSlice.reducer
Enter fullscreen mode Exit fullscreen mode

Within the dogAdded function, we are going to push our action payload into the current state like so.

import { createSlice } from "@reduxjs/toolkit" 

const dogsSlice = createSlice({
     name: "dogs",
     initialState: {
         dogs: []    
     },
     reducers: {
         dogAdded(state, action) {
             state.dogs.push(action.payload)
         }
     }
})

export const { dogAdded } = reviewsSlice.actions

export default dogsSlice.reducer
Enter fullscreen mode Exit fullscreen mode

Now I bet I know what everyone is thinking right now after seeing the code above. "why are you using push? isn't that a destructive array method? Like doesn't that mutate the original array?".

Yes, you are right the push() method does mutate the original array and is a very destructive method to use in javascript, however thanks to our toolkit. Under the hood redux toolkit uses a library called Immer to handle state updates. With this in place as long as we use createSlice Immer will make sure we are not actually mutating the state. Awesome! right?

Update and Delete

Since we got our redux ready to create new state let's setup a way for our state to be updated and deleted. Just like our other reducer we are going to start off by creating the action and a function to take state and an action

import { createSlice } from "@reduxjs/toolkit" 

const dogsSlice = createSlice({
     name: "dogs",
     initialState: {
         dogs: []    
     },
     reducers: {
         dogAdded(state, action) {
             state.dogs.push(action.payload)
         }
         dogUpdated(state, action){

         }
         dogRemoved(state, action){

         }
     }
})

export const { dogAdded, dogUpdated, dogRemoved } = reviewsSlice.actions

export default dogsSlice.reducer
Enter fullscreen mode Exit fullscreen mode

As mentioned before we can use destructive array methods within our dogsSlice thanks to our redux toolkit. So for our update and delete reducers we are going to use the splice() method.

The splice method takes three arguments the first argument is where the deleting/updating is going to start. The second argument is how many things should be deleted/updated and the last argument is what we are going to replace it with.

The splice() method is perfect for our reducer however, we are still going to need to figure out what the index for the dog that we want to delete/update is. In other words we need to figure out where our splice() method is going to start.

We accomplish finding the index by using the findIndex() method. Assuming we are still getting the whole dog object through our action payload we can compare dog id's until we find our dog index!

Let's try it out!

import { createSlice } from "@reduxjs/toolkit" 

const dogsSlice = createSlice({
     name: "dogs",
     initialState: {
         dogs: []    
     },
     reducers: {
         dogAdded(state, action) {
             state.dogs.push(action.payload)
         }
         dogUpdated(state, action){
          const index = state.dogs.findIndex((dog) => {
               return dog.id === action.payload.id
          })

         }
         dogRemoved(state, action){
             const index = state.dogs.findIndex((dog) => {
                  return dog.id === action.payload.id
             })
         }
     }
})

export const { dogAdded, dogUpdated, dogRemoved } = reviewsSlice.actions

export default dogsSlice.reducer
Enter fullscreen mode Exit fullscreen mode

Now with the index we can feed it into our splice method and with our update reducer we are going to give the whole dog object as the third argument. While our delete reducer we will leave the third argument blank.

import { createSlice } from "@reduxjs/toolkit" 

const dogsSlice = createSlice({
     name: "dogs",
     initialState: {
         dogs: []    
     },
     reducers: {
         dogAdded(state, action) {
             state.dogs.push(action.payload)
         }
         dogUpdated(state, action){
          const index = state.dogs.findIndex((dog) => {
               return dog.id === action.payload.id
          })
          state.dogs.splice(index, 1, action.payload)
         }
         dogRemoved(state, action){
             const index = state.dogs.findIndex((dog) => {
                  return dog.id === action.payload.id
             })
             state.dogs.splice(index, 1)
         }
     }
})

export const { dogAdded, dogUpdated, dogRemoved } = reviewsSlice.actions

export default dogsSlice.reducer
Enter fullscreen mode Exit fullscreen mode

Now we have completed our reducers to handle creating, updating and deleting our state but wait how are we going to get our state into our components?

Reading state with useSelector

Assuming our state looks like this.

dogs: [
    {name: "Oreo", age: 5, breed: "Golden Retriever"},
    {name: "Joey", age: 7, breed: "Pitbull"},
    {name: "Yazzy", age: 2, breed: "Poodle"}
]
Enter fullscreen mode Exit fullscreen mode

Also assuming our App component looks like this.

import React from "react"

function App () {

   return (
     <div>

     </div>
   )
}

export default App
Enter fullscreen mode Exit fullscreen mode

We can easily read our state in our components using the useSelector hook that redux gives us. We just have to pass a callback function into it. This callback function will take state as an argument and return the state we want.

import React from "react"
import Puppy from "./Puppy
import { useSelector } from "react-redux"

function App () {

const dogs = useSelector((state) => state.dogs.dogs)

const dogsToDisplay = dogs.map((dog) => <Puppy dog={dog}/>)

   return (
     <div>
        {dogsToDisplay}
     </div>
   )
}

export default App
Enter fullscreen mode Exit fullscreen mode

We gain access to all our state through the useSelector hook which we can import into any of our components. We then can select which state we need by first specifying the root state, then the name of the reducer, and then the name of the state within the reducer.

This is great! Now we have full access to our state whenever and wherever we need it! There is just one problem how will we tell our components to actually use our reducers?

useDispatch

As the title implies we are going to do this by using the useDispatch() hook. Recall when we were talking about our action creator functions since redux toolkit takes care of making those for us all we are going to have to do is feed the useDispatch hook the action creator and the action payload.

assuming we have a controlled form for creating dogs like so.

import React from "react"
import Puppy from "./Puppy
import { useDispatch } from "react-redux"

function dogForm () {

const dispatch = useDispatch()
const [name, setName] = useState("")
const [age, setAge] = useState(0)
const [breed, setBreed] = useState("")

function handleSubmit(e){
  e.preventDefault()

    const objToBeSent = {
    name: name,
    age: age,
    breed: breed
  }

  dispatch(dogAdded(objToBeSent))
}

   return (
     <div>
        <form onSubmit={handleSubmit}>
           <label> Name: </label>
           <input onChange={(e) => setName(e.target.value)} value= 
           {name}/>
           <label> Age: </label>
           <input onChange={(e) => setAge(e.target.value)} value= 
           {age}/>
           <label> Breed: </label>
           <input onChange={(e) => setBreed(e.target.value)} 
           value={breed}/>
           <button type="submit"> Submit </button>
        </form>
     </div>
   )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Here we have a controlled form which upon submission of the form we use the dispatch hook and feed it the action creator function and the action payload. This code then goes to our create reducer to add the new dog to our state.

BOOOOMMMM this is so awesome we are now using redux to Create, Read, Update, and Delete our state.

Conclusion

By putting all our state into the redux store we have effectively and efficiently managed out our state. Goodbye prop drilling and goodbye messy state within our components

Top comments (0)