DEV Community

Cover image for Build a Fullstack app with Adonis and React
Michael McShinsky
Michael McShinsky

Posted on • Edited on • Originally published at magitek.dev

Build a Fullstack app with Adonis and React

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!

Create Initial Project

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>;
Enter fullscreen mode Exit fullscreen mode

Setup Asset Bundler

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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),
})
Enter fullscreen mode Exit fullscreen mode

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*
Enter fullscreen mode Exit fullscreen mode

Configure Tailwind

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
Enter fullscreen mode Exit fullscreen mode

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: [],
}
Enter fullscreen mode Exit fullscreen mode

resources/assets/scss/index.scss

@import "tailwindcss/base"; 
@import "tailwindcss/components"; 
@import "tailwindcss/utilities";
Enter fullscreen mode Exit fullscreen mode

Create Client React App

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;
Enter fullscreen mode Exit fullscreen mode

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')
);
Enter fullscreen mode Exit fullscreen mode

resources/client/App.js

import React from 'react'

export default function App() {
  return (
    <div>
      Hello World!
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Server Routing

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')
Enter fullscreen mode Exit fullscreen mode

Development & Deployment

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;
Enter fullscreen mode Exit fullscreen mode

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\"",
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Minor notes:

  1. dev runs both the server and the client for active development.
  2. start and build are reserved for deployment in my use case

Closing Thoughts

Currently, 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.


If you found this helpful or useful, please share a ๐Ÿ’“, ๐Ÿฆ„, or ๐Ÿ”–. Thanks!

Top comments (1)

Collapse
 
lekinano profile image
djang0

Hi, thanks for this article, I'm trying to recreate this setup with adonis, did you manage to find a solution for react refresh ?