Reload the page gets 404 error using React Router - javascript

I have...
Back-End
Node.js/express server which serves files when requests are made to certain routes.
Front-End
React pages that makes calls to the back-end requesting data to populate the pages.
I'm using react-router v4 on the front end. Whenever I navigate to a route that is NOT at the exact path AND reload the page, I get a 404 error. I understand why this isn't working; the browser makes a request to a path handled by react-router, and since it doesn't find that route on the server, I get 404.
I'm seeking for a solution to this problem.
// BrowserRouter imported as Router
<Router>
<Route exact path='/main' component={Main} />
<Route path='/sub1' component={SubOne} />
<Route path='/sub2' component={SubTwo} />
</Router>
When I go to /main in the browser, <Main /> is rendered. Say that inside <Main />, there are links to /sub1 and /sub2 respectively. I click on /sub2. Component and page content renders without fail. Then I refresh page, either by accident or intentionally (say component Sub2 lifts state up to Main).
How do I avoid getting 404 after this refresh? How do I get the page/component where "I was" rendered after a refresh if I'm using React-Router?

I had the same issue you're having about 2 months ago. I had very little knowledge about server-side rendering with React. I got confused on the general concept of it. However, I didn't want to use the create-react-app cli. I wanted to use my own boilerplate. Upon doing research, I found out that I had to configure my webpack to handle my 404 reloading fallbacks.
Here is my current webpack setup:
Please note, only pay attention to the historyApiFallback: true that allows you to refresh your page without throwing a 404 error if you're using v4. In addition, i forgot to mention that this requires webpack-dev-server to work.
const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const HtmlWebPackPlugin = require('html-webpack-plugin');
var browserConfig = {
devServer: {
historyApiFallback: true,
proxy: {
"/api": "http://localhost:3012"
}
},
entry: ['babel-polyfill', __dirname + '/src/index.js'],
output: {
path: path.resolve(__dirname + '/public'),
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
query: {
presets: ['react', 'env', 'stage-0']
}
}
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: './public/index.html',
})
]
}
var serverConfig = {
target: 'node',
externals: [nodeExternals()],
entry: __dirname + '/server/main.js',
output: {
path: path.resolve(__dirname + '/public'),
filename: 'server.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
query: {
presets: ['react', 'env', 'stage-0']
}
}
}
]
}
}
module.exports = [browserConfig, serverConfig]

The reason you got 404 is refreshing the page poses network roundtrip to hit the server, while react-router is a client-side routing library, it doesn't handle server-side routing.
when the user hits the refresh button or is directly accessing a page other than the landing page, e.g. /help or /help/online as the web server bypasses the index file to locate the file at this location. As your application is a SPA, the web server will fail trying to retrieve the file and return a 404 - Not Found message to the user.
Since you're using an Express server, connect-history-api-fallback(Express middleware) can be used to proxy all received requests(including unknown ones, /sub2 in your case) through your index page. Then reboot your SPA and route to /sub2 on the client by react router.
If you're using webpack-dev-server for local development, it will be as simple as turning on devServer.historyApiFallback.

my solution is using content-history-api-fallback module
first: import history from 'connect-history-api-fallback'
then:
app.use(history({
rewrites:[
{from: /^\/api\/.*$/, to: function(context){
return context.parsedUrl.pathname;
}},
{from: /\/.*/, to: '/'}
]
}))
app.get('/', function(req, res, next){
res.render('index');
})
app.use('/api', api);

Use the "Redirect" directive to map your sub paths to actual paths
https://reacttraining.com/react-router/web/example/no-match

Just add the following "function" to your server application's app.js file:
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname+'/client/build/index.html'));
});
Replace the "client" with the name of the directory of you client application. This will act as a "catchball" for any requests that don't match the ones that you have defined. It worked for me.
For more info please watch the following tutorial: https://www.youtube.com/watch?v=xwovj4-eM3Y

Related

ChunkLoadError: Loading chunk register failed

