How to structure multiple react apps and still share code + assets - reactjs

I've built a react app which uses the following structure;
node_modules
src/
app/
index.ts
index.html
...
server/
index.ts
...
node_modules/ // using the Alle pattern here
#<custom_packages>
api
components
Now I need to add a new app. Which runs on a different domain, but should be able to use as much shared code as it can, inc our custom packages. My first attempt was to do the following;
node_modules
src/
app1/
index.ts
index.html
...
app2/
index.ts
index.html
...
server/
index.ts // Uses ENV_VAR to serve a different bundle
...
node_modules/
#<custom_packages>
api
components
The problem I'm now running into, is that both apps generate their own assets etc. But i would like to share them between apps so the client can cache them. I could decide not to use webpack to build the assets and just put them in a static folder, but then I lose the support of the offline-plugin in webpack.
Also we decided to use a mono-repo in this case. This is making CI significantly harder, but managing shared code a lot easier. I'm kind of wondering if there are any seasoned developers that have faced this situation more often.
Basically, how would you structure 2 react apps that should share as much code as possible?

I needed a similar setup but for Angular. Here is the Webpack config that I have (I left only relevant parts):
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const apps = [
{
name: 'app1',
baseUrl: '/app1'
},
{
name: 'app2',
baseUrl: '/app2'
}
];
module.exports = function (args = {}) {
const isDev = !args.PROD;
const distPath = 'dist';
var config = {};
config.entry = {};
apps.forEach(function (app) {
config.entry[getAppBundleName(app)] = './src/apps/' + app.name + '/main.ts';
});
config.output = {
path: root(distPath),
filename: '[name].js',
chunkFilename: '[name].[chunkhash].js',
publicPath: '/dist/'
};
config.resolve = {
extensions: ['.ts', '.js', '.json'],
modules: [root('src'), root('node_modules')]
};
config.module = {
rules: [
// Add your loaders here
]
};
config.plugins = [
// Add your plugins here
// This enables tree shaking of the vendor modules
new CommonsChunkPlugin({
name: 'vendor',
chunks: ['admin'].concat(apps.map(getAppBundleName)),
minChunks: module => /node_modules/.test(module.resource)
}),
new CommonsChunkPlugin({
name: 'shared',
chunks: ['admin'].concat(apps.map(getAppBundleName)),
minChunks: module => /src(\\|\/)shared/.test(module.resource)
})
];
apps.forEach(function (app) {
var otherApps = apps.slice();
var appItemIndex = otherApps.indexOf(app);
if (appItemIndex > -1) {
otherApps.splice(appItemIndex, 1);
}
config.plugins.push(new HtmlWebpackPlugin({
template: 'index_template.html',
title: app.name,
filename: getAppDevServerHtmlFileName(app),
excludeChunks: otherApps.map(getAppBundleName),
chunksSortMode: 'manual',
chunks: ['vendor', 'shared', getAppBundleName(app)],
inject: 'head',
metadata: {
baseUrl: app.baseUrl
}
}));
});
config.devServer = {
port: 4200,
stats: stats,
historyApiFallback: {
rewrites: apps.map(function (app) {
return {
from: new RegExp('^' + app.baseUrl),
to: '/dist/' + getAppDevServerHtmlFileName(app)
}
}),
},
};
return config;
}
function getAppBundleName(app) {
return app.name;
}
function getAppDevServerHtmlFileName(app) {
return app.name + '_index.html';
}
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [__dirname].concat(args));
}`
In my case, I have the following folder structure, which is similar to yours:
node_modules
src/
apps/
app1/
...
main.ts
app2/
...
main.ts
shared/
shared-module1/
...
shared-module2/
...
index.ts
...
webpack.config.js
And here is the output after compilation in the dist folder:
dist/
app1.js
app1_index.html
app2.js
app2_index.html
vender.js
shared.js
As you can see, vendor.js and shared.js contain the shared code between the apps.

Related

Module Federation for React host and NextJS remote app example/comfiguration

I didn't find an example for a react host with and nextjs remote app
My host is working with a react remote app, now that I'm trying to add a nextjs remote app, it's not working:
Ports:
Host 3000
React remote 3001
NextJS Remote 3002
In my host react app I have the following, in the webpack.config.js file
plugins: [
new ModuleFederationPlugin({
name: 'react_container',
filename: 'remoteEntry.js',
remotes: {
remote_react: 'remote_react#http://localhost:3001/remoteEntry.js', // <- This is working
remote_nextjs: 'remote_nextjs#http://localhost:3002/_next/static/chunks/remoteEntry.js', // <- Not working :-(
},
exposes: {
'./react': 'react',
'./react-dom': 'react-dom',
},
shared: {
},
}),
And in the remote nextjs app in the next.config.js file
const { withFederatedSidecar } = require("#module-federation/nextjs-mf");
module.exports = withFederatedSidecar({
name: "remote_nextjs",
filename: "static/chunks/remoteEntry.js",
exposes: {
"./ExposedComponent": "./components/ExposedComponent",
},
shared: {
},
})({
// your original next.config.js export
});
And finally, in the App.jsx of the host , I'm trying to consume the remote components like
import RemoteNav from 'remote_react/Nav'; // <- Working
import ExposedComponent from 'remote_nextjs/ExposedComponent'; // <- Not working
I have repo here
The error I'm getting its 404 from
http://localhost:3000/_next/static/chunks/components_ExposedComponent_js.js
Thanks
From the webpack config of the host, I can see that the next JS app is running on port 3002.
You are getting 404 error for next JS remote bundles when the host is trying to fetch the bundles from next JS server, it is hitting port 3000 instead of 3002. This is because the next.config.js does not have the updated publicPath.
Add the following in the place where it says //your original next.config.js export in the next.config.js file
webpack: (config) => {
config.output.publicPath = "http://localhost:3002/_next/";
return config;
}
This will set the publicPath of your next JS app to port 3002 and fetch the bundles correctly.
Referring to https://webpack.js.org/guides/public-path/ ,
on your remote app's webpack config, set publicPath would fix the path issue.
export default {
output: {
publicPath: 'auto',
},
plugins: [...],
...
}

Save files in cache with sw-precache and React

Does anyone know how I can cache my APP_SHELL files using sw-precache plugin and React?
The application was created with the cra command and I have managed to read my own service-worker. For the build and to take my files I have the following configuration:
module.exports = {
staticFileGlobs: [
'build/static/css/**.css',
'build/static/js/**.js',
'index.html',
'/'
],
swFilePath: './build/service-worker.js',
stripPrefix: 'build/',
importScripts: (['./service-worker-custom.js']),
handleFetch: false,
minify: true,
}
But I do not know in which part to indicate that I need to cache the APP_SHELL of my PWA.
I have tried to do it from my custom service worker but I can not register the files correctly, I have tried it in this way:
const cacheUrls = [
'/',
'build/static/css/**.css',
'build/static/js/**.js',
'build/media/**.jpg'
];
const cacheUrls = [
'/',
'/index.html',
'/static/css/',
'/static/js/',
'/static/media/',
];
const cacheUrls = [
'/',
'build/static/css/*.css',
'build/static/css/**/*.css',
'build/static/js/*.js',
'build/static/js/**/*.js',
'build/media/**/*.jpg',
'build/media/*.jpg'
];
But in no way registers the files in cache, only the index.html and the others when reviewing their conditional from the chrome console have the same content as index.html.
Thanks!

How to use a Service Worker to cache a virtual file?

I am attempting to implement a service worker for a boilerplate project I'm working on (https://github.com/jonnyasmar/gravity-bp feedback welcome!), but I've hit a snag :(
Problem:
I'm serving the index.html for this boilerplate virtually as an interpreted Twig template via ExpressJS. However, because I'm generating the service worker assets at build time and that is where I'm pointing it to the cacheable static assets, I can't figure out how to tell the service worker that I want it to cache the virtual index.html file served by ExpressJS at runtime.
My most successful attempts successfully cache all static assets (including the asset-manifest.json generated at build time), but will not cache a virtual index.html.
If I convert it to a static html file, the service worker does successfully cache it.
Please be sure to upvote this question to help get some visibility if you are interested in the answer!
Questions:
Is there a way to correct my code to accomplish this?
Is there anything wrong with doing it this way?
If yes to #2, how would you recommend handling this and why?
See the full source on GitHub.
Relevant code:
webpack.config.js:
output: {
filename: '[name].js',
chunkFilename: '[chunkhash].js',
path: path.resolve(__dirname, 'public'),
publicPath: '/'
},
plugins: {
new ManifestPlugin({
fileName: 'asset-manifest.json',
}),
new SWPrecacheWebpackPlugin({
cacheId: 'gravity-bp',
dontCacheBustUrlsMatching: /\.\w{8}\./,
filename: 'sw.js',
minify: true,
navigateFallback: 'index.html',
stripPrefix: 'public/',
swFilePath: 'public/sw.js',
staticFileGlobs: [
'public/index.html',
'public/**/!(*map*|*sw*)',
],
})
}
sw.ts:
const swUrl: string = 'sw.js';
export const register = (): void =>{
if(process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator){
const sw: ServiceWorkerContainer = navigator.serviceWorker;
sw.register(swUrl).then(registration =>{
registration.onupdatefound = (): any =>{
const installer: ServiceWorker = registration.installing;
installer.onstatechange = (): any =>{
if(installer.state === 'installed'){
if(sw.controller){
console.log('New content available.');
}else{
console.log('Content cached for offline use.');
}
}
};
};
}).catch((error) =>{
console.error('Failed to register service worker:', error);
});
}
};
export const unregister = (): void =>{
if('serviceWorker' in navigator){
navigator.serviceWorker.ready.then(registration =>{
registration.unregister();
});
}
};
server.ts:
import * as path from 'path';
const twig = require('twig').__express;
const express = require('express');
const compression = require('compression');
const pkg = require('../../package.json');
const version = pkg.version;
let app = express(),
ip = '0.0.0.0',
port = 3000,
views = path.resolve('./src/views');
app.use(compression());
app.use(express.static('public'));
app.set('view engine', 'twig');
app.engine('.twig', twig);
app.set('views', views);
// Routes
app.get("*", function(req: any, res: any, next: any){
// vars
res.locals.version = version;
res.render('index');
});
let server = app.listen(port, ip, function(){
let host = server.address().address;
let port = server.address().port;
console.log('Gravity Boilerplate ready at http://%s:%s', host, port);
});
Within the sw-precache-webpack-plugin documentation it talks about using sw-precache options. The one you should investigate is the dynamicUrlToDependencies setting. See some of these links for more info:
https://github.com/GoogleChromeLabs/sw-precache/issues/156
dynamicUrlToDependencies [Object⟨String,Buffer,Array⟨String⟩⟩]
For example, maybe start with this to test:
dynamicUrlToDependencies: {
'/': 'MAGIC_STRING_HERE'
},
So really, you need to configure the sw-precache WebPack plugin to load a server rendered page as the navigateFallback route.

Separate file for Environment Variable - ReactJS

I have this environment variable file that exports variable config depending on the NODE_END. Currently production and development variable are residing inside a one file, code below. How can I separate the file like development.js and production.js:
if(process.env.NODE_ENV === 'production') {
module.exports = {
API_URL: "https://test.co/api"
}
}
else {
module.exports = {
API_URL: "http://testbeta.co/api"
}
}
You can have a separate file to bridge them and export the intended module. For example, make three separate files index.js, development.js, and production.js on the same folder api.
// production.js
module.exports = {
API_URL: "https://e27.co/api"
}
// development.js
module.exports = {
API_URL: "http://e27beta.co/api"
}
// index.js
let exp
if (process.env.NODE_ENV === 'production') {
exp = require('./production.js')
} else {
exp = require('./development.js')
}
module.exports = exp
Then you can require it elsewhere like
// elsewhere.js
const api = require('path_to_api_folder')
If you using Webpack you can leverage the DefinePlugin plugin for exactly this purpose:
https://webpack.js.org/plugins/define-plugin/#use-case-service-urls
if(isProd) {
config.plugins.push(new webpack.DefinePlugin({
'SERVICE_URL': JSON.stringify("http://prod.example.com")
}));
} else {
config.plugins.push(new webpack.DefinePlugin({
'SERVICE_URL': JSON.stringify("http://dev.example.com")
}));
}
You could have 2 separate webpack configs, each with the appropriate SERVICE_URL'S.
// webpack.config.dev.js
plugins.push(new webpack.DefinePlugin({
'SERVICE_URL': JSON.stringify("http://dev-url.com")
}));
// webpack.config.prod.js
plugins.push(new webpack.DefinePlugin({
'SERVICE_URL': JSON.stringify("http://prod-url.com")
}));
To build, just pass webpack the appropriate config:
webpack --config webpack.config.prod.js
webpack --config webpack.config.dev.js

React Hot Loader not updating as expected

I am using react-hot-loader and webpack. I also use webpack-dev-server together with an express backend.
This is my relevant webpack config for development:
var frontendConfig = config({
entry: [
'./src/client/app.js',
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/dev-server'
],
output: {
path: targetDir,
publicPath: PROD ? '/build/assets/' : 'http://localhost:3000/build/assets/' ,
filename: 'app.js'
},
module: {
loaders: [
{test: /\.js$/,
exclude: /node_modules/,
loaders: PROD ? [babelLoader] : ['react-hot', babelLoader] }
]
},
plugins: [
new webpack.HotModuleReplacementPlugin({ quiet: true })
]
});
with this config I start webpack and webpack-dev-server
gulp.task('frontend-watch', function() {
new WebpackDevServer(webpack(frontendConfig), {
publicPath: frontendConfig.output.publicPath,
hot: true,
stats: { colors: true }
}).listen(3000, 'localhost', function (err, result) {
if(err) {
console.log(err);
}
else {
console.log('webpack dev server listening at localhost:3000');
}
});
});
so webpack-dev-server is running at localhost:3000 and receives app.js from webpack watcher (which now is not anymore written to file system).
my express server serves as a backend/api and has the following config:
var express = require('express');
// proxy for react-hot-loader in dev mode
var httpProxy = require('http-proxy');
var proxy = httpProxy.createProxyServer({
changeOrigin: true,
ws: true
});
var isProduction = process.env.NODE_ENV === 'production';
// It is important to catch any errors from the proxy or the
// server will crash. An example of this is connecting to the
// server when webpack is bundling
proxy.on('error', function(e) {
console.log('Could not connect to proxy, please try again...');
});
module.exports = function (app) {
// We only want to run the workflow when not in production
if (!isProduction) {
console.log('setting up proxy for webpack-dev-server..');
// Any requests to localhost:4200/build is proxied
// to webpack-dev-server
app.all('assets/app.js', function (req, res) {
proxy.web(req, res, {
target: 'http://localhost:3000'
});
console.log('request proxied to webpack-dev!');
});
}
var server = require('http').createServer(app);
app.use(express.static(homeDirectory + '/build'));
app.use(express.static(homeDirectory + '/files'));
server.listen(4200);
};
That's all good so far, the proxying work for app.js and I see successfull hot update messages in the browser console:
Now, while it looks fine it does not work as I expected:
when I change a component's render() method it updates as supposed, but when I change a helper method (that is used in render()) then I don't get any hot update. is that normal?
Another thing that bugs me, if I work like this, and do a 'hard' browser reload at some point, all changes I made are reverted to the point where I started my webpack-dev-server - all the hot updates in between have not been persisted somehow. is that normal as well? I would expect that I loose my state but not any changes I made to the code in the meantime. That has probably something to with my app.js not being written to the file system.
For your question #2, that's not normal, I have a template repo that has HMR working available here and it works just fine https://github.com/briandipalma/wp-r-template
For question #1, usually render methods display or format data, not grab it from somewhere. But if you need to format data, use a function outside of the component
Parent component would call the following once you retrieve the price
<ChildComponent price={this.state.price}
ChildComponent's render function would use props (or better yet a parameter of the function). Remember: the whole point of React is composition and data flow
return (
<div>{this.props.price}</div>
);

Resources