Skip to content

Code splitting in large webapps #37

Closed
@gaearon

Description

@gaearon

I've heard from several people they already want to use this in production, which is a pretty crazy idea, if you ask me 😉 . Still, it's better to consider production use cases early. One of such use cases is code splitting.

Large apps don't want to carry all the code in one JS bundle. Webpack and Browserify allow you to split your app into several parts. One core part loads first, the rest is loaded on demand. We want to support this.

Currently Redux forces you to declare all Stores at the root. This is sensible because if you register them lazily as components subscribe to them, some Stores might miss some actions. This is totally not obvious and fragile.

However, defining all Stores at the root robs us of some of the benefits of code splitting, as all Stores will have to be included in the application's entry point bundle.

So here's my proposal. (I haven't thought it through at all; tell me if it's silly.)

I want to support several <Root>s throughout the application. Whenever a new <Root> is mounted under an existing one, instead of initializing a new dispatcher, it adds its stores to the parent dispatcher. They do not receive the actions they missed—fair game IMO.

Open questions:

  • Is the Store state ever destroyed?
  • Should it be destroyed when a child <Root> unmounts?
  • Should it be destroyed when a particular store key is removed from the stores prop?
  • Is it time to rename <Root> to something like <Dispatcher>?

I don't know if it's a good design or not, just something to get the ball rolling.
I want to have some consistent state lifecycle story that works with big apps.

I don't like this idea. If the code loads, it should start handling the actions immediately; not when some view mounts. And what if the new view mounts, and then unmounts? Its <Root> will be gone, poof! But we don't want to erase its state.

Perhaps, a better idea is to rely on React! We got <Root> at the top. (Yo, let's call it <Dispatcher> ;-). Okay, so we got <Dispatcher> at the top. And it has a stores prop. (Not in the decorator version, but we're looking at an advanced use case.) And we got React. And React lets you change props. Get it?

// ------
// Core App
// ------

// StoreRegistry.js

let allStores = {};
let emitChange = null;

export function register(newStores) {
  allStores = { ...allStores, ...newStores }
  emitChange(allStores);
}

export function setChangeListener(listener) {
  if (emitChange) throw new Error('Can set listener once.'); // lol
  emitChange = listener;
  emitChange(allStores);
}

// App.js

import { Dispatcher } from 'redux';
import * as StoreRegistry form './stores/registry';
import * as coreStores from './stores/core';

StoreRegistry.register(coreStores);

class App extends Component {
  constructor(props) {
    super(props);
    StoreRegistry.setChangeListener(() => this.handleStoresChange);
  }

  handleStoresChange(stores) {
    if (this.state) {
      this.setState({ stores });
    } else {
      this.state = { stores };
    }
  }

  render() {
    return (
      <Dispatcher stores={this.state.stores}>
        <SomeRootView />
      </Dispatcher>
    );
  }
}


// ------
// Dynamic module loaded with code splitting
// ------

import * as StoreRegistry form './stores/registry';
import * as extraStores from './stores/extraStores';

// Boom! Will register code-splitted stores.
StoreRegistry.register(extraStores);

// Note that we want to register them when the code loads, not when view mounts.
// The view may never mount, but we want them to start listening to actions ASAP.
// Their state is never destroyed (consistent with the code never being unloaded).

Thoughts?

cc @vslinko

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions