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 (
-