Diego Avila

Frontend Engineer at Conekta

Co-organizer at IbagueJS

@diegoavilap_

Redux

Redux

Predictable state container for JavaScript apps, and a very valuable tool for organizing application state.

Redux

Store

The Redux store is the main, central bucket which stores all the states of an application. It should be considered and maintained as a single source of truthΒ for the state of the application.

Action

return {
   type: 'RemoveItem', //action type
   payload: {
       productId: '1' //payload information
   } 
}

Actions are simple JavaScript objects that are used to send information from your application to the store. They are an object with a type and an optional payload

Reducer

const reducer = (state = initialState, action) => {
   const { type, payload } = action;
   switch (type) {
      case 'REMOVE_ITEM':
         return {
         	...state,
            state.cart = state.cart.filter((product) => product.id !== payload.producId)
         }
      default:
         return state;
   }
}

Reducers are functions. They take the previous state and an action object as arguments and return the next state.

configureStore()

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

import postsReducer from "../features/posts/postsSlice";
import usersReducer from "../features/users/usersSlice";
import charactersReducer from "../features/characters/charactersSlice";

export default configureStore({
  reducer: {
    posts: postsReducer,
    users: usersReducer,
    characters: charactersReducer
  }
});

Redux Core

import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import rootReducer from "./rootReducer.js";

export const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

Redux Core

import { combineReducers } from 'redux';

import postsReducer from "../features/posts/postsSlice";
import usersReducer from "../features/users/usersSlice";
import charactersReducer from "../features/characters/charactersSlice";

export default const rootReducer = combineReducers({
  // Define a top-level state field named `posts`, handled by `postsReducer`
  posts: postsReducer,
  users: usersReducer,
  characters: charactersReducer
});

createSlice()

const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {
    postAdded: {},
    postUpdated: (state, action) => {
      const { id, title, content, userId } = action.payload;
      const existingPost = state.posts.find((post) => post.id === id);
      if (existingPost) {
        existingPost.title = title;
        existingPost.content = content;
        existingPost.user = userId;
      }
    },
    reactionAdded(state, action) {}
  }
});

createSlice()

const initialState = {
  posts: [],
  status: "loading",
  error: null
};

Redux Core

export default function postsReducer(state = initialState, action) {
  switch (action.type) {
    case 'posts/postAdded': {
      const { title, content, userId } = action.payload
      // Can return just the new posts array - no extra object around it
      return {
        ...state,
        posts: [
          ...state.posts,
          {
              id: nanoid(),
              date: new Date().toISOString(),
              title,
              content,
              user: userId
          }
        ]
        
       }
    }
    case 'posts/postUpdated': {}
    default:
      return state
  }
}

Actions creators

export const { 
  postAdded, 
  postUpdated, 
  reactionAdded 
} = postsSlice.actions;

Redux Core


export const postAdded = (data) => {
  return {
    type: 'posts/postAdded',
    payload: data
  };
};

createAsyncThunk()

createAsyncThunk()

export const fetchCharacters = createAsyncThunk(
  "characters/fetchCharacters",
  async () => {
    const response = await client.get(
      "https://rickandmortyapi.com/api/character"
    );
    return response.data.results;
  }
);

createAsyncThunk()

const charactersSlice = createSlice({
  name: "characters",
  initialState,
  reducers: {},
  extraReducers(builder) {
    builder
      .addCase(fetchCharacters.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(fetchCharacters.fulfilled, (state, action) => {
        state.status = "succeeded";
        // Add any fetched posts to the array
        state.characters = state.characters.concat(action.payload);
      })
      .addCase(fetchCharacters.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      });
  }
});

Redux Core

// types
const FETCH_CHARACTERS_SUCCESS = "FETCH_CHARACTERS_SUCCESS";
const FETCH_CHARACTERS_PENDING = "FETCH_CHARACTERS_PENDING";
const FETCH_CHARACTERS_ERROR = "FETCH_CHARACTERS_ERROR";


export const fetchCharactersSuccess = (data) => {
  return {
    type: FETCH_CHARACTERS_SUCCESS,
    payload: data
  };
};

export function fetchCharactersPending() {
  return {
    type: FETCH_CHARACTERS_PENDING
  };
}

export function fetchCharactersError(error) {
  return {
    type: FETCH_CHARACTERS_ERROR,
    error: error
  };
}

Redux Core

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_CHARACTERS_PENDING:
      return {
        ...state,
        pending: true
      };
    case FETCH_CHARACTERS_SUCCESS:
      return {
        ...state,
        pending: false,
        characters: action.payload
      };
    case FETCH_CHARACTERS_ERROR:
      return {
        ...state,
        pending: false,
        error: action.error
      };
    default:
      return state;
  }
};

Redux Core

import { fetchCharactersPending, fetchCharactersSuccess, 
  fetchCharactersError
} from "../../index";

function fetchCharacters() {
  return (dispatch) => {
    dispatch(fetchCharactersPending());
    fetch("https://rickandmortyapi.com/api/character")
      .then((res) => res.json())
    
      .then((res) => {
      
        dispatch(fetchCharactersSuccess(res.results));
        return res.results;
      
      })
    
      .catch((error) => {
        dispatch(fetchCharactersError(error));
      });
  };
}

export default fetchCharacters;

React Redux

import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";

import { Spinner } from "../../components/Spinner";
import { CharacterCard } from "./CharacterCard/CharacterCard";

import { fetchCharacters } from "./charactersSlice";

export const CharactersList = () => {
  const dispatch = useDispatch();

  const characters = useSelector((state) => state.characters.characters);

  const characterStatus = useSelector((state) => state.characters.status);
  const error = useSelector((state) => state.characters.error);

  useEffect(() => {
    if (characterStatus === "loading") {
      dispatch(fetchCharacters());
    }
  }, [characterStatus, dispatch]);

  let content;

  if (characterStatus === "loading") {
    content = <Spinner text="Loading..." />;
  } else if (characterStatus === "succeeded") {
    content = characters.map((character) => (
      <CharacterCard key={character.id} character={character} />
    ));
  } else if (characterStatus === "failed") {
    content = <div>{error}</div>;
  }

  return (
    <section className="posts-list">
      <h2>Characters</h2>
      <div className="character-list">{content}</div>
    </section>
  );
};

React Redux - Core

import React from "react";
import { connect } from "react-redux";
import fetchCharacters from "./store/features/characters/fetchCharacters";
import CharacterCard from "./store/features/characters/CharacterCard/CharacterCard";
import { Spinner } from "./components/Spinner";

import "./styles.css";

class App extends React.Component {
  componentDidMount() {
    this.props.dispatch(fetchCharacters());
  }

  render() {
    const { error, pending, characters } = this.props;

    let content;

    if (pending) {
      content = <Spinner text="Loading..." />;
    } else if (error) {
      content = <div>Error! {error.message}</div>;
    } else {
      content = characters.map((character) => (
        <CharacterCard key={character.id} character={character} />
      ));
    }

    return (
      <div className="container">
        <h1>
          Rick and Morty - Redux <span>βš›</span>️
        </h1>
        <div className="character-list">{content}</div>
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  characters: state.characters,
  pending: state.pending,
  error: state.error
});

export default connect(mapStateToProps)(App);

References

Projects

πŸ”— Redux Project

πŸ”— Β ReduxToolkit Project

Β 

Redux Toolkit

By diegoavilap

Redux Toolkit

  • 177