Jak dynamicznie ładować reduktory do dzielenia kodu w aplikacji Redux?

189

Idę migrować do Redux.

Moja aplikacja składa się z wielu części (stron, komponentów), dlatego chcę stworzyć wiele reduktorów. Przykłady Redux pokazują, że powinienem użyć combineReducers()do wygenerowania jednego reduktora.

Rozumiem również, że aplikacja Redux powinna mieć jeden sklep i jest tworzona po uruchomieniu aplikacji. Kiedy tworzony jest sklep, powinienem przekazać mój połączony reduktor. Ma to sens, jeśli aplikacja nie jest zbyt duża.

Ale co jeśli zbuduję więcej niż jeden pakiet JavaScript? Na przykład każda strona aplikacji ma własny pakiet. Myślę, że w tym przypadku jeden połączony reduktor nie jest dobry. Przejrzałem źródła Redux i znalazłem replaceReducer()funkcję. Wydaje się, że tego chcę.

Mógłbym stworzyć połączony reduktor dla każdej części mojej aplikacji i używać jej, replaceReducer()gdy przechodzę między częściami aplikacji.

Czy to dobre podejście?

Pavel Teterin
źródło

Odpowiedzi:

245

Aktualizacja: zobacz także, jak to robi Twitter .

To nie jest pełna odpowiedź, ale powinna pomóc ci zacząć. Zauważ, że nie wyrzucam starych reduktorów - po prostu dodaję nowe do listy kombinacji. Nie widzę powodu, aby wyrzucać stare reduktory nawet w największych app jesteś mało prawdopodobne, aby mieć tysiące modułów dynamicznych, czyli punkt, w którym może chcesz odłączyć niektóre reduktory w aplikacji.

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

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

  // ...
}

Może istnieć lepszy sposób wyrażenia tego - po prostu pokazuję ten pomysł.

Dan Abramov
źródło
13
Bardzo chciałbym zobaczyć tego rodzaju funkcjonalność dodaną do projektu. Możliwość dynamicznego dodawania reduktorów jest niezbędna w przypadku dzielenia kodu i dużych aplikacji. Mam całe drzewa podrzędne, do których niektórzy użytkownicy mogą nie mieć dostępu, a ładowanie wszystkich reduktorów jest marnotrawstwem. Nawet przy ignorowaniu redux duże aplikacje mogą naprawdę układać w stos reduktory.
JeffBaumgardt 27.04.16
2
Czasami „optymalizacja” czegoś nieistotnego jest większym marnotrawstwem.
XML
1
Mam nadzieję, że powyższy komentarz ma sens ... kiedy zabrakło mi miejsca. Ale w zasadzie nie widzę łatwego sposobu na połączenie reduktorów w jedną gałąź naszego drzewa stanów, gdy są one dynamicznie ładowane z różnych tras, /homepagea następnie więcej tej gałęzi jest ładowanych, gdy użytkownik przechodzi do swojego profile.przykładu. zrobienie tego byłoby niesamowite. W przeciwnym razie trudno mi spłaszczyć moje drzewo stanu lub muszę mieć bardzo specyficzne nazwy gałęzi user-permissionsiuser-personal
BryceHayden
1
A jak mam postępować, jeśli mam stan początkowy?
Stalso
3
github.com/mxstbr/react-boilerplate płyta grzewcza wykorzystuje dokładnie tę samą technikę, o której tu mowa, w celu zmniejszenia obciążenia.
Pouya Sanooei
25

W ten sposób zaimplementowałem go w bieżącej aplikacji (na podstawie kodu Dana z problemu 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
  }
}

Utwórz instancję rejestru podczas ładowania aplikacji, przekazując reduktory, które zostaną zawarte w pakiecie wejściowym:

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

Następnie podczas konfigurowania sklepu i tras użyj funkcji, którą możesz nadać rejestrowi reduktora:

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

Gdzie te funkcje wyglądają mniej więcej tak:

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
}

Oto podstawowy przykład na żywo, który został stworzony przy użyciu tej konfiguracji i jego źródła:

