# Building a Superglue App in Rails

Working with React in Rails usually comes with a tradeoff: get access to the JS frontend ecosystem at the cost of losing development velocity or becoming frustrated.

The modern Rails frontend stack, Turbo and Hotwire, is enough to achieve interactive applications without the JS downsides.

However, there are times when we might need React: to build highly interactive applications or to have access to its ecosystem, which is massive.

To solve this issue, let's introduce you to [Superglue](https://github.com/thoughtbot/superglue): a framework for working with React and Redux without slowing down or surrendering our app to an API.

In this article, we will learn **how to build a React app in Rails with Superglue** step by step and learn about the library at the same time.

Let's start by learning what Superglue is:
## What is Superglue?
Superglue helps integrate React and Redux into Rails without losing key Rails features: form helpers, tag helpers, flash messages, cookies, and more.

Besides developing Rails in API-only mode and having a separate React application, we have roughly three ways of integrating React into Rails:

- **Partially:** this implies loading React for specific components but keeping it contained within a Rails view. This means we can manually pass props and keep server-side state for the app shell. However, this approach can quickly become a mess.
- **Using a library:** solutions like `react_on_rails` which are aimed at improving the integration by providing some goodies like prop passing, bundle size optimization, i18n, among others. However, this library requires us to use Webpacker or its continuation, `shakapacker` which makes it less desirable for modern Rails apps.
- **Mixed approach:** this approach implies having our view layer replaced by React components with the difference that they're rendered using Rails controllers, meaning we can pass props, flash messages, cookies, and authenticate/authorize routes in the controllers themselves. Solutions like [Inertia Rails](https://avohq.io/blog/inertia-js-with-rails) and Superglue fall in this category. A clear advantage of this approach is we can keep a lot of what Rails gives us while also enjoying the advantages of building views with React.

Spiritually, Superglue and Inertia are very similar, but they differ in the way they work. While Inertia passes the props directly from the controller, Superglue gets them from a `.json.props` file that matches the name of the controller action.

The other difference is that Inertia is framework-agnostic while Superglue uses React with Redux, which seems a reasonable alternative considering the popularity of both libraries.
## How does it work?
Superglue bridges our Rails application with React views without the need to create an explicit API by leveraging view files that define JSON, using PropsTemplate, a JSON templating DSL also created by Thoughtbot.

React components are used to render views, and they get the data from the JSON templates we define in `.props.json` files. Then, we access that data using the `useContent` hook, which, behind the scenes, uses Redux to get that data for us.

