Migración a Redux Moderno
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
- Cómo modernizar lógica Redux "manual" para usar Redux Toolkit
- Cómo modernizar componentes React-Redux con
connectpara usar la API de hooks - Cómo modernizar lógica Redux y componentes React-Redux con TypeScript
Resumen
Redux existe desde 2015, y nuestros patrones recomendados para escribir código Redux han cambiado significativamente con los años. Al igual que React evolucionó de createClass a React.Component y luego a componentes funcionales con hooks, Redux ha evolucionado desde configuración manual de store + reducers manuales con spreads de objetos + connect de React-Redux, hacia configureStore + createSlice de Redux Toolkit + la API de hooks de React-Redux.
Muchos usuarios trabajan con codebases Redux antiguos que existían antes de estos patrones "Redux moderno". Migrar esos codebases a los patrones modernos recomendados hoy resultará en codebases mucho más pequeños y fáciles de mantener.
La buena noticia es que ¡puedes migrar tu código a Redux moderno incrementalmente, pieza por pieza, conviviendo código Redux antiguo y nuevo que funcionan juntos!
Esta página cubre los enfoques generales y técnicas que puedes usar para modernizar un codebase Redux heredado existente.
Para más detalles sobre cómo el "Redux moderno" con Redux Toolkit + hooks de React-Redux simplifica el uso de Redux, consulta estos recursos adicionales:
Modernizando la lógica Redux con Redux Toolkit
El enfoque general para migrar la lógica Redux es:
-
Reemplazar la configuración manual existente de la store Redux con
configureStorede Redux Toolkit -
Seleccionar un slice reducer existente y sus acciones asociadas. Reemplazarlos con
createSlicede RTK. Repetir para un reducer a la vez. -
Según sea necesario, reemplazar la lógica existente de obtención de datos con RTK Query o
createAsyncThunk -
Usar otras APIs de RTK como
createListenerMiddlewareocreateEntityAdaptersegún sea necesario
Siempre debes comenzar reemplazando la llamada heredada createStore por configureStore. Este es un paso único, y todos los reducers y middleware existentes seguirán funcionando como hasta ahora. configureStore incluye verificaciones en modo desarrollo para errores comunes como mutaciones accidentales y valores no serializables, por lo que tenerlos activados ayudará a identificar áreas del codebase donde ocurren estos errores.
Puedes ver este enfoque general en acción en Redux Fundamentals, Parte 8: Redux Moderno con Redux Toolkit.
Configuración de Store con configureStore
Un archivo típico de configuración de store Redux heredado realiza varios pasos:
-
Combinar los slice reducers en el root reducer
-
Crear el enhancer de middleware, usualmente con el middleware thunk, y posiblemente otro middleware en modo desarrollo como
redux-logger -
Añadir el enhancer de Redux DevTools y componer los enhancers juntos
-
Llamar a
createStore
Así podrían verse esos pasos en una aplicación existente:
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'
import { thunk } from 'redux-thunk'
import postsReducer from '../reducers/postsReducer'
import usersReducer from '../reducers/usersReducer'
const rootReducer = combineReducers({
posts: postsReducer,
users: usersReducer
})
const middlewareEnhancer = applyMiddleware(thunk)
const composeWithDevTools =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const composedEnhancers = composeWithDevTools(middlewareEnhancer)
const store = createStore(rootReducer, composedEnhancers)
Todos esos pasos pueden reemplazarse con una única llamada a la API configureStore de Redux Toolkit.
La función configureStore de RTK envuelve al método original createStore y maneja la mayor parte de la configuración del store automáticamente. De hecho, podemos reducirlo a un solo paso:
import { configureStore } from '@reduxjs/toolkit'
import postsReducer from '../reducers/postsReducer'
import usersReducer from '../reducers/usersReducer'
// Automatically adds the thunk middleware and the Redux DevTools extension
const store = configureStore({
// Automatically calls `combineReducers`
reducer: {
posts: postsReducer,
users: usersReducer
}
})
Esta única llamada a configureStore hizo todo el trabajo por nosotros:
-
Llamó a
combineReducerspara combinarpostsReduceryusersReduceren la función del reducer raíz, que manejará un estado raíz con la forma{posts, users} -
Llamó a
createStorepara crear un store de Redux usando ese reducer raíz -
Añadió automáticamente el middleware thunk y llamó a
applyMiddleware -
Añadió automáticamente middleware adicional para detectar errores comunes como mutar accidentalmente el estado
-
Configuró automáticamente la conexión con la extensión Redux DevTools
Si la configuración de tu store requiere pasos adicionales, como añadir más middleware, pasar un argumento extra al middleware thunk, o crear un reducer raíz persistido, también puedes hacerlo. Aquí hay un ejemplo más extenso que muestra cómo personalizar el middleware incorporado y activar Redux-Persist, lo que demuestra algunas de las opciones para trabajar con configureStore:
Detailed Example: Custom Store Setup with Persistence and Middleware
This example shows several possible common tasks when setting up a Redux store:
- Combining the reducers separately (sometimes needed due to other architectural constraints)
- Adding additional middleware, both conditionally and unconditionally
- Passing an "extra argument" into the thunk middleware, such as an API service layer
- Using the Redux-Persist library, which requires special handling for its non-serializable action types
- Turning the devtools off in prod, and setting additional devtools options in development
None of these are required, but they do show up frequently in real-world codebases.
import { configureStore, combineReducers } from '@reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'
import logger from 'redux-logger'
import postsReducer from '../features/posts/postsSlice'
import usersReducer from '../features/users/usersSlice'
import { api } from '../features/api/apiSlice'
import { serviceLayer } from '../features/api/serviceLayer'
import stateSanitizerForDevtools from './devtools'
import customMiddleware from './someCustomMiddleware'
// Can call `combineReducers` yourself if needed
const rootReducer = combineReducers({
posts: postsReducer,
users: usersReducer,
[api.reducerPath]: api.reducer
})
const persistConfig = {
key: 'root',
version: 1,
storage
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
// Pass previously created persisted reducer
reducer: persistedReducer,
middleware: getDefaultMiddleware => {
const middleware = getDefaultMiddleware({
// Pass in a custom `extra` argument to the thunk middleware
thunk: {
extraArgument: { serviceLayer }
},
// Customize the built-in serializability dev check
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
}).concat(customMiddleware, api.middleware)
// Conditionally add another middleware in dev
if (process.env.NODE_ENV !== 'production') {
middleware.push(logger)
}
return middleware
},
// Turn off devtools in prod, or pass options in dev
devTools:
process.env.NODE_ENV === 'production'
? false
: {
stateSanitizer: stateSanitizerForDevtools
}
})
Reducers y Acciones con createSlice
Una base de código Redux heredada típica tiene su lógica de reducers, creadores de acciones y tipos de acciones repartidos en archivos separados, y esos archivos suelen estar en carpetas separadas por tipo. La lógica del reducer se escribe usando sentencias switch y lógica de actualización inmutable escrita a mano con operadores de propagación de objetos y mapeo de arrays:
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
import { ADD_TODO, TOGGLE_TODO } from '../constants/todos'
export const addTodo = (id, text) => ({
type: ADD_TODO,
text,
id
})
export const toggleTodo = id => ({
type: TOGGLE_TODO,
id
})
import { ADD_TODO, TOGGLE_TODO } from '../constants/todos'
const initialState = []
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO: {
return state.concat({
id: action.id,
text: action.text,
completed: false
})
}
case TOGGLE_TODO: {
return state.map(todo => {
if (todo.id !== action.id) {
return todo
}
return {
...todo,
completed: !todo.completed
}
})
}
default:
return state
}
}
¡La API createSlice de Redux Toolkit fue diseñada para eliminar todo el "boilerplate" al escribir reducers, acciones y actualizaciones inmutables!
Con Redux Toolkit, hay varios cambios en ese código heredado:
-
createSliceeliminará por completo los creadores de acciones y los tipos de acciones escritos a mano -
Todos los campos con nombres únicos como
action.textyaction.idse reemplazan poraction.payload, ya sea como un valor individual o como un objeto que contenga esos campos -
Las actualizaciones inmutables escritas a mano se reemplazan por lógica de "mutación" en los reducers gracias a Immer
-
No hay necesidad de archivos separados para cada tipo de código
-
Enseñamos a tener toda la lógica para un reducer dado en un único archivo de "slice"
-
En lugar de tener carpetas separadas por "tipo de código", recomendamos organizar los archivos por "características", con el código relacionado en la misma carpeta
-
Idealmente, la nomenclatura de los reducers y acciones debe usar el tiempo pasado y describir "algo que sucedió", en lugar de un imperativo "haz esto ahora", como
todoAddeden lugar deADD_TODO
Esos archivos separados para constantes, acciones y reducers, serían reemplazados por un único archivo de "slice". El archivo de slice modernizado se vería así:
import { createSlice } from '@reduxjs/toolkit'
const initialState = []
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// Give case reducers meaningful past-tense "event"-style names
todoAdded(state, action) {
const { id, text } = action.payload
// "Mutating" update syntax thanks to Immer, and no `return` needed
state.todos.push({
id,
text,
completed: false
})
},
todoToggled(state, action) {
// Look for the specific nested object to update.
// In this case, `action.payload` is the default field in the action,
// and can hold the `id` value - no need for `action.id` separately
const matchingTodo = state.todos.find(todo => todo.id === action.payload)
if (matchingTodo) {
// Can directly "mutate" the nested object
matchingTodo.completed = !matchingTodo.completed
}
}
}
})
// `createSlice` automatically generated action creators with these names.
// export them as named exports from this "slice" file
export const { todoAdded, todoToggled } = todosSlice.actions
// Export the slice reducer as the default export
export default todosSlice.reducer
Cuando llamas a dispatch(todoAdded('Buy milk')), el valor único que pases al creador de acciones todoAdded se usará automáticamente como el campo action.payload. Si necesitas pasar múltiples valores, hazlo como un objeto, por ejemplo dispatch(todoAdded({id, text})). Alternativamente, puedes usar la notación "prepare" dentro de un reducer de createSlice para aceptar múltiples argumentos separados y crear el campo payload. La notación prepare también es útil para los casos en los que los creadores de acciones realizaban trabajo adicional, como generar IDs únicos para cada elemento.
Aunque a Redux Toolkit no le importa específicamente la estructura de tus carpetas y archivos o la nomenclatura de las acciones, estas son las mejores prácticas que recomendamos porque hemos comprobado que conducen a un código más mantenible y comprensible.
Obtención de Datos con RTK Query
La obtención de datos heredada típica en una aplicación React+Redux requiere muchas piezas móviles y tipos de código:
-
Creadores de acciones y tipos de acciones que representan acciones de "solicitud iniciada", "solicitud exitosa" y "solicitud fallida"
-
Thunks para despachar las acciones y realizar la solicitud asíncrona
-
Reducers que rastrean el estado de carga y almacenan los datos en caché
-
Selectors para leer esos valores del store
-
Despachar el thunk en un componente después del montaje, ya sea mediante
componentDidMounten un componente de clase ouseEffecten un componente funcional
Estos elementos suelen estar repartidos en múltiples archivos:
export const FETCH_TODOS_STARTED = 'FETCH_TODOS_STARTED'
export const FETCH_TODOS_SUCCEEDED = 'FETCH_TODOS_SUCCEEDED'
export const FETCH_TODOS_FAILED = 'FETCH_TODOS_FAILED'
import axios from 'axios'
import {
FETCH_TODOS_STARTED,
FETCH_TODOS_SUCCEEDED,
FETCH_TODOS_FAILED
} from '../constants/todos'
export const fetchTodosStarted = () => ({
type: FETCH_TODOS_STARTED
})
export const fetchTodosSucceeded = todos => ({
type: FETCH_TODOS_SUCCEEDED,
todos
})
export const fetchTodosFailed = error => ({
type: FETCH_TODOS_FAILED,
error
})
export const fetchTodos = () => {
return async dispatch => {
dispatch(fetchTodosStarted())
try {
// Axios is common, but also `fetch`, or your own "API service" layer
const res = await axios.get('/todos')
dispatch(fetchTodosSucceeded(res.data))
} catch (err) {
dispatch(fetchTodosFailed(err))
}
}
}
import {
FETCH_TODOS_STARTED,
FETCH_TODOS_SUCCEEDED,
FETCH_TODOS_FAILED
} from '../constants/todos'
const initialState = {
status: 'uninitialized',
todos: [],
error: null
}
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TODOS_STARTED: {
return {
...state,
status: 'loading'
}
}
case FETCH_TODOS_SUCCEEDED: {
return {
...state,
status: 'succeeded',
todos: action.todos
}
}
case FETCH_TODOS_FAILED: {
return {
...state,
status: 'failed',
todos: [],
error: action.error
}
}
default:
return state
}
}
export const selectTodosStatus = state => state.todos.status
export const selectTodos = state => state.todos.todos
import { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { fetchTodos } from '../actions/todos'
import { selectTodosStatus, selectTodos } from '../selectors/todos'
export function TodosList() {
const dispatch = useDispatch()
const status = useSelector(selectTodosStatus)
const todos = useSelector(selectTodos)
useEffect(() => {
dispatch(fetchTodos())
}, [dispatch])
// omit rendering logic here
}
Muchos usuarios pueden estar usando la biblioteca redux-saga para gestionar la obtención de datos, en cuyo caso tendrán tipos de acción "señal" adicionales para disparar las sagas, y este archivo de saga en lugar de thunks:
import { put, takeEvery, call } from 'redux-saga/effects'
import {
FETCH_TODOS_BEGIN,
fetchTodosStarted,
fetchTodosSucceeded,
fetchTodosFailed
} from '../actions/todos'
// Saga to actually fetch data
export function* fetchTodos() {
yield put(fetchTodosStarted())
try {
const res = yield call(axios.get, '/todos')
yield put(fetchTodosSucceeded(res.data))
} catch (err) {
yield put(fetchTodosFailed(err))
}
}
// "Watcher" saga that waits for a "signal" action, which is
// dispatched only to kick off logic, not to update state
export function* fetchTodosSaga() {
yield takeEvery(FETCH_TODOS_BEGIN, fetchTodos)
}
¡Todo ese código puede reemplazarse con la capa de obtención de datos y caché "RTK Query" de Redux Toolkit!
RTK Query elimina la necesidad de escribir ninguna acción, thunk, reducer, selector o efecto para gestionar la obtención de datos. (De hecho, internamente utiliza las mismas herramientas). Además, RTK Query se encarga de rastrear el estado de carga, deduplicar solicitudes y gestionar el ciclo de vida de los datos en caché (incluyendo eliminar datos expirados que ya no se necesitan).
Para migrar, configura una única definición de "API slice" de RTK Query y añade el reducer generado + middleware a tu store:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({
// Fill in your own server starting URL here
baseUrl: '/'
}),
endpoints: build => ({})
})
import { configureStore } from '@reduxjs/toolkit'
// Import the API object
import { api } from '../features/api/apiSlice'
// Import any other slice reducers as usual here
import usersReducer from '../features/users/usersSlice'
export const store = configureStore({
reducer: {
// Add the generated RTK Query "API slice" caching reducer
[api.reducerPath]: api.reducer,
// Add any other reducers
users: usersReducer
},
// Add the RTK Query API middleware
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(api.middleware)
})
Luego, añade "endpoints" que representen los datos específicos que quieres obtener y almacenar en caché, y exporta los hooks de React generados automáticamente para cada endpoint:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({
// Fill in your own server starting URL here
baseUrl: '/'
}),
endpoints: build => ({
// A query endpoint with no arguments
getTodos: build.query({
query: () => '/todos'
}),
// A query endpoint with an argument
userById: build.query({
query: userId => `/users/${userId}`
}),
// A mutation endpoint
updateTodo: build.mutation({
query: updatedTodo => ({
url: `/todos/${updatedTodo.id}`,
method: 'POST',
body: updatedTodo
})
})
})
})
export const { useGetTodosQuery, useUserByIdQuery, useUpdateTodoMutation } = api
Finalmente, usa los hooks en tus componentes:
import { useGetTodosQuery } from '../api/apiSlice'
export function TodoList() {
const { data: todos, isFetching, isSuccess } = useGetTodosQuery()
// omit rendering logic here
}
Obtención de datos con createAsyncThunk
Recomendamos específicamente usar RTK Query para obtener datos. Sin embargo, algunos usuarios nos han indicado que aún no están listos para dar ese paso. En ese caso, puedes al menos reducir el código repetitivo de los thunks y reducers manuales usando createAsyncThunk de RTK. Genera automáticamente los creadores de acciones y tipos de acción, llama a la función asíncrona que proporcionas para realizar la solicitud, y despacha esas acciones según el ciclo de vida de la promesa. El mismo ejemplo con createAsyncThunk podría verse así:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import axios from 'axios'
const initialState = {
status: 'uninitialized',
todos: [],
error: null
}
const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
// Just make the async request here, and return the response.
// This will automatically dispatch a `pending` action first,
// and then `fulfilled` or `rejected` actions based on the promise.
// as needed based on the
const res = await axios.get('/todos')
return res.data
})
export const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// any additional "normal" case reducers here.
// these will generate new action creators
},
extraReducers: builder => {
// Use `extraReducers` to handle actions that were generated
// _outside_ of the slice, such as thunks or in other slices
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
// Pass the generated action creators to `.addCase()`
.addCase(fetchTodos.fulfilled, (state, action) => {
// Same "mutating" update syntax thanks to Immer
state.status = 'succeeded'
state.todos = action.payload
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed'
state.todos = []
state.error = action.error
})
}
})
export default todosSlice.reducer
También necesitarías escribir los selectores manualmente y despachar el thunk fetchTodos tú mismo en un hook useEffect.
Lógica Reactiva con createListenerMiddleware
Muchas aplicaciones Redux tienen lógica de estilo "reactivo" que escucha acciones específicas o cambios de estado, y ejecuta lógica adicional en respuesta. Estos comportamientos a menudo se implementan usando las bibliotecas redux-saga o redux-observable.
Estas bibliotecas se usan para diversas tareas. Como ejemplo básico, una saga y un epic que escuchan una acción, esperan un segundo y luego despachan otra acción podrían verse así:
import { delay, put, takeEvery } from 'redux-saga/effects'
export function* ping() {
yield delay(1000)
yield put({ type: 'PONG' })
}
// "Watcher" saga that waits for a "signal" action, which is
// dispatched only to kick off logic, not to update state
export function* pingSaga() {
yield takeEvery('PING', ping)
}
import { filter, mapTo } from 'rxjs/operators'
import { ofType } from 'redux-observable'
const pingEpic = action$ =>
action$.pipe(ofType('PING'), delay(1000), mapTo({ type: 'PONG' }))
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { combineEpics, createEpicMiddleware } from 'redux-observable';
// skip reducers
import { pingEpic } from '../sagas/ping'
import { pingSaga } from '../epics/ping
function* rootSaga() {
yield pingSaga()
}
const rootEpic = combineEpics(
pingEpic
);
const sagaMiddleware = createSagaMiddleware()
const epicMiddleware = createEpicMiddleware()
const middlewareEnhancer = applyMiddleware(sagaMiddleware, epicMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)
sagaMiddleware.run(rootSaga)
epicMiddleware.run(rootEpic)
El middleware "listener" de RTK está diseñado para reemplazar sagas y observables, con una API más simple, tamaño de paquete menor y mejor soporte para TypeScript.
Los ejemplos de saga y epic podrían reemplazarse con el middleware listener así:
import { createListenerMiddleware } from '@reduxjs/toolkit'
// Best to define this in a separate file, to avoid importing
// from the store file into the rest of the codebase
export const listenerMiddleware = createListenerMiddleware()
export const { startListening, stopListening } = listenerMiddleware
import { createSlice } from '@reduxjs/toolkit'
import { startListening } from '../../app/listenerMiddleware'
const pingSlice = createSlice({
name: 'ping',
initialState,
reducers: {
pong(state, action) {
// state update here
}
}
})
export const { pong } = pingSlice.actions
export default pingSlice.reducer
// The `startListening()` call could go in different files,
// depending on your preferred app setup. Here, we just add
// it directly in a slice file.
startListening({
// Match this exact action type based on the action creator
actionCreator: pong,
// Run this effect callback whenever that action is dispatched
effect: async (action, listenerApi) => {
// Listener effect functions get a `listenerApi` object
// with many useful methods built in, including `delay`:
await listenerApi.delay(1000)
listenerApi.dispatch(pong())
}
})
import { configureStore } from '@reduxjs/toolkit'
import { listenerMiddleware } from './listenerMiddleware'
// omit reducers
export const store = configureStore({
reducer: rootReducer,
// Add the listener middleware _before_ the thunk or dev checks
middleware: getDefaultMiddleware =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware)
})
Migrar TypeScript para la lógica de Redux
El código Redux heredado que usa TypeScript típicamente sigue patrones muy verbosos para definir tipos. En particular, muchos usuarios definen manualmente tipos TS para cada acción individual, y luego crean "uniones de tipos de acción" que intentan limitar qué acciones específicas pueden pasarse a dispatch.
¡Recomendamos específica y enfáticamente evitar estos patrones!
import { ADD_TODO, TOGGLE_TODO } from '../constants/todos'
// ❌ Common pattern: manually defining types for each action object
interface AddTodoAction {
type: typeof ADD_TODO
text: string
id: string
}
interface ToggleTodoAction {
type: typeof TOGGLE_TODO
id: string
}
// ❌ Common pattern: an "action type union" of all possible actions
export type TodoActions = AddTodoAction | ToggleTodoAction
export const addTodo = (id: string, text: string): AddTodoAction => ({
type: ADD_TODO,
text,
id
})
export const toggleTodo = (id: string): ToggleTodoAction => ({
type: TOGGLE_TODO,
id
})
import { ADD_TODO, TOGGLE_TODO, TodoActions } from '../constants/todos'
interface Todo {
id: string
text: string
completed: boolean
}
export type TodosState = Todo[]
const initialState: TodosState = []
export default function todosReducer(
state = initialState,
action: TodoActions
) {
switch (action.type) {
// omit reducer logic
default:
return state
}
}
import { createStore, Dispatch } from 'redux'
import { TodoActions } from '../actions/todos'
import { CounterActions } from '../actions/counter'
import { TodosState } from '../reducers/todos'
import { CounterState } from '../reducers/counter'
// omit reducer setup
export const store = createStore(rootReducer)
// ❌ Common pattern: an "action type union" of all possible actions
export type RootAction = TodoActions | CounterActions
// ❌ Common pattern: manually defining the root state type with each field
export interface RootState {
todos: TodosState
counter: CounterState
}
// ❌ Common pattern: limiting what can be dispatched at the types level
export type AppDispatch = Dispatch<RootAction>
¡Redux Toolkit está diseñado para simplificar drásticamente el uso de TS, y nuestras recomendaciones incluyen inferir tipos siempre que sea posible!
Según nuestras guías estándar de configuración y uso de TypeScript, comienza configurando el archivo del store para inferir los tipos AppDispatch y RootState directamente del propio store. Esto incluirá correctamente cualquier modificación a dispatch añadida por middleware (como la capacidad de despachar thunks), y actualizará el tipo RootState cada vez que modifiques la definición de estado de un slice o añadas más slices.
import { configureStore } from '@reduxjs/toolkit'
// omit any other imports
const store = configureStore({
reducer: {
todos: todosReducer,
counter: counterReducer
}
})
// Infer the `RootState` and `AppDispatch` types from the store itself
// Inferred state type: {todos: TodosState, counter: CounterState}
export type RootState = ReturnType<typeof store.getState>
// Inferred dispatch type: Dispatch & ThunkDispatch<RootState, undefined, UnknownAction>
export type AppDispatch = typeof store.dispatch
Cada archivo de "slice" debe declarar y exportar un tipo para su propio estado local. Luego, usa el tipo PayloadAction para declarar el tipo de cualquier argumento action dentro de createSlice.reducers. Los creadores de acciones generados tendrán también el tipo correcto para el argumento que aceptan y para el action.payload que devuelven.
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface Todo {
id: string
text: string
completed: boolean
}
// Declare and export a type for the slice's state
export type TodosState = Todo[]
const initialState: TodosState = []
const todosSlice = createSlice({
name: 'todos',
// The `state` argument type will be inferred for all case reducers
// from the type of `initialState`
initialState,
reducers: {
// Use `PayloadAction<YourPayloadTypeHere>` for each `action` argument
todoAdded(state, action: PayloadAction<{ id: string; text: string }>) {
// omit logic
},
todoToggled(state, action: PayloadAction<string>) {
// omit logic
}
}
})
Modernización de componentes de React con React-Redux
El enfoque general para migrar el uso de React-Redux en componentes es:
-
Migrar un componente de clase de React existente a un componente de función
-
Reemplazar el envoltorio
connectcon el uso de los hooksuseSelectoryuseDispatchdentro del componente
Puedes hacer esto de forma individual por componente. Los componentes con connect y con hooks pueden coexistir al mismo tiempo.
Esta página no cubrirá el proceso de migrar componentes de clase a componentes de función, sino que se centrará en los cambios específicos de React-Redux.
Migración de connect a Hooks
Un componente heredado típico que usa la API connect de React-Redux podría verse así:
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import {
todoToggled,
todoDeleted,
selectTodoById,
selectActiveTodoId
} from './todosSlice'
// A `mapState` function, possibly using values from `ownProps`,
// and returning an object with multiple separate fields inside
const mapStateToProps = (state, ownProps) => {
return {
todo: selectTodoById(state, ownProps.todoId),
activeTodoId: selectActiveTodoId(state)
}
}
// Several possible variations on how you might see `mapDispatch` written:
// 1) a separate function, manual wrapping of `dispatch`
const mapDispatchToProps = dispatch => {
return {
todoDeleted: id => dispatch(todoDeleted(id)),
todoToggled: id => dispatch(todoToggled(id))
}
}
// 2) A separate function, wrapping with `bindActionCreators`
const mapDispatchToProps2 = dispatch => {
return bindActionCreators(
{
todoDeleted,
todoToggled
},
dispatch
)
}
// 3) An object full of action creators
const mapDispatchToProps3 = {
todoDeleted,
todoToggled
}
// The component, which gets all these fields as props
function TodoListItem({ todo, activeTodoId, todoDeleted, todoToggled }) {
// rendering logic here
}
// Finished with the call to `connect`
export default connect(mapStateToProps, mapDispatchToProps)(TodoListItem)
¡Con la API de hooks de React-Redux, la llamada a connect y los argumentos mapState/mapDispatch se reemplazan por hooks!
-
Cada campo individual devuelto en
mapStatese convierte en una llamada separada auseSelector -
Cada función pasada mediante
mapDispatchse convierte en una función de callback separada definida dentro del componente
import { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
todoAdded,
todoToggled,
selectTodoById,
selectActiveTodoId
} from './todosSlice'
export function TodoListItem({ todoId }) {
// Get the actual `dispatch` function with `useDispatch`
const dispatch = useDispatch()
// Select values from the state with `useSelector`
const activeTodoId = useSelector(selectActiveTodoId)
// Use prop in scope to select a specific value
const todo = useSelector(state => selectTodoById(state, todoId))
// Create callback functions that dispatch as needed, with arguments
const handleToggleClick = () => {
dispatch(todoToggled(todoId))
}
const handleDeleteClick = () => {
dispatch(todoDeleted(todoId))
}
// omit rendering logic
}
Una diferencia clave es que connect optimizaba el rendimiento de renderizado evitando que el componente envuelto se renderizara a menos que cambiaran sus stateProps+dispatchProps+ownProps. Los hooks no pueden hacer esto al estar dentro del componente. Si necesitas prevenir el comportamiento de renderizado recursivo normal de React, envuelve el componente en React.memo(MyComponent) manualmente.
Migración de TypeScript para componentes
Uno de los mayores inconvenientes de connect es que es muy difícil tiparlo correctamente, y las declaraciones de tipos acaban siendo extremadamente verbosas. Esto se debe a que es un Componente de Orden Superior (HOC) y a la flexibilidad de su API (cuatro argumentos opcionales, cada uno con múltiples sobrecargas).
La comunidad desarrolló múltiples enfoques con distintos niveles de complejidad. En el más simple, algunos usos requerían tipar state en mapState() y luego calcular los tipos de todas las props del componente:
import { connect } from 'react-redux'
import { RootState } from '../../app/store'
import {
todoToggled,
todoDeleted,
selectTodoById,
selectActiveTodoId
} from './todosSlice'
interface TodoListItemOwnProps {
todoId: string
}
const mapStateToProps = (state: RootState, ownProps) => {
return {
todo: selectTodoById(state, ownProps.todoId),
activeTodoId: selectActiveTodoId(state)
}
}
const mapDispatchToProps = {
todoDeleted,
todoToggled
}
type TodoListItemProps = TodoListItemOwnProps &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps
function TodoListItem({
todo,
activeTodoId,
todoDeleted,
todoToggled
}: TodoListItemProps) {}
export default connect(mapStateToProps, mapDispatchToProps)(TodoListItem)
El uso de typeof mapDispatch como objeto era especialmente peligroso porque fallaría si se incluían thunks.
Otros patrones requerían más esfuerzo: declarar mapDispatch como función y usar bindActionCreators para pasar un tipo dispatch: Dispatch<RootActions>, o calcular manualmente los tipos de todas las props recibidas por el componente envuelto para pasarlas como genéricos a connect.
Una alternativa mejor fue el tipo ConnectedProps<T> añadido a @types/react-redux en v7.x, que permitía inferir el tipo de todas las props pasadas al componente desde connect. Esto requería dividir la llamada a connect en dos partes:
import { connect, ConnectedProps } from 'react-redux'
import { RootState } from '../../app/store'
import {
todoToggled,
todoDeleted,
selectTodoById,
selectActiveTodoId
} from './todosSlice'
interface TodoListItemOwnProps {
todoId: string
}
const mapStateToProps = (state: RootState, ownProps) => {
return {
todo: selectTodoById(state, ownProps.todoId),
activeTodoId: selectActiveTodoId(state)
}
}
const mapDispatchToProps = {
todoDeleted,
todoToggled
}
// Call the first part of `connect` to get the function that accepts the component.
// This knows the types of the props returned by `mapState/mapDispatch`
const connector = connect(mapStateToProps, mapDispatchToProps)
// The `ConnectedProps<T> util type can extract "the type of all props from Redux"
type PropsFromRedux = ConnectedProps<typeof connector>
// The final component props are "the props from Redux" + "props from the parent"
type TodoListItemProps = PropsFromRedux & TodoListItemOwnProps
// That type can then be used in the component
function TodoListItem({
todo,
activeTodoId,
todoDeleted,
todoToggled
}: TodoListItemProps) {}
// And the final wrapped component is generated and exported
export default connector(TodoListItem)
¡La API de hooks de React-Redux es mucho más simple con TypeScript! En lugar de lidiar con capas de envoltura, inferencia de tipos y genéricos, los hooks son funciones simples que reciben argumentos y devuelven resultados. Solo necesitas pasar los tipos para RootState y AppDispatch.
Como se explica en nuestras guías de configuración de TypeScript, recomendamos crear alias "pre-tipados" para los hooks que incluyan los tipos correctos, y usar solo esos hooks pre-tipados en la aplicación.
Primero, configura los hooks:
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
Luego, utilízalos en tus componentes:
import { useAppSelector, useAppDispatch } from '../../app/hooks'
import {
todoToggled,
todoDeleted,
selectTodoById,
selectActiveTodoId
} from './todosSlice'
interface TodoListItemProps {
todoId: string
}
function TodoListItem({ todoId }: TodoListItemProps) {
// Use the pre-typed hooks in the component
const dispatch = useAppDispatch()
const activeTodoId = useAppSelector(selectActiveTodoId)
const todo = useAppSelector(state => selectTodoById(state, todoId))
// omit event handlers and rendering logic
}
Más información
Consulta estas páginas de documentación y publicaciones de blog para más detalles:
-
Tutoriales
-
Documentación Adicional
-
Artículos