DEV Community

Sianwa Atemi
Sianwa Atemi

Posted on • Edited on

Day 11 - 20

Day 11: Redux

Today I read about redux, some core concepts and when it is necessary to use. I shall create a small project for day 12 and 13 to explain the concepts that I have learnt.


Day 12: Redux (Basics)

Today I read the redux documentation and followed along with the tutorial provided. I will try to summarize all I learnt in two parts as it took two days to read through the documentation. I will then attempt to create a small project to practice what I learnt.

So what is redux? It is basically a library for managing application state. It serves as a centralized store for state that needs to be used by the entire application with certain rules that govern how state should be updated.

What makes redux different from Context api (another React state management feature covered in day 1 to 10) is that it has tooling that makes it easier when you have many state types in large applications, it makes it easier when working in teams because of its clear rules and has state slices to prevent a bloated store file, state slices help manage applications with multiple states.

Note: Using redux means writing more code and having to learn more, hence it does not need to be used for all applications. One should take the time to think about their application and the best tools to use.

Redux Terminology

Actions

This is a JavaScript object that defines something that happens in the application. It looks similar to the actions dispatched in context api

   // action
   const addUserAction = {
     type: "users/userAdded",
     payload: { username: "John", email: "john@example.com" },
   };

   // action creator
   const addUser = (data) => {
     return {
       type: "users/userAdded",
       payload: data,
     };
   };
Enter fullscreen mode Exit fullscreen mode

Redux has action creators that creates and returns an action object, this prevents us from writing action objects by hand every time.

Reducers

A reducer is a function that receives the current state of the application and an action object, decides whether to update the state and returns the new state.

Reducers have rules that must be followed, some include not including operations that cause side effects like asynchronous logic, state must remain immutable and should only calculate the new state based on state and action arguments. These rules will be seen in action in future examples.

   const counterReducer = (state = { value: 0 }, action) => {
     switch (action.type) {
       case "counter/increment":
         return {
           // state should be immutable
           ...state,
           value: state.value + 1,
         };

       default:
         break;
     }
   };
Enter fullscreen mode Exit fullscreen mode

Store

The entire applications state lives in an object called the store. Redux store accepts a reducer function as its arguments

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

   const store = configureStore({ reducer: counterReducer });
Enter fullscreen mode Exit fullscreen mode

Dispatch

Dispatch is a function in redux that is used to update the state, it receives an action object as its arguments or action creators to trigger a specific state update.

   // dispatch an action
   store.dispatch({
     type: "users/userAdded",
     payload: { username: "John", email: "john@example.com" },
   });

   // dispatch an action creator
   store.dispatch(addUser());
Enter fullscreen mode Exit fullscreen mode

Selector

These are functions that extract specific pieces of information from a store. They are mostly used in the view part of the application. Multiple selectors can be used if you have many state slices.

   const selectCounterValue = (state) => state.value;

   const currentValue = selectCounterValue(store.getState());
Enter fullscreen mode Exit fullscreen mode

Day 13 & 14: Redux Basics (Practice project)

To practice the concepts learnt in day 12, I will be refactoring the to-do list application made in day 1 that used the context api to use redux and redux tool kit.

The main action types in the to-do list will be adding a task, deleting a task, editing a task and toggling the state of a task as complete and incomplete. The original source code (using context api) is linked in day 1 of this challenge with the live demo.

To add Redux to the to-do list, I first created a store with Redux's configureStore function that takes in reducer functions as its parameter and nested the entire App component with Provider from react-redux, with the store as a prop.

import { configureStore } from "@reduxjs/toolkit";
import tasksReducer from "../tasks/tasksSlice";

export default configureStore({
  reducer: {
    tasks: tasksReducer,
  },
});
Enter fullscreen mode Exit fullscreen mode
import store from "../src/app/store";
import { Provider } from "react-redux";

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

Next I created a tasksSlice file with the createSlice function, this allows you to create a state slice making large state more managable. createSlice accepts an initial state, an object full of reducer functions and a slice name. It creates action creators and action types that correspond to the reducers state.

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

const initialState = [];

export const taskSlice = createSlice({
  name: "tasks",
  initialState,
  reducers: {
    addTask(state, action) {
      state.push(action.payload);
    },
    deleteTask(state, action) {
      const { id } = action.payload;
      console.log(id);
      return state.filter((task) => task.id !== id);
    },
   // ... more actions available in source code provided
  }
});

export const { addTask, deleteTask, editTask, toggleComplete } =
  taskSlice.actions;

export default taskSlice.reducer;


Enter fullscreen mode Exit fullscreen mode

Finally, to access the actions in the views, we use useDispatch to call the action creators and pass in payloads. useSelector is also used to read current state.

import { useSelector } from "react-redux";