![How Superglue works](https://i.postimg.cc/YS7p9RPN/how-superglue-works.webp)

The main feature behind PropsTemplate is that the specified JSON can be traversed by the library when we specify a specific node, giving us the ability to produce partial updates without the need to evaluate the whole file.

Rails views are replaced with React page components, which are nothing more than components that represent an entire view. The good thing is that we don't have to worry about API calls or passing props manually to these components because Superglue takes care of that for us.

The same happens with cookies and routing: because React is mounted after the initial view is rendered, we can perform authentication, authorization, and any other action to make sure our app is secure without having to rely on React.

Moreover, Superglue uses UJS to produce an SPA-like navigation experience and to produce partial page reloads without the need to write any JavaScript at all: we just define a link with an appropriate data attribute and `href` value.

## What we will build
For this tutorial, we will build a fictional application called Pomoflow: a project management app that helps freelancers keep track of work done for different projects.

The application will include a public-facing part consisting of a home page, a login page, and a simple blog to demonstrate how SSR works with Inertia.

It will also have an example dashboard with a global search and a projects field with pagination and a form using Superglue.

The result should look something like this:

{{gumlet video_id="687c4e040e8590900e17898d" width="1740" height="830" /}}
## Application setup
Let's start by creating a new Rails application:

```shell
rails new pomoflow --skip-javascript
```

Now, we add and install the `vite_rails` gem:

```shell
bundle add vite_rails && bundle install
```

Next, we install Vite specifically:

```shell
bundle exec vite install
```

This command does the following:

- Add a `bin/vite` executable to start the dev server.
- Installs `vite` and the `vite-plugin-ruby` library.
- Add `vite.config.ts` and `config/vite.json` configuration files.
- Creates an `application.js` entrypoint for the application that's located at `app/frontend/entrypoints`.
- Adds the `bin/vite dev` command to the `Procfile.dev` file.

Now, to test that everything's working as expected, set a test route as the `root`, run `bin/dev`, and visit the page.

It should be empty, but if we check the console, we should see the following:

![Successful Vite install in Rails](https://i.postimg.cc/267LGL86/successful-vite-install-rails.webp)

Now that we have Vite installed, let's install Superglue:
### Installing Superglue
The first step is to add the gem to the Gemfile and install it:

```shell
bundle add superglue && bundle install
```

Next, we run the installation command. To use TypeScript, we have to pass the `--typescript` flag:

```shell
rails g superglue:install # or pass the --typescript flag
```

This command adds many files inside the `app/javascript` directory:

- An `application.jsx`, the core of the React application, responsible for rendering the components for each one of the views we define in our application.
- An `application_visit.js` file: responsible for making UJS visits.
- A `store.js` file: a Redux store created using [Redux Toolkit](https://redux-toolkit.js.org/api/configureStore). It acts as a container for the application state. In this file, we define reducers, which are functions that receive the current application state and an action as an argument and return a new state. In the context of a Superglue application, they allow us to modify global state like the flash object, a shopping cart, notifications, etc.
- A `page_to_page_mapping.js` file: it's used to map our props template to the component. We have to modify this file every time we add a page-level component. However, we can automatically populate it if our build tool supports globbing. Vite does, so we won't have an issue with that.
- A `components` directory: we're supposed to add the individual components that make up our application's UI here. It includes an `Input.jsx` component for commonly used inputs and a `Layout.jsx` component that wraps our application.
- A `slices` directory: in Redux Toolkit, a slice is a collection of reducer logic and actions for a given feature in our app. Superglue includes a `flash.js` slice, which is responsible for populating and clearing the flash. We will add some custom slices to see how they work later.

Now, because we're using Vite, which uses the `frontend` directory, let's move the Superglue-added files into it:

```shell
mv app/javascript/* app/frontend
```

The next step is to rename `entrypoints/application.js` to `application.jsx` and copy the contents of the `application.jsx` from Superglue while updating the imports:

```javascript
// app/frontend/entrypoints/application.jsx
import React from "react"
import { createRoot } from "react-dom/client"
import { Application } from "@thoughtbot/superglue"
import { buildVisitAndRemote } from "../application_visit"
import { pageIdentifierToPageComponent } from "../page_to_page_mapping"
import { store } from "../store"

// Rest of the code
```

Now, we have to make sure to pass `application.jsx` to the `vite_javascript_tag` in the application layout:

```erb
<%= vite_javascript_tag 'application.jsx' %>
```

Next, let's add a sample `greet` resource to show how to integrate everything:

```ruby
resource :greet, only: [:show]
```

Then, we add a `GreetsController` and make sure we have the following in there:

```ruby
class GreetsController < ApplicationController
  before_action :use_jsx_rendering_defaults

  def show
  end
end
```

Now, we create the `greets` directory, and within it, we add a `show.json.props` file where we will define the props that our React component will receive, it essentially uses the Jbuilder syntax:

```ruby
# app/views/greet/show.json.props
json.body do
  json.greet "Hello, World!"
end

json.footer "Making Rails developers genuinely happy since 2020"
```

Then, we add our React page component inside the directory:

```javascript
// app/views/greet/show.jsx
import React from 'react'
import { useContent } from '@thoughtbot/superglue';

export default function GreetsShow() {
  const {
    body,
    footer
  } = useContent()

  const { greet } = body

  return (
    <>
      <h1>{greet}</h1>
      <span>{footer}</span>
    </>
  )
}
```

Note that we're getting the props with the `useContent` function from Superglue. The component itself doesn't need to receive props in the traditional React way.

Now, if we run our server with `bin/dev` and navigate to the greet route, we will see a blank page and the following error: `Uncaught Error: Superglue Nav component was looking for greets/show but could not find it in your mapping.`

This is because we have to manually add the mapping to the `page_to_page_mapping.js` file:

```javascript
// app/frontend/page_to_page_mapping.js
import GreetsShow from "../views/greet/show"

const pageIdentifierToPageComponent = {
  'greet/show': GreetsShow
}
export { pageIdentifierToPageComponent }
```

Now we can actually navigate to `/greet` and see the following:

![Hello world with Superglue](https://i.postimg.cc/Hkf7nNgm/hello-world-with-superglue.webp)

You might have thought that manually adding the component mapping doesn't sound too fun, so let's add the following to avoid having to add them like that:

```javascript
const pageIdentifierToPageComponent = {}
const pages = import.meta.glob("../views/**/*.jsx", { eager: true })

for (const key in pages) {
  if (pages.hasOwnProperty(key)) {
    const identifier = key.replace("../views/", "").split('.')[0];
    pageIdentifierToPageComponent[identifier] = pages[key].default;
  }
}

export { pageIdentifierToPageComponent }
```

Now, let's install Tailwind to make things look nice as we code our app:

{{warning}}
If you want to dig deep into state management with Superglue, read the documentation on [Redux fundamentals](https://redux.js.org/tutorials/fundamentals/part-1-overview) and check the [Select your own seat demo application](https://github.com/thoughtbot/select-your-own-seat-superglue) to see how to implement the feature.
{{/warning}}
### Adding Tailwind
The first step is to add Tailwind and its Vite plugin as dev dependencies:

```shell
yarn add -D tailwindcss @tailwindcss/vite
```

Then, let's add the `@tailwindcss/vite` plugin to our Vite config:

```javascript
// vite.config.ts
import { defineConfig } from 'vite'
import RubyPlugin from 'vite-plugin-ruby'
import path from "path"
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [
    RubyPlugin(),
    tailwindcss(),
  ],
})
```

Next, we have to create an `application.css` file in the `entrypoints` directory and then a separate `application.tailwind.css` within a newly created `stylesheets` directory:

```shell
touch app/frontend/entrypoints/application.css && mkdir app/frontend/stylesheets && touch app/frontend/stylesheets/application.tailwind.css
```

Then, let's add the following to the `application.tailwind.css` file:

```css
@import "tailwindcss";

@source "../../../app/views/**/*.jsx";
@source "../../../app/views/**/*.html.erb";
```

And, in the `application.css` entrypoint, let's import this:

```css
@import "../stylesheets/application.tailwind.css";
```

Finally, let's include the stylesheet in our application layout:

```erb
<%= vite_stylesheet_tag 'application' %>
```

Now, without changing anything, we should see the following when we visit `/greet`:

![Superglue with Vite and Tailwind](https://i.postimg.cc/jdTTxQ91/superglue-with-vite-and-tailwind-hello-world.webp)
## Building the application
Now that we have everything set, we can start working on our application. 

For practical reasons, we won't include everything in the form of code blocks because a lot isn't directly related to Superglue, but you can [check the repository](https://github.com/erozas/time-tracker-superglue/) if you want to see how everything was implemented.

Let's start by seeing how the flash messages work and how to abstract them to a component:
### Flash messages
Like we learned, Superglue adds a `<script>` tag to the body of the rendered page that contains information about the page component it should mount, the CSRF token, the props we're passing to it, and in the `slices` key of the initial state object, we have the `flash` object:

![Superglue injected initial page staste script tag](https://i.postimg.cc/YSNQxYcG/superglue-injected-script-tag.webp)

This is all mounted into the Redux store in order for that state to be accessible globally, which means that any React component can access it using the `useSelector` hook from Redux:

```javascript
import { useSelector } from "react-redux";

export default function Test() {
  const flash = useSelector((state) => state.flash)

  // Rest of the component
}
```

Superglue adds a rudimentary flash message feedback to the `Layout` component, but it doesn't have styling, and it feels a bit clunky to leave it there, so let's extract everything to a `Flash` component.

As we're using Tailwind, let's add the [tailwind-variants](https://www.tailwind-variants.org/) library to handle the different types of flash messages our application will have.

```shell
yarn add tailwind-variants
```

Now, let's add Lucide for the flash message icons. You can use any other library or SVG icon if you like:

```shell
yarn add lucide-react
```

Now, let's create a `Flash.jsx` component in our `components` directory:

```javascript
// app/frontend/components/Flash.jsx
import React from 'react'
import { tv } from 'tailwind-variants';
import { useSelector } from 'react-redux'
import { CircleAlert, TriangleAlert, Info } from 'lucide-react'

export default function Flash() {
  const flash = useSelector((state) => state.flash)
  const key = Object.keys(flash)[0]

  const iconMap = {
    alert: <CircleAlert />,
    notice: <Info />,
    warning: <TriangleAlert />
  }

  const flashStyles = tv({
    slots: {
      base: "w-full flex items-center space-x-2 justify-center p-4 rounded-md bg-slate-200 border border-slate-300",
      iconContainer: "text-slate-700",
      message: "text-base text-slate-700"
    },
    variants: {
      type: {
        alert: {
          base: "bg-rose-100 border-rose-200",
          iconContainer: "text-rose-800",
          message: "text-rose-800"
        },
        notice: {
          base: "bg-blue-100 border-blue-200",
          iconContainer: "text-blue-800",
          message: "text-blue-800"
        },
        warning: {
          base: "bg-yellow-100 border-yellow-200",
          iconContainer: "text-yellow-700",
          message: "text-yellow-700"
        }
      }
    }
  })

  const { base, iconContainer, message } = flashStyles({ type: key })

  if (!flash[key]) return null

  return (
    <aside className={base()}>
      <div className={iconContainer()}>
        {iconMap[key]}
      </div>
      <p className={message()}>{flash[key]}</p>
    </aside>
  )
}
```

Notice that if we set a `flash[:notice]` in our controller action, Superglue doesn't return an object like `{type: "notice", message: "Your account was created successfully"}` it just returns an object with a single key that depends on the flash type, like `{notice: "Your account was created successfully"}`.

We use that key for our component variants, and we add an `iconMap` object to return the appropriate icon.

We then add our `<Flash/>` component to the layout and set a test flash message:

```ruby
class GreetsController < ApplicationController
  def show
    flash[:notice] = "Hello! This is a test notice"
  end
end
```

And we should see the following result:

![Flash messages with Superglue](https://i.postimg.cc/TPFgfx8x/flash-messages-with-superglue.webp)


{{further_reading slug="component-variants-with-the-class-variants-gem" /}}

Now that we've added our first functionality, let's understand how Superglue uses UJS (Unobtrusive JavaScript) to improve user experience with an SPA-like experience:
## Navigation with UJS
Out of the box, Superglue acts like a multipage application: Rails handles the routing, and controllers end up rendering a layout that has the JSON for each response, which is picked up by Superglue and loaded into the view within a `<script>` tag.

However, we can use Superglue to generate visits without producing a full-page reload using the `data-sg-remote` and `data-sg-visit` data attributes in links.
### The data-sg-visit
We can use links with this data attribute whenever we want to navigate to a specific page while updating the address bar without reloading:

```jsx
<a href="/blog" data-sg-visit />
```

These links are well suited for SPA-like navigation. We can add them to any part of our application to give users the impression of an instantaneous visit.

Superglue intercepts the link visit, makes a request to the endpoint, swaps the page component for the new one, and passes the payload to it.

Navigating with links that use the `data-sg-visit` attribute looks like this:

{{gumlet video_id="687de24e0e8590900e23fabf" width="1730" height="840" /}}
### Content refresh with data-sg-remote
On top of SPA-like navigation, we can also add the `data-sg-remote` attribute to links to perform requests that only reload a part of the page. To achieve this, we have to use prop digging using the `props_at` parameter in our link's href.

Let's build a refreshable stat card in our dashboard view that fetches the content from `/dashboard` but only focuses on the part that we want to refresh.

The first step is to add the stats to the `dashboard/index.json.props` partial:

```ruby
path = param_to_dig_path(params[:props_at])

json.data(dig: path) do
  json.stats do
    json.total_hours_this_month rand(100)
    json.total_earnings_this_month 4050
    json.active_projects 3
    json.average_hourly_rate 65
  end
end
```

Now, for our dashboard view at `app/views/dashboard/index.jsx` we have the following:

```jsx
// imports

export default function DashboardIndex() {
  const user = useSelector((state) => state.user)
  const data = useContent().data
  const stats = data.stats

  return (
    <AppLayout>
      <div className="py-6 px-6">
        <div className="mb-8">
          <h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
          <p className="text-gray-600">Welcome back, {user.username}. Here's your freelance activity overview.</p>
        </div>

        <div className="grid grid-cols-12 w-full gap-x-6 gap-y-6 mb-8">
          <div className="col-span-12 md:col-span-6 lg:col-span-3">
            <StatsCard
              title="Hours This Month"
              value={stats.total_hours_this_month}
              refreshPath="/dashboard?props_at=data.data.stats"
              subtitle="vs 148 last month"
              refresh={true}
              icon={<Clock />}
              color="blue"
            />
          </div>
        </div>
      </div>
    </AppLayout>
  )
}
```

Then, in our `StatsCard` component, we include a link that looks like this:

```jsx
<a href={refreshPath} className="ml-12 flex items-center space-x-2" data-sg-remote>
  <RefreshCcw className="w-4 h-4 text-gray-500" />
  <span className="text-sm font-medium text-gray-500">Refresh</span>
</a>
```

Because the `dashboard.json` response looks like this:

```json
{
  "data": {
    "data": {
      "stats": {
        "total_hours_this_month": 74,
        "total_earnings_this_month": 4050,
        "active_projects": 3,
        "average_hourly_rate": 65
      }
    }
  }
}
```

We pass the `dashboard?props_at=data.data.stats` so it only fetches that part of the response. Right now it might not seem worth it, but as we add features to the dashboard it will eventually reach a point where it makes sense to partially fetch a part of the page.

The result looks like this:

{{gumlet video_id="68803ba5d0677e986aab164f" width="1742" height="348" /}}

## Pagination
To add pagination to a Superglue app, we need to pass the pagination links we want through the `.json.props` file.

Let's add simple pagination to our project index page using the Pagy gem:

```shell
bundle add pagy && bundle install
```

Then, we need to include the `Pagy::Backend` concern in our application controller:

```ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Pagy::Backend
end
```

And the `Pagy::Frontend` in the application helper:

```ruby
module ApplicationHelper
  include Pagy::Frontend
end
```

And, require the Pagy extras in a `pagy.rb` initializer:

```ruby
# config/initializers/pagy.rb
require 'pagy/extras/pagy'
```

Next, we add the pagination links in the `index.json.props`:

```ruby
json.pagination do
  json.prevPage pagy_prev_url(@pagy)
  json.nextPage pagy_next_url(@pagy)
end
```

Now, let's create a `Pagination` component to avoid repeating logic in views:

```jsx
import React from 'react'

const Pagination = ({ pagination, className = '' }) => {
  return (
    <nav className={`flex items-center space-x-4 ${className}`}>
      {pagination.prevPage && (
        <a href={pagination.prevPage} data-sg-visit className="text-indigo-700 font-semibold">Previous</a>
      )}
      {pagination.nextPage && (
        <a href={pagination.nextPage} data-sg-visit className="text-indigo-700 font-semibold">Next</a>
      )}
    </nav>
  )
}

export default Pagination
```

Nothing fancy for the component; we're just using the `data-sg-visit` attribute to avoid a page reload when paginating our content.

This should result in the following experience:

{{gumlet video_id="687077c417a4938d2309c45e" width="1920" height="848" /}}

{{info}}
If we need more complex pagination, we just need to design the needed links, add them into the props file and handle them in the `Pagination` component.
{{/info}}
## Working with forms
One of the best things about Superglue is that we can make use of Rails form helpers to create forms. There's no need to reinvent the wheel and solve things like CSRF protection.

We mentioned before that installing Superglue added an `Inputs.jsx` file with many of the most common inputs. Those are unstyled inputs that we can use together with the [form_props](https://github.com/thoughtbot/form_props) gem, which is a dependency for Superglue, to generate functioning forms in our apps.

Let's add a `Project` model and a form to create and edit projects using Superglue:

```shell
bin/rails generate model Project name description:text && bin/rails db:migrate
```

Let's add the routes:

```ruby
# config/routes.rb
resources :projects
```

And controller:

```ruby
class ProjectsController < ApplicationController
  before_action :use_jsx_rendering_defaults

  def new
  end

  def create
    @project = Project.new(project_params)

    if @project.save!
      redirect_to projects_path, notice: "Project created successfully"
    else
      flash.now[:alert] = "There was an error creating your project"
      render :new
    end
  end

  private

  def project_params
    params.expect(project: [:name, :description])
  end
end
```

Let's create the `index.json.props` file so we can pass the list of projects to the React component:

```ruby
# app/views/projects/index.json.props
json.projects do
  json.array! @projects do |project|
    json.name project.name
    json.description project.description
    json.url project_path(project)
  end
end
```

Now, let's add the React page component responsible for rendering the list of projects:

```jsx
// app/views/projects/index.jsx
import React from 'react'
import { useContent } from '@thoughtbot/superglue'
import { Layout } from '@components/layouts/Layout'
import Container from '@components/Container'

export default function ProjectsIndex() {
  const { projects } = useContent()

  return (
    <Layout>
      <Container>
        <div className="my-16">
          <h1 className="text-2xl font-bold mb-4">Projects</h1>
          <ul className="space-y-4">
            {projects.map((project) => (
              <li key={project.id} className="border border-gray-300 rounded-lg p-4">
                <a href={project.url} className="text-lg font-semibold">{project.name}</a>
                <p className="text-sm text-slate-500">{project.description}</p>
              </li>
            ))}
          </ul>
        </div>
      </Container>
    </Layout>
  )
}
```

After creating a couple of test projects from the console, our `/projects` view should look like this:

![Project index view using Superglue](https://i.postimg.cc/fbZKtXBL/project-list-view-with-superglue.webp)

Notice that we're lifting the `projects` variable from the Redux state with the `useContent` function provided by Superglue, which behind the scenes is using Redux's `useSelector`.

Now that we have a list of projects to redirect after creating a project, let's learn how to add `form_props` to generate the inputs that we will pass to our React component.

Even though it's not explained explicitly in the documentation, the `form_props` gem is added as a dependency.

The basic task of the gem is to generate forms with inputs as JSON using Rails form helpers. Let's add a form to the `new.json.props` file:

```ruby
# app/views/projects/new.json.props
json.new_project_form do
  form_props(model: @project) do |f|
    f.text_field :name
    f.text_field :description
    f.submit
  end
end
```

If we visit `/projects/new.json` we will see the following output:

![Superglue form props as JSON](https://i.postimg.cc/RVtVsbzW/superglue-form-props-json.webp)

{{warning}}
Note that we are passing the project as a named argument `model: @project` even though the documentation states that we can pass it as a regular argument like: `form_props(@project)` which won't work. We can also pass the `url` or a `scope`.
{{/warning}}

As you can see, the `form_props` helper generated our desired form as JSON. Now, let's see how we can go from this to a functioning form.

Let's start by creating a page component for the new project view:

```jsx
// app/views/projects/new.jsx
import React from 'react'
import { useContent } from '@thoughtbot/superglue'

import { Layout } from '@components/layouts/Layout'
import { Form, FieldBase, SubmitButton } from "@components/Inputs"
import Container from '@components/Container'

export default function ProjectsNew() {
  const { form, extras, inputs } = useContent().new_project_form

  return (
    <Layout>
      <Container>
        <div className="max-w-xl mx-auto border border-gray-300 rounded-lg p-8 my-16">
          <h1 className="text-2xl font-bold mb-4">New Project</h1>
          <Form {...form} extras={extras} className="flex flex-col">
            <div className="flex flex-col space-y-2">
              <FieldBase {...inputs.name} label="Name" className="px-3 py-2 border border-gray-300 rounded-lg" />
              <FieldBase {...inputs.description} label="Description" className="px-3 py-2 border border-gray-300 rounded-lg" />
            </div>
            <SubmitButton text="Create Project" className="bg-indigo-700 text-white font-semibold mt-6 py-2 px-6 rounded-full" />
          </Form>
        </div>
      </Container>
    </Layout>
  )
}
```

We're getting the `form`, `extras`, and `inputs` objects from the `new_project_form` we defined in the `news.json.props`. These fields represent:

- The `form` has the request method, action, and charset.
- The `extras` include the CSRF token and a `method` object, which is responsible for adding a hidden input that is required when the request is not a `GET` or `POST`.
- The `inputs` describe our desired inputs in JSON format.

Then, we import the `Form`, `FieldBase`, and `SubmitButton` from the inputs that Superglue defined for us.

Next, we pass those objects to the input components and, by adding some styling with Tailwind, we obtain the following form:

![Superglue form using helpers](https://i.postimg.cc/fR223s8F/new-project-form-superglue.webp)

With Superglue and `form_props`, we can use our typical Rails form helpers in combination with the inputs to produce working forms. 

Now, we can create a new project with this form and the controller action we've already defined without having to do anything else:

{{gumlet video_id="686b6b21fc386bd77fea85ad" width="1632" height="570" /}}

Now, let's see how we can deal with validation issues. Let's add a validation to the `Project` model:

```ruby
class Project < ApplicationRecord
  validates :name, presence: true
end
```

Now, if we submit the form without adding the name of the project, because we're using the `Flash` component in the layout, we will see the alert message:

![Flash alert in a form Superglue](https://i.postimg.cc/Zqfc8Z8G/superglue-flash-errors.webp)

If we inspect the `Inputs.jsx`, we will see that the `FieldBase` input includes a label, an input, and a `FieldError` which is a `span` that contains the error message.

To pass error messages to the form, we need to add them to the `new.json.props`:

```ruby
json.new_project_form do
  form_props(model: @project) do |f|
    f.text_field :name
    f.text_area :description
    f.submit
  end

  json.errors @project.errors.to_hash(true)
end
```

Then, we pick the errors from the `useContent` helper and pass them to the `Form` and pass the `errorKey` to the field:

```jsx
// Imports
export default function ProjectsNew() {
  const { form, extras, inputs, errors } = useContent().new_project_form

  return (
    <Layout>
      {/* Rest of the code */}
      <Form {...form} extras={extras} validationErrors={errors}>
        <FieldBase {...inputs.name} errorKey="name" label="Name" />
        <TextArea {...inputs.description} placeholder="Describe the project in a few words" label="Description" />
      </Form>
    </Layout>
  )
}
```

Now, after doing this and styling the `FieldError`, if we submit the form without a project name, we should see the following:

![Superglue form with inline errors](https://i.postimg.cc/fTK0tCjF/inline-form-errors-superglue.webp)

We can further customize the way validation errors are displayed. If we wanted, we could even add client-side validations, but that's not necessary for now.

{{info}}
To further work with forms in Superglue, we can extract and extend the individual components so we can style them as we see fit or customize the way they work. The components are defined in the [candy_wrapper](https://github.com/thoughtbot/candy_wrapper) library. They include an example of extending it using Mantine.
{{/info}}
## User authentication
Now that we know how to work with forms, let's add user authentication with the [Rails auth generator](https://avohq.io/blog/rails-8-authentication). If you're not familiar with the feature, check the tutorial so you're not lost.

To achieve it, we run the generator just like we did in the tutorial:

```shell
bin/rails generate authentication
```

This adds views for user login and password recovery flows. For the sake of brevity, we will just work with the new session page.

Let's start by defining the login form in the `sessions/new.json.props`:

```ruby
json.login_form do
  form_props(url: session_path) do |f|
    f.text_field :email_address
    f.password_field :password
    f.submit
  end
end
```

Then, let's replace the `new.html.erb` with a React component:

```jsx
// app/views/sessions/new.jsx
import React from 'react'
import { useContent } from "@thoughtbot/superglue"
import { Layout } from '@components/layouts/Layout'
import Container from '@components/Container'
import { Form, FieldBase, SubmitButton } from '@components/Inputs'


export default function SessionsNew() {
  const { form, extras, inputs } = useContent().login_form

  return (
    <Layout>
      <Container>
        <div className="max-w-lg mx-auto border border-gray-300 rounded-lg p-8 mt-16 mb-24">
          <h1 className="text-2xl font-bold">Sign in</h1>
          <p className="text-base text-slate-500 mb-4">
            Enter your email address and password to sign in.
          </p>
          <Form {...form} extras={extras} className="flex flex-col">
            <div className="flex flex-col space-y-2">
              <FieldBase {...inputs.emailAddress} label="Email address" className="px-3 py-2 border border-gray-300 rounded-lg" />
              <FieldBase {...inputs.password} label="Password" className="px-3 py-2 border border-gray-300 rounded-lg" />
              <SubmitButton text="Sign in" className="bg-indigo-700 text-white font-semibold mt-6 py-2 px-6 rounded-full" />
            </div>
          </Form>
        </div>
      </Container>
    </Layout>
  )
}
```

This results in the following login view:

![Sign in view with Superglue](https://i.postimg.cc/28t6stz4/sign-in-view-with-superglue.webp)

Then, let's modify the Rails-generated `create` action to include feedback and redirect the user to a dashboard if the login attempt is successful:

```ruby
class SessionsController < ApplicationsController
  before_action :use_jsx_rendering_defaults
  allow_unauthenticated_access only: %i[ new create ]

  def new
  end
  
  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to dashboard_index_path, notice: "Signed in successfully"
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

  # Rest of the code
end
```

Now, without doing anything else, we can actually sign in to our application and be redirected to the dashboard view:

{{gumlet video_id="686c9e4dd97356cc848fd576" width="1900" height="850" /}}
### Global user access
Because Superglue page components are mounted after the page is rendered by Rails we can protect routes like we normally do.

In the case of the auth generator, routes are protected by default. If we wanted to access the dashboard page, which is protected, without being authenticated, we would get redirected to the `session/new` page.

This is great because we don't have to deal with auth logic on the JS side of things. But, we need a way for our React components to know about the user: if it's authenticated or not and some information like a username or an ID in case we need that to make user-related requests.

We could theoretically pass the user to every page or component that requires it, but it would be impractical.

To solve this, we can add information about the user to the global `application.json.props` and the information would be included in every request:

```ruby
# app/views/layouts/application.json.props
json.data(dig: path) do
  json.user partial: ['users/user', fragment: true] do
  end
  
  yield json
end
```

Then, we define the user partial:

```ruby
# app/views/users/_user.json.props
json.id current_user.try(:id)
json.is_authenticated current_user.present?
json.email_address current_user.try(:email_address)
json.username current_user.try(:username)
json.slug current_user.try(:slug)

json.sign_out_form do
  form_props(url: session_path, method: :delete) do |f|
    f.hidden_field :authenticity_token
    f.submit
  end
end
```

If we go to the `session/new.json` path while not logged in, we should see the following:

![Global user props](https://i.postimg.cc/T1jhXF9G/global-user-props.webp)

With this, we can add a condition to the `Navbar` or any component that requires knowing the status of the user.

Let's conditionally show the **Sign Out** button if the user is logged in:

```jsx
import React from 'react'
import Container from '@components/Container'
import Logo from '@components/Logo'
import { useContent } from '@thoughtbot/superglue'
import { Form, SubmitButton } from '@components/Inputs'

export default function Navbar() {
  const { user } = useContent()
  const signedIn = user?.is_authenticated

  return (
    <nav className="bg-indigo-950">
      <Container>
        <ul className="flex items-center justify-between gap-4 py-4 px-4 md:px-6">
          <li>
            <Logo />
          </li>
          <div>
            {signedIn ? (
              <li>
                <a href="/session" data-method="delete">Sign out</a>
              </li>
            ) : (
              <li>
                <a href="/session/new" data-sg-visit>Sign in</a>
              </li>
            )}
          </div>
        </ul>
      </Container>
    </nav >
  )
}
```

Now, if we're signed in, we will see a **Sign Out** button, and if we're not, we will see a link to `session/new`:

![Sign out link with Superglue](https://i.postimg.cc/NFvLgrGm/sign-out-link-superglue.webp)

Later we will learn how to achieve the same thing using a custom Redux slice.
### Signing out
In Rails, we can get away with the feature by simply using a link that is intercepted and processed as a DELETE request by our application. With Superglue, we cannot do that, so we have to define sign out as a form with a submit button.

So, the link to `/session` with `data-method=delete` won't work: it will make at a GET request instead of the POST request with the hidden input that Rails is expecting.

You might have noticed that I added a `sign_out_form` to the user partial in the previous section, we will use that to generate the sign-out form in the `Navbar` component:

```jsx
// imports
export default function Navbar() {
  const user = useSelector((state) => state.user)
  const signedIn = user.is_authenticated
  const { form, extras } = user.sign_out_form

  return(
    <nav>
      <Container>
        <ul>
          {/* Rest of the code */}
          {signedIn ? (
            <Form {...form} extras={extras} className="flex flex-col">
              <SubmitButton text="Sign out" className="bg-indigo-700 text-white font-semibold py-1.5 px-4 rounded-full" />
            </Form>
          ) : (
            <li>
              <a href="/session/new" data-sg-visit>Sign in</a>
            </li>
          )}
        </ul>
      </Container>
    </nav>
  )
}
```

Which produces the following result:

{{gumlet video_id="686db2a49228ad62b8d86223" width="1800" height="852" /}}
## Custom slice with Redux
We added global state by passing the `user` partial to the `application.json.props`. However, we can also handle global state using a Redux slice, just like Superglue does by default with the flash.

This type of state management is especially useful when we need to sync state between the client and the server. A shopping cart is a good example of this. If the user adds an item to the cart, we update the state in order for it to contain the new item that was added to the cart, update the total and subtotal amounts and update the `items_count`.

As the application we're building is oriented to project management, let's add a pomodoro counter to our navbar to show how many 45 minute work-stints we've achieved today:

![Superglue custom Redux slice](https://i.postimg.cc/BnC8yykd/pomodoro-navbar.webp)

Ideally, this feature would involve a countdown clock that would update the counter after completing the pomodoro, but we will keep it simple by incrementing the pomodoro count with a button.

Let's start by adding the new slice:

```javascript
// app/frontend/slices/pomodoro.js
import { createSlice } from "@reduxjs/toolkit"
import { saveResponse, beforeVisit } from "@thoughtbot/superglue"

const initialState = {
  completedPomodoros: 0
}

export const pomodoroSlice = createSlice({
  name: "pomodoro",
  initialState: initialState,
  reducers: {
    increment(state) {
      state.completedPomodoros += 1
    },

    setCount(state, { payload }) {
      state.completedPomodoros = payload
    },

    resetCount(state) {
      state.completedPomodoros = 0
    }
  },
  extraReducers: builder => {
    builder.addCase(beforeVisit, (state, _action) => {
      return state
    })
    builder.addCase(saveResponse, (state, action) => {
      const { page } = action.payload

      if (page.slices.pomodoro) {
        return {
          ...state,
          ...page.slices.pomodoro
        }
      }

      return state
    })
  }
})

export const {
  increment,
  setCount,
  resetCount
} = pomodoroSlice.actions

export const selectPomodoroCount = (state) => state.pomodoro.completedPomodoros
export default pomodoroSlice.reducer
```

Then, we include the slice in the `store.js` file:

```javascript
// Rest of the imports
import { pomodoroSlice } from "./slices/pomodoro"
export const store = configureStore({
  devTools: process.env.NODE_ENV !== "production",
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [beforeFetch.type, beforeVisit.type, beforeRemote.type]
      }
    }),
  reducer: {
    superglue,
    pages,
    flash: flashSlice.reducer,
    user: userSlice.reducer,
    pomodoro: pomodoroSlice.reducer
  }
})
```

Now, I can fetch the completed pomodoro count from the store inside the navbar:

```jsx
// imports
const AppNavbar = () => {
  const dispatch = useDispatch()
  const completedPomodoros = useSelector(selectPomodoroCount)

  // Rest of the code

  const incrementPomodoro = () => {
    dispatch(increment())
  }

  return (
    <div className="w-full bg-white px-8 py-4 border-b border-zinc-200">
      <div className="w-full items-center justify-between flex">
        <div className="flex items-center gap-2 cursor-pointer" onClick={incrementPomodoro}>
          <img src={Pomodoro} alt="Pomodoro" className="w-8 h-8 fill-red-600" />
          <span className="text-indigo-950">{completedPomodoros} completed today</span>
        </div>
      </div>
    </div>
  )
}
```

After defining the slice and including it in the store, we can access the `selectPomodoroCount` action using the `useSelector` hook from Redux.

Now, with the current setup we have where we're accessing the pomodoro count using the `completedPomodoros` variable, we get the following behavior:

{{gumlet video_id="687eea70d1ad82bfd42113e2" width="1730" height="840" /}}

Because we're using the `data-sg-visit` for links in the sidebar, the Redux store is persisted through requests and we don't lose the current count.

This setup is simple, essentially a counter, and probably not very useful, but we can use the same principle to build something that better fits our needs.
## Search feature
With Superglue, we can build a search feature by leveraging the Superglue `remote` requests.

To make it work, let's start by adding the search functionality globally to our `application.json.props` template:

```ruby
path = request.format.json? ? param_to_dig_path(params[:props_at]) : nil

json.data(dig: path) do
  yield json
  json.search do
    json.results do
      json.array! Project.search(params[:q]) do |project|
        json.id project.id
        json.name project.name
        json.description project.description
        json.url project_path(project)
      end
    end
  end
end

## Rest of the template
```

We're adding a `search` key that returns an array of `results`. To make things simple we're just searching on the `Project` model but we could do a global search as well.

Then, let's create a `<SearchBar/>` component that receives a `results` array and an `onChange` function that we will pass the a text field input. If the component receives results, it displays them in an absolutely positioned `<div>` below the input.

```jsx
import React from 'react'
import { Search, X } from 'lucide-react'

export default function SearchBar({ onChange, results }) {

  return (
    <div className="w-1/3 border border-neutral-300 rounded-full relative focus:outline-none">
      <input type="text" className="w-full pl-8 py-1.5 focus:outline-none" placeholder="Search projects" onChange={onChange} />
      <Search className="w-5 h-5 absolute left-2 top-1/2 -translate-y-1/2 text-zinc-400" />
      {results.length > 0 && (
        <div className="w-full absolute top-12 rounded-lg bg-white px-4 py-4 border border-zinc-200 z-10 shadow-lg">
          <h4 className="text-base font-semibold text-gray-900">Results</h4>
          <ul>
            {results.map((result) => (
              <li className="text-gray-700" key={result.id}>
                <a href={result.url} className="hover:underline">{result.name}</a>
              </li>
            ))}
          </ul>
        </div>
      )}
      {results.length > 0 && (
        <div className="absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer">
          <X className="w-5 h-5 text-zinc-400" onClick={onChange} />
        </div>
      )}
    </div>
  )
}
```

Then, let's add the search component to the same `<AppNavbar/>` component we defined in the previous section:

```jsx
import React, { useContext, useRef } from 'react'
import { NavigationContext, useContent } from '@thoughtbot/superglue'
import SearchBar from '../SearchBar'
import { debounce } from '../../utils'

const AppNavbar = () => {
  const { remote } = useContext(NavigationContext)
  const currentPageKey = useSelector(state => state.superglue.currentPageKey)
  const searchResults = useContent().search?.results || []

  const debouncedSearch = useRef(
    debounce((value) => {
      remote(`/?q=${value}&props_at=data.search.results`, {
        pageKey: currentPageKey,
        force: true
      })
    }, 250)
  ).current
  
  const handleSearch = (e) => {
    const value = e.target.value
    debouncedSearch(value)
  }

  return (
    <div className="w-full bg-white px-8 py-4 border-b border-zinc-200">
      <div className="w-full items-center justify-between flex">
        <SearchBar onChange={handleSearch} results={searchResults} />

        {/* Pomodoro counter */}
      </div>
    </div>
  )
}

export default AppNavbar
```

We add the `<SearchBar/>` component and pass the `handleSearch` function and the results array which we will get from the `dashboard/index.json.props` partial.

Then, in the `handleSearch` function, we're debouncing the search to avoid making repeat and unnecessary requests to the backend before we require them.

The most important part is the `remote` call where the actual request is performed.

As we defined the search feature globally in the `application.json.props` template, we can obtain the results by making a request to the `/` root path. However, we need to make sure to pass the `currentPageKey` which, for the `/dashboard` path, it would be precisely `/dashboard`.

We also have to pass the `force: true` because we're making the request to `/` from the dashboard which has a different `componentIdentifier`.

After this, we have a functioning search functionality:

{{gumlet video_id="68804d06d1ad82bfd42fcc2c" width="1784" height="314" /}}
## Server-Side Rendering
Even though Superglue lets us integrate Rails goodies with React without any major efforts, React components are rendered after the server response when React is mounted into the view.

Real-world applications usually need at least some parts of the application to be server-side rendered. This is considerably important for [Rails SEO](https://avohq.io/rails-seo) where search engines require more resources to parse client-rendered pages which, in turn, means that our pages might get deprioritized or that our crawl budget might suffer.

Luckily, we can achieve it with Superglue using [Humid](https://github.com/thoughtbot/humid), a wrapper around the [mini_racer gem](https://github.com/rubyjs/mini_racer) that's used to generate SSR pages from bundling builds.

In our example application, we have a blog with a list of posts located at `posts/index` and each post has its view at `posts/show`. Currently, the blog looks like this:

{{gumlet video_id="686dda87836ec25e5f4536a3" width="1500" height="850" /}}

However, if we check what's getting loaded before React gets mounted, we will see that only the app shell is loaded:

```shell
curl -H "Accept: text/html" http://localhost:3000/blog
```

And the body of the HTML document looks like this:

```html
<body>
  <!-- BEGIN app/views/application/superglue.html.erb -->
  <script type="text/javascript">
    window.SUPERGLUE_INITIAL_PAGE_STATE={"data":{"posts":[{"id":1,"title":"Cloudflare R2 with Active Storage","slug":"cloudflare-r2-with-active-storage","excerpt":"Let's learn how to add Cloudflare R2 uploads to a Rails application with Active Storage","url":"http://localhost:3000/blog/cloudflare-r2-with-active-storage"},{"id":2,"title":"GitHub-flavored Markdown on Rails","slug":"github-flavored-markdown-on-rails","excerpt":"Your app needs Markdown? Let's add it using the Commonmarker gem","url":"http://localhost:3000/blog/github-flavored-markdown-on-rails"},{"id":3,"title":"Apple Sign in with Rails","slug":"apple-sign-in-with-rails","excerpt":"Let's add Sign in With Apple a.k.a. social login with Apple to our Rails application wi...","url":"http://localhost:3000/blog/apple-sign-in-with-rails"}],"componentIdentifier":"posts/index"},"componentIdentifier":"posts/index","defers":[],"fragments":[],"csrfToken":"xEP7TXoXOlPeNNJU7-BWCzplhA5xd_W1f6X6zJVumWz8_U-f-aNQO8BBYk2d47JOz7wiL_chDL_OTR270VsDHw","restoreStrategy":"fromCacheAndRevisitInBackground","renderedAt":1752030397,"slices":{"flash":{},"user":{"id":null,"is_authenticated":false,"email_address":null,"username":null,"slug":null,"sign_out_form":{"inputs":{"authenticityToken":{"autoComplete":"off","type":"hidden","name":"authenticity_token","id":"authenticity_token"},"submit":{"name":"commit","text":"Save ","type":"submit"}},"extras":{"method":{"name":"_method","type":"hidden","defaultValue":"delete","autoComplete":"off"},"csrf":{"name":"authenticity_token","type":"hidden","defaultValue":"wX9b4AUYy9uATpjpRWV2xPNyI_emT_LQWCAXi3KqzaExvGoIUUkdyqZpuLSeQYv-nN3e58smTEmzafA-_M_7ZA","autoComplete":"off"}},"form":{"action":"/session","acceptCharset":"UTF-8","method":"post"}}}}};
  </script>

  <div id="app"></div>
  <!-- END app/views/application/superglue.html.erb -->
</body>
```

As users, we might not notice any difference, but search engine crawlers do appreciate if we return the whole HTML from the server.

{{further_reading slug="sitemap-for-rails-applications" /}}

Using Humid and Superglue, the server-side rendering flow looks something like this:

![Server-side Rendering with Superglue](https://i.postimg.cc/kgBH1R1S/server-side-rendering-flow.webp)

We have a build script that uses the `server_rendering.js` file which defines a `setHumidRenderer` function that receives `json` a `baseUrl` and a `path`as parameters and that is capable of generating the HTML for a given view.

Then, inside a given partial or the entire application layout file, we use Humid to produce the HTML when a browser request is processed using the `Humid.render` function, which ends up producing the HTML based on the JSON props and the path.

To add server-side rendering with Superglue, let's start by installing Humid:

```shell
bundle add humid && bundle install
```

Then, we can optionally add the `source-map-support` library to support source maps:

```shell
yarn add source-map-support
```

Next, we add a `humid.rb` initializer following the instructions from [Humid's GitHub repo,](https://github.com/thoughtbot/humid?tab=readme-ov-file#configuration) and we make sure to include the following:

```ruby
# config/initializers/humid.rb
require "humid"
config.application_path = Rails.root.join("public", "builds", "server_rendering.js")
config.source_map_path = Rails.root.join("public", "builds", "server_rendering.js.map")
```

Then, in the `server_rendering.js` file, we make sure to define the `setHumidRenderer` function that returns the application as a string:

```jsx
// app/javascript/server_rendering.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Application } from '@thoughtbot/superglue';
import { buildVisitAndRemote } from './application_visit';
import { pageIdentifierToPageComponent } from './page_to_page_mapping';
import { store } from './store';

require("source-map-support").install({
  retrieveSourceMap: filename => {
    return {
      url: filename,
      map: readSourceMap(filename)
    };
  }
});

setHumidRenderer((json, baseUrl, path) => {
  const initialState = JSON.parse(json)
  return renderToString(
    <Application
      className="full-height"
      baseUrl={baseUrl}
      initialPage={initialState}
      path={path}
      buildVisitAndRemote={buildVisitAndRemote}
      store={store}
      mapping={pageIdentifierToPageComponent}
    />,
    {
      concurrentFeatures: false,
    }
  )
})

```

Then, we install the following libraries for compatibility:

```javascript
yarn add esbuild-plugin-polyfill-node text-encoding whatwg-url
```

We create a `shim.js` for the same purpose:

```javascript
export { TextEncoder, TextDecoder } from 'text-encoding'
export { URL, URLSearchParams } from 'whatwg-url'

export function MessageChannel() {
  this.port1 = {
    postMessage: function (message) {
      console.log('Message sent from port1:', message);
    },
  };

  this.port2 = {
    addEventListener: function (event, handler) {
      console.log(`Event listener added for ${event} on port2`);
      this._eventHandler = handler;
    },
    removeEventListener: function (event) {
      console.log(`Event listener removed for ${event} on port2`);
      this._eventHandler = null;
    },
    simulateMessage: function (data) {
      if (this._eventHandler) {
        this._eventHandler({ data });
      }
    },
  };
}

export const navigator = { language: "en-us" }
```

Then, we create a `build-server.mjs` file and add the following:

```javascript
import * as esbuild from 'esbuild';
import { polyfillNode } from 'esbuild-plugin-polyfill-node';

await esbuild.build({
  entryPoints: ['app/frontend/server_rendering.js'],
  bundle: true,
  platform: 'browser',
  define: {
    'process.env.NODE_ENV': '"production"',
  },
  sourcemap: true,
  outfile: 'public/builds/server_rendering.js',
  logLevel: 'info',
  loader: {
    ".js": "jsx",
    '.png': 'dataurl',
    '.jpg': 'dataurl',
    '.gif': 'dataurl',
    '.svg': 'dataurl',
    '.svg': 'dataurl',
  },
  inject: ['./shim.js'],
  plugins: [
    polyfillNode({
      globals: false,
    }),
  ],
});
```

Next, we add a custom script to generate the SSR builds:

```JSON
// package.json
  "scripts": {
    "build:ssr": "node ./build-server.mjs"
  }
```

Now, if we try running `yarn build:ssr` we should get something like this:

```
yarn run v1.22.19
$ node ./build-server.mjs

  public/builds/server_rendering.js      1.9mb ⚠️
  public/builds/server_rendering.js.map  3.6mb

✨  Done in 0.34s.
```

Finally, to add SSR only to the blog, we create an `index.html.erb` partial inside the `posts` folder and add the following:

```erb
<% initial_state = controller.render_to_string(@virtual_path ,formats: [:json], locals: local_assigns, layout: true) %>

<script type="text/javascript">
  window.SUPERGLUE_INITIAL_PAGE_STATE=<%= initial_state.html_safe %>;
</script>

<div id="app"><%= Humid.render(initial_state, request.scheme + '://' + request.host_with_port, request.fullpath).html_safe %></div>
```

Now, we start the server using `bin/dev` and we should see the post list:

![Blog index with Server-side rendering in Superglue](https://i.postimg.cc/CLYBsjKv/blog-index-with-ssr.png)

And now, if we perform a request using `curl -H "Accept: text/html" http://localhost:3000/blog` we should see the application's HTML inside the `<div id="app"></div>`.

{{warning}}
When building this application, I used the Apex Charts library to produce some demo charts in the app dashboard. Because this library is not currently compatible with SSR, I left the `superglue.html.erb` partial to render the React application client-side and then I added SSR to the specific views where it's more important, like marketing pages or pages where search engine crawling is critical.
{{/warning}}
## Summary
Rails has improved a lot in the frontend with tools like Turbo and Hotwire, but there are times when we need to build very interactive applications where a frontend using a framework like React makes more sense.

But losing most of what makes us productive and using Rails in API-only mode is not a decision to take lightly.

That's where a library like Superglue, which lets us develop most of our application using a mix of React and Redux without losing Rails features comes into place.

Superglue replaces the typical Rails views with React components, and it passes data to them using partials that use a syntax that is essentially the same as `jbuilder`.

It also uses Rails UJS to produce an SPA-like experience for navigation and to produce partial content reloads by using links and forms with specific data attributes added which are used by Superglue to intercept the requests and produce the desired results.

In this article we used Superglue to create a sample application that includes the following features:

- User authentication.
- Working with forms.
- Custom flash messages
- Global search
- Pagination
- Server-side rendering.

All in all, Superglue is a nice alternative to integrate React into Rails applications. Its approach is similar but a bit different from Inertia.

If you're looking to integrate React into your next application, I would recommend you to give both libraries a go and decide which one is a better fit for you.

I hope this article was useful and that it can help you with your future projects. Have a good one and happy coding!