Obejmuje również niezbędną konfigurację, aby umożliwić ponowne ładowanie na gorąco wszystkich reduktorów.

Jonny Buchanan
źródło
Dzięki @jonny, tylko heads-up, przykład teraz generuje błąd.
Jason J. Nathan
W twojej odpowiedzi brakuje deklaracji createReducer () (wiem, że jest to odpowiedź Dana Abrahamova, ale myślę, że włączenie jej uniknęłoby zamieszania)
Pakiet Tracer
6

Jest teraz moduł, który dodaje wtryskowe reduktory do sklepu redux. Nazywa się to Redux Injector .

Oto jak z niego korzystać:

  1. Nie łącz reduktorów. Zamiast tego umieść je w (zagnieżdżonym) obiekcie funkcji, tak jak normalnie, ale bez ich łączenia.

  2. Użyj createInjectStore z redux-injector zamiast createStore z redux.

  3. Wprowadzaj nowe reduktory za pomocą injectReducer.

Oto przykład:

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

Pełne ujawnienie: Jestem twórcą modułu.

Randall Knutson
źródło
4

Od października 2017 r .:

  • Reedux

    wdraża to, co zasugerował Dan i nic więcej, nie dotykając sklepu, projektu ani nawyków

Są też inne biblioteki, ale mogą mieć zbyt wiele zależności, mniej przykładów, skomplikowane użycie, są niekompatybilne z niektórymi programami pośrednimi lub wymagają przepisania zarządzania stanem. Skopiowano ze strony wprowadzającej Reedux:

Silviu-Marian
źródło
2

Wydaliśmy nową bibliotekę, która pomaga modulować aplikację Redux i umożliwia dynamiczne dodawanie / usuwanie reduktorów i oprogramowania pośredniego.

Proszę spojrzeć na https://github.com/Microsoft/redux-dynamic-modules

Moduły zapewniają następujące korzyści:

  • Moduły można łatwo ponownie wykorzystać w aplikacji lub między wieloma podobnymi aplikacjami.

  • Komponenty deklarują moduły, których potrzebują, a moduły redux-dynamic zapewniają, że moduł zostanie załadowany dla komponentu.

  • Moduły można dynamicznie dodawać / usuwać ze sklepu, np. podczas montowania komponentu lub wykonywania akcji przez użytkownika

cechy

  • Zgrupuj razem reduktory, oprogramowanie pośrednie i stan w jednym module wielokrotnego użytku.
  • Dodaj i usuń moduły ze sklepu Redux w dowolnym momencie.
  • Użyj dołączonego komponentu, aby automatycznie dodać moduł podczas renderowania komponentu
  • Rozszerzenia zapewniają integrację z popularnymi bibliotekami, w tym redux-saga i redux-obserwable

Przykładowe scenariusze

  • Nie chcesz ładować kodu wszystkich reduktorów z góry. Zdefiniuj moduł dla niektórych reduktorów i użyj DynamicModuleLoader oraz biblioteki, takiej jak ładowalna reakcja, aby pobrać i dodać moduł w czasie wykonywania.
  • Masz kilka popularnych reduktorów / oprogramowania pośredniego, które muszą być ponownie użyte w różnych obszarach aplikacji. Zdefiniuj moduł i łatwo umieść go w tych obszarach.
  • Masz mono-repo, które zawiera wiele aplikacji o podobnym stanie. Utwórz pakiet zawierający niektóre moduły i ponownie wykorzystaj je w swoich aplikacjach
Code Ninja
źródło
0

Oto kolejny przykład podziału kodu i sklepów redux, moim zdaniem dość prosty i elegancki. Myślę, że może to być przydatne dla tych, którzy szukają działającego rozwiązania.

Ten sklep jest nieco uproszczony, nie zmusza cię do posiadania przestrzeni nazw (reder.name) w obiekcie stanu, oczywiście może wystąpić kolizja z nazwami, ale możesz to kontrolować, tworząc konwencję nazewnictwa dla swoich reduktorów i to powinno być dobrze.

Maksym Oliinyk
źródło