Inspired by Amir Hosein Samili's post on managing Adonis and Vue as a monorepo, I want to show you how we can replicate this approach with React. As a bonus, we'll add in Tailwind as our CSS framework, but you can choose whatever styling methodology or framework that floats your boat. The goal is to let both our apps live together and allow the server pass the routing and styles down to the client.
Please note we'll be following the same flow as the referenced article, but making some changes and additions to meet our React/Tailwind needs. Please share your thanks and follow there as well!
We'll start with creating our Adonis project using the latest version (v5). If you are unfamiliar with Adonis, take some time and look over the docs at preview.adonisjs.com. When running the starting command below, make sure to select "Web Application" since we will be making use of both backend and frontend routing in a single environment. I also say yes to eslint and prettier during the cli instructions and then customize them to my own personal preferences.
yarn create adonis-ts-app <app-name>;
cd <app-name>;
With our project compiler ready, we now need to configure the server to be aware of and compile our React assets that we'll be using for the frontend.
yarn add adonis-mix-asset && yarn add -D laravel-mix laravel-mix-tailwind;
The invoke command will setup the providers, commands and webpack.mix.js we need to resolve and build the relationship between our backend and frontend.
node ace invoke adonis-mix-asset;
Since we are having Adonis and React in the same monorepo and will be letting this repo manage our React app through adonis-mix-asset (Laravel Mix), we need to have some extra webpack configurations for hot reloading. As of this article, there is a minor bug that prevents hot refreshing in the browser so you'll need to do a manual refresh when working in the React portion of the codebase. If you happen upon the fix for this, I would love to hear more about it! We're going to add a couple more dependencies that our webpack file will need. As well, since mix is managing our webpack, the file will be called webpack.mix.js
.
yarn add -D @babel/preset-react babel-loader @pmmmwh/react-refresh-webpack-plugin react-refresh;
webpack.mix.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const webpack = require('webpack')
const mix = require('laravel-mix')
require('laravel-mix-tailwind')
const isDevelopment = process.env.NODE_ENV !== 'production'
mix
.setPublicPath('public')
.js('resources/client/index.js', 'public/js/')
.react()
.sass('resources/assets/scss/index.scss', 'public/css/')
.tailwind()
.options({
processCssUrls: false
})
if (isDevelopment) {
mix.sourceMaps()
}
mix.webpackConfig({
mode: isDevelopment ? 'development' : 'production',
context: __dirname,
node: {
__filename: true,
__dirname: true,
},
module: {
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
presets: ['@babel/preset-react'],
plugins: [isDevelopment && require.resolve('react-refresh/babel')].filter(Boolean),
},
},
],
},
],
},
plugins: [
isDevelopment && new webpack.HotModuleReplacementPlugin(),
isDevelopment && new ReactRefreshWebpackPlugin(),
new webpack.ProvidePlugin({
React: 'react',
}),
].filter(Boolean),
})
We'll also add additional fields to the .gitignore and let the deployment built handle them.
.gitignore
# other settings...
mix-manifest.json
hot
public/js/*
public/css/*
public/**/*_js*
Let's go ahead and add tailwind configurations to our app.
yarn add -D tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9 sass-loader@8.* sass postcss@^8.1;
mkdir -p resources/assets/scss && touch resources/assets/scss/index.scss;
npx tailwindcss init
tailwind.config.js
module.exports = {
purge: ['./resources/client/**/*.{js,jsx,ts,tsx}', './resources/views/**/*.edge'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
resources/assets/scss/index.scss
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
Next we'll need to bring in the needed React packages and create our starter React entry files.
yarn add react react-dom;
mkdir -p resources/client && touch resources/client/index.js resources/client/App.js;
resources/client/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
resources/client/App.js
import React from 'react'
export default function App() {
return (
<div>
Hello World!
</div>
)
}
Since the server is handling the initial serve of our React app, we'll need to create an edge templating file that React will mount from. We'll do this in the resources/views folder. Our edge file for now will use direct references to our mix files instead of the mix templating syntax {{ mix('scripts/index.js') }}
due to deployment issues in Heroku (if that is what you decide to use).
touch resources/views/index.edge;
resources/views/index.edge
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/index.css">
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/js/index.js"></script>
</body>
</html>
Our server is hosting the React app so we now need let our routing know how to handle frontend routing. We'll "start" our route from the server and then from there, the React app will take over all client routing. You can manage client side routing using the popular routing library react-router
.
start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
// Other Routes...
Route.get('*', async ({ view }: HttpContextContract) => {
return view.render('app')
}).as('not_found')
I've deployed my app to Heroku so you may find this linked article helpful in setting that up. As a result the package scripts referenced that reflect that flow but you may want or need to change them for deployment to suite your server environment.
yarn add -D concurrently;
package.json
"start": "node build/server.js",
"server": "node ace serve --watch",
"client": "node ace mix:watch",
"build": "yarn client:build && yarn server:build",
"server:build": "node ace build --production",
"client:build": "node ace mix:build --production",
"dev": "concurrently \"yarn server\" \"yarn client\"",
Procfile (For Heroku deployment)
release: node ./build/ace migration:run --force && node ./build/ace db:seed
api: node build/server.js
web: node build/server.js
Minor notes:
dev
runs both the server and the client for active development.start
and build
are reserved for deployment in my use caseCurrently, Adonis combined with React as a monorepo come with a couple minor inconveniences that should be solved either through library fixes or maybe there is something that I didn't notice. Hopefully these minor adjustments are resolved soon, but that that hasn't stopped me from continuing to use this setup in current projects.
Overall, running both your server and your client in the same environment bring some unique advantages that come with monorepos. You keep all the code together and CORS & APIs are easier to manage in regards to security & convenience. Also all stylesheets, tooling, and project wide changes are easy to distribute without having to adding in third party distributable libraries to multiple apps.
On the flip side, if you want or need separate projects for the server and client, this approach probably isn't for you. You'll most likely spin up a separate create-react-app (or custom) and deploy it elsewhere while calling the API endpoints of your Adonis server. There is nothing wrong with either approach. It all comes down to company and development objectives that best align with the future scalability your are looking for.
Here is a link to the github source code for reference.