Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: Suspense for usePowerSyncWatchedQuery #100

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demos/react-supabase-todolist/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.13",
"react-router-dom": "^6.22.3"
},
"devDependencies": {
Expand Down
12 changes: 7 additions & 5 deletions demos/react-supabase-todolist/src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ const root = createRoot(document.getElementById('app')!);
root.render(<App />);

export function App() {
return <ThemeProviderContainer>
<SystemProvider>
<RouterProvider router={router} />
</SystemProvider>
</ThemeProviderContainer>;
return (
<ThemeProviderContainer>
<SystemProvider>
<RouterProvider router={router} future={{ v7_startTransition: true }} />
</SystemProvider>
</ThemeProviderContainer>
);
}
4 changes: 2 additions & 2 deletions demos/react-supabase-todolist/src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { Outlet, createBrowserRouter } from "react-router-dom";
import LoginPage from "./auth/login/page";
import RegisterPage from "./auth/register/page";
import EntryPage from "./page";
import TodoEditPage from "./views/todo-lists/edit/page";
import TodoListsPage from "./views/todo-lists/page";
import ViewsLayout from "./views/layout";
import SQLConsolePage from "./views/sql-console/page";
import TodoEditPage from "./views/todo-lists/edit/page";
import TodoListsPage from "./views/todo-lists/page";

export const TODO_LISTS_ROUTE = '/views/todo-lists';
export const TODO_EDIT_ROUTE = '/views/todo-lists/:id';
Expand Down
6 changes: 4 additions & 2 deletions demos/react-supabase-todolist/src/app/views/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
Typography,
styled
} from '@mui/material';
import React from 'react';
import React, { Suspense } from 'react';

import { useNavigationPanel } from '@/components/navigation/NavigationPanelContext';
import { useSupabase } from '@/components/providers/SystemProvider';
Expand Down Expand Up @@ -118,7 +118,9 @@ export default function ViewsLayout({ children }: { children: React.ReactNode })
</List>
</Drawer>
<S.MainBox>
{children}
<Suspense>
{children}
</Suspense>
</S.MainBox>
</S.MainBox>
);
Expand Down
97 changes: 60 additions & 37 deletions demos/react-supabase-todolist/src/app/views/sql-console/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import React, { Suspense } from 'react';
import { usePowerSyncWatchedQuery } from '@journeyapps/powersync-react';
import { Box, Button, Grid, TextField, styled } from '@mui/material';
import { Box, Button, CircularProgress, Grid, TextField, styled } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import { NavigationPage } from '@/components/navigation/NavigationPage';

import { ErrorBoundary } from "react-error-boundary";

export type LoginFormParams = {
email: string;
password: string;
Expand All @@ -14,21 +16,6 @@ const DEFAULT_QUERY = 'SELECT * FROM lists';
export default function SQLConsolePage() {
const inputRef = React.useRef<HTMLInputElement>();
const [query, setQuery] = React.useState(DEFAULT_QUERY);
const querySQLResult = usePowerSyncWatchedQuery(query);

const queryDataGridResult = React.useMemo(() => {
const firstItem = querySQLResult?.[0];

return {
columns: firstItem
? Object.keys(firstItem).map((field) => ({
field,
flex: 1
}))
: [],
rows: querySQLResult
};
}, [querySQLResult]);

return (
<NavigationPage title="SQL Console">
Expand Down Expand Up @@ -64,31 +51,67 @@ export default function SQLConsolePage() {
</S.CenteredGrid>
</S.CenteredGrid>

{queryDataGridResult ? (
<S.QueryResultContainer>
{queryDataGridResult.columns ? (
<DataGrid
autoHeight={true}
rows={queryDataGridResult.rows?.map((r, index) => ({ ...r, id: r.id ?? index })) ?? []}
columns={queryDataGridResult.columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 20
}
}
}}
pageSizeOptions={[20]}
disableRowSelectionOnClick
/>
) : null}
</S.QueryResultContainer>
) : null}
<Suspense fallback={<CircularProgress />}>
{/* Use resetKeys to dismiss the error when changing the query. */}
<ErrorBoundary
fallbackRender={fallbackRender} resetKeys={[query]}>
<SqlConsoleResults query={query} />
</ErrorBoundary>
</Suspense>

</S.MainContainer>
</NavigationPage>
);
}