I am currently trying to implement lazy loading of routes in VueJS3 and Laravel. I'm using vue-router to implement my routes.
Unfortunately, whenever I click on a link, nothing happens and after 30 seconds, I see a timeout error pop up in my browser console. If I replace lazy loading of the routes with a direct import, everything runs fine.
I would appreciate any hints as to what might be going on here. I've had the problem for a couple of weeks now. All other posts and suggestions to clear browser cache or set the public path in webpack.config.js have not worked for me. I have also replaced laravel-mix with webpack and the problem remains.
Pre-conditions:
Webpack 5.69.1
Webpack-cli ^4.9.2
Vue ^3.2.31
Vue-router ^4.0.12
Laravel 7.2
Steps to reproduce
Run npm run production
Run php artisan serve
Open browser
Enter desired URL in browser: http://127.0.0.1:8000/login
Click on "Register" link after login page opens.
Expected behavior
Npm runs successfully manifest.json is updated. Webpack generates all chunks in dists folder.
Laravel server starts
Website is reacheable over http://127.0.0.1:8000
Login page opens component configured in the route. Login.js is downloaded by the browser.
Browser downloads register.js dynamically. Registration page is displayed
Observed behavior
Npm runs successfully manifest.json is updated. Webpack generates all chunks in dists folder.
Laravel server starts
Website is reacheable over http://127.0.0.1:8000
Login page opens component configured in the route. Login.js is downloaded by the browser.
register.js chunk is never downloaded. Registration page is never displayed. After 30 seconds timeout, error is printed in browser console.
ChunkLoadError: Loading chunk register failed.
(timeout: http://127.0.0.1:8000/dist/register.d3e633a9a1aea3ebf47b.js)
at Object.__webpack_require__.f.j (main.34a1a3da92d476b41479.js:4051:29)
at main.34a1a3da92d476b41479.js:3905:40
at Array.reduce (<anonymous>)
at Function.__webpack_require__.e (main.34a1a3da92d476b41479.js:3904:67)
at component (routes.js:35:55)
at extractComponentsGuards (vue-router.esm-bundler.js:2037:40)
at eval (vue-router.esm-bundler.js:3156:22)
webpack.config.js
const path = require('path');
const {VueLoaderPlugin} = require('vue-loader');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const options = {basePath:'/dist/',fileName:'../mix-manifest.json',publicPath:'dist/'};
const webpack = require('webpack');
module.exports = {
mode: 'development',
entry: './resources/js/main.js',
output: {
clean: true,
filename: "[name].[chunkhash].js",
publicPath: './dist/',
path: path.resolve(__dirname, 'public/dist'),
chunkLoadTimeout: 30000,
},
resolve:{
alias: {
'#': path.resolve(__dirname,'resources/js'),
},
extensions: ['.js', '.vue', '.json']
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
}
]
},
plugins: [
new VueLoaderPlugin(),
new WebpackManifestPlugin(options),
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: false,
__VUE_PROD_DEVTOOLS__: false,
}),
]
};
routes.js
const routes = [
{
path: "/",
component: DefaultLayout,
children: [
{
path: "/publicidade",
name: "publicidade",
component: () => import (/* webpackChunkName: "publicity" */ '../pages/Publicidade')
},
{
path: "/login",
name: "login",
component: () => import(/* webpackChunkName: "login" */ '../pages/login.vue')
},
{
path: "/cadastro",
name: "cadastro",
component: () => import(/* webpackChunkName: "register" , webpackPrefetch: true */ '../pages/Register.vue')
},
{
path: "perfil",
name: "perfil",
component: () => import('../pages/Profile')
},
],
},
{ path: "/:catchAll(.*)", component: NotFoundPage }
];
I had the same problem in vue, I solved it using a method similar to the method Polymer uses to import the lazy loading resources.
Example:
setTimeout(() => {
for (let i in routes[0].children ){
routes[0].children[i].component();
}
}, 1000)
After a couple of weeks of head banging I was able to locate the problem by stepping through the Webpack code starting at the Webpack.require call (See my error log above).
My issue was that my chunks were being blocked by the Osano GPDR Consent service, which I use in my website to satisfy EU privacy laws.
As soon as I removed the osano script tag from the header of my site, everything just worked.
<script src="https://cmp.osano.com/AzZcqMSBLrjhQ2PZ1/4e3118ce-1798-4f27-93be-f6c9e99f1537/osano.js"></script>
Since GPDR Consent is a must here in Europe, I had to add a rule to the Dashboard of my Osano account in order to allow scripts from 127.0.0.1:8080 domain.

