Grails-React App using Vite

This lecture demonstrates how to develop an React app using Grails for the backend framework. It is a very tight integration between Grails and React because Grails severs the html file that sources the React app code (typically called public/index.html in the React app). This lecture assumes prior knowledge with Grails and React and SDK Man. The lecture will explain the integration and how to continue with implementation of the Grails-React app.

This lecture was created because Grails is no longer supporting the “react” or “react-webpack” profiles. Grails has good reasons not to support these profiles because the profiles were based on the create-react-app (CRA) project and React is no longer supporting CRA. In addition react and react-webpack Grails profiles used a Gradle community plugin that is no longer active, so the plugin is no longer unsupported.

I was always unhappy with CRA because it hid the Webpack configuration file making advance configuration difficult. I was also unhappy with the react Grails profile because the integration with React was very lose making authentication and authorization using Spring Security more difficult then needed to be for simple web applications. So I was motivated to implement this example project.

You can find the example project integrating Grails and React using Vite on GitHub

https://github.com/rpastel/grails-vite-react

Technology

To understand the implementation, you should understand the roles of Vite and RollupJS.

Vite is a Node development server supporting the proposed ES module standard.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules

ES modules permit developers to write JS programs in separate files which are linked to each other when JS runs the program. The advantage to developers able to breakup a large program into smaller files and merge them programmatically is obvious, but many browsers do not support this feature. Another advantage for the browser merging the modules during development is that Hot Module Replacement (HMR) is very fast. The entire JS program does not have to bundled and then uploaded to the server because of an edit in one file. Consequently, the developer can see the changes in the view almost instantaneously. This is the reason for the name of the development server, Vite, which means “fast” in French.

Vite uses RollupJS to build the production JS files. RollupJS is a bundler. It merges all the modules into a single file that can be uploaded to browsers. We require bundling because most browser do not support ES modules. RollupJS does more than bundle JS modules. It also bundles CSS files and small assets such as SVG files. Both Vite and RollupJS are highly configurable using plugins, but do not need much configuration to get started.

Integration

I will assume that you have access to “Example integrating Grails and React Apps using Vite” files. I will try to explain integration from the perspective of a developer creating the integration of Grails and React using Vite. Let us start development by creating a Vite React app using

./npm create vite@latest counter -- --template react

See https://vitejs.dev/guide/. The above command will create a folder named counter containing the Vite-React app. In the counter folder you can run the app out of the box by entering

npm run dev

Before we make the Grails app, we first ensure that we are using the correct version of Java using SDK Man, https://sdkman.io/.

sdk use grails 5.3.3
sdk use java 8.0.392_zulu

Also we make the Grails app using the web profile

grails create-app grailsvitereact

See https://docs.grails.org/5.3.3/ref/Command%20Line/create-app.html. This will create a folder named grailsvitereact/ containing the Grails app. Move the terminal into the folder using

cd grailsvitereact

Before running the Grails app, let us assure that we always use the correct Java version. At the root of the project enter

sdk env init

This will create the “.sdkmanrc” file that specifies the java version. Now whenever we open a terminal in the folder, we can enter

sdk env

and this will assure using the correct version of Java. You can now run the app by entering

./gradlew bootRun

Our goal is to move the Vite-React app, counter, into the Grails app, grailsvitereact. The first step is to create an endpoint in the Grails app to serve the index.html file that sources the Vite-React app. We make the endpoint by creating a controller called counter.

./grailsw create-controller Counter

Besides creating the CounterController.groovy in the controller/ folder, the command also creates a counter/ subfolder in the views/ folder. We make the endpoint.

grails-app/views/counter/index.gsp

Note the index.gsp suffix, gsp, we need it to easily implement the differences for development and production serving of the view. We will work on development serving first. In the index.gsp, add the code.

<!doctype html>
<html>
<head>
    <title>Counter</title>
    <asset:link rel="icon" href="favicon.ico" type="image/x-ico" />
