Finalizamos la serie de publicaciones donde lo dejamos: http://developinginspanish.com/2020/09/17/react-redux-tutorial-para-principiantes-iv/
Redux moderno con Redux Toolkit
En los últimos meses de 2018, Redux vio la introducción del kit de inicio de Redux, que luego se renombró como Redux Toolkit (kit de herramientas de Ewdux). Redux Toolkit tiene como objetivo simplificar Redux con una abstracción conveniente sobre el «texto estándar» del que se quejaron tantos desarrolladores.
En las siguientes secciones, exploraremos Redux Toolkit junto con el código «clásico» de Redux. Para ilustrar el kit de herramientas de Redux, veremos una aplicación JavaScript «básica», no React por ahora. Supongo que tienes un entorno de desarrollo de paquetes web. También puede usar create-react-app, simplemente reemplaza el contenido de index.js con mi código.
Redux moderno con Redux Toolkit: configureStore
Comencemos con la creación de una tienda. Vimos createStore como una forma de crear una tienda en Redux. Se necesita un reductor de raíz (rootReducer), middleware opcional y potenciadores de tienda opcionales:
import { createStore, applyMiddleware } from "redux";
const middleware = [
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
const initialState = {
token: "",
};
function rootReducer(state = initialState, action) {
// DO STUFF
return state;
}
const store = createStore(rootReducer, applyMiddleware(...middleware));
También podemos dividir reductores con combineReducers:
import { createStore, combineReducers, applyMiddleware } from "redux";
const middleware = [
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
// AUTH STATE
const authState = {
token: "",
};
function authReducer(state = authState, action) {
// DO STUFF
return state;
}
const rootReducer = combineReducers({
auth: authReducer,
});
const store = createStore(rootReducer, applyMiddleware(...middleware));
Si agregases Redux Dev Tool a la mezcla, la creación de tiendas se volvería un poco liosa:
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const middleware = [
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
// AUTH STATE
const authState = {
token: "",
};
function authReducer(state = authState, action) {
// DO STUFF
return state;
}
const rootReducer = combineReducers({
auth: authReducer,
});
const store = createStore(
rootReducer,
storeEnhancers(applyMiddleware(...middleware))
);
Con configureStore de Redux Toolkit podemos simplificar la creación de tiendas. Primero, instala el kit de herramientas con:
npm i @reduxjs/toolkit
Luego, reemplaza createStore con configureStore:
import { configureStore } from "@reduxjs/toolkit";
const middleware = [
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
// AUTH STATE
const authState = {
token: "",
};
function authReducer(state = authState, action) {
// DO STUFF
return state;
}
const store = configureStore({
reducer: {
auth: authReducer,
},
middleware,
});
No es necesario componer nada, ni pmatilla (boilerplate): configureStore acepta un objeto de configuración donde puede definir:
- Un reductor raíz
- El middleware
- Potenciadores de tienda opcionales
- Un estado precargado
¿Qué se incluye en configureStore?
- Redux Dev Tool lista para usar
- redux-thunk
Presta atención para incluir el middleware predeterminado con getDefaultMiddleware cuando pases una matriz de middleware personalizado:
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
const middleware = [
...getDefaultMiddleware(),
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
// AUTH STATE
const authState = {
token: "",
};
function authReducer(state = authState, action) {
// DO STUFF
return state;
}
const store = configureStore({
reducer: {
auth: authReducer,
},
middleware,
});
Redux moderno con Redux Toolkit: createAction
La siguiente función de utilidad de Redux Toolkit es createAction. Es una buena práctica en Redux tener creadores de acciones y acciones con nombre para casi todos los comportamientos de la aplicación.
Considere nuevamente este ejemplo «clásico» de Redux con algunas acciones: LOGIN_SUCCESS, FETCH_LINKS_REQUEST y FETCH_LINKS_SUCCESS (¡sin siquiera tener en cuenta los errores! Eso haría que el texto estándar crezca aún más en este ejemplo):
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// NAMED ACTIONS AND ACTION CREATORS
const LOGIN_SUCCESS = "LOGIN_SUCCESS";
const FETCH_LINKS_REQUEST = "FETCH_LINKS_REQUEST";
const FETCH_LINKS_SUCCESS = "FETCH_LINKS_SUCCESS";
function loginSuccess(payload) {
return { type: LOGIN_SUCCESS, payload };
}
function fetchLinksRequest() {
return { type: FETCH_LINKS_REQUEST };
}
function fetchLinksSuccess(payload) {
return { type: FETCH_LINKS_SUCCESS, payload };
}
//
const middleware = [
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
// AUTH STATE
const authState = {
token: "",
};
function authReducer(state = authState, action) {
// DO STUFF
return state;
}
const rootReducer = combineReducers({
auth: authReducer,
});
const store = createStore(
rootReducer,
storeEnhancers(applyMiddleware(...middleware))
);
Definitivamente es demasiado repetitivo. Con createAction podemos deshacernos de los creadores de acciones y las acciones con nombre para condensar todo en un solo lugar:
import {
configureStore,
getDefaultMiddleware,
createAction,
} from "@reduxjs/toolkit";
const loginSuccess = createAction("LOGIN_SUCCESS");
const fetchLinksRequest = createAction("FETCH_LINKS_REQUEST");
const fetchLinksSuccess = createAction("FETCH_LINKS_SUCCESS");
const middleware = [
...getDefaultMiddleware(),
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
// AUTH STATE
const authState = {
token: "",
};
function authReducer(state = authState, action) {
// DO STUFF
return state;
}
const store = configureStore({
reducer: {
auth: authReducer,
},
middleware,
});
Cada una de estas llamadas a createAction son creadores de acciones reales listos para ser llamados, con una carga útil opcional:
// Creates an action creator
const loginSuccess = createAction("LOGIN_SUCCESS");
// Calls the action creator
store.dispatch(loginSuccess("aPayload"))
Ahora, dirijamos nuestra atención a los reductores.
Redux moderno con Redux Toolkit: createReducer
Después de los creadores de acciones y las acciones con nombre, los reductores son donde crece la mayor parte del «modelo» de Redux. Tradicionalmente, tendría un switch con un grupo de case para manejar tipos de acción (nuevamente, de vuelta al Redux «clásico»):
// NAMED ACTIONS AND ACTION CREATORS
const LOGIN_SUCCESS = "LOGIN_SUCCESS";
const LOGIN_FAILED = "LOGIN_FAILED";
function loginSuccess(payload) {
return { type: LOGIN_SUCCESS, payload };
}
function loginFailed(payload) {
return { type: LOGIN_FAILED, payload };
}
// Reducer
const authState = {
token: "",
error: "",
};
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
// return the next state
case LOGIN_FAILED:
// return the next state
default:
return state;
}
}
El «problema» aparece pronto cuando necesitamos regresar al siguiente estado sin tocar el estado inicial. Se puede hacer de dos formas. Con Object.assign:
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
return Object.assign({}, state, { token: action.payload });
case LOGIN_FAILED:
return Object.assign({}, state, { error: action.payload });
default:
return state;
}
}
O con una extensión de objeto (ECMAScript 2018):
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
return { ...state, token: action.payload };
case LOGIN_FAILED:
return { ...state, error: action.payload };
default:
return state;
}
}
Ahora, ¿qué pasa si te digo que puedes cortar cosas con createReducer? Esta función de utilidad de Redux Toolkit toma un estado inicial y un objeto de mapeo donde:
- Las propiedades en este mapeo son tipos de acción
- Los valores son la función del reductor
Empareja esto con acciones de createAction, que tienen su toString() configurado para devolver el tipo de acción, y puedes refactorizar esto:
// NAMED ACTIONS AND ACTION CREATORS
const LOGIN_SUCCESS = "LOGIN_SUCCESS";
const LOGIN_FAILED = "LOGIN_FAILED";
function loginSuccess(payload) {
return { type: LOGIN_SUCCESS, payload };
}
function loginFailed(payload) {
return { type: LOGIN_FAILED, payload };
}
// Reducer
const authState = {
token: "",
error: "",
};
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
// return the next state
case LOGIN_FAILED:
// return the next state
default:
return state;
}
}
a esto:
import {
configureStore,
getDefaultMiddleware,
createAction,
createReducer,
} from "@reduxjs/toolkit";
const loginSuccess = createAction("LOGIN_SUCCESS");
const loginFailed = createAction("LOGIN_FAILED");
const authState = {
token: "",
error: "",
};
const authReducer = createReducer(authState, {
[loginSuccess]: (state, action) => {
// return the next state
},
[loginFailed]: (state, action) => {
// return the next state
},
});
// rest omitted for brevity
¡Espera! createReducer realmente brilla cuando se trata de mutaciones. Bajo el capó utiliza immer, que permite escribir lógica mutativa, que en realidad no altera el objeto original. Podemos refactorizar nuestro reductor «clásico» de:
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
return { ...state, token: action.payload };
case LOGIN_FAILED:
return { ...state, error: action.payload };
default:
return state;
}
}
a:
const authReducer = createReducer(authState, {
[loginSuccess]: (state, action) => {
state.token = action.payload;
},
[loginFailed]: (state, action) => {
state.error = action.payload;
},
});
Mira lo limpio que está. No es necesario devolver el siguiente estado también. Veamos ahora el panorama general con createSlice.
Redux moderno con Redux Toolkit: createSlice
createSlice es el santo grial de Redux. Es capaz de mantener todo en un solo lugar: reductores, creadores de acción y estado. Veamos qué hemos conseguido hasta ahora con Redux Toolkit:
import {
configureStore,
getDefaultMiddleware,
createAction,
createReducer,
} from "@reduxjs/toolkit";
const loginSuccess = createAction("LOGIN_SUCCESS");
const loginFailed = createAction("LOGIN_FAILED");
// we don't need this now const fetchLinksRequest = createAction("FETCH_LINKS_REQUEST");
// we don't need this now const fetchLinksSuccess = createAction("FETCH_LINKS_SUCCESS");
const middleware = [
...getDefaultMiddleware(),
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
// AUTH STATE
const authState = {
token: "",
error: "",
};
const authReducer = createReducer(authState, {
[loginSuccess]: (state, action) => {
state.token = action.payload;
},
[loginFailed]: (state, action) => {
state.error = action.payload;
},
});
const store = configureStore({
reducer: {
auth: authReducer,
},
middleware,
});
Con createSlice podemos simplificar aún más, y nuestro código se convierte en:
import {
configureStore,
getDefaultMiddleware,
createSlice,
} from "@reduxjs/toolkit";
const middleware = [
...getDefaultMiddleware(),
/*YOUR CUSTOM MIDDLEWARES HERE*/
];
// AUTH STATE
const authState = {
token: "",
error: "",
};
const authSlice = createSlice({
name: "auth",
initialState: authState,
reducers: {
loginSuccess: (state, action) => {
state.token = action.payload;
},
loginFailed: (state, action) => {
state.error = action.payload;
},
},
});
const { loginSuccess, loginFailed } = authSlice.actions;
const authReducer = authSlice.reducer;
const store = configureStore({
reducer: {
auth: authReducer,
},
middleware,
});
Al principio, no es fácil entender lo que está haciendo createSlice, pero con un poco de ayuda podemos hacerlo. Este acepta:
- Un nombre de slice (porción)
- Un estado inicial para el reductor
- un objeto con «reductores de case«
const authSlice = createSlice({
name: "auth",
initialState: authState,
reducers: {
loginSuccess: (state, action) => {
state.token = action.payload;
},
loginFailed: (state, action) => {
state.error = action.payload;
},
},
});
El nombre del segmento es el prefijo de la acción. Por ejemplo, si envío:
store.dispatch( loginSuccess("some_asasa_token") )
La acción generada es:
{ type: "auth/loginSuccess", payload: "some_asasa_token" }
Los «reductores de case» son los mismos que un bloque de switch clásico de un reductor. Este reductor:
// classic reducer
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
// return the next state
case LOGIN_FAILED:
// return the next state
default:
return state;
}
}
Se traduce en la clave reductora del slice:
const authSlice = createSlice({
name: "auth",
initialState: authState,
reducers: {
loginSuccess: (state, action) => {
state.token = action.payload;
},
loginFailed: (state, action) => {
state.error = action.payload;
},
},
});
A cambio, createSlice devuelve creadores de acciones:
const { loginSuccess, loginFailed } = authSlice.actions;
Y un reductor también:
const authReducer = authSlice.reducer;
El reductor se utiliza en configureStore:
const authReducer = authSlice.reducer;
const store = configureStore({
reducer: {
auth: authReducer,
},
middleware,
});
Con createSlice cerramos el círculo. Para obtener más información sobre todas las opciones de configuración, asegúrate de consultar la documentación oficial.
Redux moderno con Redux Toolkit: createAsyncThunk
Vimos redux-thunk como el middleware preferido para manejar la lógica asíncrona en Redux.
Con redux-thunk podemos escribir creadores de acciones asíncronas para realizar llamadas a la API, configurar temporizadores o trabajar con Promise.
Aquí hay un esqueleto típico de un creador de acción asíncrona con redux-thunk:
export function getUsers() {
return function(dispatch) {
//
};
}
Cuando se trata de llamadas a API la mayor parte del tiempo, necesitamos manejar tres acciones diferentes en Redux:
FETCH_ENTITY_REQUEST
FETCH_ENTITY_FAILURE
FETCH_ENTITY_SUCCESS
ENTITY aquí podría ser cualquier modelo que queramos obtener de la API.
Despachamos estas acciones desde una acción asíncrona en tres fases:
- cuando comienza la solicitud
- si la solicitud falla
- si la solicitud tiene éxito
Considera el siguiente ejemplo con Fetch:
function getUsers() {
return function(dispatch) {
// notify about fetch start
dispatch({ type: "FETCH_USERS_REQUEST" });
return fetch("/api/users/")
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json =>
/* notify about success*/
dispatch({
type: "FETCH_USERS_SUCCESS",
payload: json
})
)
.catch(error =>
/* notify about failure*/
dispatch({
type: "FETCH_USERS_FAILURE",
payload: error.message
})
);
};
}
Este creador de acciones puede activarse en respuesta a alguna interacción del usuario como:
const button = document.getElementById("fetch");
button.addEventListener("click", function() {
store.dispatch(getUsers());
});
Aquí he incluido acciones como objetos simples, en el mundo real las crearía con createAction, o dentro de un segmento.
Estas acciones terminan en el reductor para notificar a la interfaz y al usuario sobre el destino de la solicitud.
Redux Toolkit incluye redux-thunk listo para usar, por lo que el procesador anterior funcionará bien.
Sin embargo, podemos aprovechar createAsyncThunk para limpiar el código anterior y crear esas tres acciones automáticamente:
const getUsers = createAsyncThunk("users/getUsers", () => {
return fetch("/api/users/")
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => json);
});
Lo que createAsyncThunk hace aquí es crear automáticamente un creador de acciones para cada estado de Promise. Por ejemplo, si nombramos a nuestro procesador asíncrono «users/getUsers», genera:
- pendientes: «users/getUsers/pendientes»
- rechazados : «users/getUsers/rechazados»
- completados: «users/getUsers/completados»
Esto es lo que verá, por ejemplo, para una Promesa rechazada (registré las acciones con un middleware personalizado):
En cambio, para una Promesa resuelta, verás:
En este punto, manejarás cada acción en el slice, como reductores adicionales:
const usersSlice = createSlice({
name: "users",
initialState: {
loading: "",
error: "",
data: []
},
reducers: {
// REGULAR REDUCERS
},
extraReducers: {
[getUsers.pending]: state => {
state.loading = "yes";
},
[getUsers.rejected]: (state, action) => {
state.loading = "";
state.error = action.error.message;
},
[getUsers.fulfilled]: (state, action) => {
state.loading = "";
state.data = action.payload;
}
}
});
Para obtener el mensaje de error de la Promesa de rechazo, debes acceder a action.error.message.
Para obtener la carga útil de la API, debes acceder a action.payload.
Si necesitas acceder a los parámetros de thunk para usar dispatch o getState, pasa el parámetro thunkAPI a la función de callback (o desestructura lo que necesitas):
const getUsers = createAsyncThunk("users/getUsers", (thunkAPI) => {
return fetch("/api/users/")
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => json);
});
Si deseas pasar un argumento al creador de la acción (un endpoint, por ejemplo), pasa un parámetro en la función de devolución de llamada (callback):
const getUsers = createAsyncThunk("users/getUsers", (arg) => {
return fetch(arg)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => json);
});
Luego proporciona el argumento cuando llames al creador de la acción:
const button = document.getElementById("fetch");
button.addEventListener("click", function() {
store.dispatch(getUsers("/api/users/"));
});
Lo que también es atractivo en createAsyncThunk es la capacidad de abortar las solicitudes de Fetch.
Consulta la documentación para ver un ejemplo completo.
Terminando
¡Qué viaje! Espero que hayas aprendido algo de esta guía. Hice todo lo posible para mantener las cosas lo más simples posible. Elige Redux, juega con él y tómate tu tiempo para asimilar todos los conceptos.
Redux solía tener una gran cantidad de plantillas y piezas móviles. No te desanimes. Con Redux Toolkit ahora es más fácil escribir la lógica de Redux.
Después de leer este tutorial, puede consultar este curso gratuito de Redux donde cubro Redux desde cero hasta el kit de herramientas.
¡Gracias por leer y estad atentos a este blog!
Puedes leer el artículo original en inglés en: https://www.valentinog.com/blog/redux/