React SSR alternatives - reactjs

I am currently working on a project that someone else built and I was asked to implement server side rendering, it's a huge project and it uses a custom routing system based on a "builder JSON" taken by a main component that selects which components to render based on the route, it is meant to keep the app dynamic and to adjust to the needs of several customers.
I have been checking everywhere trying to find an answer but im new to SSR and it's a big challenge.
I am currently testing an approach using express that looks like this:
import 'babel-polyfill';
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router';
import bodyParser from 'body-parser';
import { App } from '../src/App';
const app = express();
const PORT = process.env.PORT || 8000;
app.use(bodyParser.json());
app.use(express.static('ssrBuild'))
app.get('*', (req, res) => {
const context = {}
const content = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
const html = `
<html>
<head>
</head>
<body>
<div id="root">
${content}
</div>
</body>
</html>
`;
res.send(html);
});
app.listen(PORT, () => {
console.log(`App running on port ${PORT}`);
});
The problem I am currently having is that the App component calls the "complex routing component" from another repository in the node_modules (also build by them), my webpack config is taking the App component and finding jsx, which cant obviouly be run in the server.
Webpack config:
const path = require('path');
const webpackNodeExternals = require('webpack-node-externals');
module.exports = {
target: 'node',
entry: {
server: './ssr/server.js',
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, '..', 'ssrBuild'),
publicPath: '/ssrBuild'
},
module: {
rules: [
{
test: /\js$/,
loader: 'babel-loader',
exclude: '/node_modules/',
options: {
presets: [
'#babel/react',
['#babel/preset-env', {
targets: { browsers: ['last 2 versions'] }
}]
],
plugins: [
['#babel/plugin-proposal-decorators', { 'legacy': true }],
'#babel/plugin-proposal-class-properties',
'#babel/plugin-syntax-function-bind',
'#babel/plugin-transform-async-to-generator',
'#babel/plugin-proposal-export-default-from',
'babel-plugin-jsx-control-statements',
'react-hot-loader/babel',
'lodash',
]
}
}
]
},
externals: [webpackNodeExternals()]
}
Is there a way I can tell webpack to transpile the jsx in the node_modules folder "on the go"? What other solutions could there be?
Also, the whole point of this project is to impprove the SEO for these apps, I would only need to SSR the index page and any direct link to content in it. No need to SSR the entire app is there a way this can be achieved?
I was also wondering if refactoring the entire app to use Next.js would be worth it if this were possible only for the index page and any direct link.
Thank you in advance!

If the team can afford it then you should definitely go and try a framework. This will me more maintainable on the long term. I would recommend you try Next.js over Gatsby, Both are great options but in my opinion Next has two or three advantages like Incremental Static Regeneration (Regenerate if your content change continuously) or you can choose between use Server Sider or Static generation based on your routes. You can use SSR on your dashboard and SSG on your home and landing pages for example.
If you don't need any type of pre-rendering you can go for client-side only and even then Next are gonna make a couple of optimization that will speed up your site.
On the long term it will save you a lot of time and it will be easy to maintain

Related

Nextjs styles from a shared library are not loaded on initial loading

I have a Nextjs app and a shared UI library that is being used not only for Nextjs app but also other apps (SPAs). kinda like Material-ui / semantic-ui that has a list of UIs that can be used for my apps.
This shared library app was built only for SPA initially but now that I'd like to add a Nextjs app, I'm sure it needs some modification.
The styles on Nextjs app are rendered correctly on initial load since I followed the Next's example with React-jss on their repo. (all of my apps are using react-jss) https://github.com/vercel/next.js/blob/canary/examples/with-react-jss/pages/_document.js
_document.js
import Document from "next/document";
import { SheetsRegistry, JssProvider, createGenerateId } from "react-jss";
export default class JssDocument extends Document {
static async getInitialProps(ctx) {
const registry = new SheetsRegistry();
const generateId = createGenerateId();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => (
<JssProvider registry={registry} generateId={generateId}>
<App {...props} />
</JssProvider>
)
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style id="server-side-styles">{registry.toString()}</style>
</>
)
};
}
}
But once I load this shared library app on my Nextjs app, the styles from the shared library are not applied at all on initial load. like I did in _document.js, I'm sure I need to make some changes to accommodate this case where I load another library that has its own styles in either _document.js or webpack.
The error message I got looks something like this
Warning: Prop `className` did not match. Server: "wrapper-0-2-7 wrapper-d0-0-2-25 size_2-0-2-11" Client: "wrapper-0-2-7 wrapper-d2-0-2-27 size_2-0-2-11"
The way I connect my next app to this shared library app is to use webpack.
nextapp/config/aliases.js
const path = require("path");
module.exports = {
react: path.join(__dirname, "../node_modules/react"),
"react-jss": path.join(__dirname, "../node_modules/react-jss"),
};
nextapp/next.config.js
module.exports = {
reactStrictMode: true,
webpack: config => {
config.resolve.alias = {
...(config.resolve.alias || {}),
...aliases
};
config.resolve.modules = [
"node_modules",
resolveApp("node_modules"),
"../sharedLibrary/node_modules" <-------
];
config.module.rules.push({
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: [resolveApp("../b/src"), resolveApp("./")], <-------
// include: [paths.appSrc],
loader: require.resolve("babel-loader"),
options: {
customize: require.resolve("babel-preset-react-app/webpack-overrides"),
configFile: resolveApp("./.babelrc"),
plugins: [
[
require.resolve("babel-plugin-named-asset-import"),
{
loaderMap: {
svg: {
ReactComponent: "#svgr/webpack?-svgo,+titleProp,+ref![path]"
}
}
}
]
],
// 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,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: false // TODO
}
});
config.resolve.plugins.push(
PnpWebpackPlugin,
new ModuleScopePlugin(
[resolveApp("./"), resolveApp("../sharedLibrary/src")],
[
resolveApp("package.json"),
resolveApp("../sharedLibrary/package.json")
]
)
);
return config;
}
};
Could someone please help me how to render styles from this shared library on Nextjs app?
Thank you in advance

Loading react component from url

I want to import the react component that I have bundled using web pack.
I am able to complete the task by copying it locally to that folder and then importing it like
import Any from '.dist/index'
and it is working fine.
But what I want to do is uploading this index.js file to somewhere for example Amazon s3. Now I am not able to import the component in the same way as mentioned above.
My webpack.config.js file, I have used to export my bundled component generated by webpack that I am using in another project by copying the index.js and index.css file is
var path = require("path");
var HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index_bundle.js",
libraryTarget: "commonjs2"
},
module: {
rules: [
{ test: /\.(js)$/, use: "babel-loader" },
{ test: /\.css$/, use: ["style-loader", "css-loader"] }
]
},
externals: {
react: "commonjs react"
},
mode: "production",
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html"
})
]
};
I want to import the component from file url uploaded to s3.
you can do what you are describing with micro apps. A micro app is basically nothing more than a component that is lazy loaded into the host application from a url at runtime. There is no need to install or import the component at design time. There is a library available that lets you do this with a HOC component.
import React from 'react';
import ReactDOM from 'react-dom';
import MicroApp from '#schalltech/honeycomb-react-microapp';
const App = () => {
return (
<MicroApp
config={{
View: {
Name: 'redbox-demo',
Scope: 'beekeeper',
Version: 'latest'
}
}}
/>
);
});
export default App;
You can find more information on how it works here.
https://github.com/Schalltech/honeycomb-marketplace
This is not the way you should package and deploy your React components. AWS S3 is a bucket for storage of files to serve on the web. It's purpose is not to share code files through projects.
You should publish your React components to a registry such as NPM. After you publish your package to the registry, you should be able to install the package into your app as a dependency by doing something like npm install my_package.

React router: adding dynamic URL

I am using React Router Dom 4.2.2
Currently I have one Router as follows:
import React from 'react';
import {BrowserRouter, Route, Switch, Link, NavLink} from 'react-router-dom';
import ExpensesDashboardPage from "../components/ExpensesDashboardPage";
import AddExpensePage from "../components/AddExpensePage";
import EditExpensePage from "../components/EditExpensePage";
import HelpPage from "../components/HelpPage";
import NotFoundPage from "../components/NotFoundPage";
import Header from "../components/Header";
const AppRouter = () => (
<BrowserRouter>
<div>
<Header/>
<Switch>
<Route path='/' component={ExpensesDashboardPage} exact={true}/>
<Route path='/create' component={AddExpensePage}/>
<Route path='/edit/' component={EditExpensePage}/>
<Route path='/help' component={HelpPage}/>
<Route component={NotFoundPage}/>
</Switch>
</div>
</BrowserRouter>
);
export default AppRouter;
And I have tested that we can navigate to each one of the Route.
Then I thought about using a dynamic URL as follows:
<Route path='/edit/:id' component={EditExpensePage}/>
So then the component which we should render is:
import React from 'react';
const EditExpensePage = (props) => {
console.log(props);
return (
<div>
This is the Edit Expenses page, enjoy!
</div>
);
};
export default EditExpensePage;
The question here is the following:
Why if we go to the following URL: http://localhost:8080/edit/33, the console outputs:
GET http://localhost:8080/edit/bundle.js 404 (Not Found)
33:1 Refused to execute script from 'http://localhost:8080/edit/bundle.js' because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled.
I would expect to see the page being loaded and into the match props, a params Object with the id: 33
I have also read
How to achieve Dynamic routing in React Router 4?
Thank you for your help!
I also had the same issue. My best bet you are watching the React Udemy tutorial of Andrew Mead. I tried the solution as provided by #mani.saffarnia. Didn't Work Out.
Here is how I got to make it work:
Setup the property public path as publicPath: '/'in your webpack.config.js in output block and devserver block.
Also change the default webpack server port from 8080 to 8082(any random unused port) in webpack dev server to ensure there is no caching.
Deleted Node Modules.
Clear npm cache using npm cache clean --force
Delete Public Folder and reinstalled node modules and created a fresh public folder to serve index.html and bundle.js
Finally run dev server for a working output
Here's my final webpack config for the fixed issue.
const path = require('path');
module.exports = {
entry: './src/app.js',
output:{
path: path.resolve(__dirname, 'public'),
filename: 'bundle.js',
publicPath: '/'
},
module:{
rules:[
{
loader:'babel-loader',
test: /\.js$/,
exclude: /node_modules/
},
{
test:/\.s?css$/,
use:[
'style-loader',
'css-loader',
'sass-loader'
]
}
]
},
devtool:'inline-source-map',
devServer:{
contentBase : path.resolve(__dirname, 'public'),
historyApiFallback: true,
publicPath: '/',
port: 8082
}
};
You didn't do anything wrong in your code. I know the tutorial that you are watching and I had the same problem. I just deleted my "public" and "node_modules" folders. after that, I created a new public folder(with a new index.html inside it), and also used "npm install" to install all dependencies and create node_modules again. It worked for me and I hope it works for you too.
In your index.html file.
does your script import say:
<script src="./bundle.js"></script>
if so. remove the "." in from of the "/". So the script import is
<script src="/bundle.js"></script>
This worked for me

Webpack 2 HMR on Preact App

I'm trying to setup Hot Module Reloading with Webpack 2 and Preact. It's "working", in that it's reloading the entire contents of the app every reload, but I'm getting errors between hot reloads (and I think that's why individual components aren't the only thing reloading).
Here's the relevant parts of my webpack setup:
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.NamedModulesPlugin(),
//etc.
],
entry: [
'webpack-dev-server/client?'+DEV_SERVER,
'webpack/hot/only-dev-server',
'./dev/jsx/index.jsx'
],
devServer: {
hot: true,
inline: true,
contentBase: path.join(__dirname, '/'),
publicPath: '/'
}
My index.jsx file looks like:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './AppProvider.jsx';
const renderApp = () => {
ReactDOM.render(<App/>, document.getElementById('root'));
};
renderApp();
if (module.hot) {
module.hot.accept();
module.hot.accept('./AppProvider.jsx', renderApp);
}
When I make a change in any of the project files, the app contents reload and I get the following errors:
Have any of you gotten this before? I've been Googling all day and haven't found anything...
You're rendering the old AppProvider module, which is null when HMR kicks in. You'll need to move the require()/import for AppProvider.jsx into your HMR handler as shown here.
import React from 'react';
import ReactDOM from 'react-dom';
const renderApp = () => {
let App = require('./AppProvider.jsx');
App = App.default || App; // if you're using ES Modules
ReactDOM.render(<App/>, document.getElementById('root'));
};
renderApp();
if (module.hot) {
module.hot.accept('./AppProvider.jsx', renderApp);
}

SCSS compilation in an isomorphic React app

I'm writing an isomorphic React app based on :
https://github.com/choonkending/react-webpack-node
Instead of css modules used in the examples I'd like to use scss though. And for some reason I'm having a really hard time getting them to work. My first step was to remove the css webpack loaders from both the server and the client configs replacing them with scss-specific loaders (as well as removing postcss) :
loaders: [
'style-loader',
'css-loader?modules&localIdentName=[name]_[local]_[hash:base64:3]',
'sass-loader?sourceMap',
]
But this throws ReferenceError: window is not defined when building as style-loader is apparently not suitable for server-side rendering. So my next idea was to use isomorphic-style-loader. As far as I understand to get it working I need to decorate my component with their higher order component withStyles:
import React, { PropTypes } from 'react';
import classNames from 'classnames';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from '../assets/scss/common/index.scss';
const App = (props, context) => (
<div className={classNames('app')}>
<h1 className="home_header">Welcome!</h1>
{props.children}
</div>
);
export default withStyles(s)(App);
and then do some trickery in the code rendering page on the server. But the problem is, example from the package docs shows a flux action fired inside Express (https://libraries.io/npm/isomorphic-style-loader#webpack-configuration), and the boilerplate that I'm using uses react-router. So I'm kinda lost as how should I inject this object with insertCss into context. I tried this :
import React from 'react';
import { renderToString } from 'react-dom/server';
import { RouterContext, match, createMemoryHistory } from 'react-router';
import axios from 'axios';
import { Provider } from 'react-redux';
import createRoutes from 'routes.jsx';
import configureStore from 'store/configureStore';
import headconfig from 'components/Meta';
import { fetchComponentDataBeforeRender } from 'api/fetchComponentDataBeforeRender';
const clientConfig = {
host: process.env.HOSTNAME || 'localhost',
port: process.env.PORT || '3001'
};
// configure baseURL for axios requests (for serverside API calls)
axios.defaults.baseURL = `http://${clientConfig.host}:${clientConfig.port}`;
function renderFullPage(renderedContent, initialState, head = {
title: 'cs3',
css: ''
}) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
${head.title}
${head.link}
<style type="text/css">${head.css.join('')}</style>
</head>
<body>
<div id="app">${renderedContent}</div>
<script type="text/javascript">window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};</script>
<script type="text/javascript" charset="utf-8" src="/assets/app.js"></script>
</body>
</html>
`;
}
export default function render(req, res) {
const history = createMemoryHistory();
const store = configureStore({
project: {}
}, history);
const routes = createRoutes(store);
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
const css = [];
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
const context = { insertCss: (styles) => css.push(styles._getCss()) };
const InitialView = (
<Provider context={context} store={store}>
<RouterContext {...renderProps} />
</Provider>
);
fetchComponentDataBeforeRender(store.dispatch, renderProps.components, renderProps.params)
.then(() => {
const componentHTML = renderToString(InitialView);
const initialState = store.getState();
res.status(200).end(renderFullPage(componentHTML, initialState, {
title: 'foo',
css
}));
})
.catch(() => {
res.end(renderFullPage('', {}));
});
} else {
res.status(404).send('Not Found');
}
});
}
but I'm still getting Warning: Failed context type: Required context 'insertCss' was not specified in 'WithStyles(App)'. Any ideas how to tackle this ? And more importantly - is there no easier way to do it ? This seems like a lot of additional work.
There's a few parts to handling scss compilation with webpack when you're doing server-side rendering. First of all, you don't want node trying to import .scss files into your components.
So set a global variable WEBPACK: true in your webpack config:
plugins: [
new webpack.DefinePlugin({
'process.env': {
WEBPACK: JSON.stringify(true),
}
})
],
And in your components, only attempt to import .scss files if the component is being handled by webpack (either during build or development):
if (process.env.WEBPACK) require('../assets/scss/common/index.scss');
If you only have one Sass file per component (you should) then this is always just a one-liner. Any additional Sass files can be imported inside index.scss if you need to.
Then in your config you probably still want the css loader, so it should look like this for your dev server:
{
test: /\.s?css$/,
loaders: ['style', 'css', 'sass']
},
And something like this for you build config:
{
test: /\.s?css$/,
loader: ExtractTextPlugin.extract('style', 'css!sass')
},

Resources