</head>
<body>
        <div id="app-counter"></div>
       
        <g:if env="development">
            <!-- For development, use Vite development server to serve the JS files -->
            <!-- https://vitejs.dev/guide/backend-integration.html -->
            <script type="module">
                    import RefreshRuntime from 'http://localhost:5173/@react-refresh'
                    RefreshRuntime.injectIntoGlobalHook(window)
                    window.$RefreshReg$ = () => {}
                    window.$RefreshSig$ = () => (type) => type
                    window.__vite_plugin_react_preamble_installed__ = true
              </script>

              <!-- Source scripts from vite server -->
              <script type="module" src="http://localhost:5173/@vite/client"></script>
              <script type="module" src="http://localhost:5173/src/main/webapp/app-counter/main.jsx"> 
           </script>
        </g:if>
</body>
</html>

Some of the interesting aspects of the html code are explained now. The line

<div id="app-counter"></div>

is the location on the page where the Vite-React app will be mounted. The line

<g:if env="development">

indicates that this section of the page should be shown only during development. The “<g:if env=” tag is very convenient.

The line

<script type="module" src="http://localhost:5173/src/main/webapp/app-counter/main.jsx"> 

sources the Vite-React app JS code from the Vite server. The rest of the scripts on the page interfaces React to refresh the page. See the Vite Backend Integration documentation

https://vitejs.dev/guide/backend-integration.html

Now that we have an endpoint for the Vite-React app. We can move the code from the Vite-React app into the grailsvitereact project. In the folder src/main/webapp/ in the grailsvitereact project make the subfolder app-counter/. Copy all the files from the src/ folder in the React app into the src/main/webapp/app-counter/ folder including the asset subfolder. Now copy the files from the root of the React app project into the root for the grailsvitereact project. These include

  • .eslintrc.cjs
  • package.json
  • vite.config.js

Note we do not need to move the public/index.html file into the grailsvitereact project. The views/counter/index.gsp file replaces it.

Before we can run the integrated app, we need to configure Vite. Edit the vite.config.js file so it looks like below.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import image from '@rollup/plugin-image'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [

    react(),
    // https://vite-rollup-plugins.patak.dev/#image
    // https://vitejs.dev/guide/api-plugin.html#plugin-ordering
    // https://github.com/rollup/plugins/tree/master/packages/image
    {
      ...image(),
      enforce: 'pre'
    },

  ],
  // https://vitejs.dev/guide/backend-integration.html
  build: {
    manifest: true, 
    rollupOptions: {
      input: {
        counter: 'src/main/webapp/app-counter/main.jsx', 
      },
      output: {
        // This will generate a `dist` directory containing bundles
        dir: 'dist/',
        entryFileNames: '[name]-bundle.js',
        assetFileNames: '[name]-bundle[extname]',

      }
    },
  },
})

The JS object in the plugin array

    {
      ...image(),
      enforce: 'pre'
    },

loads the Image RollupJS plugin and ensures that it runs before Vite and RollupJS code. We need the Image plugin to bundle the SVG assets into the JS code. See

https://vite-rollup-plugins.patak.dev/#image

First we need to download all the Node packages. Enter

npm install

We need to add the Image RollupJS plugin. Add it by entering

npm install @rollup/plugin-image --save-dev

We are ready to run the development servers. We will need to run two servers, the Vite server to serve the counter app JS code, CS and SVG. The other server is to run the Grails app. So we open two terminals at the root of the grailsvitereact project.

In one terminal, run the Grails app by entering

sdk env
./gradlew bootRun

In the other terminal, run the Vite server by enter

npm run dev

You can navigate your browser to localhost:8080 to view the Grails app. There should be a link for Counter Controller index view. Click on it to see the Vite-React counter app. Now you can edit code in src/main/webapp/app-counter/ and you see there effects almost immediately.

Before developing we should setup the production build. Our goal for the production build is for RollupJS to bundle the JS code and CSS. We also want the Assit-pipeline Grails plugin to prepare the bundle JS code and CSS, and for Gradle to make the WAR file. We need to modify build.gradle script. In build.gradle, add the “from” configuration in the assets section

assets {
    minifyJs = true
    minifyCss = true

    // Adds npm build to the asset-pipeline path
    from '/dist'
}

This will add the dist/ folder to Assit-pipeline search path for JS and CSS files.

http://www.asset-pipeline.com/manual/#configuration

We need to assure that Vite and RollupJS will make the dist/ folder with named bundled files. In the vite.config.js file assure that the rollupOptions with the output object looks like.

 build: {
    manifest: true, 
    rollupOptions: {
      input: {
        counter: 'src/main/webapp/app-counter/main.jsx', 
      },
      output: {
        // This will generate a `dist` directory containing bundles
        dir: 'dist/',
        entryFileNames: '[name]-bundle.js',
        assetFileNames: '[name]-bundle[extname]',

      }
    },
  },

Notice the “dir” property it specifies the output directory for the build to be “dist/”. Also notice interplay betwen the input property and the “entryFileNames” and “assetFilenames” properties. They specify the bundled file names for bundles created for the “counter” end point. The current setting will create in the dist/ folder the files

  • counter-bundle.js
  • counter-bundle.css

See the RollupJS documentation

We need to edit the views/counter/index.gsp file, so that Assit-pipeline sources the bundles files during production build. Assure that your views/counter/index.gsp looks like

<!doctype html>
<html>
<head>
    <title>Counter</title>
    <asset:link rel="icon" href="favicon.ico" type="image/x-ico" />

    <!-- https://gsp.grails.org/latest/ref/Tags/if.html -->
    <g:if env="production">
        <asset:stylesheet src="counter-bundle.css" />       
    </g:if>
    
</head>
<body>

        <div id="app-counter"></div>
        
        <g:if env="development">
            <!-- For development, use Vite development server to serve the JS files -->
            <!-- https://vitejs.dev/guide/backend-integration.html -->
            <script type="module">
                import RefreshRuntime from 'http://localhost:5173/@react-refresh'
                RefreshRuntime.injectIntoGlobalHook(window)
                window.$RefreshReg$ = () => {}
                window.$RefreshSig$ = () => (type) => type
                window.__vite_plugin_react_preamble_installed__ = true
            </script>
            <!-- Source scripts from vite server -->
            <script type="module" src="http://localhost:5173/@vite/client"></script>
            <script type="module" src="http://localhost:5173/src/main/webapp/app-counter/main.jsx"></script>
        </g:if>

        <g:if env="production">
            <asset:javascript src="counter-bundle.js" />
        </g:if>

</body>
</html>

Notice the tags

    <g:if env="production">
        <asset:stylesheet src="counter-bundle.css" />       
    </g:if>

and

        <g:if env="production">
            <asset:javascript src="counter-bundle.js" />
        </g:if>

They use the “<g:if env=” tag to use the “<asset:” tags during the production build.

We have little more edits in build.gradle file to make. We want to automate the production build so that Vite-RollupJS builds before Assit-pipeline builds. Do this we need to add a Gradle plugin and Gradle Tasks. In build.gradle add just below the buildscript section

plugins {  
    //  gradle-node-plugin only installs using plugins 
    // https://github.com/node-gradle/gradle-node-plugin
  id "com.github.node-gradle.node" version "7.0.2"
}

This tells gradle to use the Node Gradle plugin for this project builds.

https://github.com/node-gradle/gradle-node-plugin

Now we can use the Node Gradle plugin to make NPM tasks. At the bottom of the build.gradle add

// RLP adds for gradle-node-plugin
// https://github.com/node-gradle/gradle-node-plugin
node {
    version = "18.17.1"
    download = true
}

task npmBuild(type: NpmTask, dependsOn: ['npmInstall']) {
    group = 'build'
    description = 'Build frontend for production in /dist folder'
    args = ['run', 'build']
}

task dev(type: NpmTask, dependsOn: ['npmInstall']) {
    group = 'build'
    description = 'Build frontend for development and served by vite'
    args = ['run', 'dev']
}

The node function tells Node Gradle plugin to download Node version compatible with 18.17.1. Then the two task declaration create two NPM tasks, npmBulid and dev. The dev command will run the Vite server during development. The npmBulid will make the bundles for production. We can run these comand using Gradle, by entering

./gradlew div
./gradlew npmBuild

One more edit to build.gradle is needed before the build is automated. The npmBuild task needs to run before the Asset-pipeline compiles. The assetComplie Gradle task runs before any other task in the production build. It prepares the all the assets before assembling a WAR file. At the bottom of build.gradle add

