Skip to content

Commit 421e23f

Browse files
authored
Redesign plug API (#16)
1 parent 3fdca77 commit 421e23f

18 files changed

+1390
-1459
lines changed

TODO.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
- [x] Rename Slot props to `slotProps`
2+
- [x] Test new plug API in react-cosmos with yarn link
3+
- [x] Test for context memoization in plugs
4+
- [ ] Create useArraySlot
5+
- [ ] Maybe: createPluginStore
6+
7+
---
8+
19
- [x] Use TypeScript
210
- [x] Use ui-plugin
311
- [x] Map plugs per plugin
@@ -6,5 +14,5 @@
614
- [x] Only render plugs of enabled plugins
715
- [x] Support children in plugs
816
- [x] Publish Flow types
9-
- [ ] Export types for PluginsConsumer
10-
- [ ] Handle "dispatch" props in getProps (avoid re-rendering because anonymous function gets created on every getProps call)
17+
- [ ] ~~Export types for PluginsConsumer~~
18+
- [ ] ~~Handle "dispatch" props in getProps (avoid re-rendering because anonymous function gets created on every getProps call)~~

package.json

+13-15
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,21 @@
2020
"@skidding/linked-list": "^1.0.2",
2121
"array-find-index": "^1.0.2",
2222
"lodash": "^4.17.11",
23-
"react-is": "^16.6.3",
2423
"ui-plugin": "^2.0.0-alpha.2"
2524
},
2625
"devDependencies": {
27-
"@skidding/async-retry": "^1.0.2",
28-
"@types/jest": "^23.3.10",
29-
"@types/lodash": "^4.14.119",
30-
"@types/react": "^16.7.17",
31-
"@types/react-is": "^16.5.0",
32-
"@types/react-test-renderer": "^16.0.3",
33-
"jest": "^23.6.0",
34-
"prettier": "^1.15.3",
35-
"react": "^16.6.3",
36-
"react-test-renderer": "^16.6.3",
37-
"ts-jest": "^23.10.5",
38-
"tslint": "^5.11.0",
39-
"tslint-config-prettier": "^1.17.0",
40-
"typescript": "^3.2.2"
26+
"@skidding/async-retry": "^2.0.0",
27+
"@types/jest": "^24.0.12",
28+
"@types/lodash": "^4.14.123",
29+
"@types/react": "^16.8.17",
30+
"@types/react-test-renderer": "^16.8.1",
31+
"jest": "^24.8.0",
32+
"prettier": "^1.17.0",
33+
"react": "^16.8.6",
34+
"react-test-renderer": "^16.8.6",
35+
"ts-jest": "^24.0.2",
36+
"tslint": "^5.16.0",
37+
"tslint-config-prettier": "^1.18.0",
38+
"typescript": "^3.4.5"
4139
}
4240
}

src/Slot/PlugConnect.tsx

+8-46
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,20 @@
1-
import { isEqual } from 'lodash';
21
import * as React from 'react';
32
import { getPlugin, getPluginContext, onStateChange } from 'ui-plugin';
4-
import { GetProps } from '../types';
3+
import { PlugComponentType } from '../types';
54

65
type Props = {
76
pluginName: string;
8-
component: React.ComponentType;
7+
component: PlugComponentType<any, any>;
98
slotProps: object;
10-
getProps: GetProps;
119
children?: React.ReactNode;
1210
};
1311

