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 Toolkit
By diegoavilap
Redux Toolkit
- 177