// assetCompile only runs during assemble.
// This dependency insures that react app is in the dist/ folder,
// so that asset-pipeline can find it.
tasks.getByPath("assetCompile").dependsOn(npmBuild)

This tells Gradle to find all the tasks that match “assetCompile” and make them depend on the npmBuild task, i.e. assetCompile task depends on the npmBuild.

For assurance, below is the complete build.gradle file

buildscript {
    repositories {
        maven { url "https://plugins.gradle.org/m2/" }
        maven { url "https://repo.grails.org/grails/core" }

    }
    dependencies {
        classpath "org.grails:grails-gradle-plugin:$grailsGradlePluginVersion"
        classpath "gradle.plugin.com.github.erdi.webdriver-binaries:webdriver-binaries-gradle-plugin:2.6"
        classpath "org.grails.plugins:hibernate5:7.3.0"
        classpath "com.bertramlabs.plugins:asset-pipeline-gradle:3.4.7"
    }
}
plugins {  
    //  gradle-node-plugin only installs using plugins 
    // https://github.com/node-gradle/gradle-node-plugin
  id "com.github.node-gradle.node" version "7.0.2"
}

version ""
group "grailsvitereact"

apply plugin:"eclipse"
apply plugin:"idea"
apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"com.github.erdi.webdriver-binaries"
apply plugin:"org.grails.grails-gsp"
apply plugin:"com.bertramlabs.asset-pipeline"

repositories {
    mavenCentral()
    maven { url "https://repo.grails.org/grails/core" }
}

configurations {
    developmentOnly
    runtimeClasspath {
        extendsFrom developmentOnly
    }
}

dependencies {
    developmentOnly("org.springframework.boot:spring-boot-devtools")
    compileOnly "io.micronaut:micronaut-inject-groovy"
    console "org.grails:grails-console"
    implementation "org.springframework.boot:spring-boot-starter-logging"
    implementation "org.springframework.boot:spring-boot-starter-validation"
    implementation "org.springframework.boot:spring-boot-autoconfigure"
    implementation "org.grails:grails-core"
    implementation "org.springframework.boot:spring-boot-starter-actuator"
    implementation "org.springframework.boot:spring-boot-starter-tomcat"
    implementation "org.grails:grails-web-boot"
    implementation "org.grails:grails-logging"
    implementation "org.grails:grails-plugin-rest"
    implementation "org.grails:grails-plugin-databinding"
    implementation "org.grails:grails-plugin-i18n"
    implementation "org.grails:grails-plugin-services"
    implementation "org.grails:grails-plugin-url-mappings"
    implementation "org.grails:grails-plugin-interceptors"
    implementation "org.grails.plugins:cache"
    implementation "org.grails.plugins:async"
    implementation "org.grails.plugins:scaffolding"
    implementation "org.grails.plugins:hibernate5"
    implementation "org.hibernate:hibernate-core:5.6.11.Final"
    implementation "org.grails.plugins:events"
    implementation "org.grails.plugins:gsp"
    profile "org.grails.profiles:web"
    runtimeOnly "org.glassfish.web:el-impl:2.2.1-b05"
    runtimeOnly "com.h2database:h2"
    runtimeOnly "org.apache.tomcat:tomcat-jdbc"
    runtimeOnly "javax.xml.bind:jaxb-api:2.3.1"
    runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails:3.4.7"
    testImplementation "io.micronaut:micronaut-inject-groovy"
    testImplementation "org.grails:grails-gorm-testing-support"
    testImplementation "org.mockito:mockito-core"
    testImplementation "org.grails:grails-web-testing-support"
    testImplementation "org.grails.plugins:geb"
    testImplementation "org.seleniumhq.selenium:selenium-remote-driver:4.0.0"
    testImplementation "org.seleniumhq.selenium:selenium-api:4.0.0"
    testImplementation "org.seleniumhq.selenium:selenium-support:4.0.0"
    testRuntimeOnly "org.seleniumhq.selenium:selenium-chrome-driver:4.0.0"
    testRuntimeOnly "org.seleniumhq.selenium:selenium-firefox-driver:4.0.0"
}