14-
type State = {
15-
plugProps: object;
16-
};
17-
18-
export class PlugConnect extends React.Component<Props, State> {
19-
static getDerivedStateFromProps(props: Props, state: State) {
20-
const plugProps = getPlugProps(props);
21-
22-
if (plugPropsEqual(plugProps, state.plugProps)) {
23-
return null;
24-
}
25-
26-
return { plugProps };
27-
}
28-
29-
state = {
30-
plugProps: this.getPlugProps(),
31-
};
32-
12+
export class PlugConnect extends React.Component<Props> {
3313
removeStateChangeHandler: null | (() => unknown) = null;
3414

3515
render() {
3616
const { component, children } = this.props;
37-
const { plugProps } = this.state;
17+
const plugProps = this.getPlugProps();
3818

3919
return React.createElement(component, plugProps, children);
4020
}
@@ -57,31 +37,13 @@ export class PlugConnect extends React.Component<Props, State> {
5737
// rendering is async, it takes a while for the Slot components to process
5838
// plugin changes, so PlugConnect components might receive state changes
5939
// for plugins that are no longer enabled.
60-
if (!getPlugin(this.props.pluginName).enabled) {
61-
return;
62-
}
63-
64-
const newProps = this.getPlugProps();
65-
66-
// NOTE: This can be optimized. We can avoid running plug.getProps when
67-
// relevant state hasn't changed.
68-
// TODO: How to avoid comparing annonymous dispatch-like functions that get
69-
// created on every getProps call?
70-
if (!plugPropsEqual(newProps, this.state.plugProps)) {
71-
this.setState({ plugProps: newProps });
40+
if (getPlugin(this.props.pluginName).enabled) {
41+
this.forceUpdate();
7242
}
7343
};
7444

7545
getPlugProps() {
76-
return getPlugProps(this.props);
46+
const { pluginName, slotProps = {} } = this.props;
47+
return { pluginContext: getPluginContext(pluginName), slotProps };
7748
}
7849
}
79-
80-
function getPlugProps({ pluginName, slotProps, getProps }: Props) {
81-
return getProps(getPluginContext(pluginName), slotProps);
82-
}
83-
84-
function plugPropsEqual(plugProps1: object, plugProps2: object) {
85-
// IDEA: Assume functions with same name are equal
86-
return isEqual(plugProps1, plugProps2);
87-
}

src/Slot/index.tsx

+9-25
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import createLinkedList from '@skidding/linked-list';
22
import { isEqual } from 'lodash';
33
import * as React from 'react';
4-
import { isValidElementType } from 'react-is';
54
import { onPluginLoad } from 'ui-plugin';
65
import { Plug } from '../types';
76
import { getEnabledPlugsForSlot } from '../store';
@@ -11,7 +10,7 @@ import { PlugConnect } from './PlugConnect';
1110
type Props = {
1211
name: string;
1312
children?: React.ReactNode;
14-
props?: object;
13+
slotProps?: object;
1514
};
1615

1716
type State = {
@@ -26,7 +25,7 @@ export class Slot extends React.Component<Props, State> {
2625
removePluginLoadHandler: null | (() => unknown) = null;
2726

2827
render() {
29-
const { name, children, props = {} } = this.props;
28+
const { name, children, slotProps = {} } = this.props;
3029
const { plugs } = this.state;
3130
const { Provider, Consumer } = getSlotContext(name);
3231

@@ -52,7 +51,7 @@ export class Slot extends React.Component<Props, State> {
5251
}
5352

5453
return (
55-
<Provider value={next()}>{getPlugNode(plug, props, children)}</Provider>
54+
<Provider value={next()}>{getPlugNode(plug, slotProps, children)}</Provider>
5655
);
5756
}}
5857
</Consumer>
@@ -72,34 +71,19 @@ export class Slot extends React.Component<Props, State> {
7271

7372
handlePluginLoad = () => {
7473
const newPlugs = getEnabledPlugsForSlot(this.props.name);
75-
7674
if (!isEqual(newPlugs, this.state.plugs)) {
7775
this.setState({ plugs: newPlugs });
7876
}
7977
};
8078
}
8179

8280
function getPlugNode(plug: Plug, slotProps: object, children?: React.ReactNode) {
83-
const { pluginName, render, getProps } = plug;
84-
85-
if (typeof render === 'string' || !isValidElementType(render)) {
86-
return render;
87-
}
88-
89-
if (typeof getProps === 'function') {
90-
return (
91-
<PlugConnect
92-
pluginName={pluginName}
93-
component={render}
94-
slotProps={slotProps}
95-
getProps={getProps}
96-
>
97-
{children}
98-
</PlugConnect>
99-
);
100-
}
101-
102-
return React.createElement(render, slotProps, children);
81+
const { pluginName, component } = plug;
82+
return (
83+
<PlugConnect pluginName={pluginName} component={component} slotProps={slotProps}>
84+
{children}
85+
</PlugConnect>
86+
);
10387
}
10488

10589
function getFirstLinkedPlug(plugs: Plug[]) {

src/__tests__/plugCompose.tsx

+21-30
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,35 @@ it('composes with plugs previously applied on same slot', () => {
99
// The first plug opens up the possibility for a future plug to override
1010
// it or to compose with it. The latter is happening in this case.
1111
const p1 = createPlugin({ name: 'test1' });
12-
p1.plug({
13-
slotName: 'root',
14-
render: (
15-
<Slot name="root">
16-
<span>I was here first</span>
17-
</Slot>
18-
),
19-
});
12+
p1.plug('root', () => (
13+
<Slot name="root">
14+
<span>I was here first</span>
15+
</Slot>
16+
));
2017
p1.register();
2118

2219
// The second and third plugs continue to allow next plugs to override or
2320
// compose them, as well as continue to render previous plugs via children
2421
const p2 = createPlugin({ name: 'test2' });
25-
p2.plug({
26-
slotName: 'root',
27-
render: ({ children }) => (
28-
<Slot name="root">
29-
<>
30-
{children}
31-
<span>I was here second</span>
32-
</>
33-
</Slot>
34-
),
35-
});
22+
p2.plug('root', ({ children }) => (
23+
<Slot name="root">
24+
<>
25+
{children}
26+
<span>I was here second</span>
27+
</>
28+
</Slot>
29+
));
3630
p2.register();
3731

3832
const p3 = createPlugin({ name: 'test3' });
39-
p3.plug({
40-
slotName: 'root',
41-
render: ({ children }) => (
42-
<Slot name="root">
43-
<>
44-
{children}
45-
<span>I was here third</span>
46-
</>
47-
</Slot>
48-
),
49-
});
33+
p3.plug('root', ({ children }) => (
34+
<Slot name="root">
35+
<>
36+
{children}
37+
<span>I was here third</span>
38+
</>
39+
</Slot>
40+
));
5041
p3.register();
5142

5243
loadPlugins();

src/__tests__/plugDecorate.tsx

+7-13
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,16 @@ it('decorates any future plugs applied on same slot', () => {
99
// The first plug opens up the possibility for a future plugin to override
1010
// it or to compose with it. The latter is happening in this case.
1111
const p1 = createPlugin({ name: 'test1' });
12-
p1.plug({
13-
slotName: 'root',
14-
render: (
15-
<>
16-
<span>I was here first</span>
17-
<Slot name="root" />
18-
</>
19-
),
20-
});
12+
p1.plug('root', () => (
13+
<>
14+
<span>I was here first</span>
15+
<Slot name="root" />
16+
</>
17+
));
2118
p1.register();
2219

2320
const p2 = createPlugin({ name: 'test2' });
24-
p2.plug({
25-
slotName: 'root',
26-
render: <span>I was here second</span>,
27-
});
21+
p2.plug('root', () => <span>I was here second</span>);
2822
p2.register();
2923

3024
loadPlugins();

src/__tests__/plugOrder.tsx

+11-17
Original file line numberDiff line numberDiff line change
@@ -43,28 +43,22 @@ it('composes plugs registered outside-in', () => {
4343

4444
function registerPreviewIframe() {
4545
const { plug, register } = createPlugin({ name: 'preview' });
46-
plug({
47-
slotName: 'root',
48-
render: (
49-
<Slot name="root">
50-
<Preview />
51-
</Slot>
52-
),
53-
});
46+
plug('root', () => (
47+
<Slot name="root">
48+
<Preview />
49+
</Slot>
50+
));
5451
register();
5552
}
5653

5754
function registerNav() {
5855
const { plug, register } = createPlugin({ name: 'nav' });
59-
plug({
60-
slotName: 'root',
61-
render: ({ children }: { children?: React.ReactNode }) => (
62-
<div>
63-
<Nav />
64-
<Slot name="root">{children}</Slot>
65-
</div>
66-
),
67-
});
56+
plug('root', ({ children }: { children?: React.ReactNode }) => (
57+
<div>
58+
<Nav />
59+
<Slot name="root">{children}</Slot>
60+
</div>
61+
));
6862
register();
6963
}
7064

src/__tests__/plugOverride.tsx

+2-8
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,11 @@ it('overrides plug previously applied on same slot', () => {
99
// The first plugs opens up the possibility for a future plug to override
1010
// it or to compose with it. The former is happening in this case.
1111
const p1 = createPlugin({ name: 'test1 ' });
12-
p1.plug({
13-
slotName: 'root',
14-
render: <Slot name="root">I was here first</Slot>,
15-
});
12+
p1.plug('root', () => <Slot name="root">I was here first</Slot>);
1613
p1.register();
1714

1815
const p2 = createPlugin({ name: 'test2 ' });
19-
p2.plug({
20-
slotName: 'root',
21-
render: 'I was here second',
22-
});
16+
p2.plug('root', () => <>I was here second</>);
2317
p2.register();
2418

2519
loadPlugins();

0 commit comments

Comments
 (0)