Skip to content
This repository has been archived by the owner on Oct 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1411 from Shopify/quilt_rails-data-support
Browse files Browse the repository at this point in the history
Better patterns for sharing headers/data
  • Loading branch information
ismail-syed authored May 4, 2020
2 parents e0d5255 + bc5b3fd commit ed882c7
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 48 deletions.
2 changes: 2 additions & 0 deletions gems/quilt_rails/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

- Add: Expose a `data` arugment on `render_react` to share data to the React server using the `X-Quilt-Data` header ([#1411](https://github.com/Shopify/quilt/pull/1411))

## [1.11.1] - 2020-03-24

- add `allowed_push_host` in gemspec that is required to publish
Expand Down
57 changes: 19 additions & 38 deletions gems/quilt_rails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ yarn add react react-dom
# Add Typescript
yarn add typescript @types/react @types/react-dom
```

##### Define typescript config

```json
// tsconfig.json
{
Expand All @@ -122,6 +124,7 @@ yarn add typescript @types/react @types/react-dom
"include": ["app/ui"]
}
```

```sh
yarn
dev up
Expand Down Expand Up @@ -326,67 +329,45 @@ function App() {
export default App;
```

##### Example: sending headers from Rails controller
##### Example: sending custom data from Rails controller

In some cases you may want to send custom headers or basic data from Rails to your React server. Quilt facilitates this case by providing consumers with a `headers` and `data` argument on the `render_react` call.

**Note:** The data passed should be data that is unlikely or will never change over the course of the session before they render any React components.

```ruby
class ReactController < ApplicationController
include Quilt::ReactRenderable

def index
render_react(headers: { 'x-custom-header': 'header-value-a' })
render_react(headers: {'x-custom-header': 'header-value-a'}, data: {'some_id': 123})
end
end
```

You will need to serialize the result of the useRequestHeader hook in order for it to persist to the client

```tsx
// app/ui/foundation/CustomUniversalProvider.tsx
import {createContext} from 'react';
import {createUniversalProvider} from '@shopify/react-universal-provider';

export const CustomContext = createContext<string | null>(null);
export const CustomUniversalProvider = createUniversalProvider('custom-key', CustomContext);
```
The React server will serialize the provided quilt data using `x-quilt-data` as the ID. You can then get this serialized data on the client with `getSerialized` from `@shopify/react-html`.

```tsx
// app/ui/index.tsx

import React from 'react';
import {useRequestHeader} from '@shopify/react-network';
import {CustomUniversalProvider} from './foundation/CustomUniversalProvider';
import {ComponentWithCustomHeader} from './components/ComponentWithCustomHeader';
import {getSerialized} from '@shopify/react-html';

const IS_CLIENT = typeof window !== 'undefined';

function App() {
// get `x-custom-header` from the request that was sent through Rails ReactController
const customHeader = useRequestHeader('x-custom-header');
// get `x-quilt-data` from the request that was sent through Rails ReactController
const quiltData = IS_CLIENT ? getSerialized<{[key: string]: any}>('x-quilt-data') : null;

return (
<CustomUniversalProvider value={customHeader}>
<h1>My application ❤️</h1>
<ComponentWithCustomHeader />
</CustomUniversalProvider>
);
// Logs {"x-custom-header":"header-value-a","some_id":123}
console.log(quiltData);

return <h1>Data: {quiltData}</h1>;
}

export default App;
```

```tsx
// app/ui/components/ComponentWithCustomHeader.tsx

import React, {useContext} from 'react';
import {CustomContext} from '../foundation/CustomUniversalProvider';

export function ComponentWithCustomHeader() {
// get `x-custom-header` from serialized context
// will be 'header-value-a' in this example
const customHeader = useContext(CustomContext);

return <span>{customHeader}</span>;
}
```

##### Example: redirecting

```tsx
Expand Down
18 changes: 11 additions & 7 deletions gems/quilt_rails/lib/quilt_rails/react_renderable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,29 @@ module Quilt
module ReactRenderable
include ReverseProxy::Controller

def render_react(headers: {})
def render_react(headers: {}, data: {})
raise DoNotIntegrationTestError if Rails.env.test?

# Allow concurrent loading to prevent this thread from blocking class
# loading in controllers called by the Node server.
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
call_proxy(headers)
call_proxy(headers, data)
end
end

private

def call_proxy(headers)
def call_proxy(headers, data)
if defined? ShopifySecurityBase
ShopifySecurityBase::HTTPHostRestriction.whitelist([Quilt.configuration.react_server_host]) do
proxy(headers)
proxy(headers, data)
end
else
proxy(headers)
proxy(headers, data)
end
end

def proxy(headers)
def proxy(headers, data)
url = "#{Quilt.configuration.react_server_protocol}://#{Quilt.configuration.react_server_host}"
Quilt::Logger.log("[ReactRenderable] proxying to React server at #{url}")

Expand All @@ -37,7 +37,11 @@ def proxy(headers)
end

begin
reverse_proxy(url, headers: headers.merge('X-CSRF-Token': form_authenticity_token)) do |callbacks|

reverse_proxy(
url,
headers: headers.merge('X-CSRF-Token': form_authenticity_token, 'X-Quilt-Data': headers.merge(data).to_json)
) do |callbacks|
callbacks.on_response do |status_code, _response|
Quilt::Logger.log("[ReactRenderable] #{url} returned #{status_code}")
end
Expand Down
26 changes: 24 additions & 2 deletions gems/quilt_rails/test/quilt_rails/react_renderable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,41 @@ def test_render_react_calls_reverse_proxy_with_server_uri_and_csrf
Rails.env.stubs(:test?).returns(false)
url = "#{Quilt.configuration.react_server_protocol}://#{Quilt.configuration.react_server_host}"

assert_equal(render_react, reverse_proxy(url, headers: { 'X-CSRF-Token': form_authenticity_token }))
assert_equal(
render_react,
reverse_proxy(
url,
headers: { 'X-CSRF-Token': form_authenticity_token, 'X-Quilt-Data': {}.to_json }
)
)
end

def test_render_react_calls_with_custom_headers
Rails.env.stubs(:test?).returns(false)
url = "#{Quilt.configuration.react_server_protocol}://#{Quilt.configuration.react_server_host}"

render_result = render_react(headers: { 'x-custom-header': 'test' })
proxy_result = reverse_proxy(url, headers: { 'x-custom-header': 'test', 'X-CSRF-Token': form_authenticity_token })
headers = {
'x-custom-header': 'test',
'X-CSRF-Token': form_authenticity_token,
'X-Quilt-Data': { 'x-custom-header': 'test' }.to_json,
}
proxy_result = reverse_proxy(url, headers: headers)

assert_equal(render_result, proxy_result)
end

def test_render_react_calls_reverse_proxy_with_header_data
Rails.env.stubs(:test?).returns(false)
url = "#{Quilt.configuration.react_server_protocol}://#{Quilt.configuration.react_server_host}"

headers = { 'X-CSRF-Token': form_authenticity_token, 'X-Quilt-Data': { 'X-Foo': 'bar' }.to_json }
assert_equal(
render_react(data: { 'X-Foo': 'bar' }),
reverse_proxy(url, headers: headers)
)
end

def test_render_react_errors_in_tests
Rails.env.stubs(:test?).returns(true)
assert_raises Quilt::ReactRenderable::DoNotIntegrationTestError do
Expand Down
4 changes: 4 additions & 0 deletions packages/react-server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

- Add: Serialize `x-quilt-data` received from the Rails server for use on the client ([#1411](https://github.com/Shopify/quilt/pull/1411))

## [0.10.0] - 2020-03-23

- Allow `assetName` to take a function for apps which need to serve multiple sub-apps based on path [[#1332]](https://github.com/Shopify/quilt/pull/1332)
Expand Down
8 changes: 8 additions & 0 deletions packages/react-server/src/render/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
HtmlContext,
stream,
} from '@shopify/react-html/server';
import {useSerialized} from '@shopify/react-html';
import {
applyToContext,
NetworkContext,
Expand Down Expand Up @@ -37,6 +38,10 @@ export interface RenderFunction {
(ctx: Context): React.ReactElement<any>;
}

interface Data {
value: {[key: string]: any} | undefined;
}

type Options = Pick<
NonNullable<ArgumentAtIndex<typeof extract, 1>>,
'afterEachPass' | 'betweenEachPass'
Expand Down Expand Up @@ -72,11 +77,14 @@ export function createRender(render: RenderFunction, options: Options = {}) {
const hydrationManager = new HydrationManager();

function Providers({children}: {children: React.ReactElement<any>}) {
const [, Serialize] = useSerialized<Data>('x-quilt-data');

return (
<AsyncAssetContext.Provider value={asyncAssetManager}>
<HydrationContext.Provider value={hydrationManager}>
<NetworkContext.Provider value={networkManager}>
{children}
<Serialize data={() => ctx.headers['x-quilt-data']} />
</NetworkContext.Provider>
</HydrationContext.Provider>
</AsyncAssetContext.Provider>
Expand Down
15 changes: 15 additions & 0 deletions packages/react-server/src/render/test/render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ describe('createRender', () => {
expect(await readStream(ctx.body)).toContain(myCoolApp);
});

it('response contains x-quilt-data from headers', async () => {
const myCoolApp = 'My cool app';
const customHeader = {'X-foo': 'bar'};
const ctx = createMockContext({
headers: {'x-quilt-data': JSON.stringify(customHeader)},
});

const renderFunction = createRender(() => <>{myCoolApp}</>);
await renderFunction(ctx, noop);

const response = await readStream(ctx.body);
expect(response).toContain('x-quilt-data');
expect(response).toContain('X-foo');
});

it('does not clobber proxies in the context object', async () => {
const headerValue = 'some-value';
const ctx = createMockContext({headers: {'some-header': headerValue}});
Expand Down
2 changes: 1 addition & 1 deletion packages/react-server/src/server/test/server.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('createServer()', () => {
const response = await wrapper.request();

expect(await response.text()).toStrictEqual(
`<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="referrer" content="never"/></head><body><div id="app"><div>markup</div></div></body></html>`,
`<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="referrer" content="never"/></head><body><div id="app"><div>markup</div></div><script type="text/json" data-serialized-id="x-quilt-data">undefined</script></body></html>`,
);
});

Expand Down

0 comments on commit ed882c7

Please sign in to comment.