39

I want to be able to bundle my React app with Webpack such that distributed copies put onto a CDN can be sourced, called and initialised with a bunch of config relevant to a client.

After reading this and this, I'm setting up my webpack entry file as follows:

// ... React requires etc.

(() => {
  this.MyApp = (config) => {
    // some constructor code here
  }

  MyApp.prototype.init = () => {
    ReactDOM.render(<MyReactApp config={MyApp.config} />, someSelector);
  }
})();

The idea being that in my client, I can do something like the following:

<script src="./bundle.js" type="text/javascript"></script>
<script type="text/javascript">
  MyApp.init({
    some: "config"
  });
</script>

And my MyApp#init function will render my React app inside some container on the client.

Am I thinking about this in the right way? Is there a simpler or more efficient way to go about this?

My error is Uncaught TypeError: Cannot set property 'MyApp' of undefined, since this inside the IIFE is undefined. I'd really like to understand both why this is happening and advice on how to fix it.

Thanks in advance!

Community
  • 1
  • 1
mattsch
  • 1,454
  • 1
  • 14
  • 18

2 Answers2

47

So I kind of found a solution to this, as described here

If I change my webpack.config.js file to add the following attributes to the output object, i.e.

var config = {
  // ...
  output: {
    // ...
    library: 'MyApp',
    libraryTarget: 'umd',
    umdNamedDefine: true,
  }
}

This specifies the file I'm bundling with webpack as a UMD module, so if I have a function in that file and export it...

export const init = (config) => {
  ReactDOM.render(<MyReactApp config={config} />, someSelector);
}

I can then, in my client, do the following.

<script src="./bundle.js" type="text/javascript"></script>
<script type="text/javascript">
  MyApp.init({
    some: "config"
  }); 
</script>

And my React app renders.

If anyone thinks this is a daft way of doing it, I'd love to hear it!

MORE INFORMATION ON WEBPACK CONFIG

Please bear in mind I haven't touched this code in a while. Given it's Javascript, the world has likely moved on and some practises may be outdated.

This is my React entrypoint file (src/index.js)

import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import Root from './components/Root';
import configureStore from './lib/configureStore';

const store = configureStore();

export const init = (config) => {
  render(
    <Root store={store} config={config} />, 
    document.querySelector(config.selector || "")
  );
}

This is my Webpack config (webpack.config.js)

var webpack = require('webpack');
var path = require('path');
var loaders = require('./webpack.loaders');

module.exports = {
    entry: [
        'webpack-dev-server/client?http://0.0.0.0:8080', // WebpackDevServer host and port
        'webpack/hot/only-dev-server',
        './src/index.js' // Your appʼs entry point
    ],
    devtool: process.env.WEBPACK_DEVTOOL || 'source-map',
    output: {
        path: path.join(__dirname, 'public'),
        filename: 'bundle.js',
        library: 'Foo',
        libraryTarget: 'umd',
        umdNamedDefine: true,
    },
    resolve: {
        extensions: ['', '.js', '.jsx']
    },
    module: {
        loaders: loaders
    },
    devServer: {
        contentBase: "./public",
            noInfo: true, //  --no-info option
            hot: true,
            inline: true
        },
    plugins: [
        new webpack.NoErrorsPlugin()
    ]
};

As you can see, my Webpack config outputs my bundle.js which is what my front-end will ingest.

mattsch
  • 1,454
  • 1
  • 14
  • 18
  • Hi, mattsch. This is a nice solution. The only question that I have is how could I check that widget is integrated on client's website. So let say a have an app and build a widget. I'd check in app that some user (by id or hostname) embedded a widget on their site. – To_wave Oct 09 '17 at 03:53
  • @To_wave I guess as part of an `init` function you could ping a request back to your server with details of the client - that might work? – mattsch Oct 09 '17 at 15:42
  • I thought about that, but that solution doesn't allow me check if client deleted a widget from theirs website. So with callback I could the only check is widget integrated, but not removed – To_wave Oct 09 '17 at 19:39
  • 1
    @mattsch is this still the approach your using ? Any issues you faced? Or is it working beautifully – jasan Feb 17 '18 at 08:36
  • 2
    @jasan we're still using this approach and it's not causing us any issues that we know of. – mattsch Feb 19 '18 at 18:47
  • What happens when I am using some third party elements in my react widget? would it leak style to the parent page where the widget is embedded? – Manan Vaghasiya Feb 23 '18 at 14:00
  • @MananVaghasiya I think it would depend on how you managed your styles. For the most part, we use CSS Modules with React, so "leaking" styles to other components on the page isn't really an issue. – mattsch Feb 28 '18 at 17:04
  • This is probably a silly question. What file are you exporting your `init` func from? – Thomas Valadez Aug 02 '19 at 13:50
  • 1
    @ThomasValadez it's exported in the `bundle.js`. – mattsch Aug 20 '19 at 13:34
  • @mattsch Did you eject from a create-react-app and then add the library/umd attributes to `webpack.config.js`, or did you have a custom webpack config to build your react app to begin with? Also, I'm not understanding where exactly you're exporting your `init` function? Are you adding the init export to the bottom of your bundle.min.js file? or in index.js, where you typically have `ReactDOM.render(, selector);`, are you wrapping the init export around that? Please help, thanks! – Bryan Elliott Aug 22 '19 at 19:54
  • @mattsch in which file did you add the function `export const init = (config)` in? I'm having trouble to make it work, I defined it in the index.js file (my react app entrypoint). do you mind to share you entire webpack config file and the file where you declare the function? My webpack config is: ` output: { library: 'MyApp', libraryTarget: 'umd', umdNamedDefine: true, }, ` `MyApp` is equal to `{}` and it does not have the init function in it. It prints `MyApp.init is not a function` – Rigoni Sep 26 '19 at 01:42
  • To clarify for @ThomasValadez, @Bryan Elliot and @Rigoni, the `init` function is exported by my React entrypoint file (`src/index.js`). Webpack bundles this into my `bundle.js` file which is what I include client-side. I've added the whole webpack config to the post if that helps. – mattsch Oct 02 '19 at 10:50
  • @mattsch, when I try to call, exported init function window.myLibrary.init({selector: '#root'}), render function throws error "Target container is not a DOM element." Any ideas? I confirmed I am loading bundle.js file at last and using init function after dom ready – sanjeev Nov 23 '19 at 12:50
0

You have enclosure around your class.

MyApp needs to be exported or attached to the global window object before you can call it like that.

In your situation you are actually not calling MyApp.init() but you are calling window.MyApp.init(), but global object window does not have object MyApp attached to it.

// ... Simple attaching MyApp to the window (as a function)
window.MyApp = (config) => {
  ...
}

// ... Using class and export
class MyApp {
  constructor(config){...}
}
export default MyApp

// or simply attach to the window
window.MyApp = MyApp

I would prefer to create class and export module using export. Then create another file just for attaching to the window. Since it is not considered best practice to attach classes to the window like that.

// Import module and attach it to the window
import MyApp from '.my-app'
window.MyApp = MyApp

You can look for advanced options of exporting modules as UMD, AMD...

Marcel Mandatory
  • 1,447
  • 13
  • 25