@lit/react
Version:
150 lines (148 loc) • 5.89 kB
JavaScript
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const microtask = Promise.resolve();
/**
* An implementation of ReactiveControllerHost that is driven by React hooks
* and `useController()`.
*/
class ReactControllerHost {
constructor(kickCount, kick) {
this._controllers = [];
/* @internal */
this._updatePending = true;
/* @internal */
this._isConnected = false;
this._kickCount = kickCount;
this._kick = kick;
this._updateCompletePromise = new Promise((res, _rej) => {
this._resolveUpdate = res;
});
}
addController(controller) {
this._controllers.push(controller);
}
removeController(controller) {
// Note, if the indexOf is -1, the >>> will flip the sign which makes the
// splice do nothing.
this._controllers?.splice(this._controllers.indexOf(controller) >>> 0, 1);
}
requestUpdate() {
if (!this._updatePending) {
this._updatePending = true;
// Trigger a React update by updating some state
microtask.then(() => this._kick(++this._kickCount));
}
}
get updateComplete() {
return this._updateCompletePromise;
}
/* @internal */
_connected() {
this._isConnected = true;
this._controllers.forEach((c) => c.hostConnected?.());
}
/* @internal */
_disconnected() {
this._isConnected = false;
this._controllers.forEach((c) => c.hostDisconnected?.());
}
/* @internal */
_update() {
this._controllers.forEach((c) => c.hostUpdate?.());
}
/* @internal */
_updated() {
this._updatePending = false;
const resolve = this._resolveUpdate;
// Create a new updateComplete Promise for the next update,
// before resolving the current one.
this._updateCompletePromise = new Promise((res, _rej) => {
this._resolveUpdate = res;
});
this._controllers.forEach((c) => c.hostUpdated?.());
resolve(this._updatePending);
}
}
/**
* Creates and stores a stateful ReactiveController instance and provides it
* with a ReactiveControllerHost that drives the controller lifecycle.
*
* Use this hook to convert a ReactiveController into a React hook.
*
* @param React the React module that provides the base hooks. Must provide
* `useState` and `useLayoutEffect`.
* @param createController A function that creates a controller instance. This
* function is given a ReactControllerHost to pass to the controller. The
* create function is only called once per component.
*/
const useController = (React, createController) => {
const { useState, useLayoutEffect } = React;
// State to force updates of the React component
const [kickCount, kick] = useState(0);
// Create and store the controller instance. We use useState() instead of
// useMemo() because React does not guarantee that it will preserve values
// created with useMemo().
// TODO (justinfagnani): since this controller are mutable, this may cause
// issues such as "shearing" with React concurrent mode. The solution there
// will likely be to snapshot the controller state with something like
// `useMutableSource`:
// https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md
// We can address this when React's concurrent mode is closer to shipping.
let shouldDisconnect = false;
const [host] = useState(() => {
const host = new ReactControllerHost(kickCount, kick);
const controller = createController(host);
host._primaryController = controller;
// Note, calls to `useState` are expected to produce no side effects and in
// StrictMode this is enforced by not running effects for the first render.
//
// This happens in StrictMode:
// 1. Throw away render: component function runs but does not call effects
// 2. Real render: component function runs and *does* call effects,
// 2.a. if first render, run effects and
// 2.a.1 mount,
// 2.a.2 unmount,
// 2.a.3 remount
// 2b. if not first render, just run effects
//
// To preserve update lifecycle ordering and run it before this hook
// returns, run connected here but schedule and async disconnect (handles
// lifecycle balance for `(1) Throw away render`).
// The disconnect is cancelled if the effects actually run (handles
// `(2.a.1) Real render, mount`).
host._connected();
shouldDisconnect = true;
microtask.then(() => {
if (shouldDisconnect) {
host._disconnected();
}
});
return host;
});
host._updatePending = true;
// This effect runs only on mount/unmount of the component (via the empty
// deps array). If the controller has just been created, it's scheduled
// a disconnect so that it behaves correctly in StrictMode (see above).
// The returned callback here disconnects the host when the component is
// unmounted (handles `(2.a.2) Real render, unmount` above).
// And finally, if the component is disconnected when the effect runs, we
// connect it (handles `(2.a.3) Real render, remount`).
useLayoutEffect(() => {
shouldDisconnect = false;
if (!host._isConnected) {
host._connected();
}
return () => host._disconnected();
}, []);
// We use useLayoutEffect because we need updated() called synchronously
// after rendering.
useLayoutEffect(() => host._updated());
// TODO (justinfagnani): don't call in SSR
host._update();
return host._primaryController;
};
export { useController };
//# sourceMappingURL=use-controller.js.map