function SqlConsoleResults(props: { query: string }) {
const querySQLResult = usePowerSyncWatchedQuery(props.query);

const queryDataGridResult = React.useMemo(() => {
const firstItem = querySQLResult?.[0];

return {
columns: firstItem
? Object.keys(firstItem).map((field) => ({
field,
flex: 1
}))
: [],
rows: querySQLResult
};
}, [querySQLResult]);

return queryDataGridResult ? (
<S.QueryResultContainer>
{queryDataGridResult.columns ? (
<DataGrid
autoHeight={true}
rows={queryDataGridResult.rows?.map((r, index) => ({ ...r, id: r.id ?? index })) ?? []}
columns={queryDataGridResult.columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 20
}
}
}}
pageSizeOptions={[20]}
disableRowSelectionOnClick
/>
) : null}
</S.QueryResultContainer>
) : null
}

function fallbackRender(options: { error: any }) {
return (
<div role="alert">
<pre style={{ color: "red" }}>{options.error.message}</pre>
</div>
);
}


namespace S {
export const MainContainer = styled(Box)`
padding: 20px;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { useSupabase } from '@/components/providers/SystemProvider';
import { TodoItemWidget } from '@/components/widgets/TodoItemWidget';
import { TodoListsWidget } from '@/components/widgets/TodoListsWidget';
import { LISTS_TABLE, TODOS_TABLE, TodoRecord } from '@/library/powersync/AppSchema';
import { usePowerSync, usePowerSyncWatchedQuery } from '@journeyapps/powersync-react';
import AddIcon from '@mui/icons-material/Add';
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Grid,
List,
TextField,
Typography,
styled
} from '@mui/material';
import Fab from '@mui/material/Fab';
import React, { Suspense } from 'react';
import React from 'react';
import { useParams } from 'react-router-dom';
import { NavigationPage } from '@/components/navigation/NavigationPage';

/**
* useSearchParams causes the entire element to fall back to client side rendering
Expand All @@ -35,7 +36,6 @@ const TodoEditSection = () => {
const [listRecord] = usePowerSyncWatchedQuery<{ name: string }>(`SELECT name FROM ${LISTS_TABLE} WHERE id = ?`, [
listID
]);

const todos = usePowerSyncWatchedQuery<TodoRecord>(
`SELECT * FROM ${TODOS_TABLE} WHERE list_id=? ORDER BY created_at DESC, id`,
[listID]
Expand Down Expand Up @@ -104,17 +104,24 @@ const TodoEditSection = () => {
<AddIcon />
</S.FloatingActionButton>
<Box>
<List dense={false}>
{todos.map((r) => (
<TodoItemWidget
key={r.id}
description={r.description}
onDelete={() => deleteTodo(r.id)}
isComplete={r.completed == 1}
toggleCompletion={() => toggleCompletion(r, !r.completed)}
/>
))}
</List>
<Grid container spacing={2}>
<Grid item xs={4}>
<TodoListsWidget selectedId={listID} />
</Grid>
<Grid item xs={8}>
<List dense={false}>
{todos.map((r) => (
<TodoItemWidget
key={r.id}
description={r.description}
onDelete={() => deleteTodo(r.id)}
isComplete={r.completed == 1}
toggleCompletion={() => toggleCompletion(r, !r.completed)}
/>
))}
</List>
</Grid>
</Grid>
</Box>
{/* TODO use a dialog service in future, this is just a simple example app */}
<Dialog
Expand Down Expand Up @@ -152,9 +159,7 @@ const TodoEditSection = () => {
export default function TodoEditPage() {
return (
<Box>
<Suspense fallback={<CircularProgress />}>
<TodoEditSection />
</Suspense>
<TodoEditSection />
</Box>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { useSupabase } from '@/components/providers/SystemProvider';
import { TodoListsWidget } from '@/components/widgets/TodoListsWidget';
import { LISTS_TABLE } from '@/library/powersync/AppSchema';
import { usePowerSync } from '@journeyapps/powersync-react';
import AddIcon from '@mui/icons-material/Add';
import {
Expand All @@ -13,10 +17,7 @@ import {
} from '@mui/material';
import Fab from '@mui/material/Fab';
import React from 'react';
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { useSupabase } from '@/components/providers/SystemProvider';
import { TodoListsWidget } from '@/components/widgets/TodoListsWidget';
import { LISTS_TABLE } from '@/library/powersync/AppSchema';


export default function TodoListsPage() {
const powerSync = usePowerSync();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { TODO_LISTS_ROUTE } from '@/app/router';
import { LISTS_TABLE, ListRecord, TODOS_TABLE } from '@/library/powersync/AppSchema';
import { usePowerSync, usePowerSyncWatchedQuery } from "@journeyapps/powersync-react";
import { List } from "@mui/material";
import { usePowerSync, usePowerSyncWatchedQuery } from '@journeyapps/powersync-react';
import { List } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { ListItemWidget } from "./ListItemWidget";
import { ListItemWidget } from './ListItemWidget';
import React from 'react';

export type TodoListsWidgetProps = {
selectedId?: string;
}
};

const description = (total: number, completed: number = 0) => {
return `${total - completed} pending, ${completed} completed`;
Expand All @@ -16,17 +17,18 @@ const description = (total: number, completed: number = 0) => {
export function TodoListsWidget(props: TodoListsWidgetProps) {
const powerSync = usePowerSync();
const navigate = useNavigate();
const [isPending, startTransition] = React.useTransition();

const listRecords = usePowerSyncWatchedQuery<ListRecord & { total_tasks: number; completed_tasks: number }>(`
SELECT
SELECT
${LISTS_TABLE}.*, COUNT(${TODOS_TABLE}.id) AS total_tasks, SUM(CASE WHEN ${TODOS_TABLE}.completed = true THEN 1 ELSE 0 END) as completed_tasks
FROM
FROM
${LISTS_TABLE}
LEFT JOIN ${TODOS_TABLE}
LEFT JOIN ${TODOS_TABLE}
ON ${LISTS_TABLE}.id = ${TODOS_TABLE}.list_id
GROUP BY
GROUP BY
${LISTS_TABLE}.id;
`);
`);

