Search code examples
reactjswebpacksinatracode-splitting

Splitting React components in seperate files without explicite import


I am about writing a React app based on Ruby Sinatra backend. A main.js file renders the app:

import React from 'react';
import ReactDOM from 'react-dom';
import Galery from './components/Galery'

ReactDOM.render(
  <Galery />,
  document.getElementById('app')
);

I used to have all my components within one file, but want to split them into seperate files. I only managed to make this run, if I import child components in each parent's component file, like this in Galery.js:

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

class Galery extends Component {
  ...
    <Image ... />
  ...
}

Is it possible to avoid importing the required components explicitly and instead load them within the main.js file? It would also be fine not to import the Component module in each file.

Here is my webpack config:

module.exports = {
  entry: './react/main.js',
  output: {
    path: __dirname,
    filename: './public/bundle.js'
  },
  resolve: {
    root: __dirname,
    alias: {

    },
    extensions: ['', '.js', '.jsx']
  },
  module: {
    loaders: [
      {
        loader: 'babel-loader',
        query: {
          presets: ['react', 'es2015']
        },
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/
      }
    ]
  }
};

Solution

  • Short answer:

    It is highly recommended to use explicit imports wherever you need because tools like webpack do intelligent optimisations around bundle size by removing functions that are not used. Keeping this in mind, the shortest answer is: The way you're using webpack + babel + React, it's not possible to avoid defining imports per file.


    Longer answer:

    Yes, you can do it but it's not straight forward. Unlike in Ruby, the constant/variable look up doesn't work the same way in JavaScript. In Ruby, the following works just fine:

    # a.rb
    A = 10
    
    # b.rb
    require "./a.rb"
    puts a # => 10
    

    This is because when the file a.rb is parsed and included into b.rb, there's no extra sub-namespace created in Ruby. All the top-level units exist as if they were defined inside b.rb. More on this

    To compare this to JS, I need to clear up a little about the way module inclusion works. It's a pretty complicated situation as of now. Let's consider NodeJS first. In this non-browser environment, there is no import functionality implemented yet, except in the bleeding edge versions with extra flags (v 9 as of this day). So when you use something like webpack and import, what internally happens is that these get converted to webpack's own require shim. And the way import and require get converted is subtly different because the former is a "static" style loader, whereas the latter is a dynamic style loader. At a very basic level this means import statements should be at the top of the file, and require statements can be anywhere, and the file resolution happens when the interpreter encounters that line. This has weird effects as you'll see below.

    The way NodeJS require works is by identifying a module.exports object from the included file. That object specifies which functions/objects are exposed outside. Hence, unlike Ruby, there is already an implicit local namespace (or grouping if you prefer) of module.exports, instead of a global $LOADED_FEATURES one:

    // a.js
    const a = 10;
    module.exports = { a: a };
    
    // b.js
    
    const a = require('./a.js');
    console.log(a); // { a: 10 };
    

    One way to hack around this is global variables. Just like Ruby, JavaScript has an implicit global namespace—especially more common in browsers via window, and global in NodeJS. One idea is the following:

    // main.js
    
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    import Image from 'components/image.js';
    import Gallery from 'components/gallery.js';
    
    window.React = React;
    window.ReactDOM = ReactDOM;
    window.Image = Image;
    window.Gallery = Gallery;
    
    // gallery.js
    
    export default class Gallery extends React.Component {
      render() {
        return <Image />;
      }
    }
    

    However, this doesn't work. In order to emulate the actual ES6 import functionality—which is a statically defined set of files and functions, webpack tries to emulate them via webpack's own require function. And this doesn't make the globally attached constants available by the time the imports are done. So to make this work, one work around is to use the older require style loading which changes the way webpack's own require function works. By changing the above to:

    // main.js
    
    const React = require('react');
    const ReactDOM = require('react-dom');
    
    window.React = React;
    window.ReactDOM = ReactDOM;
    
    const Image = require('components/image.js').default;
    const Gallery = require('components/gallery.js').default;
    
    window.Image = Image;
    window.Gallery = Gallery;
    
    // gallery.js
    
    export default class Gallery extends React.Component {
      render() {
        return <Image />;
      }
    }
    

    As you can see, this is way too much work to get a JavaScript application running. But the major problem is that webpack cannot do any intelligent optimisation because it doesn't know which functions you're not using. So it's best to avoid doing all this.