multiple pages in Vue.js CLI

I'm having trouble figuring out how to have multiple pages in a Vue CLI project. Right now I have my home page with a few components and I want to create another page but I do not know how to do that. Am I supposed to create multiple html files where the index.html by default is? In a simple file structure with css js img folder and html files as pages I know that creating another html file means making another page. But I don't understand how this works with Vue CLI project.
I saw stuff like vue-router and "pages" in Vue documentation but I do not understand them very well. What are my alternatives? Is there a guide that explains that in detail, because I wasn't able to find any, let alone detailed. Would be very happy if you could help! Thank you!
First: always read the official documentation. With Vue you can build a SPA, and a MPA is also no problem. Just follow the guides:
https://cli.vuejs.org/
https://cli.vuejs.org/guide/html-and-static-assets.html#building-a-multi-page-app
https://cli.vuejs.org/config/#pages
You should create a new project with Vue CLI 3. Once you've created your project set it to be manually configured. Make sure you don't choose the SPA option. Vue will then create a nice "start" project using a MPA approach. After that, just repeat the config on vue.config.js.
Updated #1
It seems that some updates on Vue Cli, have changed the way to build a MPA app, so:
Create a new application vue create test
Choose Manual configuration
The boilerplate created will be for a SPA. So make the following changes:
Create a folder under src named pages (optional)
Into this folder create your own pages: Home, About, etc.
Copy and paste the App.vue and main.js from src, into your new folders - Home, etc.
Format the App.vue into this folders, to your liking.
Create a vue.config.js and set it like this: https://cli.vuejs.org/config/#pages
Below, I have three images demonstrating this:
First: a fresh new app
Second: this same app, with the changes I made above
Third: the vue.config.js from this app
You don't need to create the pages folder, this is just to get the idea.
Link to GitHub: Building a MPA App
EDIT: Vue has this built-in. Skip to the bottom for more.
Original answer:
There are two ways to interpret your question, and therefore to answer it.
The first interpretation is: "how can I support routing to different pages within the same single-page app, e.g. localhost:8080/about and localhost:8080/report etc?". The answer to this is to use the router. It's reasonably straightforward and works well.
The second interpretation is: "my app is complex, and I have multiple single-page applications, e.g. one app for the 'website' part, one app for consumers to log in and do work, one app for admins, etc - how can vue do this, without making three entirely separate repositories?"
The answer to the latter is a single repository with multiple single-page apps. This demo looks like exactly what you're after:
https://github.com/Plortinus/vue-multiple-pages/
Look in particular at: https://github.com/Plortinus/vue-multiple-pages/blob/master/vue.config.js
Updated answer:
It turns out that vuejs has the idea of multiple top-level pages built-in. I mean, it makes sense - it's going to be really common, despite what many incorrect answers are saying about "no, it's for single page apps"!
You want the pages option in the vue.config.js file:
https://cli.vuejs.org/config/#pages
If your project doesn't have that file in the root directory, create it and vuejs will discover it.
There is a long and a short way to define each page. I used the short form here:
module.exports = {
pages: {
index: 'src/pages/index/main.ts',
experiment: 'src/pages/experiment/main.ts'
}
}
You don't have to put your work under "pages". It could be "/src/apps/index/index.ts" or whatever.
After moving code around and changing some imports from:
import HelloWorld from './components/HelloWorld'
to
import HelloWorld from '#/components/HelloWorld'
The app works - but the "experiment" app in my repo had to be loaded like this:
http://localhost:8080/experiment.html
Pretty ugly, and even worse because it uses the router which resulted in URLs like:
http://localhost:8080/experiment.html/about
Ugh.
Fortunately, this stackoverflow answer solved it. Update the vue.config.js file to include devServer options (make sure this is at the top level of the exported object:
devServer: {
historyApiFallback: {
rewrites: [
{ from: /\/index/, to: '/index.html' },
{ from: /\/experiment/, to: '/experiment.html' }
]
}
}
Then also modify the router.ts file to append the extra path (in my case "experiment/":
export default new Router({
mode: 'history',
base: process.env.BASE_URL + 'experiment/',
...
Then URLs resolve nicely, e.g.: http://localhost:8080/experiment/about
This may not be relevant to the question, but bear with me, maybe my answer can help someone.
I use webpack+vue, and I have figured out how to build multiple pages applications. Here my webpack.config.js:
const path = require('path');
const fs = require('fs')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
entry: {
app: './src/app.js',
mgmt: ['./src/modules/mgmt/mgmt.js'],
login: './src/modules/login/login.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
// publicPath: '/ahezime/',
filename: (chunkData) => {
console.log('chuckData.chunk.name => ', chunkData.chunk.name)
return chunkData.chunk.name === 'app' ? './[name].bundle.js' : './[name]/[name].bundle.js';
}
},
optimization: {
minimizer: [
new TerserPlugin(),
new OptimizeCSSAssetsPlugin({})
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
}),
new CleanWebpackPlugin(['dist']),
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
title: 'app',
template: './src/app.html',
// inject: false,
chunks: ['app'],
filename: './index.html'
}),
new HtmlWebpackPlugin({
title: 'mgmt',
template: './src/modules/mgmt/mgmt.html',
// inject: false,
chunks: ['mgmt'],
filename: './mgmt/index.html'
}),
new HtmlWebpackPlugin({
title: 'login',
template: './src/modules/login/login.html',
// inject: false,
chunks: ['login'],
filename: './login/index.html'
})
],
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['#babel/preset-env'],
plugins: ['#babel/plugin-proposal-object-rest-spread']
}
}
}
],
rules: [
{
test: /\.vue$/,
exclude: /node_modules/,
loader: 'vue-loader'
},
{
test: /\.css$/,
use: [
'vue-style-loader',
'style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.scss?$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
}
]
}
};
And here's my directory structure:
https://i.stack.imgur.com/uFvKx.png
And you can jump pages:
<template>
<div>
<h1>App</h1>
<div>
Please click me, and let take you into the login page!!!
</div>
<span>Before computed: {{ message }} </span>
<br>
<span>Afer computed: {{ computedMessage() }} </span>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello World!'
}
},
computed: {
reversedMessage: function() {
return this.message.split('').reverse().join('')
}
},
methods: {
computedMessage: function() {
return this.message.split('').reverse().join('')
}
}
}
</script>
Note pointing users to what should be the accepted answer
At the moment of posting my initial answer I wasn't aware of the possibility of actually building MPAs in VueJS. My answer doesn't address the question asked therefore I will recommend to take a look at the answer provided by PJ.Wanderson bellow which should be the accepted answer
Inital Answer
Vue.js projects are SPAs(single page applications). You only have one .html file in the entire project which is the index.html file you mentioned. The "pages" you want to create, in vue.js are referred to as components. They will be plugged into the index.html file and rendered in the browser. A vue.js component comprises 3 parts:
<template>
</template>
<script>
export default {
}
</script>
<style>
</style>
Template: it contains all the html your page should display (this is where you put the html of your pages)
Script: it contains all JavaScript code that will be executed on the page/component
Style: it contains the CSS that will style that specific component/page
You can check this tutorial out for a quick-start Vue.js 2 Quickstart Tutorial 2017
It explains vue.js project structure and how the various files relate to each other

express-handlebars & webpack - can't export partials and helpers in multiple paged app

I am using handlebars for my templating needs. I obviously need to render some templates on the server to handle building standard layout (with header, footer, navbar and content panel etc). But I also require some client side templating so that I can use ajax to fetch data and then render it in templates.
I chose the express-handlebars module in node because it automatically compiles templates and layouts in the server.
Several articles suggested that I can configure webpack to export my templates, partials and helper to the client so I can reuse them.
So I created the following webpack config:
et path = require('path');
module.exports = {
entry: {
login: './public/js/users/login.index.js',
logout: './public/js/users/logout.index.js',
newListing: './public/js/listing/newListing.index.js',
listingCollection: './public/js/listing/newListing.index.js',
listingDetails: './public/js/listing/listingDetails.index.js',
userRegistration: './public/js/listing/newListing.index.js',
},
output: {
path: path.join(__dirname, '/public/js/bundles'),
publicPath: "/public/",
filename: "[name].bundle.js"
},
module: {
rules: [
{
test: /\.handlebars$/,
include: [
path.resolve(__dirname, 'views/listing')
],
loader: "handlebars-loader"
}
]
}
};
I don't want a single paged app so I have configured to export just the necessary .js file bundled with each page.
My app.js fragment responsible for the express-handlebars config is as follows:
const exphbs = require('express-handlebars');
const routes = require('./app/routes');
const app = express();
const hbs = exphbs.create({
defaultLayout: 'main',
partialsDir: [
'views/partials',
'views/partials/common',
'views/partials/listing/paymentsAndExchange'
],
helpers: {
isListingType: function (v1, v2, options) {
if (v1 === v2) {
return options.fn(this);
}
}
}
});
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');
My problem is that none of these things are exported to the client/browser (partials and helpers - I have examined the exported bundles and they are definitely not available).
Eventually I gave up trying to get this to work and resorted to just recreating the partials in the client by writing the markup inside script tag as follows:
<script id="a-template" type="text/x-handlebars-template">
{{#unless somecondition}}
Say this
{{/unless}}
</script>
And then I get a reference to this script and compile the template and inject data etc as per the usual method.
But...this is not possible because the server side template compilation parses this script as though it was part of the page view and by the time its rendered in the client I have lost all of the handlebars notation {{}}.
Ideally I just want to be able to configure express-handlebars / webpack combination to export the correct partials and helpers with each page so that I can just use them.
Lots and lots of people must have struggled with this exact use case because handlebars is very popular and I know it isnt just supposed to be used in SPA's.
Id be really grateful if anybody has any suggestions for me.
For anybody who has been struggling with this type of problem. I needed to correctly configure the webpack to know where to find my partials and helpers (I though express-handlebars config did this, but that only applies to serverside).
let path = require('path');
module.exports = {
entry: {
login: './public/js/users/login.index.js',
logout: './public/js/users/logout.index.js',
newListing: './public/js/listing/newListing.index.js',
listingCollection: './public/js/listing/newListing.index.js',
listingDetails: './public/js/listing/listingDetails.index.js',
userRegistration: './public/js/listing/newListing.index.js',
},
output: {
path: path.join(__dirname, '/public/js/bundles'),
publicPath: "/public/",
filename: "[name].bundle.js"
},
module: {
loaders: [{
test: /\.handlebars$/,
loader: "handlebars-loader",
query: {
partialDirs: [
path.join(__dirname, 'views', 'partials')
],
helperDirs: [
path.join(__dirname, 'views', 'helpers')
]
}
}]
}
};
This makes the compiled partials and helpers available in the client, but I then still need to register them in client code.
let mangaDetailsPartial = require("../../../views/partials/manga_specific_details.handlebars");
let animeDetailsPartial = require("../../../views/partials/anime_specific_details.handlebars");
let itemSpecificsPartial = require("../../../views/partials/listing/itemSpecifics.handlebars");
Handlebars.registerPartial("manga_specific_details", mangaDetailsPartial);
Handlebars.registerPartial("anime_specific_details", animeDetailsPartial);
Handlebars.registerPartial("item_specifics", itemSpecificsPartial);
Now everything works.
There are some example in a link at the bottom of the npm page for 'handlebars-loader' module.

Universal React.JS - Requiring Styles and Images

Lately I've been dabbling with React and Webpack, and so far i've been loving it. Sure it took a lot of reading, looking at exemples, and trying some stuff out, but eventually, react itself as well as hot reloading my components grew on me and I'm pretty sure I want to continue that way.
For the past couple of days though I've been trying to make my pretty simple "application" (so far it's just a menu with a couple of sub components and react router to display a dummy page) render server side.
Here's my project's layout:
Here's what my webpack config looks like so far:
let path = require("path"),
webpack = require("webpack"),
autoprefixer = require("autoprefixer");
module.exports = {
entry: [
"webpack-hot-middleware/client",
"./client"
],
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.js",
publicPath: "/static/"
},
resolve: {
root: path.resolve(__dirname, "common")
},
module: {
loaders: [
{
test: /\.js$/,
loader: "babel",
exclude: /node_modules/,
include: __dirname
},
{ test: /\.scss$/, loader: "style!css!sass!postcss" },
{ test: /\.svg$/, loader: "file" }
]
},
sassLoader: {
includePaths: [path.resolve(__dirname, "common", "scss")]
},
plugins: [
new webpack.DefinePlugin({
"process.env": {
BROWSER: JSON.stringify(true),
NODE_ENV: JSON.stringify( process.env.NODE_ENV || "development" )
}
}),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
],
postcss: [autoprefixer({remove: false})]
}
And, omitting the module requirements, my server:
// Initialize express server
const server = express();
const compiler = webpack(webpackConfig);
server.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath }));
server.use(webpackHotMiddleware(compiler));
// Get the request
server.use((req, res) => {
// Use React Router to match our request with our components
const location = createLocation(req.url);
match({routes, location}, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
res.send(renderFullPage(renderProps));
} else {
res.status(404).send("Not found");
}
})
});
function renderFullPage(renderProps) {
let html = renderToString(<RoutingContext {...renderProps}/>);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="app">${html}</div>
<script src="/static/bundle.js"></script>
</body>
</html>
`;
}
server.listen(3000);
Little React Router server-side specificities here but I can get my head around it.
Then came an issue with requiring styles inside a component. After a while I figured out this bit (thanks to a github issue thread):
if (process.env.BROWSER) {
require("../scss/components/menu.scss");
logo = require("../images/flowychart-logo-w.svg") ;
}
And along with it the fact that I still need to declare a router on my client, re-using the routes my server already uses:
// Need to implement the router on the client too
import React from "react";
import ReactDOM from "react-dom";
import { Router } from "react-router";
import createBrowserHistory from "history/lib/createBrowserHistory";
import routes from "../common/Routes";
const history = createBrowserHistory();
ReactDOM.render(
<Router children={routes} history={history} />,
document.getElementById("app")
);
And now there I am. My style is applied and everything works. I'm still no expert, but so far I kind of understand everything that's going on (which is the main reason why I'm not using an existing universal js / react boilerplate - actually understanding what's going on).
But now my next issue to solve is how to require my images inside my components.
With this setup, it actually does work, however I'm getting this warning:
Warning: React attempted to reuse markup in a container but the
checksum was invalid.
This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting.
React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
(client) "><img class="logo" src="/static/31d8270
(server) "><img class="logo" alt=""
And it makes sense, as you've seen in a previous code snippet, I actually set a logo variable when I'm in the browser environment, and I use that variable to set the src tag of my components. Sure enough on the server, no src attributes is then given, thus the error.
Now to my question:
Am i doing that style / image requirement right ? What am I missing to get rid of that warning ?
The one thing I love about that webpack / react association is how I can keep those dependencies close to the actual part of my UI that uses them. However the one thing I've found out was omitted from most of the tutorials out there was how to keep this sweetness working when rendering server side.
I'd be very grateful if you guys could give me some insight.
Thanks a lot !
I would recommend webpack-isomorphic-tools as a solution here.
I have a section on how to import SVG's into React apps in an example project: https://github.com/peter-mouland/react-lego#importing-svgs
Here is the code that was required to turn my Universal React app into one that accepts SVG's : https://github.com/peter-mouland/react-lego/compare/svg
There's at least two approaches you can take for this:
Try to de-webpackify your code to run in Node.
Compile your client code with Webpack using target 'node'.
The first option requires you to remove Webpack specific code such as require.ensure / System.import and teaching node your module resolution rules and how to convert non js assets into js. This is achievable with a few babel transformations and maybe modifications to the NODE_PATH however this approach does mean you're constantly chasing your tail, i.e. if you start using a new Webpack feature / loader / plugin you need to make sure you're able to support the equivalent functionality in Node.
The second approach is probably more practical as you can be sure that whichever Webpack features you're using will also work on the server. This is one way you may do it:
// webpack.config.js
module.exports = [
{
target: 'web',
entry: './client',
output: {
path: dist,
filename: 'client.js'
}
}, {
target: 'node',
entry: './server',
output: {
path: dist,
filename: 'server.js',
libraryTarget: 'commonjs2'
}
}
];
// client.js
renderToDom(MyApp, document.body);
// server.js
module.exports = (req, res, next) => {
res.send(`
<!doctype html>
<html>
<body>
${renderToString(MyApp)}`
<script src="/dist/client.js"></script>
</body>
</html>
`);
}
// index.js
const serverRenderer = require('./dist/server');
server.use(serverRenderer);
server.listen(6060);

