Skip to content

Commit

Permalink
Add notes about states useSignals calls may transition between
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewiggins committed Oct 12, 2023
1 parent 1d6ab07 commit 6b388bf
Showing 1 changed file with 61 additions and 8 deletions.
69 changes: 61 additions & 8 deletions packages/react/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,35 @@ interface Effect {
_dispose(): void;
}

/**
* An enum defining how this store is used:
* - 0: unknown usage (bare useSignals call )
*
* - 1: component usage + try/finally
*
* Invoked directly in a component's render method whose body is wrapped in a
* try/finally that finishes the effect store returned by the hook (e.g. what
* react-transform does)
*
* - 2: hook usage + try/finally
*
* Invoked in a hook whose body is wrapped in a try/finally that finishes the
* effect store returned by the hook (e.g. what react-transform does)
*/
type EffectStoreUsage = 0 | 1 | 2;

export interface EffectStore {
/**
* An enum defining how this hook is used and whether it is invoked in a
* component's body or hook body. See the comment on `EffectStoreUsage` for
* more details.
*/
_usage?: EffectStoreUsage;
effect: Effect;
subscribe(onStoreChange: () => void): () => void;
getSnapshot(): number;
/** startEffect - begin tracking signals used in this component */
_start(): void;
/** finishEffect - stop tracking the signals used in this component */
f(): void;
[symDispose](): void;
Expand All @@ -55,19 +80,27 @@ function setCurrentStore(store?: EffectStore) {
const clearCurrentStore = () => setCurrentStore();

/**
* A redux-like store whose store value is a positive 32bit integer (a 'version').
* A redux-like store whose store value is a positive 32bit integer (a
* 'version').
*
* React subscribes to this store and gets a snapshot of the current 'version',
* whenever the 'version' changes, we tell React it's time to update the component (call 'onStoreChange').
* whenever the 'version' changes, we tell React it's time to update the
* component (call 'onStoreChange').
*
* How we achieve this is by creating a binding with an 'effect', when the `effect._callback' is called,
* we update our store version and tell React to re-render the component ([1] We don't really care when/how React does it).
* How we achieve this is by creating a binding with an 'effect', when the
* `effect._callback' is called, we update our store version and tell React to
* re-render the component ([1] We don't really care when/how React does it).
*
* [1]
* @see https://react.dev/reference/react/useSyncExternalStore
* @see https://github.com/reactjs/rfcs/blob/main/text/0214-use-sync-external-store.md
* @see
* https://github.com/reactjs/rfcs/blob/main/text/0214-use-sync-external-store.md
*
* @param _usage An enum defining how this hook is used and whether it is
* invoked in a component's body or hook body. See the comment on
* `EffectStoreUsage` for more details.
*/
function createEffectStore(): EffectStore {
function createEffectStore(_usage?: EffectStoreUsage): EffectStore {
let effectInstance!: Effect;
let version = 0;
let onChangeNotifyReact: (() => void) | undefined;
Expand All @@ -81,6 +114,7 @@ function createEffectStore(): EffectStore {
};

return {
_usage,
effect: effectInstance,
subscribe(onStoreChange) {
onChangeNotifyReact = onStoreChange;
Expand All @@ -104,6 +138,25 @@ function createEffectStore(): EffectStore {
getSnapshot() {
return version;
},
_start() {
// TODO: implement state machine to transition between effect stores:
//
// - 0 -> 0: finish previous effect (unknown to unknown)
// - 0 -> 1: finish previous effect
// Assume previous invocation was another component or hook from another
// component. Nested component renders (renderToStaticMarkup) not
// supported with bare useSignals calls.
// - 0 -> 2: capture & restore
// Previous invocation could be a component or a hook. Either way,
// restore it after our invocation so that it can continue to capture
// any signals after we exit.
// - 1 -> 0: ? do nothing since it'll be captured by current effect store?
// - 1 -> 1: capture & restore (e.g. component calls renderToStaticMarkup)
// - 1 -> 2: capture & restore (e.g. hook)
// - 2 -> 0: ? do nothing since it'll be captured by current effect store?
// - 2 -> 1: capture & restore (e.g. hook calls renderToStaticMarkup)
// - 2 -> 2: capture & restore (e.g. nested hook calls)
},
f() {
clearCurrentStore();
},
Expand All @@ -120,7 +173,7 @@ const _queueMicroTask = Promise.prototype.then.bind(Promise.resolve());
* Custom hook to create the effect to track signals used during render and
* subscribe to changes to rerender the component when the signals change.
*/
export function useSignals(): EffectStore {
export function useSignals(_usage?: EffectStoreUsage): EffectStore {
clearCurrentStore();
if (!finalCleanup) {
finalCleanup = _queueMicroTask(() => {
Expand All @@ -131,7 +184,7 @@ export function useSignals(): EffectStore {

const storeRef = useRef<EffectStore>();
if (storeRef.current == null) {
storeRef.current = createEffectStore();
storeRef.current = createEffectStore(_usage);
}

const store = storeRef.current;
Expand Down

0 comments on commit 6b388bf

Please sign in to comment.