Thursday, February 23, 2017

Using React Router and Webpack 2 how to require external libraries only on certain routes?

Leave a Comment

Currently working

Using Webpack 2 and React Router v4 I have been able to set up working code splitting. There is an intermediate <AsyncComponent> that resolves the promise and returns the component (a pattern found on github issues).

An example set of routes below:

<Switch>     <Route          path="me"          render={(props) =>              <AsyncComponent promise={ require.ensure([], (require) => require('./modules/Profile'), 'profile') } props={props} />         }      />     <Route          path="credit-card"          render={(props) =>              <AsyncComponent promise={ require.ensure([], (require) => require('./modules/CreditCard'), 'credit-card') } props={props} />         }      /> </Switch> 

Objective

I would like to extend that further, and for certain routes only, load in additional libraries. In the example above, I would like to fetch the StripeJS (https://js.stripe.com/v2/) library only when on the credit card route.

I want to emphasise that I can load Stripe directly into the footer and all works normally. There are multiple libraries, I'm using Stripe as an easy to digest example.

Attempted

The following has been attempted to little success:

  • Marking the lib external within the webpack config. This (correctly) flags the library as being external and won't attempt to bundle it during the resolve sequence of a require. However the expectation is that the library is manually brought in.
  • I have played with the idea of using a pseudo script loader (when hitting that route, manually create a <script> with a src attribute, wait for it to load, then let the component do its thing. This works ok, but is really horrible from a maintainability (if two or more libraries are needed, I then need to replicate the clumsy manual script load) point of view and seems to work against the webpack "way".

Relevant parts of the config

const core = [     'lodash',     'react',     'react-dom',     'axios',     'react-router-dom', ];  const config = {     context: path.resolve(__dirname, './ts_build'),     node: {         fs: "empty"     },     entry: {         app: './app.js',         core: core,     },     output: {         filename: '[name].js',         chunkFilename: '[name].[id].chunk.js',         path: path.resolve(__dirname, './../../public'),         publicPath: 'http://example.org/',     },     resolve: {         modules: [             path.resolve('./src'),         ],     },     plugins: [         new webpack.optimize.CommonsChunkPlugin({             names: ['core'],              minChunks: Infinity,         }),         new webpack.NamedModulesPlugin(),     ], }; 

2 Answers

Answers 1

If you are using Webpack 2 you use import() in your React Router config file

export default {  component: App,  childRoutes: [    {      path: '/',      getComponent(location, cb) {        import('external-library-here')        .then(function(){          return System.import('pages/Home');        })       .then(loadRoute(cb)).catch(errorLoading);      }    },    {      path: 'blog',      getComponent(location, cb) {        import('pages/Blog').then(loadRoute(cb)).catch(errorLoading);      }    },    {      path: 'about',      getComponent(location, cb) {        import('pages/About').then(loadRoute(cb)).catch(errorLoading);      }    },  ] }; 

You can also use the getComponent or getComponents props in the Router component to pass in modules you want specifically for that route.

Answers 2

There are some cases in this question that should be taken in detail.

Let's start.

Observations

  • You should switch the use of react-router from components to PlainRoute object, this will give you more flexibility when it comes to doing the code splitting, and also skips the creation of <AsyncComponent> component
  • I'm pretty sure you are going to have more nested components within your route, so what if a nested component within credit-card route needs a library?

Suggestions

  • Use your route component as an entry point, so it doesn't become a huge implementation in a single component
  • Given the above suggestion, you can add there an index with your library dependencies and use them within that route (in you nested components)
  • You should not import these libraries in any part of the code otherwise this functionality breaks
  • Once you have loaded these libraries inject them in the window object. and your nested components will able to access globally to your libraries

This is a suggestion that has not been implemented, but I'm trying to take you in the right direction regarding your question

In the end, this is the approach that I have suggested:

import App from 'components/App';  function errorLoading(err) {   console.error('Dynamic page loading failed', err); }  function loadRoute(cb) {   return (module) => cb(null, module.default); }  function injectLibraries(libraries) {   Object.keys(libraries).forEach(key => {     window[key] = libraries[key];   }); }  export default {   component: App,   childRoutes: [     {       path: '/(index.html)',       name: 'home',       getComponent(location, cb) {         const importModules = Promise.all([           import('components/HomePage/'),           import('components/HomePage/libraries'),         ]);          const renderRoute = loadRoute(cb);          importModules.then(([component, libraries]) => {           injectLibraries(libraries);           renderRoute(component);         });          importModules.catch(errorLoading);       },     },     {       path: '/credit-card',       name: 'credit-card',       getComponent(nextState, cb) {         const importModules = Promise.all([           import('components/CreditCardPage/'),           import('components/CreditCardPage/libraries'),         ]);          const renderRoute = loadRoute(cb);          importModules.then(([component, libraries]) => {           injectLibraries(libraries);           renderRoute(component);         });          importModules.catch(errorLoading);       },     },   ], }; 

Your libraries file should look like this:

import stripe from 'stripe';  export default {   stripe  }; 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment