¿Cómo cargar dinámicamente reductores para la división de código en una aplicación Redux?


189

Voy a migrar a Redux.

Mi aplicación consta de muchas partes (páginas, componentes), por lo que quiero crear muchos reductores. Los ejemplos de Redux muestran que debería usar combineReducers()para generar un reductor.

Además, según tengo entendido, la aplicación Redux debe tener una tienda y se crea una vez que se inicia la aplicación. Cuando se crea la tienda, debería pasar mi reductor combinado. Esto tiene sentido si la aplicación no es demasiado grande.

¿Pero qué pasa si construyo más de un paquete de JavaScript? Por ejemplo, cada página de la aplicación tiene su propio paquete. Creo que en este caso el único reductor combinado no es bueno. Revisé las fuentes de Redux y encontré la replaceReducer()función. Parece ser lo que quiero.

Podría crear un reductor combinado para cada parte de mi aplicación y usarlo replaceReducer()cuando me muevo entre partes de la aplicación.

¿Es este un buen enfoque?

Respuestas:


245

Actualización: vea también cómo lo hace Twitter .

Esta no es una respuesta completa, pero debería ayudarlo a comenzar. Tenga en cuenta que no estoy desechando los reductores antiguos, solo estoy agregando otros nuevos a la lista de combinaciones. No veo ninguna razón para deshacerse de los reductores antiguos, incluso en la aplicación más grande es poco probable que tenga miles de módulos dinámicos, que es el punto en el que es posible que desee desconectar algunos reductores en su aplicación.

reducers.js

import { combineReducers } from 'redux';
import users from './reducers/users';
import posts from './reducers/posts';

export default function createReducer(asyncReducers) {
  return combineReducers({
    users,
    posts,
    ...asyncReducers
  });
}

store.js

import { createStore } from 'redux';
import createReducer from './reducers';

export default function configureStore(initialState) {
  const store = createStore(createReducer(), initialState);
  store.asyncReducers = {};
  return store;
}

export function injectAsyncReducer(store, name, asyncReducer) {
  store.asyncReducers[name] = asyncReducer;
  store.replaceReducer(createReducer(store.asyncReducers));
}

routes.js

import { injectAsyncReducer } from './store';

// Assuming React Router here but the principle is the same
// regardless of the library: make sure store is available
// when you want to require.ensure() your reducer so you can call
// injectAsyncReducer(store, name, reducer).

function createRoutes(store) {
  // ...

  const CommentsRoute = {
    // ...

    getComponents(location, callback) {
      require.ensure([
        './pages/Comments',
        './reducers/comments'
      ], function (require) {
        const Comments = require('./pages/Comments').default;
        const commentsReducer = require('./reducers/comments').default;

        injectAsyncReducer(store, 'comments', commentsReducer);
        callback(null, Comments);
      })
    }
  };

  // ...
}

Puede haber una forma más ordenada de expresar esto: solo estoy mostrando la idea.


13
Me encantaría ver este tipo de funcionalidad agregada al proyecto. La capacidad de agregar reductores dinámicamente es imprescindible cuando se trata de división de código y aplicaciones grandes. Tengo subárboles enteros a los que algunos usuarios no pueden acceder y cargar todos los reductores es un desperdicio. Incluso con redux-ignore, las aplicaciones grandes realmente pueden acumular reductores.
JeffBaumgardt

2
A veces, es un desperdicio mayor 'optimizar' algo intrascendente.
XML

1
Espero que el comentario anterior tenga sentido ... ya que me quedé sin espacio. Pero básicamente no veo una manera fácil de combinar los reductores en una sola rama en nuestro árbol de estado cuando se cargan dinámicamente desde diferentes rutas /homepagey luego se carga más de esa rama cuando el usuario va a su profile.Un ejemplo de cómo hacer esto, sería genial. De lo contrario, me resulta difícil aplanar mi árbol de estado o tengo que tener nombres de sucursales muy específicos user-permissionsyuser-personal
BryceHayden

1
¿Y cómo debo actuar si tengo un estado inicial?
Stalso

3
github.com/mxstbr/react-boilerplate boilerplate utiliza exactamente la misma técnica mencionada aquí para cargar los reductores.
Pouya Sanooei

25

Así es como lo implementé en una aplicación actual (¡basada en el código de Dan de un problema de GitHub!)

// Based on https://github.com/rackt/redux/issues/37#issue-85098222
class ReducerRegistry {
  constructor(initialReducers = {}) {
    this._reducers = {...initialReducers}
    this._emitChange = null
  }
  register(newReducers) {
    this._reducers = {...this._reducers, ...newReducers}
    if (this._emitChange != null) {
      this._emitChange(this.getReducers())
    }
  }
  getReducers() {
    return {...this._reducers}
  }
  setChangeListener(listener) {
    if (this._emitChange != null) {
      throw new Error('Can only set the listener for a ReducerRegistry once.')
    }
    this._emitChange = listener
  }
}

Cree una instancia de registro al iniciar su aplicación, pasando reductores que se incluirán en el paquete de entrada:

// coreReducers is a {name: function} Object
var coreReducers = require('./reducers/core')
var reducerRegistry = new ReducerRegistry(coreReducers)

Luego, al configurar la tienda y las rutas, use una función que puede asignar al registro reductor para:

var routes = createRoutes(reducerRegistry)
var store = createStore(reducerRegistry)

Donde estas funciones se parecen a:

function createRoutes(reducerRegistry) {
  return <Route path="/" component={App}>
    <Route path="core" component={Core}/>
    <Route path="async" getComponent={(location, cb) => {
      require.ensure([], require => {
        reducerRegistry.register({async: require('./reducers/async')})
        cb(null, require('./screens/Async'))
      })
    }}/>
  </Route>
}

function createStore(reducerRegistry) {
  var rootReducer = createReducer(reducerRegistry.getReducers())
  var store = createStore(rootReducer)

  reducerRegistry.setChangeListener((reducers) => {
    store.replaceReducer(createReducer(reducers))
  })

  return store
}

Aquí hay un ejemplo en vivo básico que se creó con esta configuración y su fuente:

También cubre la configuración necesaria para permitir la recarga en caliente de todos sus reductores.


Gracias @jonny, solo un aviso, el ejemplo es arrojar un error ahora.
Jason J. Nathan el

Falta la declaración createReducer () en su respuesta (sé que está en la respuesta de Dan Abrahamov pero creo que incluirla evitaría confusión)
Packet Tracer

6

Ahora hay un módulo que agrega reductores de inyección en la tienda redux. Se llama Redux Injector .

Aquí está cómo usarlo:

  1. No combine reductores. En su lugar, póngalos en un objeto (anidado) de funciones como lo haría normalmente pero sin combinarlos.

  2. Use createInjectStore desde redux-injector en lugar de createStore desde redux.

  3. Inyecte nuevos reductores con injectReducer.

Aquí hay un ejemplo:

import { createInjectStore, injectReducer } from 'redux-injector';

const reducersObject = {
   router: routerReducerFunction,
   data: {
     user: userReducerFunction,
     auth: {
       loggedIn: loggedInReducerFunction,
       loggedOut: loggedOutReducerFunction
     },
     info: infoReducerFunction
   }
 };

const initialState = {};

let store = createInjectStore(
  reducersObject,
  initialState
);

// Now you can inject reducers anywhere in the tree.
injectReducer('data.form', formReducerFunction);

Divulgación completa: soy el creador del módulo.


4

A partir de octubre de 2017:

  • Reedux

    implementa lo que Dan sugirió y nada más, sin tocar tu tienda, tu proyecto o tus hábitos

También hay otras bibliotecas, pero pueden tener demasiadas dependencias, menos ejemplos, uso complicado, son incompatibles con algunos middlewares o requieren que reescriba su administración de estado. Copiado de la página de introducción de Reedux:


2

Lanzamos una nueva biblioteca que ayuda a modular una aplicación Redux y permite agregar / eliminar dinámicamente Reductores y middlewares.

Por favor, eche un vistazo a https://github.com/Microsoft/redux-dynamic-modules

Los módulos proporcionan los siguientes beneficios:

  • Los módulos se pueden reutilizar fácilmente en la aplicación o entre múltiples aplicaciones similares.

  • Los componentes declaran los módulos que necesitan y redux-dynamic-modules asegura que el módulo se cargue para el componente.

  • Los módulos se pueden agregar / quitar de la tienda dinámicamente, ej. cuando se monta un componente o cuando un usuario realiza una acción

Caracteristicas

  • Agrupe los reductores, el middleware y el estado en un solo módulo reutilizable.
  • Agregue y elimine módulos de una tienda Redux en cualquier momento.
  • Use el componente incluido para agregar automáticamente un módulo cuando se representa un componente
  • Las extensiones proporcionan integración con bibliotecas populares, incluidas redux-saga y redux-observable

Escenarios de ejemplo

  • No desea cargar el código de todos sus reductores por adelantado. Defina un módulo para algunos reductores y use DynamicModuleLoader y una biblioteca como react-loadable para descargar y agregar su módulo en tiempo de ejecución.
  • Tiene algunos reductores / middleware comunes que deben reutilizarse en diferentes áreas de su aplicación. Defina un módulo e inclúyalo fácilmente en esas áreas.
  • Tiene un repositorio mono que contiene múltiples aplicaciones que comparten un estado similar. Cree un paquete que contenga algunos módulos y reutilícelos en sus aplicaciones

0

Aquí hay otro ejemplo con división de código y tiendas redux, bastante simple y elegante en mi opinión. Creo que puede ser bastante útil para aquellos que buscan una solución de trabajo.

Esta tienda está un poco simplificada, no lo obliga a tener un espacio de nombres (reducer.name) en su objeto de estado, por supuesto, puede haber una colisión con los nombres, pero puede controlar esto creando una convención de nomenclatura para sus reductores y debería estar bien.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.