Skip to content

Commit

Permalink
add useOnChange, deep watch
Browse files Browse the repository at this point in the history
  • Loading branch information
a-type committed May 4, 2024
1 parent c5da823 commit c701cfd
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 30 deletions.
7 changes: 7 additions & 0 deletions .changeset/late-weeks-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@verdant-web/react': major
'@verdant-web/cli': minor
'@verdant-web/store': patch
---

Add React hook "useOnChange" (fires a callback on entity changes). Add "deep" option to React hook "useWatch" to watch for deep changes. Remove the "field" variant of "useWatch." CLI must be updated to correctly generate new React hook types.
19 changes: 11 additions & 8 deletions packages/cli/src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,21 @@ export interface GeneratedHooks<Presence, Profile> {
useFindPeers: (query: (peer: UserInfo<Profile, Presence>) => boolean, options?: { includeSelf: boolean }) => UserInfo<Profile, Presence>[];
useSyncStatus: () => boolean;
useWatch<T extends AnyEntity<any, any, any> | null>(
entity: T
): EntityDestructured<T>;
useWatch<
T extends AnyEntity<any, any, any> | null,
P extends keyof EntityShape<T>
>(
entity: T,
prop: P
): EntityDestructured<T>[P];
options?: { deep?: boolean },
): EntityDestructured<T>;
useWatch<T extends EntityFile | null>(
file: T
): string | null;
useOnChange<T extends AnyEntity<any, any, any> | null>(
entity: T,
callback: (info: { isLocal: boolean; target?: AnyEntity<any, any, any> }) => void,
options?: { deep?: boolean },
): void;
useOnChange<T extends EntityFile | null>(
file: T,
callback: () => void,
): void;
useUndo(): () => void;
useRedo(): () => void;
useCanUndo(): boolean;
Expand Down
17 changes: 10 additions & 7 deletions packages/cli/test/__snapshots__/generated.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -640,15 +640,18 @@ export interface GeneratedHooks<Presence, Profile> {
useSyncStatus: () => boolean;
useWatch<T extends AnyEntity<any, any, any> | null>(
entity: T,
options?: { deep?: boolean },
): EntityDestructured<T>;
useWatch<
T extends AnyEntity<any, any, any> | null,
P extends keyof EntityShape<T>,
>(
entity: T,
prop: P,
): EntityDestructured<T>[P];
useWatch<T extends EntityFile | null>(file: T): string | null;
useOnChange<T extends AnyEntity<any, any, any> | null>(
entity: T,
callback: (info: {
isLocal: boolean;
target?: AnyEntity<any, any, any>;
}) => void,
options?: { deep?: boolean },
): void;
useOnChange<T extends EntityFile | null>(file: T, callback: () => void): void;
useUndo(): () => void;
useRedo(): () => void;
useCanUndo(): boolean;
Expand Down
12 changes: 3 additions & 9 deletions packages/react/src/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
import { collection, schema } from '@verdant-web/store';
import { schema } from '@verdant-web/store';
import { describe, it, expect } from 'vitest';
import { createHooks } from './hooks.js';

describe('generated hooks', () => {
it('should create singular and plural hooks for all collections', () => {
const authors = collection({
const authors = schema.collection({
name: 'author',
primaryKey: 'id',
fields: {
id: {
type: 'string',
indexed: true,
},
name: {
type: 'string',
},
},
compounds: {},
synthetics: {},
});
const tallies = collection({
const tallies = schema.collection({
name: 'tally',
primaryKey: 'id',
fields: {
id: {
type: 'string',
indexed: true,
},
count: {
type: 'number',
},
},
compounds: {},
synthetics: {},
});
const testSchema = schema({
version: 1,
Expand Down
51 changes: 45 additions & 6 deletions packages/react/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import {
useContext,
useEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
} from 'react';
import { suspend } from 'suspend-react';
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector.js';
import { EntityChangeInfo } from '../../store/src/entities/types.js';

function isQueryCurrentValid(query: Query<any>) {
return !(query.status === 'initial' || query.status === 'initializing');
Expand Down Expand Up @@ -84,11 +86,22 @@ export function createHooks<Presence = any, Profile = any>(
return suspend(() => ctx.readyPromise, ['lofi_' + ctx.namespace]) as any;
}

function useWatch(liveObject: Entity | EntityFile | null, prop?: any) {
function useWatch(
liveObject: Entity | EntityFile | null,
options?: { deep?: boolean },
) {
return useSyncExternalStore(
(handler) => {
if (liveObject) {
return (liveObject as any).subscribe('change', handler);
if ('isFile' in liveObject) {
return liveObject.subscribe('change', handler);
} else {
if (options?.deep) {
return liveObject.subscribe('change', handler);
} else {
return liveObject.subscribe('change', handler);
}
}
}
return () => {};
},
Expand All @@ -97,10 +110,6 @@ export function createHooks<Presence = any, Profile = any>(
if (liveObject instanceof EntityFile) {
return liveObject.url;
} else {
if (prop) {
return liveObject.get(prop);
}

return liveObject.getAll();
}
}
Expand All @@ -110,6 +119,35 @@ export function createHooks<Presence = any, Profile = any>(
);
}

function useOnChange(
liveObject: Entity | EntityFile | null,
handler: (info: EntityChangeInfo & { target?: Entity }) => void,
options?: { deep?: boolean },
) {
const handlerRef = useRef(handler);
handlerRef.current = handler;

return useEffect(() => {
if (!liveObject) return;

if ('isFile' in liveObject) {
return liveObject?.subscribe('change', () => {
handlerRef.current({});
});
} else {
if (options?.deep) {
return liveObject?.subscribe('changeDeep', (target, info) => {
handlerRef.current({ ...info, target: target as Entity });
});
}
return liveObject?.subscribe('change', (info) => {
info.isLocal ??= false;
handlerRef.current(info);
});
}
}, [liveObject, handlerRef]);
}

function useSelf() {
const storage = useStorage();
return useSyncExternalStore(
Expand Down Expand Up @@ -350,6 +388,7 @@ export function createHooks<Presence = any, Profile = any>(
useClient: useStorage,
useUnsuspendedClient,
useWatch,
useOnChange,
useSelf,
usePeerIds,
usePeer,
Expand Down
3 changes: 3 additions & 0 deletions packages/store/src/files/EntityFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
get downloadRemote() {
return this._downloadRemote;
}
get isFile() {
return true;
}

[UPDATE] = (fileData: FileData) => {
this._loading = false;
Expand Down

0 comments on commit c701cfd

Please sign in to comment.