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

Hot ejected CRA not working (followed recipe and generic setup) #1440

Closed
preciselywilliam opened this issue Apr 20, 2020 · 18 comments
Closed

Comments

@preciselywilliam
Copy link

preciselywilliam commented Apr 20, 2020

Description

I have a project based on CRA which is ejected. Hot-reloading reducers and sagas works perfectly fine (with standard if(module.hot) module.hot.accept(...) snippets). However, I cannot seem to get hot reloading of the react App to work. I've tried both accepting changes on App.js and also implementing RHL according to the README.

Expected behavior

I expect the hot reloading to catch changes when I modify the code.

Actual behavior

Nothing is triggered, and the page reloads as if no hot reloading was implemented.

Environment

From package.json:

"react-hot-loader": "^4.12.20",
"react-dom": "npm:@hot-loader/react-dom", (installed with yarn name resolution as per instructions)
"@hot-loader/react-dom": "^16.13.0",
 "react": "16.8.6",
"webpack": "3.8.1",
"webpack-dev-server": "2.11.3",

(- "react-dom": "16.8.4", removed)
Node version: v10.0.0
Npm version: 5.6.0
Operating system: macOS
Browser: Chrome 80.0.3987.163 (Official Build) (64-bit)

Relevant code/settings:
These are the only changes I've made to the default CRA settings:

App.js

+import { hot } from 'react-hot-loader/root';

...

const withDragAndDrop = DragDropContext(HTML5Backend);
const withState = connect(createStructuredSelector({...}));
export default hot(withDragAndDrop(withState(App)));

webpack.config.dev.js

