Skip to content

Commit 05f50f1

Browse files
Add demo for instant query caching with localStorage
1 parent a0e0c70 commit 05f50f1

File tree

2 files changed

+79
-40
lines changed

2 files changed

+79
-40
lines changed

demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { configureFts } from '@/app/utils/fts_setup';
2-
import { AppSchema } from '@/library/powersync/AppSchema';
2+
import { AppSchema, ListRecord, LISTS_TABLE, TODOS_TABLE } from '@/library/powersync/AppSchema';
33
import { SupabaseConnector } from '@/library/powersync/SupabaseConnector';
44
import { CircularProgress } from '@mui/material';
55
import { PowerSyncContext } from '@powersync/react';
6-
import { createBaseLogger, LogLevel, PowerSyncDatabase } from '@powersync/web';
6+
import {
7+
ArrayComparator,
8+
createBaseLogger,
9+
GetAllQuery,
10+
IncrementalWatchMode,
11+
LogLevel,
12+
PowerSyncDatabase,
13+
WatchedQuery
14+
} from '@powersync/web';
715
import React, { Suspense } from 'react';
816
import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext';
917

@@ -17,10 +25,65 @@ export const db = new PowerSyncDatabase({
1725
}
1826
});
1927

28+
export type EnhancedListRecord = ListRecord & { total_tasks: number; completed_tasks: number };
29+
30+
export type QueryStore = {
31+
lists: WatchedQuery<EnhancedListRecord[]>;
32+
};
33+
34+
const QueryStore = React.createContext<QueryStore | null>(null);
35+
export const useQueryStore = () => React.useContext(QueryStore);
36+
2037
export const SystemProvider = ({ children }: { children: React.ReactNode }) => {
21-
const [connector] = React.useState(new SupabaseConnector());
38+
const [connector] = React.useState(() => new SupabaseConnector());
2239
const [powerSync] = React.useState(db);
2340

41+
const [queryStore] = React.useState<QueryStore>(() => {
42+
const listsQuery = db.incrementalWatch({ mode: IncrementalWatchMode.COMPARISON }).build({
43+
comparator: new ArrayComparator({
44+
compareBy: (item) => JSON.stringify(item)
45+
}),
46+
watch: {
47+
// This provides instant caching of the query results.
48+
// SQLite calls are asynchronous - therefore on page refresh the placeholder data will be used until the query is resolved.
49+
// This uses localStorage to synchronously display a cached version while loading.
50+
// Note that the TodoListsWidget is wraped by a GuardBySync component, which will prevent rendering until the query is resolved.
51+
// Disable GuardBySync to see the placeholder data in action.
52+
placeholderData: JSON.parse(localStorage.getItem('listscache') ?? '[]') as EnhancedListRecord[],
53+
query: new GetAllQuery<EnhancedListRecord>({
54+
sql: /* sql */ `
55+
SELECT
56+
${LISTS_TABLE}.*,
57+
COUNT(${TODOS_TABLE}.id) AS total_tasks,
58+
SUM(
59+
CASE
60+
WHEN ${TODOS_TABLE}.completed = true THEN 1
61+
ELSE 0
62+
END
63+
) as completed_tasks
64+
FROM
65+
${LISTS_TABLE}
66+
LEFT JOIN ${TODOS_TABLE} ON ${LISTS_TABLE}.id = ${TODOS_TABLE}.list_id
67+
GROUP BY
68+
${LISTS_TABLE}.id;
69+
`
70+
})
71+
}
72+
});
73+
74+
// This updates a cache in order to display results instantly on page load.
75+
listsQuery.subscribe({
76+
onData: (data) => {
77+
// Store the data in localStorage for instant caching
78+
localStorage.setItem('listscache', JSON.stringify(data));
79+
}
80+
});
81+
82+
return {
83+
lists: listsQuery
84+
};
85+
});
86+
2487
React.useEffect(() => {
2588
const logger = createBaseLogger();
2689
logger.useDefaults(); // eslint-disable-line
@@ -30,7 +93,7 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => {
3093

3194
powerSync.init();
3295
const l = connector.registerListener({
33-
initialized: () => { },
96+
initialized: () => {},
3497
sessionStarted: () => {
3598
powerSync.connect(connector);
3699
}
@@ -47,11 +110,13 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => {
47110

48111
return (
49112
<Suspense fallback={<CircularProgress />}>
50-
<PowerSyncContext.Provider value={powerSync}>
51-
<SupabaseContext.Provider value={connector}>
52-
<NavigationPanelContextProvider>{children}</NavigationPanelContextProvider>
53-
</SupabaseContext.Provider>
54-
</PowerSyncContext.Provider>
113+
<QueryStore.Provider value={queryStore}>
114+
<PowerSyncContext.Provider value={powerSync}>
115+
<SupabaseContext.Provider value={connector}>
116+
<NavigationPanelContextProvider>{children}</NavigationPanelContextProvider>
117+
</SupabaseContext.Provider>
118+
</PowerSyncContext.Provider>
119+
</QueryStore.Provider>
55120
</Suspense>
56121
);
57122
};

demos/react-supabase-todolist/src/components/widgets/TodoListsWidget.tsx

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { LISTS_TABLE, ListRecord, TODOS_TABLE } from '@/library/powersync/AppSchema';
21
import { List } from '@mui/material';
3-
import { useQuery } from '@powersync/react';
4-
import { ArrayComparator } from '@powersync/web';
2+
import { useWatchedQuerySubscription } from '@powersync/react';
3+
import { useQueryStore } from '../providers/SystemProvider';
54
import { ListItemWidget } from './ListItemWidget';
65

76
export type TodoListsWidgetProps = {
@@ -13,35 +12,10 @@ const description = (total: number, completed: number = 0) => {
1312
};
1413

1514
export function TodoListsWidget(props: TodoListsWidgetProps) {
16-
const { data: listRecords, isLoading } = useQuery<ListRecord & { total_tasks: number; completed_tasks: number }>(
17-
/* sql */ `
18-
SELECT
19-
${LISTS_TABLE}.*,
20-
COUNT(${TODOS_TABLE}.id) AS total_tasks,
21-
SUM(
22-
CASE
23-
WHEN ${TODOS_TABLE}.completed = true THEN 1
24-
ELSE 0
25-
END
26-
) as completed_tasks
27-
FROM
28-
${LISTS_TABLE}
29-
LEFT JOIN ${TODOS_TABLE} ON ${LISTS_TABLE}.id = ${TODOS_TABLE}.list_id
30-
GROUP BY
31-
${LISTS_TABLE}.id;
32-
`,
33-
[],
34-
{
35-
processor: {
36-
mode: 'comparison',
37-
comparator: new ArrayComparator({
38-
compareBy: (item) => JSON.stringify(item)
39-
})
40-
}
41-
}
42-
);
15+
const queries = useQueryStore();
16+
const { data: listRecords, isLoading } = useWatchedQuerySubscription(queries!.lists);
4317

44-
if (isLoading) {
18+
if (isLoading && listRecords.length == 0) {
4519
return <div>Loading...</div>;
4620
}
4721

0 commit comments

Comments
 (0)