README.md in quilt_rails-1.5.0 vs README.md in quilt_rails-1.6.0

- old
+ new

@@ -1,50 +1,82 @@ # quilt_rails A turn-key solution for integrating server-rendered react into your Rails app using Quilt libraries. -This document focuses on Rails integration. For details of `@shopify/react-server`'s configuration and usage, see the [react-server documentation](/packages/react-server/README.md). - ## Table of Contents 1. [Quick Start](#quick-start) + 1. [Generate Rails boilerplate](#generate-rails-boilerplate) + 1. [Add Ruby dependencies](#add-ruby-dependencies) + 1. [Generate Quilt boilerplate](#generate-quilt-boilerplate) + 1. [Try it out](#try-it-out) +1. [Manual Install](#manual-installation) + 1. [Install Dependencies](#install-dependencies) + 1. [Setup the Rails app](#setup-the-rails-app) + 1. [Add JavaScript](#add-javascript) + 1. [Run the server](#run-the-server) +1. [Application Layout](#application-layout) +1. [API](#api) + 1. [ReactRenderable](#reactrenderable) + 1. [Engine](#engine) + 1. [Generators](#generators) 1. [Advanced Use](#advanced-use) + 1. [Testing](#testing) + 1. [Interacting with the request and response in React code](#interacting-with-the-request-and-response-in-react-code) + 1. [Dealing with isomorphic state](#dealing-with-isomorphic-state) + 1. [Customizing the node server](#customizing-the-node-server) ## 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` -First, create a Rails project using `dev init`. Next, run `rails generate quilt:install`. This will install the Node dependencies, provide a basic React app (in TypeScript) and mounts the Quilt engine inside of your `config/routes.rb` file. +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 -You can also perform the steps within the rake task manually by following the guide below. +An application can also be setup manually using the following steps. ### Install Dependencies ```sh -# Add Node dependencies +# Add core Node dependencies yarn add @shopify/sewing-kit @shopify/react-server -# Optional - add Polaris and quilt libraries -yarn add @shopify/polaris @shopify/react-self-serializers react react-dom +# Add Polaris and React +yarn add @shopify/polaris react react-dom yarn dev up ``` -### Add JavaScript +### Setup the Rails app -sewing_kit looks for JavaScript in `app/ui/index.js`. The code in `index.js` (and any imported JS/CSS) will be built into a `main` bundle. - -### Rails Setup - There are 2 ways to consume this package. -### Option 1: Mount the Engine +#### Option 1: Mount the Engine Add the engine to `routes.rb`. ```ruby # config/routes.rb @@ -52,21 +84,21 @@ # ... mount Quilt::Engine, at: '/' end ``` -Where `at` is the path where your App will respond with the React App. If you only want a sub-section of routes to respond with the React App, you can pass in the path to that sub-section here. For example: +If only a sub-section of routes should respond with the React App, it can be configured using the `at` parameter. ```ruby # config/routes.rb Rails.application.routes.draw do # ... mount Quilt::Engine, at: '/path/to/react' end ``` -### Option 2: Add your react controller and routes +#### Option 2: Add a React controller and routes Create a `ReactController` to handle react requests. ```ruby class ReactController < ApplicationController @@ -76,31 +108,22 @@ render_react end end ``` -Have your routes wired up to default to your react controller. +Add routes to default to the `ReactController`. ```ruby get '/*path', to: 'react#index' root 'react#index' ``` -## Minimal Project Layout +### Add JavaScript -``` -├── 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 - └── controllers - └─- react_controller (see above) -``` +`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. -## Example minimal React/Polaris/Quilt entrypoint +We will add a basic entrypoint using React with Polaris components. ```tsx // app/ui/index.tsx import React from 'react'; @@ -117,26 +140,209 @@ } export default App; ``` -## Rails Generators +### Run the server -### `quilt:install` +`dev server` -Installs the Node dependencies, provide a basic React app (in TypeScript) and mounts the Quilt engine inside of your `config/routes.rb` file. +Will run the application, starting up both servers and compiling assets. -### `sewing_kit:install` +## 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, Polaris, 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) + │ └── settings.scss (global vars and @polaris overrides) + │ + └── 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} +``` + +## 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`. + +```ruby +class ReactController < ApplicationController + include Quilt::ReactRenderable + + def index + render_react + end +end +``` + +### Engine + +`Quilt::Engine` provides a preconfigured controller which consumes `ReactRenderable` and provides an index route which uses it. + +```ruby +# config/routes.rb +Rails.application.routes.draw do + # ... + mount Quilt::Engine, at: '/path/to/react' +end +``` + +### Configuration + +The `configure` method allows customization of the address the service will proxy to for UI rendering. + +```ruby + # config/initializers/quilt.rb + Quilt.configure do |config| + config.react_server_host = "localhost:3000" + config.react_server_protocol = 'https' + end +``` + +### 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. ## Advanced use -### Interacting with the request / response in your React code +### Testing -React-server sets up [@shopify/react-network](https://github.com/Shopify/quilt/blob/master/packages/react-network/src/hooks.ts#L25) for you, so most interactions with the request or response can be done from inside your React code. +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`](https://github.com/Shopify/sewing-kit/blob/master/docs/commands/test.md#L3) to run all `.test.{js|ts}x` files in the `app/ui` directory. [Jest](https://jestjs.io/) is used as a test runner, with customization available via [its sewing-kit plugin](https://github.com/Shopify/sewing-kit/blob/master/docs/plugins/jest.md). + +For testing React applications we provide and support [`@shopify/react-testing`](https://github.com/Shopify/quilt/tree/master/packages/react-testing). + +#### Example + +Given a component `MyComponent.tsx` + +```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. + +```tsx +// 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'); + }); +}); +``` + +### Test setup files + +By default, the jest plugin will look for test setup files under `/app/ui/tests`. + +`setup` can be used to add any custom polyfills needed for the testing environment. + +```tsx +// app/ui/tests/setup.ts + +import 'isomorphic-fetch'; +import 'raf/polyfill'; +import {URL, URLSearchParams} from 'url'; + +(global as any).URL = URL; +(global as any).URLSearchParams = URLSearchParams; +``` + +`each-test` can be used for any logic that needs to run for each individual test suite. Any setup logic that needs to happen with `jest` globals in scope, such as importing custom matchers, should also be done here. + +```tsx +// app/ui/tests/each-test.ts + +// we cannot import these in `setup` because `expect` will not be defined +import '@shopify/react-testing/matchers'; + +beforeAll(() => { + console.log('I will run before every test suite'); +}); + +beforeEach(() => { + console.log('I will run before every test case'); +}); + +afterEach(() => { + console.log('I will run after every test case'); +}); + +afterAll(() => { + console.log('I will run after every test suite'); +}); +``` + +For more complete documentation of the jest plugin see [it's documentation](https://github.com/Shopify/sewing-kit/tree/master/docs/plugins/jest.md). + +### Interacting with the request and response in React code + +React-server sets up [@shopify/react-network](https://github.com/Shopify/quilt/blob/master/packages/react-network/src/hooks.ts#L25) automatically, so most interactions with the request or response can be done from inside the React app. + #### Example: getting headers ```tsx // app/ui/index.tsx @@ -184,20 +390,26 @@ } 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`](https://github.com/Shopify/quilt/tree/master/packages/react-html)'s [`useSerialized`](https://github.com/Shopify/quilt/tree/master/packages/react-html#in-your-application-code) hook. + +`useSerialized` can be used to implement [universal-providers](https://github.com/Shopify/quilt/tree/master/packages/react-universal-provider#what-is-a-universal-provider-), 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](https://github.com/Shopify/quilt/tree/master/packages/react-csrf-universal-provider), [GraphQL](https://github.com/Shopify/quilt/tree/master/packages/react-graphql-universal-provider), [I18n](https://github.com/Shopify/quilt/tree/master/packages/react-i18n-universal-provider), and the [Shopify App Bridge](https://github.com/Shopify/quilt/tree/master/packages/react-app-bridge-universal-provider). + ### Customizing the node server -By default, sewing-kit bundles in `@shopify/react-server-webpack-plugin` for `quilt_rails` applications to get you up and running fast without needing to manually write any node server code. If you would like to customize what data your react application receives from the incoming request, you can add your own `server.js` / `server.ts` file to the app folder. +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 - └─- index.js - └─- server.js + └─- app.{js|ts}x + └─- index.{js|ts} + └─- server.{js|ts}x ``` ```tsx // app/ui/server.tsx import '@shopify/polyfills/fetch'; @@ -205,29 +417,45 @@ import {Context} from 'koa'; import React from 'react'; import App from './app'; -// you could create your own server from scratch, but the easiest way to is using `@shopify/react-server` +// 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', - serverMiddleware: [(ctx, next) => { - // you can add your own middleware to extend the server's functionality. - console.log('I am a custom middleware!'); - await next(); - }] 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; ``` -### Isomorphic state +### Fixing rejected CSRF tokens for new user sessions -With SSR enabled React apps, state must be serialized on the server and deserialized on the client to keep it consistent. With `@shopify/react-server`, the main way you will accomplish is using [`@shopify/react-html`](https://github.com/Shopify/quilt/tree/master/packages/react-html)'s [`useSerialized`](https://github.com/Shopify/quilt/tree/master/packages/react-html#in-your-application-code) hook to implement [self-serializers](https://github.com/Shopify/quilt/blob/master/packages/react-self-serializers/README.md#self-serializers). We offer some common ones out of the box in [`@shopify/react-self-serializers`](https://github.com/Shopify/quilt/blob/master/packages/react-self-serializers/README.md#self-serializers). +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.: + +```rb +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 +```