const deleteList = async (id: string) => {
await powerSync.writeTransaction(async (tx) => {
Expand All @@ -47,10 +49,10 @@ export function TodoListsWidget(props: TodoListsWidgetProps) {
selected={r.id == props.selectedId}
onDelete={() => deleteList(r.id)}
onPress={() => {
navigate(TODO_LISTS_ROUTE + '/' + r.id)
navigate(TODO_LISTS_ROUTE + '/' + r.id);
}}
/>
))}
</List>
);
}
}
35 changes: 35 additions & 0 deletions packages/powersync-react/src/QueryStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AbstractPowerSyncDatabase, SQLWatchOptions } from '@journeyapps/powersync-sdk-common';
import { WatchedQuery } from './WatchedQuery';

export class QueryStore {
cache = new Map<string, WatchedQuery>();

constructor(private db: AbstractPowerSyncDatabase) {}

getQuery(query: string, parameters: any[], options: SQLWatchOptions) {
const key = `${query} -- ${JSON.stringify(parameters)} -- ${JSON.stringify(options)}`;
if (this.cache.has(key)) {
return this.cache.get(key);
}
const disposer = () => {
this.cache.delete(key);
};
const q = new WatchedQuery(this.db, query, parameters, options, disposer);
this.cache.set(key, q);

return q;
}
}

let queryStores: WeakMap<AbstractPowerSyncDatabase, QueryStore> | undefined = undefined;

export function getQueryStore(db: AbstractPowerSyncDatabase): QueryStore {
queryStores ||= new WeakMap();
const existing = queryStores.get(db);
if (existing) {
return existing;
}
const store = new QueryStore(db);
queryStores.set(db, store);
return store;
}
Loading
Loading