From 5ec9c97cfdac273b99f41e700f02aff7434dc819 Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 14:06:52 -0700 Subject: [PATCH 01/13] Add license banner to new file --- src/context.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/context.js b/src/context.js index 40e73a83b3..5cd3ccf09b 100644 --- a/src/context.js +++ b/src/context.js @@ -1,4 +1,10 @@ -// @flow +/** Copyright (c) 2019 Uber Technologies, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ import * as React from 'react'; import {createPlugin} from 'fusion-core'; import type FusionApp, {FusionPlugin, Middleware} from 'fusion-core'; From 16d03be6b45fc1461bfac0fdf55ac83ceacca700 Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 14:07:38 -0700 Subject: [PATCH 02/13] Move return type to generic type param --- src/context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/context.js b/src/context.js index 5cd3ccf09b..21eb694736 100644 --- a/src/context.js +++ b/src/context.js @@ -16,7 +16,7 @@ export const ServiceContext = React.createContext(() => {}); type ReturnsType = () => T; export function useService(token: ReturnsType): TService { - const getService: (ReturnsType) => TService = React.useContext( + const getService = React.useContext<(ReturnsType) => TService>( ServiceContext ); const provides = getService(token); From 51f5429c133167be619970a5b139c1256ea718e7 Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 14:08:44 -0700 Subject: [PATCH 03/13] Move withServices to context, remove React.createContext ponyfill in lieu of ServiceConsumer --- src/context.js | 41 +++++++++++++++ src/injector.js | 131 ------------------------------------------------ 2 files changed, 41 insertions(+), 131 deletions(-) delete mode 100644 src/injector.js diff --git a/src/context.js b/src/context.js index 21eb694736..83af4f445d 100644 --- a/src/context.js +++ b/src/context.js @@ -70,3 +70,44 @@ export function serviceContextPlugin(app: FusionApp): FusionPlugin { }, }); } + +type Dependencies = {[string]: ReturnsType}; +type Services = {[string]: ReturnsType}; +type Props = {[string]: any}; +type Mapper = Services => Props; + +function getServices(getService, deps: Dependencies): Services { + const services = {}; + + Object.keys(deps).forEach((name: string) => { + services[name] = getService(deps[name]); + }); + + return services; +} + +const identity = i => i; + +export function withServices( + deps: Dependencies, + mapServicesToProps: Mapper = identity +) { + function resolve(getService) { + const services = getServices(getService, deps); + const serviceProps = mapServicesToProps(services); + + return serviceProps; + } + + return (Component: React.ComponentType<*>) => { + return function WithServices(props?: Props) { + return ( + + {(getService: (ReturnsType) => TService) => ( + + )} + + ); + }; + }; +} diff --git a/src/injector.js b/src/injector.js deleted file mode 100644 index 6bc4e42e18..0000000000 --- a/src/injector.js +++ /dev/null @@ -1,131 +0,0 @@ -/** Copyright (c) 2018 Uber Technologies, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import * as React from 'react'; -import FusionApp, {createPlugin} from 'fusion-core'; -import PropTypes from 'prop-types'; - -type Dependencies = {[string]: any}; -type Services = {[string]: any}; -type Props = {[string]: any}; -type Mapper = Services => Props; - -// React.createContext ponyfill -function createContext(value) { - if ('createContext' in React) { - return React.createContext<(Dependencies) => Services>(value); - } - - const key = `_fusionContextPonyfill${Math.random()}`; - - class Provider extends React.Component<*> { - getChildContext() { - return {[key]: this.props.value || value}; - } - - render() { - return this.props.children; - } - } - - Provider.childContextTypes = { - [key]: PropTypes.any.isRequired, - }; - - function Consumer(props: *, context: *) { - return props.children(context[key]); - } - - Consumer.contextTypes = { - [key]: PropTypes.any.isRequired, - }; - - return { - Provider, - Consumer, - }; -} - -let InjectorContext; - -function getServices(app: FusionApp, deps: Dependencies): Services { - const services = {}; - - Object.entries(deps).forEach(([name, token]) => { - // To be addressed in a future Flow-focued PR. - // $FlowFixMe - services[name] = app.getService(token); - }); - - return services; -} - -// istanbul ignore next -function defaultInject(deps: Dependencies): Services { - return {}; -} - -function defaultMap(services: Services): Props { - return services; -} - -export function registerInjector(app: FusionApp) { - // Lazily create context for easier testing - InjectorContext = createContext(defaultInject); - - function inject(deps: Dependencies): Services { - return getServices(app, deps); - } - - function renderProvider(children) { - return ( - - {children} - - ); - } - - const injectorPlugin = createPlugin({ - middleware: () => (ctx, next) => { - ctx.element = ctx.element && renderProvider(ctx.element); - - return next(); - }, - }); - - app.register(injectorPlugin); -} - -export function withServices( - deps: Dependencies, - mapServicesToProps: Mapper = defaultMap -) { - function resolve(inject, props) { - const services = inject(deps); - const serviceProps = mapServicesToProps(services); - - return { - ...serviceProps, - ...props, - }; - } - - function renderConsumer(Component, props) { - return ( - - {inject => } - - ); - } - - return (Component: React.ComponentType<*>) => { - return function WithServices(props: *) { - return renderConsumer(Component, props); - }; - }; -} From a46a3e1ae2480ef916ec098a4d524a0931ad15af Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 14:09:35 -0700 Subject: [PATCH 04/13] Update import from injector, remove injector call --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index d22f25ecd7..339c42eb20 100644 --- a/src/index.js +++ b/src/index.js @@ -16,7 +16,6 @@ import FusionApp, { } from 'fusion-core'; import {prepare} from './async/index.js'; import PrepareProvider from './async/prepare-provider'; -import {registerInjector, withServices} from './injector.js'; import serverRender from './server'; import clientRender from './client'; @@ -24,12 +23,14 @@ import clientRender from './client'; import ProviderPlugin from './plugin'; import ProvidedHOC from './hoc'; import Provider from './provider'; + import { FusionContext, ServiceConsumer, ServiceContext, serviceContextPlugin, useService, + withServices, } from './context.js'; export type Render = (el: React.Element<*>, context: Context) => any; @@ -88,7 +89,6 @@ export default class App extends FusionApp { }, }); super(root, renderer); - registerInjector(this); this.register(serviceContextPlugin(this)); } } From 1e4c1043262f9e03e0d37f0efef445beb6d64cfd Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 14:10:42 -0700 Subject: [PATCH 05/13] Remove call to serviceContextPlugin, update test and error descriptions --- src/__tests__/context.node.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/__tests__/context.node.js b/src/__tests__/context.node.js index b755a9a192..2efa787ecd 100644 --- a/src/__tests__/context.node.js +++ b/src/__tests__/context.node.js @@ -13,12 +13,11 @@ import {getSimulator} from 'fusion-test-utils'; import App from '../index'; import { FusionContext, - serviceContextPlugin, ServiceConsumer, useService, } from '../context.js'; -test('useService hook', async t => { +test('context#useService', async t => { const TestToken = createToken('test'); const TestPlugin = createPlugin({provides: () => 3}); let didRender = false; @@ -33,7 +32,6 @@ test('useService hook', async t => { const element = React.createElement(TestComponent); const app = new App(element); app.register(TestToken, TestPlugin); - app.register(serviceContextPlugin(app)); const sim = getSimulator(app); const ctx = await sim.render('/'); t.ok(typeof ctx.body === 'string' && ctx.body.includes('hello'), 'renders'); @@ -41,7 +39,7 @@ test('useService hook', async t => { t.end(); }); -test('context error', async t => { +test('context#useService - unregistered token', async t => { let didRender = false; function TestComponent() { const TestToken = createToken('test'); @@ -51,7 +49,6 @@ test('context error', async t => { } const element = React.createElement(TestComponent); const app = new App(element); - app.register(serviceContextPlugin(app)); const sim = getSimulator(app); try { await sim.render('/'); @@ -65,7 +62,7 @@ test('context error', async t => { t.end(); }); -test('context error with optional token', async t => { +test('context#useService - optional token', async t => { let didRender = false; function TestComponent() { const TestToken = createToken('test'); @@ -75,14 +72,13 @@ test('context error with optional token', async t => { } const element = React.createElement(TestComponent); const app = new App(element); - app.register(serviceContextPlugin(app)); const sim = getSimulator(app); await sim.render('/'); - t.ok(didRender); + t.ok(didRender, 'renders without error'); t.end(); }); -test('context consumer component', async t => { +test('context#ServiceConsumer', async t => { const TestToken = createToken('test'); const TestPlugin = createPlugin({provides: () => 3}); let didRender = false; From 59a5f5a298b9105ec45e54f4f1064d8e3eedd30d Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 14:11:32 -0700 Subject: [PATCH 06/13] Add test for withServices, delete injector test file --- src/__tests__/context.node.js | 33 +++++++++++++- src/__tests__/injector.node.js | 82 ---------------------------------- 2 files changed, 32 insertions(+), 83 deletions(-) delete mode 100644 src/__tests__/injector.node.js diff --git a/src/__tests__/context.node.js b/src/__tests__/context.node.js index 2efa787ecd..c4c0f4015c 100644 --- a/src/__tests__/context.node.js +++ b/src/__tests__/context.node.js @@ -15,6 +15,7 @@ import { FusionContext, ServiceConsumer, useService, + withServices, } from '../context.js'; test('context#useService', async t => { @@ -96,7 +97,37 @@ test('context#ServiceConsumer', async t => { const element = React.createElement(TestComponent); const app = new App(element); app.register(TestToken, TestPlugin); - app.register(serviceContextPlugin(app)); + const sim = getSimulator(app); + const ctx = await sim.render('/'); + t.ok(typeof ctx.body === 'string' && ctx.body.includes('hello'), 'renders'); + t.ok(didRender); + t.end(); +}); + +test('context#withServices', async t => { + const TestToken1 = createToken('test-1'); + const TestToken2 = createToken('test-2'); + const TestPlugin1 = createPlugin({provides: () => 1}); + const TestPlugin2 = createPlugin({provides: () => 2}); + let didRender = false; + function TestComponent({mappedOne, mappedTwo, propValue}) { + didRender = true; + t.equal(mappedOne, 1, 'gets registered service'); + t.equal(mappedTwo, 2, 'gets registered service'); + t.equal(propValue, 3, 'passes props through'); + return React.createElement('div', null, 'hello'); + } + const WrappedComponent = withServices( + { + test1: TestToken1, + test2: TestToken2, + }, + deps => ({mappedOne: deps.test1, mappedTwo: deps.test2}) + )(TestComponent); + const element = React.createElement(WrappedComponent, {propValue: 3}); + const app = new App(element); + app.register(TestToken1, TestPlugin1); + app.register(TestToken2, TestPlugin2); const sim = getSimulator(app); const ctx = await sim.render('/'); t.ok(typeof ctx.body === 'string' && ctx.body.includes('hello'), 'renders'); diff --git a/src/__tests__/injector.node.js b/src/__tests__/injector.node.js deleted file mode 100644 index 918f4b7c78..0000000000 --- a/src/__tests__/injector.node.js +++ /dev/null @@ -1,82 +0,0 @@ -/** Copyright (c) 2018 Uber Technologies, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import tape from 'tape-cup'; -import * as React from 'react'; -import {createPlugin, createToken} from 'fusion-core'; -import {getSimulator} from 'fusion-test-utils'; -import App, {withServices} from '../index'; - -import type {Context} from 'fusion-core'; - -async function injectServices(t) { - const HelloToken = createToken('hola'); - const HelloPlugin = createPlugin({ - provides() { - return 'world'; - }, - }); - - const GoodbyeToken = createToken('adios'); - const GoodbyePlugin = createPlugin({ - provides() { - return 'moon'; - }, - }); - - function TestComponent({hi, bye}) { - t.equal(hi, 'world'); - t.equal(bye, 'moon'); - - return ( -
- {hi} {bye} -
- ); - } - - const TestComponentContainer = withServices({ - hi: HelloToken, - bye: GoodbyeToken, - })(TestComponent); - - const element = React.createElement(TestComponentContainer); - const app = new App(element); - - app.register(HelloToken, HelloPlugin); - app.register(GoodbyeToken, GoodbyePlugin); - - const sim = getSimulator(app); - const {body}: Context = await sim.render('/'); - - t.ok(body && typeof body === 'string' && body.match(/\bworld\b.*\bmoon\b/)); -} - -tape('inject services', async t => { - await injectServices(t); - - t.end(); -}); - -// Can't overwrite React import due to * import. -// Re-enable when we migrate to using fusion-cli to run tests. -// tape('inject services (legacy)', async t => { -// const createContext = React.createContext; - -// try { -// // $FlowFixMe -// React.createContext = undefined; - -// await injectServices(t); -// } finally { -// // $FlowFixMe -// React.createContext = createContext; -// } - -// t.end(); -// }); From 428b24de896df06c033bb1e9dc3aac98e0fc061d Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 14:12:39 -0700 Subject: [PATCH 07/13] Add compose devDep - guilt-free --- package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 2deb4dfef1..c06b01c879 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "flow-bin": "^0.93.0", "fusion-core": "^1.10.4-0", "fusion-test-utils": "^1.3.1", + "just-compose": "^1.1.0", "nyc": "^13.1.0", "prettier": "^1.15.3", "react": "^16.7.0", diff --git a/yarn.lock b/yarn.lock index 2ebd48f3e9..210245f90b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3609,6 +3609,11 @@ jsx-ast-utils@^2.0.1: dependencies: array-includes "^3.0.3" +just-compose@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/just-compose/-/just-compose-1.1.0.tgz#ee914e4f04e044895d2908fec08b1e5f48ff5ac1" + integrity sha512-X9X19Xe3uigbsAL9084V0yWyAv0ZoFnvTSeYg/ElL/ui12lz3kL+enzUimCL9bSJmL7FuL4dfICYBmeSJLtMkg== + keygrip@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" From 4c52946c97b3fdcd6cd3a7d671143ad4a5ac3889 Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 14:13:35 -0700 Subject: [PATCH 08/13] Update HOC to not use legacy context if token was passed, add tests --- src/__tests__/hoc.node.js | 52 ++++++++++++++++++++++++--- src/hoc.js | 74 ++++++++++++++++++++++++++------------- 2 files changed, 97 insertions(+), 29 deletions(-) diff --git a/src/__tests__/hoc.node.js b/src/__tests__/hoc.node.js index 57cb805e25..47672ad6a2 100644 --- a/src/__tests__/hoc.node.js +++ b/src/__tests__/hoc.node.js @@ -8,14 +8,15 @@ import tape from 'tape-cup'; import * as React from 'react'; -import {createPlugin} from 'fusion-core'; +import {createToken, createPlugin} from 'fusion-core'; import {getSimulator} from 'fusion-test-utils'; import PropTypes from 'prop-types'; import App from '../index'; import hoc from '../hoc'; import plugin from '../plugin'; +import compose from 'just-compose'; -tape('hoc', async t => { +tape('hoc#legacy', async t => { const withTest = hoc.create('test'); const testProvides = {hello: 'world'}; let didRender = false; @@ -39,7 +40,7 @@ tape('hoc', async t => { t.end(); }); -tape('hoc with mapProvidesToProps', async t => { +tape('hoc#legacy with mapProvidesToProps', async t => { const withTest = hoc.create('test', provides => { return {mapped: provides}; }); @@ -66,7 +67,7 @@ tape('hoc with mapProvidesToProps', async t => { t.end(); }); -tape('hoc with custom provider', async t => { +tape('hoc#legacy with custom provider', async t => { const withTest = hoc.create('test'); const testProvides = {hello: 'world'}; @@ -106,3 +107,46 @@ tape('hoc with custom provider', async t => { t.ok(didUseCustomProvider); t.end(); }); + +tape('hoc', async t => { + const TestToken1 = createToken('test-token-1'); + const TestToken2 = createToken('test-token-2'); + const TestToken3 = createToken('test-token-3'); + const withTest = compose( + hoc.create('test1', undefined, TestToken1), + hoc.create('test2', undefined, TestToken2), + hoc.create('test3', provides => ({mapped: provides}), TestToken3) + ); + const testProvides1 = {hello: 1}; + const testProvides2 = {hello: 2}; + const testProvides3 = {hello: 3}; + let didRender = false; + function TestComponent(props) { + didRender = true; + t.deepLooseEqual(props.test1, testProvides1, 'works with plain plugin'); + t.deepLooseEqual( + props.test2, + testProvides2, + 'works with legacy PluginProvider' + ); + t.deepLooseEqual(props.mapped, testProvides3, 'maps service to props'); + t.notok(props.ctx, 'does not pass ctx through by default'); + return React.createElement('div', null, 'hello'); + } + const testPlugin1 = createPlugin({provides: () => testProvides1}); + const testPlugin2 = plugin.create( + 'test2', + createPlugin({provides: () => testProvides2}) + ); + const testPlugin3 = createPlugin({provides: () => testProvides3}); + const element = React.createElement(withTest(TestComponent)); + const app = new App(element); + app.register(TestToken1, testPlugin1); + app.register(TestToken2, testPlugin2); + app.register(TestToken3, testPlugin3); + const sim = getSimulator(app); + const ctx = await sim.render('/'); + t.ok(typeof ctx.body === 'string' && ctx.body.includes('hello')); + t.ok(didRender); + t.end(); +}); diff --git a/src/hoc.js b/src/hoc.js index 7241958bd0..b124222b64 100644 --- a/src/hoc.js +++ b/src/hoc.js @@ -7,40 +7,64 @@ */ import * as React from 'react'; - +import {useService} from './context.js'; import PropTypes from 'prop-types'; +import type {Token} from 'fusion-core'; + +function capitalize(str: string): string { + return str.replace(/^./, c => c.toUpperCase()); +} type ReactHOC = (React.ComponentType<*>) => React.ComponentType<*>; export default { - create: (name: string, mapProvidesToProps?: Object => Object): ReactHOC => { + create: ( + name: string, + mapProvidesToProps?: Object => Object, + token?: Token<*> + ): ReactHOC => { const mapProvides = mapProvidesToProps ? mapProvidesToProps : provides => ({[name]: provides}); - return (Component: React.ComponentType<*>) => { - class HOC extends React.Component<*> { - provides: any; + const _token = token; // Make token constant for flow + if (_token) { + // Use new Context through useService hook + return (Component: React.ComponentType<*>) => { + const Wrapper = (props?: {[string]: any}) => { + const service = useService(_token); - constructor(props: *, ctx: *) { - super(props, ctx); - this.provides = ctx[name]; - } - render() { - const props = {...this.props, ...mapProvides(this.provides)}; - return React.createElement(Component, props); + return React.createElement(Component, { + ...props, + ...mapProvides(service), + }); + }; + const displayName = + Component.displayName || Component.name || 'Anonymous'; + Wrapper.displayName = `With${capitalize(name)}(${displayName})`; + return Wrapper; + }; + } else { + // Use legacy Context + return (Component: React.ComponentType<*>) => { + class HOC extends React.Component<*> { + provides: any; + + constructor(props: *, ctx: *) { + super(props, ctx); + this.provides = ctx[name]; + } + render() { + const props = {...this.props, ...mapProvides(this.provides)}; + return React.createElement(Component, props); + } } - } - const displayName = - Component.displayName || Component.name || 'Anonymous'; - HOC.displayName = - 'With' + - name.replace(/^./, c => c.toUpperCase()) + - '(' + - displayName + - ')'; - HOC.contextTypes = { - [name]: PropTypes.any.isRequired, + const displayName = + Component.displayName || Component.name || 'Anonymous'; + HOC.displayName = `With${capitalize(name)}(${displayName})`; + HOC.contextTypes = { + [name]: PropTypes.any.isRequired, + }; + return HOC; }; - return HOC; - }; + } }, }; From 5a283802db27172673433a5547677e30dfaf00fc Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 17:27:39 -0700 Subject: [PATCH 09/13] Deprecation notices in README --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82c9e7a4ef..319223285f 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,8 @@ Calls all plugin cleanup methods. Useful for testing. #### Provider +**[DEPRECATED]** When using `useService`, `ServiceConsumer`, or `withServices` it is no longer necessary to add a `Provider` to your application. Services are made available through a generic `Context` instance in the `fusion-react` app class. + **Provider.create** ```js @@ -155,6 +157,8 @@ const ProviderComponent: React.Component = Provider.create((name: string)); #### ProviderPlugin +**[DEPRECATED]** When using `useService`, `ServiceConsumer`, or `withServices` it is no longer necessary to register a `ProviderPlugin` in place of a `Plugin`. This is handled within the `fusion-react` app class. + ```js import {ProviderPlugin} from 'fusion-react'; ``` @@ -178,6 +182,8 @@ const plugin: Plugin = ProviderPlugin.create( #### ProvidedHOC +**[DEPRECATED]** See [`withServices`](#withservices) for a generic HOC. For applications still using `ProvidedHOC`, note that this will work without registering a `ProviderPlugin` to wrap your `Plugin`, but it is recommended to migrate to using `useService`, `ServiceConsumer`, or `withServices` instead. + ```js import {ProvidedHOC} from 'fusion-react'; ``` @@ -343,7 +349,7 @@ function Component() { } ``` -- `ctx: Context` The Fusion middleware context for this request. +- `ctx: Context` The Fusion middleware context for this request. Instance of `React.createContext()` FusionContext is provided in the case where a plugin may be memoized based on request, i.e.: From dcbbdcba96c5a4d3cf984fd8b67c3ca25f55df40 Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 17:42:08 -0700 Subject: [PATCH 10/13] Add docs for withServices --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 319223285f..a8530d85cd 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ as bundle splitting and `fusion-react` provides tools to do it easily. - [useService](#useservice) - [ServiceConsumer](#serviceconsumer) - [FusionContext](#fusioncontext) + - [withServices](#withservices) - [Examples](#examples) --- @@ -359,6 +360,32 @@ const session = Session.from(ctx); In this case, you will need to not only use `useService` to get the service you are interested in, but you will also have to get the FusionContext to pass into your service. +#### withServices + +```js +import {withServices} from 'fusion-react'; +import {ExampleToken} from 'fusion-tokens'; + +function Component({exampleProp}) { + return ( +

{exampleProp}

+ ); +} + +export default withServices( + { + example: ExampleToken, + }, + deps => ({ exampleProp: deps.example }), +)(Component); +``` + +- `deps: {[string]: Token}` - Required. Object whose values are Tokens. +- `mapServicesToProps: {[string]: TService} => {[string]: any}` - Optional. Function receives an object whose values are resolved services and returns an object to spread as props to a component. If omitted, the deps object is returned as-is. +- `HOC: Component => Component` - An HOC is returned that passes the result of `mapServicesToProps` to it's Component argument. + +`withServices` is a generic HOC creator that takes a set of Tokens and an optional mapping function and returns a higher-order component that will pass the resolved services into the given Component. + --- ### Examples From 6f21f07ca503954f9bbada1bba07b675ac2487d5 Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 18:04:02 -0700 Subject: [PATCH 11/13] Update docs on ProvdedHOC --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a8530d85cd..d65004db29 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,8 @@ const hoc: HOC = ProvidedHOC.create( ``` - `name: string` - Required. The name of the property set in `context` by the corresponding provider component. -- `mapProvidesToProps: Object => Object` - Optional. Defaults to `provides => ({[name]: provides})`. Determines what props are exposed by the HOC +- `mapProvidesToProps: Object => Object` - Optional. Defaults to `provides => ({[name]: provides})`. Determines what props are exposed by the HOC. +- `token: Token` - Optional. By supplying a token, the HOC will return a component that uses the `useService` hook instead of the legacy Context API. - returns `hoc: Component => Component` #### split From db977f4fe270d8ecfa529c621a26e94e1e97f16d Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Wed, 17 Apr 2019 18:05:24 -0700 Subject: [PATCH 12/13] Remove ProviderPlugin/ProvidedHOC example --- README.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.md b/README.md index d65004db29..3a1d2c679c 100644 --- a/README.md +++ b/README.md @@ -406,32 +406,6 @@ const render = __NODE__ const app = new App(root, render); ``` -#### Creating a Provider/HOC pair - -```js -// in src/plugins/my-plugin.js -import {createPlugin} from 'fusion-core'; - -const plugin = createPlugin({ - provides() { - return console; - }, -}); - -export const Plugin = ProviderPlugin.create('console', plugin); -export const HOC = ProvidedHOC.create('console'); - -// in src/main.js -import {Plugin} from './plugins/my-plugin.js'; -app.register(Plugin); - -// in components/some-component.js -const component = ({console}) => { - return ; -}; -export default HOC(component); -``` - #### Data fetching ```js From 3ca086b316eba973e3658af3055ffd9b6e9ec2f8 Mon Sep 17 00:00:00 2001 From: Mickey Burks Date: Fri, 19 Apr 2019 14:24:50 -0700 Subject: [PATCH 13/13] Rearrange API docs, move deprecated methods down, add back example, remove broken link --- README.md | 250 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 141 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 3a1d2c679c..21d999792e 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,17 @@ as bundle splitting and `fusion-react` provides tools to do it easily. - [Usage](#usage) - [API](#api) - [App](#app) - - [Provider](#provider) - - [ProviderPlugin](#providerplugin) - - [ProvidedHOC](#providedhoc) - - [middleware](#middleware) - - [split](#split) - - [prepare](#prepare) - - [prepared](#prepared) - - [exclude](#exclude) - [useService](#useservice) - [ServiceConsumer](#serviceconsumer) - [FusionContext](#fusioncontext) - [withServices](#withservices) + - [split](#split) + - [prepare](#prepare) + - [prepared](#prepared) + - [exclude](#exclude) + - [Provider - DEPRECATED](#provider) + - [ProviderPlugin - DEPRECATED](#providerplugin) + - [ProvidedHOC - DEPRECATED](#providedhoc) - [Examples](#examples) --- @@ -139,71 +138,97 @@ Calls all plugin cleanup methods. Useful for testing. --- -#### Provider - -**[DEPRECATED]** When using `useService`, `ServiceConsumer`, or `withServices` it is no longer necessary to add a `Provider` to your application. Services are made available through a generic `Context` instance in the `fusion-react` app class. +#### useService -**Provider.create** +*React Hooks were introduced in React v16.8. Make sure you are using a compatible version.* ```js -import {Provider} from 'fusion-react'; -``` +import {useService} from 'fusion-react'; +import {ExampleToken} from 'fusion-tokens'; -```js -const ProviderComponent: React.Component = Provider.create((name: string)); +function Component() { + const service = useService(ExampleToken); + return ( + + ); +} ``` -- `name: string` - Required. The name of the property set in `context` by the provider component. `name` is also used to generate the `displayName` of `ProviderComponent`, e.g. if `name` is `foo`, `ProviderComponent.displayName` becomes `FooProvider` -- returns `ProviderComponent: React.Component` - A component that sets a context property on a class that extends BaseComponent +- `token: Token` - Required. The token used to look up the registered plugin that resolves to `TService`. +- `service: TService` - The service provided by the registered plugin. -#### ProviderPlugin +If no plugin has been registered to this token, an exception is thrown. If you intend you use an optional Token, you can suppress this exception by using `useService(Token.optional)`. -**[DEPRECATED]** When using `useService`, `ServiceConsumer`, or `withServices` it is no longer necessary to register a `ProviderPlugin` in place of a `Plugin`. This is handled within the `fusion-react` app class. +#### ServiceConsumer ```js -import {ProviderPlugin} from 'fusion-react'; +import {ServiceConsumer} from 'fusion-react'; +import {ExampleToken} from 'fusion-tokens'; + +function Component() { + return ( + + {service => ( + + )} + + ); +} ``` -Creates a plugin that wraps the React tree with a context provider component. +- `token: Token` - Required. The token used to lookup the registered plugin. +- `children: TService => React.Element` - Required. Render prop that is passed the registered service. Should return the React Element to render. +- `service: TService` - The service provided by the registered plugin. -**ProviderPlugin.create** +This is the same pattern as the `useService` hook. Opt for using the hook. `ServiceConsumer` is provided as a replacement for any legacy Context usage that may exist. Use `Token.optional` to if you intend to use an optional plugin. + +#### FusionContext ```js -const plugin: Plugin = ProviderPlugin.create( - (name: string), - (plugin: Plugin), - (ProviderComponent: React.Component) -); -``` +import {useContext} from 'react'; +import {FusionContext} from 'fusion-react'; -- `name: string` - Required. The name of the property set in `context` by the provider component. `name` is also used to generate the `displayName` of `ProviderComponent`, e.g. if `name` is `foo`, `ProviderComponent.displayName` becomes `FooProvider` -- `plugin: Plugin` - Required. Creates a provider for this plugin. -- `ProviderComponent: React.Component` - Optional. An overriding provider component for custom logic -- `Plugin: Plugin` - A plugin that registers its provider onto the React tree +function Component() { + const ctx = useContext(FusionContext); + // ... +} +``` -#### ProvidedHOC +- `ctx: Context` The Fusion middleware context for this request. Instance of `React.createContext()` -**[DEPRECATED]** See [`withServices`](#withservices) for a generic HOC. For applications still using `ProvidedHOC`, note that this will work without registering a `ProviderPlugin` to wrap your `Plugin`, but it is recommended to migrate to using `useService`, `ServiceConsumer`, or `withServices` instead. +FusionContext is provided in the case where a plugin may be memoized based on request, i.e.: ```js -import {ProvidedHOC} from 'fusion-react'; +const session = Session.from(ctx); ``` -Creates a HOC that exposes a value from React context to the component's props. +In this case, you will need to not only use `useService` to get the service you are interested in, but you will also have to get the FusionContext to pass into your service. -**ProvidedHOC.create** +#### withServices ```js -const hoc: HOC = ProvidedHOC.create( - (name: string), - (mapProvidesToProps: Object => Object) -); +import {withServices} from 'fusion-react'; +import {ExampleToken} from 'fusion-tokens'; + +function Component({exampleProp}) { + return ( +

{exampleProp}

+ ); +} + +export default withServices( + { + example: ExampleToken, + }, + deps => ({ exampleProp: deps.example }), +)(Component); ``` -- `name: string` - Required. The name of the property set in `context` by the corresponding provider component. -- `mapProvidesToProps: Object => Object` - Optional. Defaults to `provides => ({[name]: provides})`. Determines what props are exposed by the HOC. -- `token: Token` - Optional. By supplying a token, the HOC will return a component that uses the `useService` hook instead of the legacy Context API. -- returns `hoc: Component => Component` +- `deps: {[string]: Token}` - Required. Object whose values are Tokens. +- `mapServicesToProps: {[string]: TService} => {[string]: any}` - Optional. Function receives an object whose values are resolved services and returns an object to spread as props to a component. If omitted, the deps object is returned as-is. +- `HOC: Component => Component` - An HOC is returned that passes the result of `mapServicesToProps` to it's Component argument. + +`withServices` is a generic HOC creator that takes a set of Tokens and an optional mapping function and returns a higher-order component that will pass the resolved services into the given Component. #### split @@ -295,101 +320,108 @@ const NewComponent = exclude(Component); Stops `prepare` traversal at `Component`. Useful for optimizing the `prepare` traversal to visit the minimum number of nodes. -#### useService +#### Provider -*React Hooks were introduced in React v16.8. Make sure you are using a compatible version.* +**[DEPRECATED]** When using `useService`, `ServiceConsumer`, or `withServices` it is no longer necessary to add a `Provider` to your application. Services are made available through a generic `Context` instance in the `fusion-react` app class. + +**Provider.create** ```js -import {useService} from 'fusion-react'; -import {ExampleToken} from 'fusion-tokens'; +import {Provider} from 'fusion-react'; +``` -function Component() { - const service = useService(ExampleToken); - return ( - - ); -} +```js +const ProviderComponent: React.Component = Provider.create((name: string)); ``` -- `token: Token` - Required. The token used to look up the registered plugin that resolves to `TService`. -- `service: TService` - The service provided by the registered plugin. +- `name: string` - Required. The name of the property set in `context` by the provider component. `name` is also used to generate the `displayName` of `ProviderComponent`, e.g. if `name` is `foo`, `ProviderComponent.displayName` becomes `FooProvider` +- returns `ProviderComponent: React.Component` - A component that sets a context property on a class that extends BaseComponent -If no plugin has been registered to this token, an exception is thrown. If you intend you use an optional Token, you can suppress this exception by using `useService(Token.optional)`. +#### ProviderPlugin -#### ServiceConsumer +**[DEPRECATED]** When using `useService`, `ServiceConsumer`, or `withServices` it is no longer necessary to register a `ProviderPlugin` in place of a `Plugin`. This is handled within the `fusion-react` app class. ```js -import {ServiceConsumer} from 'fusion-react'; -import {ExampleToken} from 'fusion-tokens'; +import {ProviderPlugin} from 'fusion-react'; +``` -function Component() { - return ( - - {service => ( - - )} - - ); -} +Creates a plugin that wraps the React tree with a context provider component. + +**ProviderPlugin.create** + +```js +const plugin: Plugin = ProviderPlugin.create( + (name: string), + (plugin: Plugin), + (ProviderComponent: React.Component) +); ``` -- `token: Token` - Required. The token used to lookup the registered plugin. -- `children: TService => React.Element` - Required. Render prop that is passed the registered service. Should return the React Element to render. -- `service: TService` - The service provided by the registered plugin. +- `name: string` - Required. The name of the property set in `context` by the provider component. `name` is also used to generate the `displayName` of `ProviderComponent`, e.g. if `name` is `foo`, `ProviderComponent.displayName` becomes `FooProvider` +- `plugin: Plugin` - Required. Creates a provider for this plugin. +- `ProviderComponent: React.Component` - Optional. An overriding provider component for custom logic +- `Plugin: Plugin` - A plugin that registers its provider onto the React tree -This is the same pattern as the `useService` hook. Opt for using the hook. `ServiceConsumer` is provided as a replacement for any legacy Context usage that may exist. Use `Token.optional` to if you intend to use an optional plugin. +#### ProvidedHOC -#### FusionContext +**[DEPRECATED]** See [`withServices`](#withservices) for a generic HOC. For applications still using `ProvidedHOC`, note that this will work without registering a `ProviderPlugin` to wrap your `Plugin`, but it is recommended to migrate to using `useService`, `ServiceConsumer`, or `withServices` instead. ```js -import {useContext} from 'react'; -import {FusionContext} from 'fusion-react'; - -function Component() { - const ctx = useContext(FusionContext); - // ... -} +import {ProvidedHOC} from 'fusion-react'; ``` -- `ctx: Context` The Fusion middleware context for this request. Instance of `React.createContext()` +Creates a HOC that exposes a value from React context to the component's props. -FusionContext is provided in the case where a plugin may be memoized based on request, i.e.: +**ProvidedHOC.create** ```js -const session = Session.from(ctx); +const hoc: HOC = ProvidedHOC.create( + (name: string), + (mapProvidesToProps: Object => Object) +); ``` -In this case, you will need to not only use `useService` to get the service you are interested in, but you will also have to get the FusionContext to pass into your service. +- `name: string` - Required. The name of the property set in `context` by the corresponding provider component. +- `mapProvidesToProps: Object => Object` - Optional. Defaults to `provides => ({[name]: provides})`. Determines what props are exposed by the HOC. +- `token: Token` - Optional. By supplying a token, the HOC will return a component that uses the `useService` hook instead of the legacy Context API. +- returns `hoc: Component => Component` -#### withServices +--- -```js -import {withServices} from 'fusion-react'; -import {ExampleToken} from 'fusion-tokens'; +### Examples -function Component({exampleProp}) { - return ( -

{exampleProp}

- ); -} +#### Using a service -export default withServices( - { - example: ExampleToken, - }, - deps => ({ exampleProp: deps.example }), -)(Component); -``` +```js +// src/plugins/my-plugin.js +import {createPlugin, createToken} from 'fusion-core'; -- `deps: {[string]: Token}` - Required. Object whose values are Tokens. -- `mapServicesToProps: {[string]: TService} => {[string]: any}` - Optional. Function receives an object whose values are resolved services and returns an object to spread as props to a component. If omitted, the deps object is returned as-is. -- `HOC: Component => Component` - An HOC is returned that passes the result of `mapServicesToProps` to it's Component argument. +export const MyToken = createToken('my-token'); +export const MyPlugin = createPlugin({ + provides() { + return console; + }, +}); -`withServices` is a generic HOC creator that takes a set of Tokens and an optional mapping function and returns a higher-order component that will pass the resolved services into the given Component. +// src/main.js +import {MyPlugin, MyToken} from './plugins/my-plugin.js'; ---- +export default (app: FusionApp) => { + app.register(MyToken, MyPlugin); + // ... +}; -### Examples +// components/some-component.js +import {MyToken} from '../plugins/my-plugin.js'; +import {useService} from 'fusion-react'; + +exoprt default Component (props) { + const console = useService(MyToken); + return ( + + ); +} +``` #### Disabling server-side rendering