// Get all tasks
const tasks = useSelector((state) => state.tasks);
Enter fullscreen mode Exit fullscreen mode
import { useDispatch } from "react-redux";

 const dispatch = useDispatch();

// add task on form submit by calling add task with a payload of id, name and completed
   dispatch(
      addTask({
        id: nanoid(),
        name: newTask,
        completed: false,
      })
    );

Enter fullscreen mode Exit fullscreen mode

This is a summary of what I did, the source code can be found here


Day 15: Asynchronous code in Redux

To demonstrate how to handle async code in redux, I will use the starwars api to fetch a list of films. Below is my redux store with an initial state holding an empty array of films and a loading state to show the state of requests.

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

export const LOADING_STATE = {
  WAITING: "waiting",
  REQUESTED: "requested",
  ERROR: "error",
  DONE: "done",
};

const initialState = { film: [], status: LOADING_STATE.WAITING };

export const movieSlice = createSlice({
  initialState,
  name: "movies",
  reducers: {
    uploadMovies(state, action) {
      state.film.push(action.payload);
    },
    loadingState(state, action) {
      state.status = action.payload;
    },
  },
});

export const { uploadMovies, loadingState} = movieSlice.actions;

export default movieSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

NOTE: One of the rules of reducers states that they must not do any asynchronous logic or other "side effects".

In the component, a useEffect hook can be used to dispatch the actions and fetch data from the api.

  useEffect(() => {
    dispatch(loadingState(LOADING_STATE.REQUESTED));
    fetch("https://swapi.dev/api/films")
      .then((res) => res.json())
      .then((data) => {
        data.results.forEach((el) => {
          dispatch(uploadMovies(el));
        });
        dispatch(loadingState(LOADING_STATE.DONE));
      })
      .catch((err) => dispatch(LOADING_STATE.ERROR));
  }, [dispatch]);
Enter fullscreen mode Exit fullscreen mode

This method works alright but has some drawbacks, it puts too much logic in the component, it is not reusable as this code has to be written in multiple controllers or you might have to call the component every time.

This can be solved using redux middleware.


Day 16: Redux Middleware

For simple applications, the above approach in day 15 would work but when apps get larger you may have difficulties with that approach, for example you may have to repeat certain chunks of code just to carry out the same functionality.

Redux middleware works by first handling an event, like a user click or page reload, an object or function is then dispatched, once dispatched, the value looks for a middleware to make an async call to. Finally when the async call completes, a real action object is dispatched. A better explanation can be found here, I probably butchered it 😬😂.

Anyway, thunk middlweare allows us to write functions in our reducers that get dispatch and getState as arguments

A thunk is a piece of code that does some delayed work. Link

Since I used the official redux toolkit package, I did not have to configure the store to add thunkMiddleware. In a different scenario, you have to install redux-thunk and enable it in the store as shown below.


import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers/index'

const store = createStore(rootReducer, applyMiddleware(thunk))

Enter fullscreen mode Exit fullscreen mode

All I did was add a thunk function in my reducer that now fetches all the movies. This is in the movie-slice.js file.

export const fetchMovies = () => {
  return (dispatch, getState) => {
    dispatch(loadingState(LOADING_STATE.REQUESTED));
    return fetch("https://swapi.dev/api/films")
      .then((res) => res.json())
      .then((data) => {
        data.results.forEach((el) => {
          dispatch(uploadMovies(el));
        });
        dispatch(loadingState(LOADING_STATE.DONE));
      })
      .catch((err) => dispatch(LOADING_STATE.ERROR));
  };
};

Enter fullscreen mode Exit fullscreen mode

Now my component is much more lean as I can now dispatch the fetchMovies function in a useEffect hook.


import {fetchMovies} from "./movies/movie-slice";

  useEffect(() => {
    dispatch(fetchMovies());
  }, [dispatch]);

Enter fullscreen mode Exit fullscreen mode

This approach makes fetching movies reusable and separates concerns which can be really beneficial in large projects. I probably did not explain the concept too well and went on rambling but this is how I understood it, I shall do another practice project to make the concept stick. The code can be found in this branch


Day 17 to 19: Shopping cart in react-redux

I have been quite busy with a job and commuting, but I will try to push this small project soon and continue to day 20.

UPDATE
I finally managed to finish the shopping cart, here is the link to the source code.

Day 20: What I learnt

Day 11 to 20 of this challenge were the hardest for me because I lost motivation on day 19, It took me 2 months to complete the shopping cart project. This was not because of its complexity but because of a little bit of laziness combined with the daily struggles of working a 9 to 5 and commuting.

This is what I have learnt from day 11 to 20

  • How to use redux 😁😀
  • Showing up is better than not doing anything at all.
  • The final product doesn't have to be perfect (This is just a coding and learning challenge)
  • One loses motivation but you should never give up.

Top comments (0)