Skip to content

Commit c3a929d

Browse files
Merge 16bf453 into 4ea5030
2 parents 4ea5030 + 16bf453 commit c3a929d

9 files changed

Lines changed: 449 additions & 16 deletions

File tree

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ packages/labs/react/test/
128128
packages/labs/react/node_modules/
129129
packages/labs/react/index.*
130130
packages/labs/react/create-component.*
131+
packages/labs/react/use-controller.*
132+
131133
packages/labs/scoped-registry-mixin/development/
132134
packages/labs/scoped-registry-mixin/test/
133135
packages/labs/scoped-registry-mixin/node_modules/

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ packages/labs/react/test/
116116
packages/labs/react/node_modules/
117117
packages/labs/react/index.*
118118
packages/labs/react/create-component.*
119+
packages/labs/react/use-controller.*
120+
119121
packages/labs/scoped-registry-mixin/development/
120122
packages/labs/scoped-registry-mixin/test/
121123
packages/labs/scoped-registry-mixin/node_modules/

packages/labs/react/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
/test/
33
/node_modules/
44
/index.*
5-
/create-component.*
5+
/create-component.*
6+
/use-controller.*

packages/labs/react/CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1717
<!-- ### Removed -->
1818
<!-- ### Fixed -->
1919

20+
## Unreleased
21+
22+
<!-- ### Changed -->
23+
24+
### Added
25+
26+
- Added `useControler()` hook for creating React hooks from Reactive Controllers ([#1532](https://github.com/Polymer/lit-html/pulls/1532)).
27+
28+
<!-- ### Removed -->
29+
<!-- ### Fixed -->
30+
2031
## [1.0.0-pre.2] - 2021-03-31
2132

2233
### Changed
@@ -27,4 +38,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2738

2839
### Added
2940

30-
- Adds react component wrapper for custom elements. Use by calling `createComponent` ([#1420](https://github.com/Polymer/lit-html/pulls/1420)).
41+
- Adds React component wrapper for custom elements. Use by calling `createComponent` ([#1420](https://github.com/Polymer/lit-html/pulls/1420)).

packages/labs/react/README.md

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# @lit-labs/react
22

3-
A React component wrapper for web components.
3+
React integration for Web Components and Reactive Controllers.
44

55
## Overview
66

7+
## `createComponent`
8+
79
While React can render Web Components, it [cannot](https://custom-elements-everywhere.com/libraries/react/results/results.html)
810
easily pass React props to custom element properties or event listeners.
911

@@ -12,7 +14,7 @@ React component wrapper for a custom element class. The wrapper correctly
1214
passes React `props` to properties accepted by the custom element and listens
1315
for events dispatched by the custom element.
1416

15-
## How it works
17+
### How it works
1618

1719
For properties, the wrapper interrogates the web component class to discover
1820
its available properties. Then any React `props` passed with property names are
@@ -23,15 +25,7 @@ to events fired by the custom element. For example passing `{onfoo: 'foo'}`
2325
means a function passed via a `prop` named `onfoo` will be called when the
2426
custom element fires the foo event with the event as an argument.
2527

26-
## Installation
27-
28-
From inside your project folder, run:
29-
30-
```bash
31-
$ npm install @lit-labs/react
32-
```
33-
34-
## Usage
28+
### Usage
3529

3630
Import `React`, a custom element class, and `createComponent`.
3731

@@ -61,6 +55,69 @@ React component.
6155
/>
6256
```
6357

58+
## `useController`
59+
60+
Reactive Controllers allow developers to hook a component's lifecycle to bundle
61+
together state and behavior related to a feature. They are similar to React
62+
hooks in the user cases and capabilities, but are plain JavaScript objects
63+
instead of functions with hidden state.
64+
65+
`useController` is a React hook that create and stores a Reactive Controller
66+
and drives its lifecycle using React hooks like `useState` and
67+
`useLayoutEffect`.
68+
69+
### How it works
70+
71+
`useController` uses `useState` to create and store an instance of a controller and a `ReactControllerHost`. It then calls the controller's lifecycle from the hook body and `useLayoutEffect` callbacks, emulating the `ReactiveElement` lifecycle as closely as possible. `ReactControllerHost` implements `addController` so that controller composition works and nested controller lifecycles are called correctly. `ReactControllerHost` also implements `requestUpdate` by calling a `useState` setter, so that a controller with new renderable state can cause its host component to re-render.
72+
73+
Controller timings are implemented as follows:
74+
75+
| Controller API | React hook equivalent |
76+
| ---------------- | ----------------------------------- |
77+
| constructor | useState initial value |
78+
| hostConnected | useState initial value |
79+
| hostDisconnected | useLayoutEffect cleanup, empty deps |
80+
| hostUpdate | hook body |
81+
| hostUpdated | useLayoutEffect |
82+
| requestUpdate | useState setter |
83+
| updateComplete | useLayoutEffect |
84+
85+
### Usage
86+
87+
```jsx
88+
import * as React from 'react';
89+
import {useController} from '@lit-labs/react/use-controller.js';
90+
import {MouseController} from '@example/mouse-controller';
91+
92+
// Write a React hook function:
93+
const useMouse = () => {
94+
// Use useController to create and store a controller instance:
95+
const controller = useController(React, (host) => new MouseController(host));
96+
// return the controller: return controller;
97+
// or return a custom object for a more React-idiomatic API:
98+
return controller.position;
99+
};
100+
101+
// Now use the new hook in a React component:
102+
const Component = (props) => {
103+
const mousePosition = useMouse();
104+
return (
105+
<pre>
106+
x: {mousePosition.x}
107+
y: {mousePosition.y}
108+
</pre>
109+
);
110+
};
111+
```
112+
113+
## Installation
114+
115+
From inside your project folder, run:
116+
117+
```bash
118+
$ npm install @lit-labs/react
119+
```
120+
64121
## Contributing
65122

66123
Please see [CONTRIBUTING.md](./CONTRIBUTING.md).

packages/labs/react/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222
"/src/",
2323
"!/src/test/",
2424
"/index.{d.ts,d.ts.map,js,js.map}",
25-
"/create-component.{d.ts,d.ts.map,js,js.map}"
25+
"/create-component.{d.ts,d.ts.map,js,js.map}",
26+
"/use-controller.{d.ts,d.ts.map,js,js.map}"
2627
],
2728
"scripts": {
2829
"build": "npm run clean && npm run build:ts --build && rollup -c",
2930
"build:watch": "rollup -c --watch",
3031
"build:ts": "tsc --build && treemirror development . '**/*.d.ts{,.map}'",
3132
"build:ts:watch": "tsc --build --watch",
32-
"clean": "rm -rf {index,create-component}.{js,js.map,d.ts} development/ test/ *.tsbuildinfo",
33+
"clean": "rm -rf {index,create-component,use-controller}.{js,js.map,d.ts} development/ test/ *.tsbuildinfo",
3334
"dev": "scripts/dev.sh",
3435
"test": "npm run test:dev && npm run test:prod",
3536
"test:dev": "cd ../../tests && npx wtr '../labs/react/development/**/*_test.js'",

packages/labs/react/rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ import {litProdConfig} from '../../../rollup-common.js';
88

99
export default litProdConfig({
1010
classPropertyPrefix: 'Ω',
11-
entryPoints: ['index', 'create-component'],
11+
entryPoints: ['index', 'create-component', 'use-controller'],
1212
});
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt
6+
* The complete set of authors may be found at
7+
* http://polymer.github.io/AUTHORS.txt
8+
* The complete set of contributors may be found at
9+
* http://polymer.github.io/CONTRIBUTORS.txt
10+
* Code distributed by Google as part of the polymer project is also
11+
* subject to an additional IP rights grant found at
12+
* http://polymer.github.io/PATENTS.txt
13+
*/
14+
15+
// import * as ReactModule from 'react';
16+
import 'react/umd/react.development.js';
17+
import 'react-dom/umd/react-dom.development.js';
18+
import {useController} from '../use-controller.js';
19+
import {assert} from '@esm-bundle/chai';
20+
import {
21+
ReactiveController,
22+
ReactiveControllerHost,
23+
} from '@lit/reactive-element';
24+
25+
const {React, ReactDOM} = window;
26+
27+
suite('useController', () => {
28+
let container: HTMLElement;
29+
let ctorCallCount = 0;
30+
31+
setup(() => {
32+
container = document.createElement('div');
33+
document.body.appendChild(container);
34+
ctorCallCount = 0;
35+
});
36+
37+
teardown(() => {
38+
if (container && container.parentNode) {
39+
container.parentNode.removeChild(container);
40+
}
41+
});
42+
43+
class TestController implements ReactiveController {
44+
host: ReactiveControllerHost;
45+
a: string;
46+
47+
log: Array<string> = [];
48+
49+
constructor(host: ReactiveControllerHost, a: string) {
50+
this.host = host;
51+
this.a = a;
52+
host.addController(this);
53+
ctorCallCount++;
54+
}
55+
56+
hostConnected() {
57+
this.log.push('connected');
58+
}
59+
60+
hostDisconnected() {
61+
this.log.push('disconnected');
62+
}
63+
64+
hostUpdate() {
65+
this.log.push('update');
66+
}
67+
68+
hostUpdated() {
69+
this.log.push('updated');
70+
}
71+
}
72+
73+
const useTest = (a: string) => {
74+
return useController(
75+
React,
76+
(host: ReactiveControllerHost) => new TestController(host, a)
77+
);
78+
};
79+
80+
test('basic lifecycle', () => {
81+
let testController!: TestController;
82+
83+
const TestComponent = ({x}: {x: number}) => {
84+
testController = useTest('a');
85+
return React.createElement('div', {className: 'foo'}, [
86+
`x:${x}, a:${testController.a}`,
87+
]);
88+
};
89+
90+
const render = (props: any) => {
91+
ReactDOM.render(React.createElement(TestComponent, props), container);
92+
};
93+
94+
render({x: 1});
95+
assert.equal(ctorCallCount, 1);
96+
assert.equal(container.innerHTML, `<div class="foo">x:1, a:a</div>`);
97+
assert.deepEqual(testController.log, ['connected', 'update', 'updated']);
98+
const firstTestController = testController;
99+
100+
testController.log.length = 0;
101+
render({x: 2});
102+
assert.equal(ctorCallCount, 1);
103+
assert.equal(container.innerHTML, `<div class="foo">x:2, a:a</div>`);
104+
assert.deepEqual(testController.log, ['update', 'updated']);
105+
assert.strictEqual(testController, firstTestController);
106+
});
107+
108+
test('requestUpdate', async () => {
109+
let testController!: TestController;
110+
111+
const TestComponent = ({x}: {x: number}) => {
112+
testController = useTest('a');
113+
return React.createElement('div', {className: 'foo'}, [
114+
`x:${x}, a:${testController.a}`,
115+
]);
116+
};
117+
118+
const render = (props: any) => {
119+
ReactDOM.render(React.createElement(TestComponent, props), container);
120+
};
121+
122+
render({x: 1});
123+
assert.deepEqual(testController.log, ['connected', 'update', 'updated']);
124+
testController.log.length = 0;
125+
testController.a = 'b';
126+
testController.host.requestUpdate();
127+
128+
await new Promise((r) => setTimeout(r, 0));
129+
130+
assert.equal(container.innerHTML, `<div class="foo">x:1, a:b</div>`);
131+
assert.deepEqual(testController.log, ['update', 'updated']);
132+
});
133+
134+
test('disconnect', () => {
135+
let testController!: TestController;
136+
137+
const TestComponent = ({x}: {x: number}) => {
138+
testController = useTest('a');
139+
return React.createElement('div', {className: 'foo'}, [
140+
`x:${x}, a:${testController.a}`,
141+
]);
142+
};
143+
144+
ReactDOM.render(React.createElement(TestComponent, {x: 1}), container);
145+
assert.deepEqual(testController.log, ['connected', 'update', 'updated']);
146+
testController.log.length = 0;
147+
148+
ReactDOM.render(React.createElement('div'), container);
149+
assert.equal(container.innerHTML, `<div></div>`);
150+
assert.deepEqual(testController.log, ['disconnected']);
151+
});
152+
153+
test('updateComplete', async () => {
154+
let testController!: TestController;
155+
let updateCompleteCount = 0;
156+
let lastNestedUpdate: boolean | undefined;
157+
let rerender = false;
158+
159+
const TestComponent = ({x}: {x: number}) => {
160+
testController = useTest('a');
161+
testController.host.updateComplete.then((nestedUpdate) => {
162+
updateCompleteCount++;
163+
lastNestedUpdate = nestedUpdate;
164+
});
165+
if (rerender) {
166+
// prevent an infinite loop
167+
rerender = false;
168+
testController.host.requestUpdate();
169+
}
170+
return React.createElement('div', {className: 'foo'}, [
171+
`x:${x}, a:${testController.a}`,
172+
]);
173+
};
174+
175+
const render = (props: any) => {
176+
ReactDOM.render(React.createElement(TestComponent, props), container);
177+
};
178+
179+
render({x: 1});
180+
assert.deepEqual(testController.log, ['connected', 'update', 'updated']);
181+
testController.log.length = 0;
182+
await 0;
183+
assert.equal(updateCompleteCount, 1);
184+
assert.strictEqual(lastNestedUpdate, false);
185+
186+
// cause a requestUpdate() during render
187+
rerender = true;
188+
render({x: 2});
189+
await 0;
190+
// Expect only one more renders since requestUpdate() is called
191+
// during render
192+
assert.equal(updateCompleteCount, 2);
193+
assert.strictEqual(lastNestedUpdate, false);
194+
195+
await 0;
196+
197+
assert.equal(updateCompleteCount, 2);
198+
});
199+
});

0 commit comments

Comments
 (0)