bootRun {
    ignoreExitValue true
    jvmArgs(
        '-Dspring.output.ansi.enabled=always', 
        '-noverify', 
        '-XX:TieredStopAtLevel=1',
        '-Xmx1024m')
    sourceResources sourceSets.main
    String springProfilesActive = 'spring.profiles.active'
    systemProperty springProfilesActive, System.getProperty(springProfilesActive)
}

tasks.withType(GroovyCompile) {
    configure(groovyOptions) {
        forkOptions.jvmArgs = ['-Xmx1024m']
    }
}

tasks.withType(Test) {
    useJUnitPlatform()
}

webdriverBinaries {
    if (!System.getenv().containsKey('GITHUB_ACTIONS')) {
        chromedriver {
            version = '2.45.0'
            fallbackTo32Bit = true
        }
        geckodriver '0.30.0'
    }
}

tasks.withType(Test) {
    systemProperty "geb.env", System.getProperty('geb.env')
    systemProperty "geb.build.reportsDir", reporting.file("geb/integrationTest")
    if (!System.getenv().containsKey('GITHUB_ACTIONS')) {
        systemProperty 'webdriver.chrome.driver', System.getProperty('webdriver.chrome.driver')
        systemProperty 'webdriver.gecko.driver', System.getProperty('webdriver.gecko.driver')
    } else {
        systemProperty 'webdriver.chrome.driver', "${System.getenv('CHROMEWEBDRIVER')}/chromedriver"
        systemProperty 'webdriver.gecko.driver', "${System.getenv('GECKOWEBDRIVER')}/geckodriver"
    }
}

assets {
    minifyJs = true
    minifyCss = true

    // Adds npm build to the asset-pipeline path
    from '/dist'
}

// RLP adds for gradle-node-plugin
// https://github.com/node-gradle/gradle-node-plugin
node {
    version = "18.17.1"
    download = true
}

task npmBuild(type: NpmTask, dependsOn: ['npmInstall']) {
    group = 'build'
    description = 'Build frontend for production in /dist folder'
    args = ['run', 'build']
}

task dev(type: NpmTask, dependsOn: ['npmInstall']) {
    group = 'build'
    description = 'Build frontend for development and served by vite'
    args = ['run', 'dev']
}

// assetCompile only runs during assemble.
// This dependent insures that react app is in the dist/ folder,
// so that asset-pipeline can find it.
tasks.getByPath("assetCompile").dependsOn(npmBuild)

We can make the WAR file now by entering at the grailsvitereact project root

./gradlew --console="verbose" assemble

Notice the “–console=”verbose””. This will output in the terminal all the task names and the action taken for the tasks during the build. Using this feature is how, I could guess at how to modify the build to include the npmBulid.

You should be able to run the War by entering

java -jar build/libs/grailsvitereact.war

Development

You could reconstruct these step during your development or you can use this project code to initialize your project. If you use this project there are some edits that you probably want to make before developing in earnest.

First, you probably want to change the “rootProjectName” in setting.gradle to something other than “grailsvitereact”.

Second, you probably want to change the default package name to something other than “grailsvitereact”. At the top of the grails-app/conf/application.yml, change “grailsvitereact”

grails:
    profile: web
    codegen:
        defaultPackage: grailsvitereact

If you change the default package name then you deifinitly should change the package name in all the created files and change the containing directory name. They are

  • grails-app/controller/grailsvitereact/CounterController.groovy
  • grails-app/controller/grailsvitereact/UrlMapping.groovy
  • grails-app/init/grailsvitereact/Application.groovy
  • grails-app/init/grailsvitereact/Application.groovy

Note you must change the contain directory names of the file above to the default package name and the package declaration in the files. They all must match.

Test by running bootRun. If you have issues when making new controllers then check the directory and package names.

Note that we did not make any modifications to the files in the counter/src files during integration. So as long as your app does not need to fetch from the server you can develop using only the Vite server. But you will need to copy these files over to the Grails project.

Also note that with this setup the Grails app can serve more than one Vite-React app. To have more than one Vite-React app you should:

  1. Make a new entry point for the Vite-React app by making a new controller and index.gsp for the controller.
  2. Edit the vite.config.js to add the new entry point in the rollupOptions input. Give the new input an appropriate name to use in the production build.
  3. Make a new subfolder in /src/main/webapp/ and start coding

Happy coding