Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code splitting and v4 #3968

Closed
danturu opened this issue Sep 28, 2016 · 10 comments
Closed

Code splitting and v4 #3968

danturu opened this issue Sep 28, 2016 · 10 comments

Comments

@danturu
Copy link

danturu commented Sep 28, 2016

Today I tried to figure out how to refactor our huge application (~4000 modules) with amazing v4. We follow the fractal project structure and use a webpack's code splitting feature to reduce the initial bundle size. I didn't find a complete v4 example for that case usage, so I implemented it from scratch.

Let's say I have the following hierarchy:

├── src                   
│   ...
│   │
│   ├── routes               
│   │   ├── index.js        
│   │   └── campaigns
│   │       ├── index.js 
│   │       ├── components  
│   │       ├── containers 
│   │       ├── views 
│   │       ├── logic
│   │       └── routes 
│   │           └── reports
│   │               ├── index.js 
│   │               ├── components  
│   │               ├── containers
│   │               ├── views
│   │               └── logic

First of all I imagined how a route definition could look like:

// src/routes/campaigns/index.js

import React from 'react';
import asyncContainer from 'containers/asyncContainer';

const Campaigns = (cb, { injectReducer, injectSaga }) => require.ensure([], (require) => {
  const CampaignsView = require('./views/CampaignsView').default;

  injectReducer(/* require async reducers */);
  injectSaga(/* require async sagas */);

  cb(null, CampaignsView);
});

export default asyncContainer(Campaigns);
// src/routes/apps/views/CampaignsView.js

import React from 'react';
import { Match } from 'react-router';
import Reports from '../routes/templates'; // it won't load until the route matches

const CampaignsView = ({ pathname }) => (
  <div>
    <div>Header</div>
    <Match pattern={`${pathname}/reports`} component={Reports}/> 
  </div>
);

export default CampaignsView;

The src/routes/campaigns/routes/reports/index.js and src/routes/campaigns/routes/reports/view/ReportsView.js components reflect the code above.

Then I created a container in order to load components asynchronously and inject logic on demand:

// asyncContainer.js

import React, { Component, PropTypes } from 'react';

const asyncContainer = (loader) => {
  class AsyncContainer extends Component {
    _isMounted = true;

    state = {
      AsyncComponent: null,
    };

    static contextTypes = {
      store: PropTypes.object.isRequired, // hmm...
    };

    static setDisplayName({ displayName, name }) {
      const asyncComponentName = displayName || name || 'asyncComponent';

      AsyncContainer.displayName = `asyncContainer(${asyncComponentName})`;
    }

    componentDidMount() {
      const { injectSaga, injectReducer } = this.context.store; // hmm...

      loader((_, AsyncComponent) => {
        if (this._isMounted) {
          AsyncContainer.setDisplayName(AsyncComponent);

          this.setState({ AsyncComponent });
        }
     }, { injectSaga, injectReducer });
    }

    componentWillUnmount() {
      this.isMounted = false;
    }

    render() {
      const { AsyncComponent } = this.state;

      if (AsyncComponent) {
        return <AsyncComponent {...this.props} />;
      }

      return <div>Loading...</div>
    }
  }

  AsyncContainer.setDisplayName({});

  return AsyncContainer;
};

export default asyncContainer;

Is the idea correct?

@sheepsteak
Copy link

I've been trying to do exactly the same thing and came up with a similar structure. However, I was trying to use the Webpack bundle-loader. I had the injectReducers and injectSaga code outside of the fractals/modules and each module had getReducers and getSagas functions.

How are you handling certain routes needing the user to be logged in? I ended up making some HOCs similar to your asyncContainer that checked the store to see if they were logged in and then redirected if they weren't.

@danturu
Copy link
Author

danturu commented Sep 28, 2016

I had the injectReducers and injectSaga code outside of the fractals/modules and each module had getReducers and getSagas functions.

What do you mean? Could you please provide a simple example? A store instance is required to inject both reducer and saga, so it could be injectReducer(store, ...) or store.injectReducer(...). Currently (with v2) we pass a store through the routes tree this way:

//  src/routes/campaigns/index.js

export default store => ({
  path: 'campaigns',

  childRoutes: [
    reportRoutes(store), // and later injectReducer(store, ...)
  ],
});

But I really think that react's context is a better option here.

How are you handling certain routes needing the user to be logged in?

