UNPKG

7.12 kBMarkdownView Raw
1# `@shopify/react-effect`
2
3[![Build Status](https://github.com/Shopify/quilt/workflows/Node-CI/badge.svg?branch=main)](https://github.com/Shopify/quilt/actions?query=workflow%3ANode-CI)
4[![Build Status](https://github.com/Shopify/quilt/workflows/Ruby-CI/badge.svg?branch=main)](https://github.com/Shopify/quilt/actions?query=workflow%3ARuby-CI)
5[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) [![npm version](https://badge.fury.io/js/%40shopify%2Freact-effect.svg)](https://badge.fury.io/js/%40shopify%2Freact-effect.svg) [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@shopify/react-effect.svg)](https://img.shields.io/bundlephobia/minzip/@shopify/react-effect.svg)
6
7This package contains a component and set of utilities for performing multiple effects during a single pass of server-side rendering in a universal React application.
8
9## Installation
10
11```bash
12$ yarn add @shopify/react-effect
13```
14
15## Usage
16
17### `useServerEffect()`
18
19This package is largely built around a hook, `useServerEffect`. The only mandatory argument is a function, which is the "effect" you wish to perform during each pass of server rendering:
20
21```tsx
22import {useServerEffect} from '@shopify/react-effect';
23
24export default function MyComponent() {
25 useServerEffect(() => console.log('Doing something!'));
26 return null;
27}
28```
29
30This callback can return anything, but returning a promise has a special effect: it will be waited for on the server when calling `extract()`.
31
32This hook also accepts a second, optional argument: the effect "kind". This should be an object that:
33
34- Must have an `id` that is a unique symbol
35- Optionally has `betweenEachPass` and/ or `afterEachPass` functions that add additional logic to the `betweenEachPass` and `afterEachPass` options for `extract()`
36
37### `<Effect />`
38
39This is a component version of `useServerEffect`. Its `perform` prop will be run as a server effect, and its `kind` prop is used as the second argument to `useServerEffect`. Where possible, prefer the `useServerEffect` hook.
40
41### `extract()`
42
43You can call `extract()` on a React tree in order to perform all of the effects within that tree. This function repeatedly calls a render function (by default, `react-dom`’s `renderToStaticMarkup`), collects any `Effect` promises and, if there are promises, waits on them before performing another pass. This process ends when no more promises are collected during a pass of your tree.
44
45> **Note**: this flow is significantly different from the previous version, which relied on a custom tree walk. Calling `extract()` no longer waits for promises collected higher in the tree before processing the rest. Instead, it relies on multiple passes, which gives application code the option to process promises at many layers of the app in parallel, rather than in sequence.
46
47This function returns a promise that resolves when the tree has been fully processed.
48
49```tsx
50import {renderToString} from 'react-dom/server';
51import {extract} from '@shopify/react-effect/server';
52
53async function app(ctx) {
54 const app = <App />;
55 await extract(app);
56 ctx.body = renderToString(app);
57}
58```
59
60You may optionally pass an options object that contains the following keys (all of which are optional):
61
62- `include`: an array of symbols that should be collected during tree traversal. These IDs must align with the `kind.id` field on `<Extract />` elements in your application.
63
64 ```tsx
65 import {renderToString} from 'react-dom/server';
66 import {EFFECT_ID as I18N_EFFECT_ID} from '@shopify/react-i18n';
67 import {extract} from '@shopify/react-effect/server';
68
69 async function app(ctx) {
70 const app = <App />;
71 // will only perform @shopify/react-i18n extraction
72 await extract(app, {include: [I18N_EFFECT_ID]});
73 ctx.body = renderToString(app);
74 }
75 ```
76
77- `maxPasses`: a number that limits the number of render/ resolve cycles `extract` is allowed to perform. This option defaults to `5`.
78
79- `afterEachPass`: a function that is called after each pass of your tree, regardless of whether traversal is "finished". This function can return a promise, and it will be waited on before continuing. This function is called with the same argument as the `betweenEachPass` option. Returning `false` (or a promise for `false`) from this method will bail out of subsequent passes.
80
81- `betweenEachPass`: a function that is called after a pass of your tree that did not "finish" (that is, there were still promises that got collected, and we are still less than `maxPasses`). This function can return a promise, and it will be waited on before continuing. It is called with a single argument: a `Pass` object, which contains the `index`, `finished`, `cancelled` (`maxPasses` reached), `renderDuration` and `resolveDuration` of the just-completed pass. If there is another pass to perform, this method is called **after** `afterEachPass`.
82
83- `decorate`: a function that takes the root React element in your tree and returns a new tree to use. You can use this to wrap your application in context providers that only your server render requires.
84
85 ```tsx
86 import {renderToString} from 'react-dom/server';
87 import {extract} from '@shopify/react-effect/server';
88 import {HtmlContext, HtmlManager} from '@shopify/react-html/server';
89
90 async function app(ctx) {
91 const htmlManager = new HtmlManager();
92 const app = <App />;
93
94 await extract(app, {
95 decorate(element) {
96 return (
97 <HtmlContext.Provider value={htmlManager}>
98 {element}
99 </HtmlContext.Provider>
100 );
101 },
102 });
103
104 ctx.body = renderToString(app);
105 }
106 ```
107
108- `renderFunction`: an alternative function to `renderToStaticMarkup` for traversing the tree.
109
110## Gotchas
111
112A common mistake is initializing a provider entirely within your application component, and setting some details on this provider during the extraction. There is nothing implicitly wrong with this, but it will usually not have the effect you are after. When you call `renderToString()` to actually generate your HTML, the app will be reinitialized, and all of the work you did in the extraction call will be lost. To avoid this, pass any "stateful" managers/ providers into your application:
113
114```tsx
115class StatefulManager {}
116const {Provider, Consumer} = React.createContext();
117
118// bad
119export default function App() {
120 return (
121 <Provider value={new StatefulManager()}>
122 <Consumer>
123 {manager => <Effect perform={() => (manager.value = true)} />}
124 </Consumer>
125 </Provider>
126 );
127}
128
129const app = <App />;
130await extract(app);
131
132// All your work is lost now, because the components are reinitialized
133renderToString(app);
134
135// good
136export default function App({manager}) {
137 return (
138 <Provider value={manager}>
139 <Consumer>
140 {manager => <Effect perform={() => (manager.value = true)} />}
141 </Consumer>
142 </Provider>
143 );
144}
145
146const manager = new StatefulManager();
147const app = <App manager={manager} />;
148await extract(app);
149
150// All your work is preserved, because you passed in the same manager
151renderToString(app);
152```