Interstellar Code
Guía de Redux Toolkit
Aprende a utilizar la librería mas popular para la gestión del estado global en aplicaciones de React, Redux Toolkit es una version moderna de lo que en su día fue Redux, esta librería nos permite manejar el estado global de nuestra aplicación de una manera sencilla y eficiente, abstrae muchos de los conceptos complejos que tenia Redux y nos permite crear un store de una manera mucho mas simple y rápida.
29 de agosto de 2025
Tabla de contenido:
Guía de Redux Toolkit
Redux Toolkit es la librería oficial recomendada para la gestión del estado global en aplicaciones React. Proporciona una forma sencilla y eficiente de manejar el estado de la aplicación, simplificando muchas de las tareas que tradicionalmente eran complejas con Redux. Redux Toolkit abstrae conceptos complicados y ofrece una API intuitiva que facilita la creación y gestión del estado global.
Configuración del store
import { configureStore, type Middleware } from "@reduxjs/toolkit"
import { THEME_STORAGE_KEY } from "../../config/constants/storage.constant"
import { authSlice } from "./auth/auth.slice"
import { eventsSlice } from "./events/events.slice"
import { tasksSlice } from "./tasks/tasks.slice"
import { uiSlice } from "./ui/ui.slice"
const persistenceLocalStorageMiddleware: Middleware = (store) => (next) => (action) => {
next(action)
const currentTheme = store.getState().ui.theme
window.localStorage.setItem(THEME_STORAGE_KEY, currentTheme)
document.documentElement.setAttribute("data-theme", currentTheme)
}
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
ui: uiSlice.reducer,
events: eventsSlice.reducer,
tasks: tasksSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
immutableCheck: false,
}).concat(persistenceLocalStorageMiddleware),
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
import { Provider } from 'react-redux'
import Router from './Router'
import { store } from './store/store'
function App() {
return (
<Provider store={store}>
<Router />
</Provider>
)
}
export default App
Slices
import { createSlice } from "@reduxjs/toolkit"
import { Views, type View } from "react-big-calendar"
import { CALENDAR_VIEW_STORAGE_KEY, TASK_VIEW_STORAGE_KEY, THEME_STORAGE_KEY } from "../../../config/constants/storage.constant"
import { THEMES_OPTIONS } from "../../../config/constants/themes.constant"
import type { Theme } from "../../../infrastructure/interfaces/theme.interface"
interface UIState {
modal: null | "bell" | "settings" | "user"
theme: Theme
calendarView: View
taskView: "list" | "kanban"
}
const initialState: UIState = (() => {
const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY)
const storedCalendarView = window.localStorage.getItem(CALENDAR_VIEW_STORAGE_KEY) as View || Views.WEEK
const storedTaskView = window.localStorage.getItem(TASK_VIEW_STORAGE_KEY) || "list"
if (!storedTheme) {
window.localStorage.setItem(THEME_STORAGE_KEY, "indigo")
return {
modal: null,
theme: "indigo",
taskView: storedTaskView as "list" | "kanban",
calendarView: storedCalendarView as View,
}
}
return {
modal: null,
theme: storedTheme as Theme || "indigo",
calendarView: storedCalendarView as View,
taskView: storedTaskView as "list" | "kanban",
}
})()
export const uiSlice = createSlice({
initialState: initialState,
name: "ui",
reducers: {
openModal: (state, action) => {
state.modal = action.payload
},
closeModal: (state) => {
state.modal = null
},
setTheme: (state, action) => {
const newTheme = action.payload
if (THEMES_OPTIONS.includes(newTheme)) {
state.theme = newTheme
}
},
setCalendarView: (state, action) => {
const newView = action.payload
if (Object.values(Views).includes(newView)) {
state.calendarView = newView
window.localStorage.setItem(CALENDAR_VIEW_STORAGE_KEY, newView)
}
},
setTaskView: (state, action) => {
const newView = action.payload
if (newView === "list" || newView === "kanban") {
state.taskView = newView
window.localStorage.setItem(TASK_VIEW_STORAGE_KEY, newView)
}
},
},
})
export const {
openModal,
closeModal,
setTheme,
setCalendarView,
setTaskView,
} = uiSlice.actions
export default uiSlice.reducer
import { createSlice } from "@reduxjs/toolkit"
import type { User } from "../../../domain/entities/user.entity"
export interface AuthState {
status: 'checking' | 'authenticated' | 'unauthenticated' | 'error'
user: User
errorMessage?: string
}
export const initialAuthState: AuthState = {
status: 'checking',
user: {
id: '',
name: '',
fullName: '',
email: '',
avatarUrl: undefined,
},
errorMessage: undefined,
}
export const authSlice = createSlice({
name: 'auth',
initialState: initialAuthState,
reducers: {
onCheckingCredentials: (state) => {
state.status = 'checking'
state.user = initialAuthState.user
state.errorMessage = undefined
},
login: (state, action) => {
const user = action.payload as User
state.status = 'authenticated'
state.user = user
state.errorMessage = undefined
},
logout: (state, action) => {
state.status = 'unauthenticated'
state.user = initialAuthState.user
state.errorMessage = action.payload as string | undefined
},
onError: (state, action) => {
state.status = 'error'
state.errorMessage = action.payload as string
},
clearErrorMessage: (state) => {
state.errorMessage = undefined
}
}
})
export const {
onCheckingCredentials,
login,
logout,
onError,
clearErrorMessage
} = authSlice.actions
export default authSlice.reducer
Usando el store
import type { View } from "react-big-calendar"
import type { Theme } from "../../infrastructure/interfaces/theme.interface"
import { openModal, setCalendarView, setTaskView, setTheme } from "../store/ui/ui.slice"
import { useMeetlyDispatch, useMeetlySelector } from "./use-store"
export default function useUI() {
const { modal, theme, calendarView, taskView } = useMeetlySelector((state) => state.ui)
const dispatch = useMeetlyDispatch()
const handleOpenModal = (modalType: "bell" | "settings" | "user") => {
dispatch(openModal(modalType))
}
const handleCloseModal = () => {
dispatch(openModal(null))
}
const handleSetTheme = (newTheme: Theme) => {
dispatch(setTheme(newTheme))
}
const handleSetCalendarView = (newView: View) => {
dispatch(setCalendarView(newView))
}
const handleSetTaskView = (newView: "list" | "kanban") => {
dispatch(setTaskView(newView))
}
return {
modal,
theme,
calendarView,
taskView,
openModal: handleOpenModal,
closeModal: handleCloseModal,
setTheme: handleSetTheme,
setCalendarView: handleSetCalendarView,
setTaskView: handleSetTaskView,
}
}
Trabajar con Thunks
import { createSlice } from '@reduxjs/toolkit'
export const journalSlice = createSlice({
name: 'journal',
initialState: {
isSaving: false,
messageSaved: '',
notes: [],
active: null,
},
reducers: {
savingNewNote: (state) => {
state.isSaving = true
},
addNewEmptyNote: (state, action) => {
state.notes.push(action.payload)
state.isSaving = false
},
setActiveNote: (state, action) => {
state.active = action.payload
state.messageSaved = ''
},
setNotes: (state, action) => {
state.notes = action.payload
},
setSaving: (state) => {
state.isSaving = true
state.messageSaved = ''
},
updateNote: (state, action) => {
state.isSaving = false
state.notes = state.notes.map((note) => {
if (note.id === action.payload.id) {
return action.payload
}
return note
})
state.messageSaved = `${action.payload.title}, actualizada correctamente`
},
setPhotosToActiveNote: (state, action) => {
state.active.imageUrls = [...state.active.imageUrls, ...action.payload]
state.isSaving = false
},
clearNotesLogout: (state) => {
state.isSaving = false
state.messageSaved = ''
state.notes = []
state.active = null
},
deleteNoteById: (state, action) => {
state.active = null
state.notes = state.notes.filter((note) => note.id !== action.payload)
}
}
})
export const {
addNewEmptyNote,
clearNotesLogout,
deleteNoteById,
savingNewNote,
setActiveNote,
setNotes,
setPhotosToActiveNote,
setSaving,
updateNote
} = journalSlice.actions
import { collection, deleteDoc, doc, setDoc } from 'firebase/firestore'
import { FirebaseDB } from '../../firebase/config'
import { fileUpload, loadNotes } from '../../helpers'
import { addNewEmptyNote, deleteNoteById, savingNewNote, setActiveNote, setNotes, setPhotosToActiveNote, setSaving, updateNote } from './journalSlice'
export const startNewNote = () => {
return async (dispatch, getState) => {
dispatch(savingNewNote())
const { uid } = getState().auth
const newNote = {
title: '',
body: '',
date: new Date().getTime(),
}
const newDoc = doc(collection(FirebaseDB, `${uid}/journal/notes`))
await setDoc(newDoc, newNote)
newNote.id = newDoc.id
dispatch(addNewEmptyNote(newNote))
dispatch(setActiveNote(newNote))
}
}
export const startLoadingNotes = () => {
return async (dispatch, getState) => {
const { uid } = getState().auth
if (!uid) throw new Error('El UID del usuario no existe')
const notesCollection = await loadNotes(uid)
dispatch(setNotes(notesCollection))
}
}
export const startSaveNote = () => {
return async (dispatch, getState) => {
dispatch(setSaving())
const { uid } = getState().auth
const { active: note } = getState().journal
const noteToFireStore = { ...note }
delete noteToFireStore.id
const docRef = doc(FirebaseDB, `${uid}/journal/notes/${note.id}`)
await setDoc(docRef, noteToFireStore, { merge: true })
dispatch(updateNote(note))
}
}
export const startUploadingFiles = (files = []) => {
return async (dispatch) => {
dispatch(setSaving())
const filesUploadPromises = []
for (const file of files) {
filesUploadPromises.push(fileUpload(file))
}
const photosUrls = await Promise.all(filesUploadPromises)
dispatch(setPhotosToActiveNote(photosUrls))
}
}
export const startDeletingNote = () => {
return async (dispatch, getState) => {
const { uid } = getState().auth
const { active: note } = getState().journal
const docRef = doc(FirebaseDB, `${uid}/journal/notes/${note.id}`)
await deleteDoc(docRef)
dispatch(deleteNoteById(note.id))
}
}
import { useDispatch, useSelector } from 'react-redux'
import { startNewNote } from '../store/journal/thunks'
export const JournalPage = () => {
const dispatch = useDispatch()
const { isSaving } = useSelector((state) => state.journal)
const handleClickNewNote = () => {
dispatch(startNewNote())
}
return (
<button
disabled={isSaving}
onClick={handleClickNewNote}
className='btn btn-primary'
>
Nueva nota
</button>
)
}
Middlewares y Cambios Optimistas
import { configureStore, type Middleware } from '@reduxjs/toolkit'
import { toast } from 'sonner'
import usersReducer, { rollbackUser } from './users/slice'
import uiReducer from './ui/slice'
/* Creamos un middleware para redux, donde la store tiene acceso al estado,
next es la función que va a hacer que se continue con el reducer que va a cambiar
el estado, y la action es la acción que se esta ejecutando con dispatch mediante
la cual se va a modificar el estado */
const persistanceLocalStorageMiddleware: Middleware = (store) => (next) => (action) => {
// Previous state before the action
// console.log(store.getState()) // Accedemos al valor antes de ser modificado
next(action) // Continuamos con la acción que se va a ejecutar
// New state after the action
// console.log(store.getState()) // Accedemos al valor después de ser modificado
window.localStorage.setItem('__redux__state__', JSON.stringify(store.getState()))
}
const syncWithDatabaseMiddleware: Middleware = (store) => (next) => (action) => {
const { type, payload } = action // Obtenemos el type y el payload de la acción
const previousState = store.getState() as RootState // Accedemos al estado previo
next(action)
/* Evaluamos la acción ejecutada en el slice users */
if (type === 'users/deleteUserById') { // Eliminando el usuario
const userIdToRemove = payload
const userToRemove = previousState.users.find((user) => user.id === userIdToRemove)
fetch(`https://jsonplaceholder.typicode.com/users/${userIdToRemove}`, {
method: 'DELETE'
})
.then((res) => {
if (res.ok) {
toast.success(`Usuario ${payload} eliminado correctamente`)
}
// throw new Error('Error al eliminar el usuario')
})
.catch((err) => {
toast.error(`Error al eliminar el usuario ${userIdToRemove}`)
if (userToRemove) store.dispatch(rollbackUser(userToRemove))
console.log(err)
console.log('Error')
})
}
}
/* Creamos nuestra store para Redux, la cual sera como una pizza en la cual va a tener
muchas rebanadas (slices) de estados diferentes, los cuales se conocen como reducers,
donde por ejemplo tenemos un slice para almacenar el estado de los usuarios y otra
para almacenar el estado de la interfaz de usuario. También podemos configurar
los middlewares, los cuales son funciones las cuales se van a ejecutar cuando una acción
es lanzada con un dispatch */
export const store = configureStore({
reducer: {
users: usersReducer,
ui: uiReducer,
},
middleware: [
persistanceLocalStorageMiddleware,
syncWithDatabaseMiddleware,
],
})
/* Exportamos el estado global de nuestra store como RootState */
export type RootState = ReturnType<typeof store.getState>
/* Exportamos el dispatch para acciones de nuestra store como AppDispatch */
export type AppDispatch = typeof store.dispatch