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

How do I set up HMR for a React project using Shakapacker? #92

Closed
jameshibbard opened this issue Mar 30, 2022 · 11 comments
Closed

How do I set up HMR for a React project using Shakapacker? #92

jameshibbard opened this issue Mar 30, 2022 · 11 comments

Comments

@jameshibbard
Copy link
Contributor

jameshibbard commented Mar 30, 2022

Hi,

I'm having trouble setting up a React project with HMR.

I tried following the steps outlined here: https://github.com/shakacode/shakapacker/blob/master/docs/customizing_babel_config.md

Whenever I make a change to a CSS file, HMR happens as expected.

However, when I edit a React component, I see the following error message:

[HMR] Cannot apply update. Need to do a full reload! 
[HMR] Aborted because ./app/javascript/application.js is not accepted
Update propagation: ./app/javascript/application.js
...

What am I doing wrong?

Is it possible to set things up so that when I edit a React component, the changes are reflected on the page without a full refresh and that no warning is shown in the console? I'm using Rails 7 and React 18 (although I was getting the same error under React 17)

Grateful for any help.

Steps to reproduce
  1. Create a new Rails app:
rails new myapp --skip-javascript
cd myapp
bundle add shakapacker --strict
./bin/bundle install
./bin/rails webpacker:install
yarn add react react-dom @babel/preset-react
yarn add css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin
yarn add --dev @pmmmwh/react-refresh-webpack-plugin react-refresh
  1. Generate controller
rails g controller site index
echo '<div id="root"></div>' > app/views/site/index.html.erb
  1. Create CSS file:
touch app/javascript/App.css
  1. app/javascript/application.js:
import React from 'react';
import { createRoot } from 'react-dom/client';
import './App.css';

const container = document.getElementById('root');
const root = createRoot(container);

document.addEventListener('DOMContentLoaded', () => {
  root.render(<p>Hello, World!!</p>);
});
  1. app/javascript/App.css:
p { color: blue; }
  1. Enable HMR in config/webpacker.yml:
hmr: true
  1. Remove the Babel configuration from package.json
  2. Create a babel.config.js file in the root of project and add the sample config.
  3. Start servers:
rails s
./bin/webpacker-dev-server
  1. Hit: http://localhost:3000/site/index
  2. Edit React component in application.js and observe the error in the browser console.
@tomdracz
Copy link
Collaborator

tomdracz commented Mar 30, 2022

Hey @jameshibbard I think there's a piece of the puzzle missing - webpack plugin @pmmmwh/react-refresh-webpack-plugin needs to be applied so you need to customise webpack config also. Docs at https://github.com/shakacode/shakapacker#webpack-configuration talk about the procedure.

You can see how this is applied in the example repo (keep in mind there's few bits of additional config there so you might need to adjust this to your own usecase)
https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/development.js

I think after this it should all work but away from my laptop. Try this out and ping back if it still doesn't play ball and I'll take a closer look

CC @justin808 - possibly one to improve in the docs!

@jameshibbard
Copy link
Contributor Author

Also, I tried creating a custom config and importing that into config/webpack.config.js, but with the same result.

// config/webpack.config.js
const { webpackConfig, merge } = require('shakapacker')
const vueConfig = require('./custom')
module.exports = merge(vueConfig, webpackConfig)

And:

// config/custom.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';

module.exports = {
  mode: isDevelopment ? 'development' : 'production',
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: require.resolve('babel-loader'),
            options: {
              plugins: [isDevelopment && require.resolve('react-refresh/babel')].filter(Boolean),
            },
          },
        ],
      },
    ],
  },
  plugins: [isDevelopment && new ReactRefreshWebpackPlugin()].filter(Boolean),
};

@tomdracz
Copy link
Collaborator

@jameshibbard I think I figured it out! It's because you trying to edit root directly so the hot update bails out. For more info, see issue here and the comment pmmmwh/react-refresh-webpack-plugin#177 (comment)

See the repo here for the implementation https://github.com/tomdracz/test-hmr-app/commits/main

  • First commit is your repro steps
  • Second commit is adding refresh plugin to webpack plugin and installing correct version
  • Third commit moves the inline JSX to a separate component - BINGO! That does it, updating component will cause it to be updated through HMR as expected