entry: [
    // We ship a few polyfills by default:
    require.resolve('./polyfills'),
    // Include an alternative client for WebpackDevServer. A client's job is to
    // connect to WebpackDevServer by a socket and get notified about changes.
    // When you save a file, the client will either apply hot updates (in case
    // of CSS changes), or refresh the page (in case of JS changes). When you
    // make a syntax error, this client will display a syntax error overlay.
    // Note: instead of the default WebpackDevServer client, we use a custom one
    // to bring better experience for Create React App users. You can replace
    // the line below with these two lines if you prefer the stock client:
    // require.resolve('webpack-dev-server/client') + '?/',
    // require.resolve('webpack/hot/dev-server'),
    require.resolve('react-dev-utils/webpackHotDevClient'),
		'react-hot-loader/patch',
		// Finally, this is your app's code:
    paths.appIndexJs,
    // We include the app code last so that if there is a runtime error during
    // initialization, it doesn't blow up the WebpackDevServer client, and
    // changing JS code would still trigger a refresh.

...

  module: {
    strictExportPresence: true,
    rules: [
      {
        // "oneOf" will traverse all following loaders until one will
        // match the requirements. When no loader matches it will fall
        // back to the "file" loader at the end of the loader list.
        oneOf: [
          // "url" loader works like "file" loader except that it embeds assets
          // smaller than specified limit in bytes as data URLs to avoid requests.
          // A missing `test` is equivalent to a match.
          {
            test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            loader: require.resolve('url-loader'),
            options: {
              limit: 10000,
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
          // Process JS with Babel.
          {
            test: /\.(js|jsx|mjs)$/,
            include: paths.appSrc,
            loader: require.resolve('babel-loader'),
            options: {

              // This is a feature of `babel-loader` for webpack (not Babel itself).
              // It enables caching results in ./node_modules/.cache/babel-loader/
              // directory for faster rebuilds.
              cacheDirectory: true,
							plugins: ['react-hot-loader/babel'],
            },
					},
          // "postcss" loader applies autoprefixer to our CSS.
          // "css" loader resolves paths in CSS and adds assets as dependencies.
          // "style" loader turns CSS into JS modules that inject <style> tags.
          // In production, we use a plugin to extract that CSS to a file, but
          // in development "style" loader enables hot editing of CSS.
          {
            test: /\.css$/,
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  importLoaders: 1,
                },
              },
              {
                loader: require.resolve('postcss-loader'),
                options: {
                  // Necessary for external CSS imports to work
                  // https://github.com/facebookincubator/create-react-app/issues/2677
                  ident: 'postcss',
                  plugins: () => [
                    require('postcss-flexbugs-fixes'),
                    autoprefixer({
                      browsers: [
                        '>1%',
                        'last 4 versions',
                        'Firefox ESR',
                        'not ie < 9', // React doesn't support IE8 anyway
                      ],
                      flexbox: 'no-2009',
                    }),
                  ],
                },
              },
            ],
          },
          // "file" loader makes sure those assets get served by WebpackDevServer.
          // When you `import` an asset, you get its (virtual) filename.
          // In production, they would get copied to the `build` folder.
          // This loader doesn't use a "test" so it will catch all modules
          // that fall through the other loaders.
          {
            // Exclude `js` files to keep "css" loader working as it injects
            // its runtime that would otherwise processed through "file" loader.
            // Also exclude `html` and `json` extensions so they get processed
            // by webpacks internal loaders.
            exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
            loader: require.resolve('file-loader'),
            options: {
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
        ],
      },
      // ** STOP ** Are you adding a new loader?
      // Make sure to add the new loader(s) before the "file" loader.
    ],
  },

Any ideas om what could be wrong with my setup?

@theKashey
Copy link
Collaborator

And how it is not working?

@preciselywilliam
Copy link
Author

And how it is not working?

I worked on it a bit more and realized the hot reloading works for the App component itself, but not for pages rendered by it. Yesterday I thought nothing worked (since I was changing code in one of the pages to test).

Is this expected behaviour? I lazy import my app and all pages.

My App is rendered as such, where Router renders a Switch with all routes. App is hot exported.

<App>
  <Router />
</App>

@theKashey
Copy link
Collaborator

It depends on how you lazy load. If you use React.lazy - it should work. If any other solution - it might not - loader should "know" about React-Hot-Loader.

@preciselywilliam
Copy link
Author

It depends on how you lazy load. If you use React.lazy - it should work. If any other solution - it might not - loader should "know" about React-Hot-Loader.

I use react.lazy with a retry function

import { lazy } from 'react';
lazyWithRetry: importFunction => lazy(() => retry(importFunction)),

@theKashey
Copy link
Collaborator

🤷‍♂️should work. Please provide source code for retry

@preciselywilliam
Copy link
Author

preciselywilliam commented Apr 21, 2020

🤷‍♂️should work. Please provide source code for retry

I just tried changing it to lazyWithRetry: lazy,, i.e. use react.lazy out of the box. Same result.

I tried wrapping my page's export in hot, and hot reloading works just fine then.

Could the problem of hot not being propagated down from App to pages be related to the way/order I render App and Routes?

index.js

import React, { Suspense } from 'react';
...
import { reactUtils } from '../utils/react'; // lazy with retry helper
...
import { Router } from './components/Router'; // route Switch
...
const App = reactUtils.lazyWithRetry(() => import('./components/App'));
const store = configureStore();
ReactDOM.render(
...
								<App>
									<Router />
								</App>
...
);

Router is being passed down as children and App is lazy imported after Router. In App I instanciate hot from 'react-hot-loader/root' for the first time.

Should that make a difference or do you have other ideas? Thanks so much for the help so far by the way @theKashey

edit:
Just tried wrapping the Router export in hot, doesn't work either.

@theKashey
Copy link
Collaborator

Dam, I was going to say that you have to wrap Router, not App. So the rule is simple - wrap "where update is going out", so you HAVE to to wrap it in any case...
However, if it will be a case - you will get a full page update from a "missed" update. But that's not what's happening... And this is VERY VERY strange.

@preciselywilliam
Copy link
Author

Dam, I was going to say that you have to wrap Router, not App. So the rule is simple - wrap "where update is going out", so you HAVE to to wrap it in any case...
However, if it will be a case - you will get a full page update from a "missed" update. But that's not what's happening... And this is VERY VERY strange.

I'll try some other tweaks when Router is wrapped later today, might get it to work. Otherwise I guess I'll just have wrap each page component - a bit tedious but gets the job done.

@theKashey
Copy link
Collaborator

Have fun 😅. Sometimes this is the only advice I could provide.

@preciselywilliam
Copy link
Author

@theKashey do you think it is problematic to store routes in an array where each route object looks like below?

	SearchRoute: {
		getPath: () => '/search',
		component: reactUtils.lazyWithRetry(() => import('../pages/SearchPage')),
	},

it is then mapped out in Router as below

			<Switch location={location}>
				{routeMatchOrder.map(({
					isExact,
					route,
				}) => {
					const path = route.getPath();

					return (
						<Route
							key={path}
							exact={isExact}
							location={location}
							path={path}
							render={() => {

								return <route.component />;
							}}
						/>
					);
				})}
			</Switch>

@preciselywilliam
Copy link
Author

preciselywilliam commented Apr 21, 2020

Testing more and it seems that components directly rendered by App (which is wrapped in hot) don't hot reload. Hot reloading app itself works, however.

Also, when wrapping a component that isn't App (e.g. a page) with hot, the child-components hot reload correctly.

Could it be that I need to change from import on */root to the index file? Or that I should use hot(module) instead of just hot()?

Edit:
I've also tried importing below in my index.js file (where I run ReactDOM.render), doesn't change things.

import 'react-hot-loader';
import 'react-hot-loader/patch';

@preciselywilliam
Copy link
Author

I've also tried wrapping my routes (when I map them out in my Switch) in my own HotProvider.

	<Switch location={location}>
				{routeMatchOrder.map(({
					isExact,
					route,
				}) => {
					const path = route.getPath();

					return (
						<Route
							key={path}
							exact={isExact}
							location={location}
							path={path}
							render={() => {
								return (
									<HotProvider>
										<route.component />
									</HotProvider>
								);
							}}
						/>
					);
				})}
			</Switch>

HotProvider.js:

import { hot } from 'react-hot-loader/root';

const HotProvider = ({ children }) => children;

export default hot(HotProvider);

Doesn't work either, I need to wrap each page file where it's exported it seems.

@theKashey
Copy link
Collaborator

I think if reactUtils.lazyWithRetry(() => import('../pages/SearchPage') would be exported as a top level variable, then magic will happen.

const searchPage = reactUtils.lazyWithRetry(() => import('../pages/SearchPage'))

@preciselywilliam
Copy link
Author

preciselywilliam commented Apr 22, 2020

Tried it now and it doesn't seem to work. (Unless I misunderstood). See below.

import { hot } from 'react-hot-loader/root';
import React, { Component } from 'react';
import {
	Route,
	Switch,
} from 'react-router-dom';
import { TopLevelPage, routeMatchOrder } from '../../services/routes';

class RouterComponent extends Component {
	static propTypes = {
		location: PropTypes.object.isRequired,
	};

	render() {
		const { location } = this.props;

		return (
			<Switch location={location}>
				{routeMatchOrder.map(({
					isExact,
					route,
				}) => {
					const path = route.getPath();

					return (
						<Route
							key={path}
							exact={isExact}
							location={location}
							path={path}
							render={() => {
								return <TopLevelPage />;
							}}
						/>
					);
				})}
			</Switch>
		);
	}
}

const mapStateToProps = createStructuredSelector({
	location: getLocation,
});

export const Router = hot(connect(mapStateToProps)(RouterComponent));

TopLevelPage export:

export const TopLevelPage = lazy(() => import('../pages/SearchPage'));

@preciselywilliam
Copy link
Author

Another question if you have the time, what about looking at fast refresh? Seems to be some experimental support even though the README says to use react-hot-loader:
https://github.com/WebHotelier/webpack-fast-refresh
https://dutzi.party/react-fast-refresh/

@theKashey

@theKashey
Copy link
Collaborator

React-Hot-Loader and fast refresh are roughly the same things nowadays, except the module update handling - fast refresh(actually a webpack plugin, which is not a part of fast refresh) is going to catch the update on the module boundary, not on the custom placed hot, so it shall not experience problem with not updated lazy.

Problem with lazy is simple - something has to execute import, so the module behind would be updated in real.

However, I've just remembered one thing which might help you - configuration.trackTailUpdates option - https://github.com/gaearon/react-hot-loader/blob/54e796e22193b3375223f08fb690cad2f092049e/README.md#out-of-bound-warning

import {setConfig} from 'react-hot-loader';
setConfig({ trackTailUpdates:false })

I shall fix your problem removing some automagic, and making lazy update a bit more explicit.

@theKashey
Copy link
Collaborator

Probably related to #1425

@preciselywilliam
Copy link
Author

I managed to get it to work. The problem was that my routes were (lazy) imported in another file (than my router), e.g:

old Router.js:

// import routeMatchOrder from routes.js (which contains objects with lazy imported components)
// return jsx (map out routeMatchOrder)

new Router.js:

// define routeMatchOrder (which contains objects with lazy imported components)
// return jsx (map out routeMatchOrder)

Top level exports or not (in routes.js) did not make a difference.

@theKashey thanks a lot, your tips were of great help when debugging

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

No branches or pull requests

2 participants