@shopify/react-effect
Advanced tools
Comparing version 1.1.0-beta.1 to 2.0.0
@@ -10,2 +10,12 @@ # Changelog | ||
## [2.0.0] | ||
### Changed | ||
- Removed `react-tree-walker` as a way to process the React element. Instead, the application is rendered to a string repeatedly until no more promises have been queued. For full details on migrating to the new API, please read the [upgrade guide](./documentation/migrating-version-1-to-2.md). [#477](https://github.com/Shopify/quilt/pull/477) | ||
## [1.0.3] | ||
- Manual release | ||
## [1.0.1] | ||
@@ -12,0 +22,0 @@ |
@@ -1,15 +0,11 @@ | ||
import * as React from 'react'; | ||
import { METHOD_NAME, Extractable } from './extractable'; | ||
/// <reference types="react" /> | ||
import { Omit } from '@shopify/useful-types'; | ||
import { EffectManager } from './context'; | ||
import { EffectKind } from './types'; | ||
interface Props { | ||
kind?: symbol; | ||
serverOnly?: boolean; | ||
clientOnly?: boolean; | ||
perform(): void; | ||
kind?: EffectKind; | ||
manager?: EffectManager; | ||
perform(): any; | ||
} | ||
export default class Effect extends React.PureComponent<Props> implements Extractable { | ||
componentDidMount(): void; | ||
[METHOD_NAME](include: boolean | symbol[]): void; | ||
render(): {} | null; | ||
private perform; | ||
} | ||
export default function Effect(props: Omit<Props, 'manager'>): JSX.Element | null; | ||
export {}; |
@@ -5,31 +5,27 @@ "use strict"; | ||
var React = tslib_1.__importStar(require("react")); | ||
var extractable_1 = require("./extractable"); | ||
var Effect = /** @class */ (function (_super) { | ||
tslib_1.__extends(Effect, _super); | ||
function Effect() { | ||
var context_1 = require("./context"); | ||
var ConnectedEffect = /** @class */ (function (_super) { | ||
tslib_1.__extends(ConnectedEffect, _super); | ||
function ConnectedEffect() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
Effect.prototype.componentDidMount = function () { | ||
var serverOnly = this.props.serverOnly; | ||
return this.perform(!serverOnly); | ||
}; | ||
Effect.prototype[extractable_1.METHOD_NAME] = function (include) { | ||
var clientOnly = this.props.clientOnly; | ||
return this.perform(clientOnly ? false : include); | ||
}; | ||
Effect.prototype.render = function () { | ||
ConnectedEffect.prototype.render = function () { | ||
this.perform(); | ||
return this.props.children || null; | ||
}; | ||
Effect.prototype.perform = function (include) { | ||
var _a = this.props, kind = _a.kind, perform = _a.perform; | ||
if (!include) { | ||
return undefined; | ||
ConnectedEffect.prototype.perform = function () { | ||
var _a = this.props, kind = _a.kind, manager = _a.manager, perform = _a.perform; | ||
if (manager == null || (kind != null && !manager.shouldPerform(kind))) { | ||
return; | ||
} | ||
if (include === true || (kind != null && include.includes(kind))) { | ||
return perform(); | ||
} | ||
return undefined; | ||
manager.add(perform(), kind); | ||
}; | ||
return Effect; | ||
return ConnectedEffect; | ||
}(React.PureComponent)); | ||
function Effect(props) { | ||
if (typeof window !== 'undefined') { | ||
return null; | ||
} | ||
return (React.createElement(context_1.EffectContext.Consumer, null, function (manager) { return React.createElement(ConnectedEffect, tslib_1.__assign({ manager: manager }, props)); })); | ||
} | ||
exports.default = Effect; |
export { default as Effect } from './Effect'; | ||
export { Extractable, METHOD_NAME } from './extractable'; | ||
export { EffectManager } from './context'; | ||
export { EffectKind } from './types'; |
@@ -5,3 +5,3 @@ "use strict"; | ||
exports.Effect = Effect_1.default; | ||
var extractable_1 = require("./extractable"); | ||
exports.METHOD_NAME = extractable_1.METHOD_NAME; | ||
var context_1 = require("./context"); | ||
exports.EffectManager = context_1.EffectManager; |
@@ -1,5 +0,10 @@ | ||
import { ReactElement } from 'react'; | ||
export interface Middleware { | ||
(instance: any): any; | ||
import * as React from 'react'; | ||
interface Options { | ||
include?: symbol[] | boolean; | ||
decorate?(element: React.ReactElement<any>): React.ReactElement<any>; | ||
renderFunction?(element: React.ReactElement<{}>): string; | ||
betweenEachPass?(): any; | ||
afterEachPass?(): any; | ||
} | ||
export declare function extract(app: ReactElement<any>, include?: symbol[] | boolean, middleware?: Middleware[]): Promise<void>; | ||
export declare function extract(app: React.ReactElement<any>, { include, decorate, renderFunction, betweenEachPass, afterEachPass, }?: Options): Promise<string>; | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var tslib_1 = require("tslib"); | ||
var react_traverse_nodes_1 = require("@shopify/react-traverse-nodes"); | ||
var extractable_1 = require("./extractable"); | ||
var defaultContext = { | ||
reactExtractRunning: true, | ||
}; | ||
function extract(app, include, middleware) { | ||
if (include === void 0) { include = true; } | ||
if (middleware === void 0) { middleware = []; } | ||
var extractors = tslib_1.__spread([ | ||
function (instance) { | ||
return extractable_1.isExtractable(instance) ? instance[extractable_1.METHOD_NAME](include) : undefined; | ||
} | ||
], middleware); | ||
return react_traverse_nodes_1.traverse(app, function (_, instance) { | ||
return Promise.all(extractors.map(function (extractor) { return Promise.resolve(extractor(instance)); })); | ||
}, tslib_1.__assign({}, defaultContext)); | ||
var React = tslib_1.__importStar(require("react")); | ||
var server_1 = require("react-dom/server"); | ||
var context_1 = require("./context"); | ||
function extract(app, _a) { | ||
var _b = _a === void 0 ? {} : _a, include = _b.include, _c = _b.decorate, decorate = _c === void 0 ? identity : _c, _d = _b.renderFunction, renderFunction = _d === void 0 ? server_1.renderToStaticMarkup : _d, betweenEachPass = _b.betweenEachPass, afterEachPass = _b.afterEachPass; | ||
var manager = new context_1.EffectManager({ include: include }); | ||
var element = (React.createElement(context_1.EffectContext.Provider, { value: manager }, decorate(app))); | ||
return (function perform() { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var result; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
result = renderFunction(element); | ||
if (!manager.finished) return [3 /*break*/, 4]; | ||
return [4 /*yield*/, manager.afterEachPass()]; | ||
case 1: | ||
_a.sent(); | ||
if (!afterEachPass) return [3 /*break*/, 3]; | ||
return [4 /*yield*/, afterEachPass()]; | ||
case 2: | ||
_a.sent(); | ||
_a.label = 3; | ||
case 3: return [2 /*return*/, result]; | ||
case 4: return [4 /*yield*/, manager.resolve()]; | ||
case 5: | ||
_a.sent(); | ||
return [4 /*yield*/, manager.betweenEachPass()]; | ||
case 6: | ||
_a.sent(); | ||
if (!betweenEachPass) return [3 /*break*/, 8]; | ||
return [4 /*yield*/, betweenEachPass()]; | ||
case 7: | ||
_a.sent(); | ||
_a.label = 8; | ||
case 8: return [4 /*yield*/, manager.afterEachPass()]; | ||
case 9: | ||
_a.sent(); | ||
if (!afterEachPass) return [3 /*break*/, 11]; | ||
return [4 /*yield*/, afterEachPass()]; | ||
case 10: | ||
_a.sent(); | ||
_a.label = 11; | ||
case 11: return [2 /*return*/, perform()]; | ||
} | ||
}); | ||
}); | ||
})(); | ||
} | ||
exports.extract = extract; | ||
function identity(value) { | ||
return value; | ||
} |
{ | ||
"name": "@shopify/react-effect", | ||
"version": "1.1.0-beta.1", | ||
"version": "2.0.0", | ||
"license": "MIT", | ||
@@ -26,6 +26,7 @@ "description": "A component and set of utilities for performing effects within a universal React app", | ||
"dependencies": { | ||
"@shopify/react-traverse-nodes": "^0.1.0-beta.2", | ||
"@shopify/useful-types": "^1.1.0", | ||
"tslib": "^1.9.3" | ||
}, | ||
"devDependencies": { | ||
"@types/react-dom": "^16.0.11", | ||
"typescript": "~3.0.1" | ||
@@ -32,0 +33,0 @@ }, |
@@ -28,7 +28,2 @@ # `@shopify/react-effect` | ||
By default, this callback will run in two ways: | ||
- On the client, during `componentDidMount` | ||
- On the server, when called using the [`extract`](#extract) function documented below | ||
This callback can return anything, but returning a promise has a special effect: it will be waited for on the server when calling `extract()`. | ||
@@ -38,5 +33,3 @@ | ||
- `kind`: a symbol detailing the "category" of the effect. This will be used to optionally skip some categories when calling `extract()` | ||
- `clientOnly`: will only call the effect in `componentDidMount`, not when extracting on the server | ||
- `serverOnly`: will only call the effect during extraction, not in `componentDidMount` | ||
- `kind`: a description of the effect. This kind, if provided, must have an `id` that is a unique symbol, and can optionally have `betweenEachPass` and `afterEachPass` functions that add additional logic to the `betweenEachPass` and `afterEachPass` options for `extract()`. | ||
@@ -47,6 +40,5 @@ This component also accepts children which are rendered as-is. | ||
You can call `extract()` on a React tree in order to perform all of the effects within that tree. This function uses the [`react-tree-walker`](https://github.com/ctrlplusb/react-tree-walker) package to perform the tree walk, which creates some implications for the return value of your `perform` actions: | ||
You 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. | ||
- Returning a promise will wait on the promise before processing the rest of the tree | ||
- Returning `false` will prevent the rest of the tree from being processed | ||
> **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. | ||
@@ -66,41 +58,46 @@ This function returns a promise that resolves when the tree has been fully processed. | ||
You may optionally pass a second argument to this function: an array of symbols representing the categories to include in your processing of the tree (matching the `kind` prop on `Extract` components). | ||
You may optionally pass an options object that contains the following keys (all of which are optional): | ||
```tsx | ||
import {renderToString} from 'react-dom/server'; | ||
import {EFFECT_ID as I18N_EFFECT_ID} from '@shopify/react-i18n'; | ||
import {extract} from '@shopify/react-effect/server'; | ||
- `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. | ||
async function app(ctx) { | ||
const app = <App />; | ||
// will only perform @shopify/react-i18n extraction | ||
await extract(app, [I18N_EFFECT_ID]); | ||
ctx.body = renderToString(app); | ||
} | ||
``` | ||
```tsx | ||
import {renderToString} from 'react-dom/server'; | ||
import {EFFECT_ID as I18N_EFFECT_ID} from '@shopify/react-i18n'; | ||
import {extract} from '@shopify/react-effect/server'; | ||
### Custom extractable components | ||
async function app(ctx) { | ||
const app = <App />; | ||
// will only perform @shopify/react-i18n extraction | ||
await extract(app, [I18N_EFFECT_ID]); | ||
ctx.body = renderToString(app); | ||
} | ||
``` | ||
Usually, the `Extract` component will do what you need, but you may occasionally need your own component to directly implement the "extractable" part. This can be the case when your component must do something in `extract` that ends up calling `setState`. In these cases, you can use two additional exports from this module, `METHOD_NAME` and the `Extractable` interface, to manually implement a method that will be called during extraction: | ||
- `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). This function can return a promise, and it will be waited on before continuing. | ||
```ts | ||
import {METHOD_NAME, Extractable} from '@shopify/react-effect'; | ||
- `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. | ||
export const EFFECT_ID = Symbol('MyComponentEffect'); | ||
- `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. | ||
class MyComponent extends React.Component implements Extractable { | ||
[METHOD_NAME](include: boolean | symbol[]) { | ||
// When implementing your own version of this, you should | ||
// implement your own check for the effect "kind". The | ||
// Effect component does this automatically. | ||
if ( | ||
include === true || | ||
(Array.isArray(include) && include.includes(EFFECT_ID)) | ||
) { | ||
this.setState({extracting: true}); | ||
} | ||
```tsx | ||
import {renderToString} from 'react-dom/server'; | ||
import {extract} from '@shopify/react-effect/server'; | ||
import {createApolloBridge} from '@shopify/react-effect-apollo'; | ||
async function app(ctx) { | ||
const ApolloBridge = createApolloBridge(); | ||
const app = <App />; | ||
await extract(app, { | ||
decorate(element) { | ||
return <ApolloBridge>{element}</ApolloBridge>; | ||
}, | ||
}); | ||
ctx.body = renderToString(app); | ||
} | ||
} | ||
``` | ||
``` | ||
- `renderFunction`: an alternative function to `renderToStaticMarkup` for traversing the tree. | ||
## Gotchas | ||
@@ -107,0 +104,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
16598
15
232
0
2
142
1
+ Added@shopify/useful-types@^1.1.0
+ Added@shopify/useful-types@1.3.0(transitive)
+ Added@types/prop-types@15.7.13(transitive)
+ Added@types/react@18.3.12(transitive)
+ Addedcsstype@3.1.3(transitive)