Conflicting server/client rendering and Webpack’s local css modules

I'm using Fluxible to help create an isomorphic app on a new project and it's going swimmingly. I love it so far. I have run into a speed bump, though, and wonder how to get over it.
Here is my Header component thus far:
import React from 'react'
import Nav from '../Nav/Nav'
import classNames from 'classnames'
if (process.env.BROWSER) var styles = require('./Header.css')
class Header extends React.Component {
render() {
// Header classes
var theClasses = process.env.BROWSER ? classNames({
[styles.Header]: true
}) : ''
return (
<header className={theClasses}>
<Nav selected={this.props.selected} links={this.props.links} />
</header>
)
}
}
export default Header
You'll see I'm using process.env.BROWSER to detect which ENV I'm on. If we're in the client, I require the CSS. If we're on the server, I skip it. That works wonderfully.
The problem comes later in the file, where I build theClasses object based on the contents of the Header.css file, then use those classes on the Header like so:
<header className={theClasses}>
<Nav selected={this.props.selected} links={this.props.links} />
</header>
The problem is because I'm not loading the css on the server, theClasses ends up being empty, and the content rendered for the client ends up different than the content on the server. React displays this warning:
Warning: React attempted to reuse markup in a container but the
checksum was invalid. This generally means that you are using server
rendering and the markup generated on the server was not what the
client was expecting. React injected new markup to compensate which
works but you have lost many of the benefits of server rendering.
Instead, figure out why the markup being generated is different on the
client or server:
(client) n28"><header class="Header--Header_ip_OK
(server) n28"><header class="" data-reactid=".2ei
What would you recommend to get this resolved?
UPDATE September 24, 2015
The original issue was that I couldn't get CSS to compile on the server side, so I started checking for the BROWSER like this:
if (process.env.BROWSER) var styles = require('./Application.css')
If I remove the if (process.env.BROWSER) bit I get this error:
SyntaxError: src/components/Application/Application.css: Unexpected token (2:0)
1 |
> 2 | #import 'styles/index.css';
| ^
3 |
In the following simple CSS file:
#import 'styles/index.css';
.Application {
box-shadow: 0 0 0 1px var(--medium-gray);
box-sizing: border-box;
lost-center: 1080px 32px;
}
I started this project with the Fluxible Yo Generator which provides two Webpack config files here: https://github.com/yahoo/generator-fluxible/tree/master/app/templates
I updated mine with a few loaders:
var webpack = require('webpack');
var path = require('path');
module.exports = {
resolve: {
extensions: ['', '.js', '.jsx']
},
entry: [
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./client.js'
],
output: {
path: path.resolve('./build/js'),
publicPath: '/public/js/',
filename: 'main.js'
},
module: {
loaders: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loaders: [
require.resolve('react-hot-loader'),
require.resolve('babel-loader')
]
}, {
test: /\.css$/,
loader: 'style-loader!css-loader?modules&localIdentName=[name]__[local]_[hash:base64:5]!postcss-loader'
}, {
test: /\.(png|jpg|svg)$/,
loader: 'url?limit=25000'
}, {
test: /\.json$/,
loader: 'json-loader'
}
]
},
postcss: function () {
return [
require('lost'),
require('postcss-import')({
path: ['./src/'],
onImport: function (files) {
files.forEach(this.addDependency);
}.bind(this)
}),
require('postcss-mixins'),
require('postcss-custom-properties'),
require('autoprefixer')({
browsers: ['last 3 versions']
})
];
},
node: {
setImmediate: false
},
plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
BROWSER: JSON.stringify(true)
}
})
],
devtool: 'eval'
};
So that's where I am… not sure how to get CSS compiled server side. Appreciate any help I can get.
You can try css-modules-require-hook
webpack.config.js
{
test: /\.css$/,
loader: 'style-loader!css-loader?modules&localIdentName=[name]__[local]___[hash:base64:5]'
}
server.js
require('css-modules-require-hook')({
// This path should match the localIdentName in your webpack css-loader config.
generateScopedName: '[name]__[local]___[hash:base64:5]'
})
You need to ensure the server is generating the same markup as the client, which means that you need to use CSS Modules on the server too.
We achieve this in my current project by using Webpack to compile our Node code too. James Long wrote a really good guide on how to set this up, spread across three blog posts:
Backend Apps with Webpack (Part I)
Backend Apps with Webpack (Part II)
Backend Apps with Webpack (Part III)
You should certainly be rendering your server side HTML with the classes otherwise you are missing out on the time to glass perks of an isomorphic application. It means you will still have to wait for the JS to load in order to apply the CSS styles which defeats some of the purpose of building the HTML server side. It also means you can't "cut the mustard" and serve old browsers an application with Javascript turned off.
The question is why aren't you rendering the CSS classes on the server? This also confused me for a while but my guess is you don't have two Webpack entry points? One for the client, and one for the server.
If my assumption is correct then take a look at a sandbox repo here where I'm doing multiple builds for the Node server entrypoint and the Browser entrypoint also using Fluxible.
However I would take it all with a pinch of salt for now as it was just a test project for personal use. I have also used this approach with local css where it builds it on both the server and client and it works like a charm.
EDIT: I see you are using ES6 so I assume you are indeed building server side? If so what is the reason you don't include the CSS?
So you have to have two Webpack configs, the first is for compiling browser bundle, the second is for compiling server bundle.
With a standard configuration you have css-loader?...:
{
test: /\.css$/,
loader: 'style-loader!css-loader?modules&localIdentName=[name]__[local]_[hash:base64:5]'
}
And you get two different bundles.
On a server. All classes are in css.locals object:
let css = require('styles.scss');
css.locals == { fooClass: 'some_foo_class' }
On a browser there is no .locals:
let css = require('styles.scss');
css == { fooClass: 'some_foo_class' }
So you need to get rid of .locals in a server version, so that it should be like in a browser.
You can do it using css-loader/locals?... in a webpack server config:
{
test: /\.scss$/i,
loader: "css/locals?modules&importLoaders=1&-minimize&localIdentName=[local]_[hash:3]!sass"
}
More details here https://github.com/webpack/css-loader/issues/59#issuecomment-109793167

Categories

Resources