Comment charger dynamiquement des réducteurs pour le fractionnement de code dans une application Redux?

189

Je vais migrer vers Redux.

Mon application se compose de nombreuses parties (pages, composants) donc je souhaite créer de nombreux réducteurs. Les exemples Redux montrent que je devrais utiliser combineReducers()pour générer un réducteur.

Aussi, si je comprends bien, l'application Redux devrait avoir un magasin et il est créé une fois que l'application démarre. Lorsque le magasin est en cours de création, je devrais passer mon réducteur combiné. Cela a du sens si l'application n'est pas trop grande.

Mais que faire si je construis plus d'un bundle JavaScript? Par exemple, chaque page d'application possède son propre bundle. Je pense que dans ce cas, le réducteur combiné n'est pas bon. J'ai regardé à travers les sources de Redux et j'ai trouvé la replaceReducer()fonction. Cela semble être ce que je veux.

Je pourrais créer un réducteur combiné pour chaque partie de mon application et l'utiliser replaceReducer()lorsque je me déplace entre les parties de l'application.

Est-ce une bonne approche?

Pavel Teterin
la source

Réponses:

245

Mise à jour: découvrez également comment Twitter le fait .

Ce n'est pas une réponse complète mais devrait vous aider à démarrer. Notez que je ne jette pas d'anciens réducteurs - j'en ajoute simplement de nouveaux à la liste de combinaisons. Je ne vois aucune raison de jeter les anciens réducteurs - même dans la plus grande application, il est peu probable que vous ayez des milliers de modules dynamiques, ce qui est le point où vous voudrez peut -être déconnecter certains réducteurs de votre application.

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);
      })
    }
  };

  // ...
}

Il y a peut-être une manière plus ordonnée d'exprimer cela - je ne fais que montrer l'idée.

Dan Abramov
la source
13
J'aimerais voir ce type de fonctionnalité ajoutée au projet. La possibilité d'ajouter des réducteurs de manière dynamique est indispensable pour gérer le fractionnement de code et les applications volumineuses. J'ai des sous-arbres entiers auxquels certains utilisateurs peuvent ne pas accéder et le chargement de tous les réducteurs est un gaspillage. Même avec redux-ignore les grandes applications peuvent vraiment empiler des réducteurs.
JeffBaumgardt
2
Parfois, c'est un plus gros gaspillage d '«optimiser» quelque chose sans importance.
XML
1
J'espère que le commentaire ci-dessus a du sens ... car je n'ai plus de place. Mais fondamentalement, je ne vois pas de moyen facile de combiner les réducteurs lorsqu'ils sont dans une seule branche de notre arbre d'état lorsqu'ils sont chargés dynamiquement à partir de différentes routes /homepage, puis une plus grande partie de cette branche est chargée lorsque l'utilisateur accède à leur profile.Un exemple de la façon dont pour ce faire, ce serait génial. Sinon, j'ai du mal à aplatir mon arbre d'état ou je dois avoir des noms de branche très spécifiques user-permissionsetuser-personal
BryceHayden
1
Et comment dois-je agir, si j'ai l'état initial?
Stalso
3
github.com/mxstbr/react-boilerplate passe-partout utilise exactement la même technique que celle mentionnée ici pour charger les réducteurs.
Pouya Sanooei
25

C'est ainsi que je l'ai implémenté dans une application actuelle (basé sur le code de Dan à partir d'un problème 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
  }
}

Créez une instance de registre lors du démarrage de votre application, en passant des réducteurs qui seront inclus dans le bundle d'entrée:

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

Ensuite, lors de la configuration du magasin et des routes, utilisez une fonction à laquelle vous pouvez attribuer le registre du réducteur:

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

Où ces fonctions ressemblent à quelque chose comme:

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
}

Voici un exemple de base en direct qui a été créé avec cette configuration, et sa source:

Il couvre également la configuration nécessaire pour activer le rechargement à chaud de tous vos réducteurs.

Jonny Buchanan
la source
Merci @jonny, juste un avertissement, l'exemple lance une erreur maintenant.
Jason J.Nathan
La déclaration createReducer () est absente de votre réponse (je sais que c'est dans la réponse de Dan Abrahamov mais je pense que l'inclure éviterait la confusion)
Packet Tracer
6

Il existe maintenant un module qui ajoute des réducteurs d'injection dans le magasin redux. Il s'appelle Redux Injector .

Voici comment l'utiliser:

  1. Ne combinez pas de réducteurs. Placez-les plutôt dans un objet (imbriqué) de fonctions comme vous le feriez normalement, mais sans les combiner.

  2. Utilisez createInjectStore depuis redux-injector au lieu de createStore depuis redux.

  3. Injectez de nouveaux réducteurs avec injectReducer.

Voici un exemple:

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);

Divulgation complète: je suis le créateur du module.

Randall Knutson
la source
4

Depuis octobre 2017:

  • Reedux

    met en œuvre ce que Dan a suggéré et rien de plus, sans toucher à votre magasin, votre projet ou vos habitudes

Il existe également d'autres bibliothèques mais elles peuvent avoir trop de dépendances, moins d'exemples, une utilisation compliquée, sont incompatibles avec certains middlewares ou vous obligent à réécrire votre gestion d'état. Copié de la page d'introduction de Reedux:

Silviu-Marian
la source
2

Nous avons publié une nouvelle bibliothèque qui aide à moduler une application Redux et permet d'ajouter / supprimer dynamiquement des réducteurs et des middlewares.

Veuillez jeter un œil à https://github.com/Microsoft/redux-dynamic-modules

Les modules offrent les avantages suivants:

  • Les modules peuvent être facilement réutilisés dans l'application ou entre plusieurs applications similaires.

  • Les composants déclarent les modules dont ils ont besoin et redux-dynamic-modules garantit que le module est chargé pour le composant.

  • Les modules peuvent être ajoutés / supprimés du magasin de manière dynamique, ex. lorsqu'un composant monte ou lorsqu'un utilisateur effectue une action

Caractéristiques

  • Regroupez les réducteurs, les intergiciels et l'état dans un seul module réutilisable.
  • Ajoutez et supprimez des modules d'un magasin Redux à tout moment.
  • Utilisez le composant inclus pour ajouter automatiquement un module lors du rendu d'un composant
  • Les extensions fournissent une intégration avec les bibliothèques populaires, y compris redux-saga et redux-observable

Exemples de scénarios

  • Vous ne voulez pas charger le code pour tous vos réducteurs à l'avance. Définissez un module pour certains réducteurs et utilisez DynamicModuleLoader et une bibliothèque telle que react-loadable pour télécharger et ajouter votre module au moment de l'exécution.
  • Vous disposez de certains réducteurs / intergiciels communs qui doivent être réutilisés dans différents domaines de votre application. Définissez un module et incluez-le facilement dans ces zones.
  • Vous disposez d'un mono-repo qui contient plusieurs applications partageant un état similaire. Créez un package contenant certains modules et réutilisez-les dans vos applications
Code Ninja
la source
0

Voici un autre exemple de partage de code et de magasins redux, assez simple et élégant à mon avis. Je pense que cela peut être très utile pour ceux qui recherchent une solution efficace.

Ce magasin est un peu simplifié, il ne vous oblige pas à avoir un espace de noms (reducer.name) dans votre objet d'état, bien sûr, il peut y avoir une collision avec les noms mais vous pouvez contrôler cela en créant une convention de dénomination pour vos réducteurs et cela devrait être bien.

Maksym Oliinyk
la source