@jameshibbard
Copy link
Contributor Author

jameshibbard commented Mar 31, 2022

Awesome! Thank you. That works now.

Would you like me to send a PR updating the React config with more explicit steps?

P.S. Sorry, I deleted my first reply, as I had also (kinda) figured it out and was going to post a more comprehensive update.

@tomdracz
Copy link
Collaborator

@jameshibbard If you can get some updated docs going, it would be awesome! Might not all fit at https://github.com/shakacode/shakapacker/blob/master/docs/customizing_babel_config.md but we can find a good place for this to go when we have a copy!

@jameshibbard
Copy link
Contributor Author

Sure thing. I'll see what I can do.

@jameshibbard
Copy link
Contributor Author

jameshibbard commented Apr 3, 2022

Hi @tomdracz,

As promised I wrote up some basic instructions. Please feel free to take as much, or as little from this as you want (e.g. not sure how much value the basic demo provides).

I wrote this thinking that maybe one could link to it from here https://github.com/shakacode/shakapacker#react ?

Thanks again for the help with my original issue.


Basic Setup

These steps describe how to create a Rails/React app, using Shakapacker as the bundler.

Before starting, ensure that you have Yarn installed:

npm i -g yarn

Create a new Rails app as per the installation instructions in the README.

Add React, as well as the necessary libraries to enable CSS support in your application:

yarn add react react-dom @babel/preset-react
yarn add css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin

Update the Babel configuration in the package.json file:

"babel": {
  "presets": [
    "./node_modules/shakapacker/package/babel/preset.js",
+   "@babel/preset-react"
  ]
},

And that's it. You can now create a React app using app/javascript/application.js as your entry point.

Enabling Hot Module Replacement (HMR)

With HMR enabled, Shakapacker will automatically update only that part of the page that changed when it detects changes in your project files. This has the nice advantage of preserving your app’s state.

To enable HMR in a React app, proceed as follows:.

In config/webpacker.yml set hmr is set to true.

Install the react-refresh package, as well as @pmmmwh/react-refresh-webpack-plugin:

yarn add --dev react-refresh git+https://github.com/pmmmwh/react-refresh-webpack-plugin

Note that this installs react-refresh-webpack-plugin directly from GitHub, as the current release (0.5.4) throws an error with the latest version of React.

Alter config/webpack/webpack.config.js like so:

const { webpackConfig, inliningCss } = require('shakapacker');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';

if (isDevelopment && inliningCss) {
  webpackConfig.plugins.push(
    new ReactRefreshWebpackPlugin({
      overlay: {
        sockPort: webpackConfig.devServer.port,
      },
    })
  );
}

module.exports = webpackConfig;

This applies the plugin to the webpack configuration.

Delete the Babel configuration from package.json:

- "babel": {
-   "presets": [
-     "./node_modules/shakapacker/package/babel/preset.js",
-     "@babel/preset-react"
-   ]
- },

Then create a babel.config.js file in the root of project and add the following:

module.exports = function (api) {
  const defaultConfigFunc = require('shakapacker/package/babel/preset.js')
  const resultConfig = defaultConfigFunc(api)
  const isDevelopmentEnv = api.env('development')
  const isProductionEnv = api.env('production')
  const isTestEnv = api.env('test')

  const changesOnDefault = {
    presets: [
      [
        '@babel/preset-react',
        {
          development: isDevelopmentEnv || isTestEnv,
          useBuiltIns: true
        }
      ]
    ].filter(Boolean),
    plugins: [
      isProductionEnv && ['babel-plugin-transform-react-remove-prop-types',
        {
          removeImport: true
        }
      ],
      process.env.WEBPACK_SERVE && 'react-refresh/babel'
    ].filter(Boolean),
  }

  resultConfig.presets = [...resultConfig.presets, ...changesOnDefault.presets]
  resultConfig.plugins = [...resultConfig.plugins, ...changesOnDefault.plugins ]

  return resultConfig
}

HMR for your React app is now enabled. 🚀

A Basic Demo App

To test that all of the above is working, you can follow these instructions to create a basic React app using Shakapacker.

  1. Create a new Rails app:
rails new myapp --skip-javascript
cd myapp
bundle add shakapacker --strict
./bin/bundle install
./bin/rails webpacker:install
yarn add react react-dom @babel/preset-react
yarn add css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin
  1. Generate a controller
rails g controller site index
echo '<div id="root"></div>' > app/views/site/index.html.erb
  1. Create a CSS file and a React component:
touch app/javascript/App.css app/javascript/App.js
  1. Edit app/javascript/application.js like so:
import React from 'react';
import { createRoot } from 'react-dom/client';
import HelloMessage from './App';

const container = document.getElementById('root');
const root = createRoot(container);

document.addEventListener('DOMContentLoaded', () => {
  root.render(<HelloMessage name="World" />);
});
  1. Add the following to app/javascript/App.js:
import React from 'react';
import 'App.css';
const HelloMessage = ({ name }) => <h1>Hello, {name}!</h1>;
export default HelloMessage;
  1. Add the following to app/javascript/App.css:
h1 { color: blue; }
  1. Enable HMR in config/webpacker.yml:
hmr: true
  1. Install the react-refresh package, as well as @pmmmwh/react-refresh-webpack-plugin:
yarn add --dev react-refresh git+https://github.com/pmmmwh/react-refresh-webpack-plugin
  1. Alter config/webpack/webpack.config.js like so:
const { webpackConfig, inliningCss } = require('shakapacker');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';

if (isDevelopment && inliningCss) {
  webpackConfig.plugins.push(
    new ReactRefreshWebpackPlugin({
      overlay: {
        sockPort: webpackConfig.devServer.port,
      },
    })
  );
}

module.exports = webpackConfig;
  1. Remove the Babel configuration from package.json
- "babel": {
-   "presets": [
-     "./node_modules/shakapacker/package/babel/preset.js"
-   ]
- },
  1. Create a babel.config.js file in the project root and add the following sample code:
module.exports = function (api) {
  const defaultConfigFunc = require('shakapacker/package/babel/preset.js')
  const resultConfig = defaultConfigFunc(api)
  const isDevelopmentEnv = api.env('development')
  const isProductionEnv = api.env('production')
  const isTestEnv = api.env('test')

  const changesOnDefault = {
    presets: [
      [
        '@babel/preset-react',
        {
          development: isDevelopmentEnv || isTestEnv,
          useBuiltIns: true
        }
      ]
    ].filter(Boolean),
    plugins: [
      isProductionEnv && ['babel-plugin-transform-react-remove-prop-types',
        {
          removeImport: true
        }
      ],
      process.env.WEBPACK_SERVE && 'react-refresh/babel'
    ].filter(Boolean),
  }

  resultConfig.presets = [...resultConfig.presets, ...changesOnDefault.presets]
  resultConfig.plugins = [...resultConfig.plugins, ...changesOnDefault.plugins ]

  return resultConfig
}
  1. Start the Rails server and the webpack-dev-server in separate console windows:
rails s
./bin/webpacker-dev-server
  1. Hit: http://localhost:3000/site/index

  2. Edit either the React component at app/javascript/App.js or the CSS file at app/javascript/App.css and observe the HMR goodness.

Note that HMR will not work if you edit app/javascript/application.js and you experience a full refresh with a warning in the console. For more info on this, see here: pmmmwh/react-refresh-webpack-plugin#177

@justin808
Copy link
Member

@tomdracz @Judahmeek we should get this into the docs directory.

@jameshibbard
Copy link
Contributor Author

It is now no longer necessary to install the react-refresh-webpack-plugin directly from GitHub, as they have published a new version (0.5.5) which plays nicely with the latest version of React.

Would you like me to make a PR with the above instructions (updated to include the new version of react-refresh-webpack-plugin)? I'd create a new file under shakapacker/docs/react.md.

@justin808
Copy link
Member

@jameshibbard YES! that would be fabulous!

If you can submit a PR for updating https://github.com/shakacode/react_on_rails_demo_ssr_hmr, that would be great!

@jameshibbard
Copy link
Contributor Author

Here ya go: #110

I'm afraid I'm not familiar with react_on_rails and it seems to have a lot of moving parts, so I'm probably not the best person to update that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants