Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
División de Código
En aplicaciones web grandes, a menudo es deseable dividir el código de la aplicación en múltiples paquetes JS que puedan cargarse bajo demanda. Esta estrategia, llamada 'división de código', ayuda a mejorar el rendimiento de tu aplicación reduciendo el tamaño de la carga útil JS inicial que debe obtenerse.
Para implementar división de código con Redux, necesitamos poder añadir reductores dinámicamente al store. Sin embargo, Redux solo tiene una única función reductora raíz. Este reductor raíz normalmente se genera llamando a combineReducers() o una función similar cuando se inicializa la aplicación. Para añadir más reductores dinámicamente, necesitamos llamar a esa función nuevamente para regenerar el reductor raíz. A continuación, discutimos algunos enfoques para resolver este problema y mencionamos dos bibliotecas que ofrecen esta funcionalidad.
Principio Básico
Usando replaceReducer
El store de Redux expone una función replaceReducer, que reemplaza la función reductora raíz activa actual por una nueva. Al llamarla, se intercambiará la referencia interna a la función reductora y se despachará una acción para ayudar a inicializar cualquier reductor de slice recién añadido:
const newRootReducer = combineReducers({
existingSlice: existingSliceReducer,
newSlice: newSliceReducer
})
store.replaceReducer(newRootReducer)
Enfoques de Inyección de Reductores
Esta sección cubrirá algunas recetas manuales utilizadas para inyectar reductores.
Definiendo una función injectReducer
Probablemente querremos llamar a store.replaceReducer() desde cualquier parte de la aplicación. Por ello, es útil definir una función reutilizable injectReducer() que mantenga referencias a todos los reductores de slice existentes y adjuntarla a la instancia del store.
import { createStore } from 'redux'
// Define the Reducers that will always be present in the application
const staticReducers = {
users: usersReducer,
posts: postsReducer
}
// Configure the store
export default function configureStore(initialState) {
const store = createStore(createReducer(), initialState)
// Add a dictionary to keep track of the registered async reducers
store.asyncReducers = {}
// Create an inject reducer function
// This function adds the async reducer, and creates a new combined reducer
store.injectReducer = (key, asyncReducer) => {
store.asyncReducers[key] = asyncReducer
store.replaceReducer(createReducer(store.asyncReducers))
}
// Return the modified store
return store
}
function createReducer(asyncReducers) {
return combineReducers({
...staticReducers,
...asyncReducers
})
}
Ahora, solo necesitas llamar a store.injectReducer para añadir un nuevo reductor al store.
Usando un 'Gestor de Reductores'
Otro enfoque es crear un objeto 'Gestor de Reductores' que registre todos los reductores y exponga una función reduce(). Considera el siguiente ejemplo:
export function createReducerManager(initialReducers) {
// Create an object which maps keys to reducers
const reducers = { ...initialReducers }
// Create the initial combinedReducer
let combinedReducer = combineReducers(reducers)
// An array which is used to delete state keys when reducers are removed
let keysToRemove = []
return {
getReducerMap: () => reducers,
// The root reducer function exposed by this object
// This will be passed to the store
reduce: (state, action) => {
// If any reducers have been removed, clean up their state first
if (keysToRemove.length > 0) {
state = { ...state }
for (let key of keysToRemove) {
delete state[key]
}
keysToRemove = []
}
// Delegate to the combined reducer
return combinedReducer(state, action)
},
// Adds a new reducer with the specified key
add: (key, reducer) => {
if (!key || reducers[key]) {
return
}
// Add the reducer to the reducer mapping
reducers[key] = reducer
// Generate a new combined reducer
combinedReducer = combineReducers(reducers)
},
// Removes a reducer with the specified key
remove: key => {
if (!key || !reducers[key]) {
return
}
// Remove it from the reducer mapping
delete reducers[key]
// Add the key to the list of keys to clean up
keysToRemove.push(key)
// Generate a new combined reducer
combinedReducer = combineReducers(reducers)
}
}
}
const staticReducers = {
users: usersReducer,
posts: postsReducer
}
export function configureStore(initialState) {
const reducerManager = createReducerManager(staticReducers)
// Create a store with the root reducer function being the one exposed by the manager.
const store = createStore(reducerManager.reduce, initialState)
// Optional: Put the reducer manager on the store so it is easily accessible
store.reducerManager = reducerManager
}
Para añadir un nuevo reductor, ahora se puede llamar a store.reducerManager.add("asyncState", asyncReducer).
Para eliminar un reductor, ahora se puede llamar a store.reducerManager.remove("asyncState")
Redux Toolkit
Redux Toolkit 2.0 incluye utilidades diseñadas para simplificar la división de código con reductores y middleware, incluyendo soporte sólido para TypeScript (un desafío común con reductores y middleware cargados de forma diferida).
combineSlices
La utilidad combineSlices está diseñada para permitir una fácil inyección de reductores. También reemplaza a combineReducers, ya que puede usarse para combinar múltiples slices y reductores en un único reductor raíz.
En la configuración, acepta un conjunto de slices y mapas de reductores, y devuelve una instancia de reductor con métodos adjuntos para inyección.
Un "slice" para combineSlices típicamente se crea con createSlice, pero puede ser cualquier objeto "tipo slice" con propiedades reducerPath y reducer (lo que significa que las instancias de API RTK Query también son compatibles).
const withUserReducer = rootReducer.inject({
reducerPath: 'user',
reducer: userReducer
})
const withApiReducer = rootReducer.inject(fooApi)
Por simplicidad, esta estructura { reducerPath, reducer } se describirá en esta documentación como un "slice".
Los slices se montarán en su reducerPath, y los elementos de los objetos mapa de reductores se montarán bajo su clave respectiva.
const rootReducer = combineSlices(counterSlice, baseApi, {
user: userSlice.reducer,
auth: authSlice.reducer
})
// is like
const rootReducer = combineReducers({
[counterSlice.reducerPath]: counterSlice.reducer,
[baseApi.reducerPath]: baseApi.reducer,
user: userSlice.reducer,
auth: authSlice.reducer
})
Ten cuidado para evitar colisiones de nombres: las claves posteriores sobrescribirán las anteriores, pero TypeScript no podrá tener esto en cuenta.
Inyección de Slices
Para inyectar un slice, debes llamar a rootReducer.inject(slice) en la instancia del reductor devuelta por combineSlices. Esto inyectará el slice bajo su reducerPath en el conjunto de reductores y devolverá una instancia del reductor combinado tipada para reconocer que el slice ha sido inyectado.
Alternativamente, puedes llamar a slice.injectInto(rootReducer), lo que devuelve una instancia del slice que es consciente de haber sido inyectado. Incluso podrías hacer ambas cosas, ya que cada llamada devuelve algo útil, y combineSlices permite inyectar la misma instancia de reducer en el mismo reducerPath sin problemas.
const withCounterSlice = rootReducer.inject(counterSlice)
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
Una diferencia clave entre la inyección típica de reducers y el enfoque de "meta-reducer" de combineSlice es que replaceReducer nunca se llama para combineSlice. La instancia del reducer pasada al store no cambia.
Una consecuencia de esto es que no se despacha ninguna acción cuando se inyecta un slice, y por lo tanto el estado del slice inyectado no aparece inmediatamente en el estado. El estado solo aparecerá en el estado del store cuando se despache una acción.
Sin embargo, para evitar que los selectores tengan que manejar estados potencialmente undefined, combineSlices incluye algunas utilidades de selectores útiles.
Declaración de slices de carga diferida
Para que los slices de carga diferida aparezcan en el tipo de estado inferido, se proporciona un helper withLazyLoadedSlices. Esto te permite declarar slices que planeas inyectar más tarde, para que aparezcan como opcionales en el tipo de estado.
Para evitar completamente importar el slice diferido en el archivo del reducer combinado, se puede usar la ampliación de módulos.
// file: reducer.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './staticSlice'
export interface LazyLoadedSlices {}
export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
// file: counterSlice.ts
import type { WithSlice } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'
interface CounterState {
value: number
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 } as CounterState,
reducers: {
increment: state => void state.value++
},
selectors: {
selectValue: state => state.value
}
})
declare module './reducer' {
// WithSlice utility assumes reducer is under slice.reducerPath
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}
// if it's not, just use a normal key
export interface LazyLoadedSlices {
aCounter: CounterState
}
}
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
const injectedACounterSlice = counterSlice.injectInto(rootReducer, {
reducerPath: 'aCounter'
})
Utilidades de selectores
Además de inject, la instancia del reducer combinado tiene un método .selector que se puede usar para envolver selectores. Envuelve el objeto de estado en un Proxy y proporciona un estado inicial para cualquier reducer que haya sido inyectado pero que aún no haya aparecido en el estado.
El resultado de llamar a inject está tipado para saber que el slice inyectado siempre estará definido cuando se llame al selector.
const selectCounterValue = (state: RootState) => state.counter?.value // number | undefined
const withCounterSlice = rootReducer.inject(counterSlice)
const selectCounterValue = withCounterSlice.selector(
state => state.counter.value // number - initial state used if not in store
)
Una instancia "inyectada" de un slice hará lo mismo para los selectores del slice: se proporcionará un estado inicial si no está presente en el estado pasado.
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
console.log(counterSlice.selectors.selectValue({})) // runtime error
console.log(injectedCounterSlice.selectors.selectValue({})) // 0
Uso típico
combineSlices está diseñado para que el slice se inyecte tan pronto como se necesite (es decir, cuando un selector o acción se importa desde un componente que se ha cargado).
Esto significa que el uso típico se parecerá a lo siguiente.
// file: reducer.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './staticSlice'
export interface LazyLoadedSlices {}
export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
// file: store.ts
import { configureStore } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'
export const store = configureStore({ reducer: rootReducer })
// file: counterSlice.ts
import type { WithSlice } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => void state.value++
},
selectors: {
selectValue: state => state.value
}
})
export const { increment } = counterSlice.actions
declare module './reducer' {
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}
}
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
export const { selectValue } = injectedCounterSlice.selectors
// file: Counter.tsx
// by importing from counterSlice we guarantee
// the injection happens before this component is defined
import { increment, selectValue } from './counterSlice'
import { useAppDispatch, useAppSelector } from './hooks'
export default function Counter() {
const dispatch = usAppDispatch()
const value = useAppSelector(selectValue)
return (
<>
<p>{value}</p>
<button onClick={() => dispatch(increment())}>Increment</button>
</>
)
}
// file: App.tsx
import { Provider } from 'react-redux'
import { store } from './store'
// lazily importing the component means that the code
// doesn't actually get pulled in and executed until the component is rendered.
// this means that the inject call only happens once Counter renders
const Counter = React.lazy(() => import('./Counter'))
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
)
}
createDynamicMiddleware
La utilidad createDynamicMiddleware crea un "meta-middleware" que permite inyectar middleware después de la inicialización del store.
import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import reducer from './reducer'
const dynamicMiddleware = createDynamicMiddleware()
const store = configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(dynamicMiddleware.middleware)
})
dynamicMiddleware.addMiddleware(logger)
addMiddleware
addMiddleware añade la instancia del middleware a la cadena de middlewares manejada por la instancia de middleware dinámico. El middleware se aplica en orden de inyección y se almacena por referencia de función (por lo que el mismo middleware solo se aplica una vez, independientemente de cuántas veces se inyecte).
Es importante recordar que todos los middlewares inyectados estarán contenidos dentro de la instancia original de middleware dinámico.
import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import reducer from './reducer'
const dynamicMiddleware = createDynamicMiddleware()
const store = configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(dynamicMiddleware.middleware)
})
dynamicMiddleware.addMiddleware(logger)
// middleware chain is now [thunk, logger]
Si se desea tener más control sobre el orden, se pueden usar múltiples instancias.
import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import reducer from './reducer'
const beforeMiddleware = createDynamicMiddleware()
const afterMiddleware = createDynamicMiddleware()
const store = configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware()
.prepend(beforeMiddleware.middleware)
.concat(afterMiddleware.middleware)
})
beforeMiddleware.addMiddleware(logger)
afterMiddleware.addMiddleware(logger)
// middleware chain is now [logger, thunk, logger]
withMiddleware
withMiddleware es un creador de acciones que, cuando se despacha, hace que el middleware añada cualquier middleware incluido y devuelve una versión pre-tipada de dispatch con cualquier extensión añadida.
const listenerDispatch = store.dispatch(
withMiddleware(listenerMiddleware.middleware)
)
const unsubscribe = listenerDispatch(addListener({ actionCreator, effect }))
// ^? () => void
Esto es principalmente útil en contextos sin React. Con React, es más conveniente usar la integración con React.
Integración con React
Cuando se importa desde el punto de entrada @reduxjs/toolkit/react, la instancia de middleware dinámico tendrá algunos métodos adicionales.
createDispatchWithMiddlewareHook
Este método llama a addMiddleware y devuelve una versión de useDispatch tipada para reconocer el middleware inyectado.
import { createDynamicMiddleware } from '@reduxjs/toolkit/react'
const dynamicMiddleware = createDynamicMiddleware()
const useListenerDispatch = dynamicMiddleware.createDispatchWithMiddlewareHook(
listenerMiddleware.middleware
)
function Component() {
const dispatch = useListenerDispatch()
useEffect(() => {
const unsubscribe = dispatch(addListener({ actionCreator, effect }))
return unsubscribe
}, [dispatch])
}
El middleware se inyecta cuando se llama a createDispatchWithMiddlewareHook, no cuando se llama al hook useDispatch.
createDispatchWithMiddlewareHookFactory
Este método toma una instancia de contexto de React y crea una instancia de createDispatchWithMiddlewareHook que usa ese contexto (ver Proporcionar contexto personalizado).
import { createContext } from 'react'
import { createDynamicMiddleware } from '@reduxjs/toolkit/react'
import type { ReactReduxContextValue } from 'react-redux'
const context = createContext<ReactReduxContextValue | null>(null)
const dynamicMiddleware = createDynamicMiddleware()
const createDispatchWithMiddlewareHook =
dynamicMiddleware.createDispatchWithMiddlewareHookFactory(context)
const useListenerDispatch = createDispatchWithMiddlewareHook(
listenerMiddleware.middleware
)
function Component() {
const dispatch = useListenerDispatch()
useEffect(() => {
const unsubscribe = dispatch(addListener({ actionCreator, effect }))
return unsubscribe
}, [dispatch])
}
Bibliotecas y frameworks de terceros
Existen algunas buenas bibliotecas externas que pueden ayudarte a implementar automáticamente la funcionalidad anterior: