diff --git a/README.md b/README.md index 60dccb560c..21d999792e 100644 --- a/README.md +++ b/README.md @@ -25,17 +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) + - [useService](#useservice) + - [ServiceConsumer](#serviceconsumer) + - [FusionContext](#fusioncontext) + - [withServices](#withservices) - [split](#split) - [prepare](#prepare) - [prepared](#prepared) - [exclude](#exclude) - - [useService](#useservice) - - [ServiceConsumer](#serviceconsumer) - - [FusionContext](#fusioncontext) + - [Provider - DEPRECATED](#provider) + - [ProviderPlugin - DEPRECATED](#providerplugin) + - [ProvidedHOC - DEPRECATED](#providedhoc) - [Examples](#examples) --- @@ -138,64 +138,97 @@ Calls all plugin cleanup methods. Useful for testing. --- -#### Provider +#### 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)`. + +#### 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'; + +function Component() { + const ctx = useContext(FusionContext); + // ... +} ``` -- `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 +- `ctx: Context` The Fusion middleware context for this request. Instance of `React.createContext()` -#### ProvidedHOC +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 -- 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 @@ -287,76 +320,109 @@ 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. +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` --- ### Examples +#### Using a service + +```js +// src/plugins/my-plugin.js +import {createPlugin, createToken} from 'fusion-core'; + +export const MyToken = createToken('my-token'); +export const MyPlugin = createPlugin({ + provides() { + return console; + }, +}); + +// src/main.js +import {MyPlugin, MyToken} from './plugins/my-plugin.js'; + +export default (app: FusionApp) => { + app.register(MyToken, MyPlugin); + // ... +}; + +// 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 Sometimes it is desirable to avoid server-side rendering. To do that, override the `render` argument when instantiating `App`: @@ -372,33 +438,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 -import {HOC} from '../plugins/my-plugin.js'; -const component = ({console}) => { - return ; -}; -export default HOC(component); -``` - #### Data fetching ```js 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/src/__tests__/context.node.js b/src/__tests__/context.node.js index b755a9a192..c4c0f4015c 100644 --- a/src/__tests__/context.node.js +++ b/src/__tests__/context.node.js @@ -13,12 +13,12 @@ import {getSimulator} from 'fusion-test-utils'; import App from '../index'; import { FusionContext, - serviceContextPlugin, ServiceConsumer, useService, + withServices, } 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 +33,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 +40,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 +50,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 +63,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 +73,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; @@ -100,7 +97,37 @@ test('context consumer component', 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__/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/__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(); -// }); diff --git a/src/context.js b/src/context.js index 40e73a83b3..83af4f445d 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'; @@ -10,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); @@ -64,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/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; - }; + } }, }; 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)); } } 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); - }; - }; -} 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"