quilt_rails

A turn-key solution for integrating Quilt client-side libraries into your Rails app, with support for server-side-rendering using @shopify/react-server, integration with @shopify/sewing-kit for building, testing and linting, and front-end performance tracking through @shopify/performance.

Table of Contents

Server-side-rendering

Alpha functionality - do not use in high-traffic production applications

Warning: quilt_rails's server-side-rendering module ReactRenderable does not work at scale. Improvements to its architecture are being investigated. In its current state, it can be used for:

  • Workshop applications
  • Proof of concept applications
  • Low traffic applications

For a description of the current architecture's problems, see this Github comment.

The "decide on a scalable quilt_rails architecture" issue will track discussion of future architectures.

To scale up existing quilt_rails applications, skip server-side queries in your components. e.g.:

useQuery(MyQuery, {
  skip: typeof document === 'undefined',
});

Quick start

Using the magic of generators, we can spin up a basic app with a few console commands.

Generate Rails boilerplate

dev init

When prompted, choose rails. This will generate a basic Rails application scaffold.

Add Ruby dependencies

bundle add sewing_kit quilt_rails

This will install our ruby dependencies and update the project's gemfile.

Generate Quilt boilerplate

rails generate quilt:install

This will install the Node dependencies, provide a basic React app (in TypeScript) and mounts the Quilt engine inside of config/routes.rb.

Try it out

dev server

Will run the application, starting up both servers and compiling assets.

Manual installation

An application can also be setup manually using the following steps.

Install dependencies

# Add core Node dependencies
yarn add @shopify/sewing-kit @shopify/react-server

# Add React
yarn add react react-dom

yarn
dev up

Setup the Rails app

There are 2 ways to consume this package.

Option 1: Mount the Engine

Add the engine to routes.rb.

# config/routes.rb
Rails.application.routes.draw do
  # ...
  mount Quilt::Engine, at: '/'
end

If only a sub-section of routes should respond with the React App, it can be configured using the at parameter.

# config/routes.rb
Rails.application.routes.draw do
  # ...
  mount Quilt::Engine, at: '/path/to/react'
end
Option 2: Add a React controller and routes

Create a ReactController to handle react requests.

class ReactController < ApplicationController
  include Quilt::ReactRenderable

  def index
    render_react
  end
end

Add routes to default to the ReactController.

  get '/*path', to: 'react#index'
  root 'react#index'

Add JavaScript

sewing_kit looks for the top level component of your React app in app/ui/index. The component exported from this component (and any imported JS/CSS) will be built into a main bundle, and used to render the initial server-rendered markup.

We will add a basic entrypoint using React with some HTML.

// app/ui/index.tsx

import React from 'react';

function App() {
  return <h1>My application ❤️</h1>;
}

export default App;

Run the server

dev server

Will run the application, starting up both servers and compiling assets.

Application layout

Minimal

The basic layout for an app using quilt_rails and friends will have a ui folder nested inside the normal Rails app folder, containing at least an index.js file exporting a React component.

├── Gemfile (must contain "gem 'sewing_kit" and "gem 'quilt_rails'")
├── package.json (must specify '@shopify/sewing-kit' and `@shopify/react-server` as 'dependencies')
│
└── app
   └── ui
   │   └─- index.{js|ts} (exports a React component)
   └── controllers
       └─- react_controller.rb (see above)

Rails and React

A more complex application will want a more complex layout. The following shows scalable locations for:

  • Global SCSS settings
  • App sections (roughly analogous to Rails routes)
  • Components
  • Co-located CSS modules
  • Co-located unit tests
  • Test setup files
└── app
    └── ui
        ├─- index.{js|ts} (exports a React component)
        ├── styles (optional)
        └── shared.scss (common functions/mixins you want available in every scss file. Requires configuring `plugin.sass`'s `autoInclude` option in `sewing-kit.config.js`)
        │
        └── tests (optional)
        │   └── each-test.{js|ts}
        │   └── setup.{js|ts}
        └── features (optional)
            ├── App
            │   ├── index.{js|ts}
            │   ├── App.{js|ts}x
            │   └── tests
            │       └── App.test.{js|ts}x
            │
            ├-─ MyComponent
            │   ├-─ index.{js|ts}
            │   ├-─ MyComponent.{js|ts}x
            │   ├── MyComponent.scss (optional; component-scoped CSS styles, mixins, etc)
            │   └── tests
            │       └── MyComponent.test.{js|ts}x
            │
            └── sections (optional; container views that compose presentation components into UI blocks)
                └── Home
                    ├-─ index.{js|ts}
                    └── Home.{js|ts}

Advanced use

Testing

For fast tests with consistent results, test front-end components using the tools provided by sewing-kit instead of Rails integration tests.

Use sewing-kit test to run all .test.{js|ts}x files in the app/ui directory. Jest is used as a test runner, with customization available via its sewing-kit plugin.

For testing React applications we provide and support @shopify/react-testing.

Example

Given a component MyComponent.tsx

// app/ui/components/MyComponent/MyComponent.tsx
export function MyComponent({name}: {name: string}) {
  return <div>Hello, {name}!</div>;
}

A test would be written using Jest and @shopify/react-testing's mount feature.

// app/ui/components/MyComponent/tests/MyComponent.test.tsx
import {MyComponent} from '../MyComponent';

describe('MyComponent', () => {
  it('greets the given named person', () => {
    const wrapper = mount(<MyComponent name="Kokusho" />);

    // toContainReactText is a custom matcher provided by @shopify/react-testing/matchers
    expect(wrapper).toContainReactText('Hello, Kokusho');
  });
});
Customizing the test environment

Often you will want to hook up custom polyfills, global mocks, or other logic that needs to run either before the initialization of the test environment, or once for each test suite.

By default, sewing-kit will look for such test setup files under /app/ui/tests. Check out the documentation for more details.

Interacting with the request and response in React code

React-server sets up @shopify/react-network automatically, so most interactions with the request or response can be done from inside the React app.

Example: getting headers
// app/ui/index.tsx

import React from 'react';
import {useRequestHeader} from '@shopify/react-network';

function App() {
  // get `some-header` from the request that was sent through Rails
  const someHeaderICareAbout = useRequestHeader('some-header');

  return (
    <>
      <h1>My application ❤️</h1>
      <div>{someHeaderICareAbout}</div>
    </>
  );
}

export default App;
Example: redirecting
// app/ui/index.tsx

import React from 'react';
import {useRedirect} from '@shopify/react-network';

function App() {
  // redirect to google as soon as we render
  useRedirect('www.google.com');

  return <h1>My application ❤️</h1>;
}

export default App;

Isomorphic state

With SSR enabled React apps, state must be serialized on the server and deserialized on the client to keep it consistent. When using @shopify/react-server, the best tool for this job is @shopify/react-html's useSerialized hook.

useSerialized can be used to implement universal-providers, allowing application code to manage what is persisted between the server and client without adding any custom code to client or server entrypoints. We offer some for common use cases such as CSRF, GraphQL, I18n, and the Shopify App Bridge.

Customizing the node server

By default, sewing-kit bundles in @shopify/react-server-webpack-plugin for quilt_rails applications to get apps up and running fast without needing to manually write any node server code. If what it provides is not sufficient, a custom server can be defined by adding a server.js or server.ts file to the app folder.

└── app
   └── ui
      └─- app.{js|ts}x
      └─- index.{js|ts}
      └─- server.{js|ts}x
// app/ui/server.tsx
import '@shopify/polyfills/fetch';
import {createServer} from '@shopify/react-server';
import {Context} from 'koa';
import React from 'react';

import App from './app';

// The simplest way to build a custom server that will work with this library is to use the APIs provided by @shopify/react-server.
// https://github.com/Shopify/quilt/blob/master/packages/react-server/README.md#L8
const app = createServer({
  port: process.env.PORT ? parseInt(process.env.PORT, 10) : 8081,
  ip: process.env.IP,
  assetPrefix: process.env.CDN_URL || 'localhost:8080/assets/webpack',
  render: (ctx, {locale}) => {
    const whatever = /* do something special with the koa context */;
    // any special data we add to the incoming request in our rails controller we can access here to pass into our component
    return <App server someCustomProp={whatever} location={ctx.request.url} locale={locale} />;
  },
});

export default app;

Fixing rejected CSRF tokens for new user sessions

If a React component calls back to a Rails endpoint (e.g., /graphql), Rails may throw a Can't verify CSRF token authenticity exception. This stems from the Rails CSRF tokens not persisting until after the first UiController call ends.

To fix this:

  • Add an X-Shopify-Server-Side-Rendered: 1 header to all server-side GraphQL requests
  • Add a protect_from_forgery with: Quilt::TrustedUiServerCsrfStrategy override to Node-accessed controllers

e.g.:

class GraphqlController < ApplicationController
  protect_from_forgery with: Quilt::TrustedUiServerCsrfStrategy

  def execute
    # Get GraphQL query, etc

    result = MySchema.execute(query, operation_name: operation_name, variables: variables, context: context)

    render(json: result)
  end
end

Performance tracking a React app

Using Quilt::Performance::Reportable and @shopify/react-performance it's easy to add performance tracking to apps usingsewing_kit for client-side-rendering or quilt_rails for server-side-rendering.

Install dependencies

  1. Install the gem (if your app is not already using quilt_rails).
bundle add quilt_rails
  1. Install @shopify/react-performance, the library we will use to annotate our React application and send performance reports to our server.
yarn add @shopify/react-performance

Setup an endpoint for performance reports

If your application is not using Quilt::Engine, you will need to manually configure the server-side portion of performance tracking. If it is using the engine, the following will be done automatically.

  1. Add a PerformanceController and the corresponding routes to your Rails app.
# app/controllers/performance_report_controller.rb

class PerformanceReportController < ActionController::Base
  include Quilt::Performance::Reportable
  protect_from_forgery with: :null_session

  def create
    process_report

    render json: { result: 'success' }, status: 200
  rescue ActionController::ParameterMissing => error
    render json: { error: error.message, status: 422 }
  end
end
  1. Add a route pointing at the controller.
# config/routes.rb

post '/performance_report', to: 'performance_report#create'

# rest of routes

Add annotations

Add a <PerformanceMark /> to each of your route-level components.

// app/ui/features/Home/Home.tsx
import {PerformanceMark} from '@shopify/react-performance';

export function Home() {
  return (
    <div>
      My Cool Home Page
      {/* tell the library the page has finished rendering completely */}
      <PerformanceMark stage="complete" id="Home" />
    </div>
  );
}

Send the report

Add a usePerformanceReport call to your top-level <App /> component.

// app/ui/foundation/App/App.tsx
import {usePerformanceReport} from '@shopify/react-performance';

export function App() {
  // send the report to the server
  usePerformanceReport('/performance_report');

  return <>{/* your app JSX goes here*/}</>;
}

For more details on how to use the APIs from @shopify/react-performance check out its documentation.

Verify in development

By default quilt_rails will not send metrics in development mode. To verify your app is setup correctly you can check in your network tab when visiting your application and see that POST requests are sent to /performance_report, and recieve a 200 OK response.

If you want more insight into what distributions would be sent in production, you can use the on_distribution callback provided by the library to setup logging.

# app/controllers/performance_report_controller.rb

class PerformanceReportController < ActionController::Base
  include Quilt::Performance::Reportable
  protect_from_forgery with: :null_session

  def create
    # customize process_report's behaviour with a block
    process_report do |client|
      client.on_distribution do |name, value, tags|
        # We log out the details of each distribution that would be sent in production.
        Rails.logger.debug("Distribution: #{name}, #{value}, #{tags}")
      end
    end

    render json: { result: 'success' }, status: 200
  rescue ActionController::ParameterMissing => error
    render json: { error: error.message, status: 422 }
  end
end

Now you can check your Rails console output and verify that metrics are reported as expected.

Configure StatsD for production

Attention Shopifolk! If using dev your StatsD endpoint will already be configured for you in production. You should not need to do the following. ✨

To tell Quilt::Performance::Reportable where to send it's distributions, setup the environment variables detailed documentation.

API

ReactRenderable

The ReactRenderable mixin is intended to be used in Rails controllers, and provides only the render_react method. This method handles proxying to a running @shopify/react-server.

class ReactController < ApplicationController
  include Quilt::ReactRenderable

  def index
    render_react
  end
end

Performance

Reportable

The Quilt::Performance::Reportable mixin is intended to be used in Rails controllers, and provides only the process_report method. This method handles parsing an incoming report from @shopify/react-performance's <PerformanceReport /> component (or a custom report in the same format) and sending it to your application's StatsD endpoint as distributions using StatsD-Instrument.

Note Quilt::Performance::Reportable does not require you to use the React::Renderable mixin, React-Server, or even any server-side-rendering solution at all. It should work perfectly fine for applications using something like sewing_kit_script_tag based client-side-rendering.

class PerformanceController < ApplicationController
  include Quilt::Performance::Reportable

  def create
    process_report
  end
end

The params sent to the controller are expected to be of type application/json. Given the following example JSON sent by @shopify/react-performance,

{
  "connection": {
    "rtt": 100,
    "downlink": 2,
    "effectiveType": "3g",
    "type": "4g"
  },
  "navigations": [
    {
      "details": {
        "start": 12312312,
        "duration": 23924,
        "target": "/",
        "events": [
          {
            "type": "script",
            "start": 23123,
            "duration": 124
          },
          {
            "type": "style",
            "start": 23,
            "duration": 14
          }
        ],
        "result": 0
      },
      "metadata": {
        "index": 0,
        "supportsDetailedTime": true,
        "supportsDetailedEvents": true
      }
    }
  ],
  "events": [
    {
      "type": "ttfb",
      "start": 2,
      "duration": 1000
    }
  ]
}

the above controller would send the following metrics:

StatsD.distribution('time_to_first_byte', 2, ['browser_connection_type:3g'])
StatsD.distribution('time_to_first_byte', 2, ['browser_connection_type:3g'])
StatsD.distribution('navigation_complete', 23924, ['browser_connection_type:3g'])
StatsD.distribution('navigation_usable', 23924, ['browser_connection_type:3g'])
Customizing process_report with a block

The behaviour of process_report can be customized by manipulating the Quilt::Performance::Client instance yielded into its implicit block parameter.

process_report do |client|
  # client.on_distribution do ....
end

Client

The Quilt::Performance::Client class is yielded into the block parameter for process_report, and is the primary API for customizing what metrics are sent for a given POST.

Client#on_distribution

The on_distribution method takes a block which is run for each distribution (including custom ones) sent during process_report.

The provided callback can be used to easily add logging or other side-effects to your measurements.

client.on_distribution do |metric_name, value, tags|
  Rails.logger.debug "#{metric_name}: #{value}, tags: #{tags}"
end
Client#on_navigation

The on_navigation method takes a block which is run once per navigation reported to the performance controller before the default distributions for the navigation are sent.

The provided callback can be used to add tags to the default distributions for a given navigation.

client.on_navigation do |navigation, tags|
  # add tags to be sent with each distribution for this navigation
  tags[:connection_rtt] = navigation.connection.rtt
  tags[:connection_type] = navigation.connection.type
  tags[:navigation_target] = navigation.target
end

It can also be used to compute and send entirely custom metrics.

client.on_navigation do |navigation, tags|
  # calculate and then send an additional distribution
  weight = navigation.events_with_size.reduce(0) do |total, event|
    total + event.size
  end
  client.distribution('navigation_total_resource_weight', weight, tags)
end
Client#on_event

The on_event method takes a block which is run once per event reported to the performance controller before the default distributions for the event are sent.

The provided callback can be used to add tags to the default distributions for a given event, or perform other side-effects.

client.on_event do |event, tags|
  # add tags to be sent with each distribution for this event
  tags[:connection_rtt] = event.connection.rtt
  tags[:connection_type] = event.connection.type
end

Engine

Quilt::Engine provides:

  • a preconfigured UiController which consumes ReactRenderable
  • a preconfigured PerformanceReportController which consumes Performance::Reportable
  • a /performance_report route mapped to performance_report#index
  • a catch-all index route mapped to the UiController#index
# config/routes.rb
Rails.application.routes.draw do
  # ...
  mount Quilt::Engine, at: '/my-front-end'
end

The above is the equivalent of

  post '/my-front-end/performance_report', to: 'performance_report#create'
  get '/my-front-end/*path', to: 'ui#index'
  get '/my-front-end', to: 'ui#index'

Configuration

The configure method allows customization of the address the service will proxy to for UI rendering.

  # config/initializers/quilt.rb
  Quilt.configure do |config|
    config.react_server_host = "localhost:3000"
    config.react_server_protocol = 'https'
  end

StatsD environment variables

The Performance::Reportable mixin uses https://github.com/Shopify/statsd-instrument to send distributions. For detailed instructions on configuring where it sends data see the documentation.

Generators

quilt:install

Installs the Node dependencies, provide a basic React app (in TypeScript) and mounts the Quilt engine in config/routes.rb.

sewing_kit:install

Adds a basic sewing-kit.config.ts file.