At first glance I don't see a problem here. Tomorrow we'll continue experimenting with v4 and protected routes are in the list.

@phyllisstein
Copy link

I wound up slightly modifying the basic idea in https://github.com/jtmthf/react-router-match-async in my repo:

// util/match-async.jsx

import React, {Component, PropTypes} from 'react';
import _ from 'lodash/fp';
import {Match} from 'react-router';

class MatchAsync extends Component {
  static propTypes = {
    getComponent: PropTypes.func.isRequired
  }

  displayName = 'MatchAsync';

  state = {
    RouteComponent: null
  };

  componentWillMount() {
    this.getComponent();
  }

  shouldComponentUpdate(nextProps, nextState) {
    const {RouteComponent: lastRouteComponent} = this.state;
    const {RouteComponent: nextRouteComponent} = nextState;

    if (!lastRouteComponent && nextRouteComponent) {
      return true;
    }

    return false;
  }

  getComponent = () => {
    const {getComponent} = this.props;
    const maybePromise = getComponent();
    if (_.isFunction(maybePromise.then)) {
      maybePromise.then(RouteComponent => this.setState({RouteComponent}));
    }
  }

  render() {
    const {RouteComponent} = this.state;
    const matchProps = _.omit(['getComponent'])(this.props);

    return (
      <Match {...matchProps} render={props => RouteComponent ? <RouteComponent {...props} /> : null} />
    );
  }
}

export default MatchAsync;

Then in the views...

<MatchAsync getComponent={async () => await System.import('./login')} pattern="/login" />

Login redirects and other jazz can be handled in the same way the documentation currently recommends.

@sheepsteak
Copy link

sheepsteak commented Sep 29, 2016

@rosendi, something roughly like this:

'routes/index.js'

const bundleCache = {};

// This gets called and passed into a `Loader` component similar to your `asyncContainer`.
function loadBundle(bundle, store) {
    return new Promise((resolve, reject) => {
        // Early exit if we've loaded this bundle before. Prevents sagas being added multiple times 
        if (bundleCache[bundle]) {
            return resolve(bundleCache[bundle]);
        }

        try {
      // Use the `bundle-loader` to automatically get all matching files and their imports
            require('bundle!./' + bundle + '/entry')(c => {
                setupBundle(c, store);

                // Add to cache for next time
                bundleCache[bundle] = c.default;

                resolve(c.default);
            });
        } catch (err) {
            reject(err);
        }
    });
}

// Asks each bundle for its reducers and sagas
function setupBundle(bundle, store) {
    if (bundle.getReducers) {
        const newReducers = bundle.getReducers();
        injectAsyncReducers(store, newReducers);
    }

    if (bundle.getSagas) {
        const newSagas = bundle.getSagas();
        injectAsyncSagas(newSagas);
    }
}

// flow is from 'lodash/fp/flow'
const asyncAuthRoute = flow(loadBundle, loader, requireAuth); <-- auth is done here rather than in module/fractal
export const Routes = ({ store }) => (
  <Match pattern="/some-url" component={asyncAuthRoute('some-bundle', store)} />
);

Then one of the bundles might look like this:

'routes/some-bundle/entry.js'

import reducers from './reducers';
import { watchSagaOne, watchSagaTwo } from './sagas';

export default from './components/some-bundle'; <-- Main page. This would typically be a component with more `<Match>` components in it

export function getReducers() {
    return {
        someReducer: reducers
    };
}

export function getSagas() {
    return [
        watchSagaOne,
        watchSagaTwo
    ];
}

The benefit of the bundle-loader is that you don't need lots of require.ensure calls everywhere since it does them for you.

I'm still working out if this is the best route to take or not.

@gabrielbull
Copy link

These solutions won't work with server side rendering.

@sheepsteak
Copy link

@ryanflorence
Copy link
Member

This is no longer in scope of this project, but if it should be possible and I'm sure people will figure out some great API that works with the router. As some patterns emerge, we should link to them from the docs, and maybe hint at some strategies.

@gabrielbull
Copy link

Here's the solution I built for those interested. It works with server side rendering too.

@pke
Copy link

pke commented Mar 10, 2017

@sheepsteak I wonder how would injectReducers and injectSagas look like?

@sheepsteak
Copy link

@pke there's a full example in react-boilerplate and you can see how they use it and set it up too.

@lock lock bot locked as resolved and limited conversation to collaborators Jan 19, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants