Frontend Engineer at Conekta
Co-organizer at IbagueJS
@diegoavilap_
Predictable state container for JavaScript apps, and a very valuable tool for organizing application state.
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.
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
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.
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
}
});
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))
);
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
});
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) {}
}
});
const initialState = {
posts: [],
status: "loading",
error: null
};
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
}
}
export const {
postAdded,
postUpdated,
reactionAdded
} = postsSlice.actions;
export const postAdded = (data) => {
return {
type: 'posts/postAdded',
payload: data
};
};
export const fetchCharacters = createAsyncThunk(
"characters/fetchCharacters",
async () => {
const response = await client.get(
"https://rickandmortyapi.com/api/character"
);
return response.data.results;
}
);
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;
});
}
});
// 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
};
}
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;
}
};
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;
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>
);
};
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);