# React Hooks for PowerSync
The `powersync/react` package provides React hooks for use with the JavaScript Web SDK or React Native SDK. These hooks are designed to support reactivity, and can be used to automatically re-render React components when query results update or to access PowerSync connectivity status changes.
## Usage
### Context
Configure a PowerSync DB connection and add it to a context provider.
```JSX
// App.jsx
import { PowerSyncDatabase } from '@powersync/web';
// or for React Native
// import { PowerSyncDatabase } from '@powersync/react-native';
import { PowerSyncContext } from "@powersync/react";
export const App = () => {
const powerSync = React.useMemo(() => {
// Setup PowerSync client
}, [])
return
{/** Insert your components here */ }
}
```
### Accessing PowerSync
The provided PowerSync client is available with the `usePowerSync` hook.
```JSX
// TodoListDisplay.jsx
import { usePowerSync } from "@powersync/react";
export const TodoListDisplay = () => {
const powersync = usePowerSync();
const [lists, setLists] = React.useState([]);
React.useEffect(() => {
powersync.getAll('SELECT * from lists').then(setLists)
}, []);
return
{lists.map(list =>
{list.name}
)}
}
```
## Accessing PowerSync Status
The provided PowerSync client status is available with the `useStatus` hook.
```JSX
import { useStatus } from "@powersync/react";
const Component = () => {
const status = useStatus();
return (
<>
{status.connected ? 'wifi' : 'wifi-off'}
{!status.hasSynced ? 'Busy syncing...' : 'Data is here'}
>
)
};
```
## Reactive Queries
The `useQuery` hook allows you to access the results of a watched query. Queries will automatically update when a dependent table is updated unless you set the `runQueryOnce` flag. You are also able to use a compilable query (e.g. [Kysely queries](https://github.com/powersync-ja/powersync-js/tree/main/packages/kysely-driver)) as a query argument in place of a SQL statement string.
```JSX
// TodoListDisplay.jsx
import { useQuery } from "@powersync/react";
export const TodoListDisplay = () => {
const { data: todoLists } = useQuery('SELECT * FROM lists WHERE id = ?', ['id-1'], {runQueryOnce: false});
return
{todoLists.map((l) => (
{JSON.stringify(l)}
))}
}
```
### Query Loading
The response from `useQuery` includes the `isLoading` and `isFetching` properties, which indicate the current state of data retrieval. This can be used to show loading spinners or conditional widgets.
```JSX
// TodoListDisplay.jsx
import { useQuery } from "@powersync/react";
export const TodoListsDisplayDemo = () => {
const { data: todoLists, isLoading, isFetching } = useQuery('SELECT * FROM lists');
return (
Todo Lists {isFetching ? '⟳' : ''}
Loading todo lists...
{todoLists.map(() => (
{JSON.stringify(l)}
))}
);
};
```
### Suspense
The `useSuspenseQuery` hook also allows you to access the results of a watched query, but its loading and fetching states are handled through [Suspense](https://react.dev/reference/react/Suspense). Unlike `useQuery`, the hook doesn't return `isLoading` or `isFetching` for the loading states nor `error` for the error state. These should be handled with variants of `` and `` respectively.
```JSX
// TodoListDisplaySuspense.jsx
import { ErrorBoundary } from 'react-error-boundary';
import { Suspense } from 'react';
import { useSuspenseQuery } from '@powersync/react';
const TodoListContent = () => {
const { data: todoLists } = useSuspenseQuery("SELECT * FROM lists");
return (
{todoLists.map((list) => (
{list.name}
))}
);
};
export const TodoListDisplaySuspense = () => {
return (
Something went wrong}>
Loading todo lists...}>
);
};
```
#### Blocking navigation on Suspense
When you provide a Suspense fallback, suspending components will cause the fallback to render. Alternatively, React's [startTransition](https://react.dev/reference/react/startTransition) allows navigation to be blocked until the suspending components have completed, preventing the fallback from displaying. This behavior can be facilitated by your router — for example, react-router supports this with its [startTransition flag](https://reactrouter.com/en/main/upgrading/future#v7_starttransition).
> Note: In this example, the `` boundary is intentionally omitted to delegate the handling of the suspending state to the router.
```JSX
// routerAndLists.jsx
import { RouterProvider } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { useSuspenseQuery } from '@powersync/react';
export const Index() {
return
}
const TodoListContent = () => {
const { data: todoLists } = useSuspenseQuery("SELECT * FROM lists");
return (
{todoLists.map((list) => (
{list.name}
))}
);
};
export const TodoListsPage = () => {
return (
Something went wrong}>
);
};
```
#### Managing Suspense When Updating `useSuspenseQuery` Parameters
When data in dependent tables changes, `useSuspenseQuery` automatically updates without suspending. However, changing the query parameters causes the hook to restart and enter a suspending state again, which triggers the suspense fallback. To prevent this and keep displaying the stale data until the new data is loaded, wrap the parameter changes in React's [startTransition](https://react.dev/reference/react/startTransition) or use [useDeferredValue](https://react.dev/reference/react/useDeferredValue).
```JSX
// TodoListDisplaySuspenseTransition.jsx
import { ErrorBoundary } from 'react-error-boundary';
import React, { Suspense } from 'react';
import { useSuspenseQuery } from '@powersync/react';
const TodoListContent = () => {
const [query, setQuery] = React.useState('SELECT * FROM lists');
const { data: todoLists } = useSuspenseQuery(query);
return (
{todoLists.map((list) => (
{list.name}
))}
);
};
export const TodoListDisplaySuspense = () => {
return (
Something went wrong}>
Loading todo lists...}>
);
};
```
and
```JSX
// TodoListDisplaySuspenseDeferred.jsx
import { ErrorBoundary } from 'react-error-boundary';
import React, { Suspense } from 'react';
import { useSuspenseQuery } from '@powersync/react';
const TodoListContent = () => {
const [query, setQuery] = React.useState('SELECT * FROM lists');
const deferredQueryQuery = React.useDeferredValue(query);
const { data: todoLists } = useSuspenseQuery(deferredQueryQuery);
return (
{todoLists.map((list) => (
{list.name}
))}
);
};
export const TodoListDisplaySuspense = () => {
return (
Something went wrong}>
Loading todo lists...}>
);
};
```
## Preventing Unnecessary Renders
The `useQuery` hook returns a stateful object which contains query fetching/loading state values and the query result set data.
```tsx
function MyWidget() {
// ... Widget code
// result is an object which contains `isLoading`, `isFetching`, `data` members.
const result = useQuery(...)
// ... Widget code
}
```
### High Order Components
The returned object is a new JS object reference whenever the internal state changes e.g. if the query `isFetching` alternates in value. The parent component which calls `useQuery` will render each time the watched query state changes - this can result in other child widgets re-rendering if they are not memoized. Using the `result` object in child component props will cause those children to re-render on any state change of the watched query. The first step to avoid re-renders is to call `useQuery` in a Higher Order Component which passes query results to memoized children.
```tsx
function MyWidget() {
// ... Widget code
// result is an object which contains `isLoading`, `isFetching`, `data` members.
const {data, error, isLoading} = useQuery(...)
// ... Widget code
return (
// ... Other components
// If MyWatchedWidget is not memoized
// - It will rerender on any state change of the watched query. E.g. if isFetching alternates
// If MyWatchedWidget is memoized
// - It will re-render if the data reference changes. By default the data reference changes after any
// change to the query's dependent tables. This can be optimized by using Incremental queries.
)
}
```
### Incremental Queries
By default watched queries are queried whenever a change to the underlying tables has been detected. These changes might not be relevant to the actual query, but will still trigger a query and `data` update.
```tsx
function MyWidget() {
// ... Widget code
// This query will update with a new data Array whenever any change is made to the `cats` table
// E.g. `INSERT INTO cats(name) VALUES ('silvester')` will return a new Array reference for `data`
const { data } = useQuery(`SELECT * FROM cats WHERE name = 'bob'`)
// ... Widget code
return (
// Other components
// This will rerender for any change to the `cats` table
// Memoization cannot prevent this component from re-rendering since `data[0]` is always new object reference
// whenever a query has been triggered
)
}
```
Incremental watched queries ensure that the `data` member of the `useQuery` result maintains the same Array reference if the result set is unchanged.
Additionally, the internal array items maintain object references when unchanged.
```tsx
function MyWidget() {
// ... Widget code
// This query will be fetched/queried whenever any change is made to the `cats` table.
// The `data` reference will only be changed if there have been changes since the previous value.
// This method performs a comparison in memory in order to determine changes.
// Note that isFetching is set (by default) whenever the query is being fetched/checked.
// This will result in `MyWidget` re-rendering for any change to the `cats` table.
const { data, isLoading, isFetching } = useQuery(`SELECT * FROM cats WHERE breed = 'tabby'`, [], {
rowComparator: {
keyBy: (item) => item.id,
compareBy: (item) => JSON.stringify(item)
}
})
// ... Widget code
return (
// Other components
// The data array is the same reference if no changes have occurred between fetches
// Note: The array is a new reference is there are any changes in the result set (individual row object references are preserved for unchanged rows)
// Note: CatCollection requires memoization in order to prevent re-rendering (due to the parent re-rendering on fetch)
)
}
```
`useQuery` can be configured to disable reporting `isFetching` status. Disabling this setting reduces the number of events emitted from the hook, which can reduce renders in some circumstances.
```tsx
function MyWidget() {
// ... Widget code
// This query will be fetched/queried whenever any change is made to the `cats` table.
// The `data` reference will only be changed if there have been changes since the previous value.
// When reportFetching == false the object returned from useQuery will only be changed when the data, isLoading or error state changes.
// This method performs a comparison in memory in order to determine changes.
const { data, isLoading } = useQuery(`SELECT * FROM cats WHERE breed = 'tabby'`, [], {
rowComparator: {
keyBy: (item) => item.id,
compareBy: (item) => JSON.stringify(item)
}
reportFetching: false
})
// ... Widget code
return (
// Other components
// The data array is the same reference if no changes have occurred between fetches
// Note: The array is a new reference is there are any changes in the result set (individual row object references are not preserved)
)
}
```
## Query Subscriptions
The `useWatchedQuerySubscription` hook lets you access the state of an externally managed `WatchedQuery` instance. Managing a query outside of a component enables in-memory caching and sharing of results between multiple subscribers. This reduces async loading time during component mount (thanks to in-memory caching) and minimizes the number of SQLite queries (by sharing results between multiple components).
```jsx
// The lifecycle of this query is managed outside of any individual React component.
// The data is kept up-to-date in the background and can be shared by multiple subscribers.
const listsQuery = powerSync.query({ sql: 'SELECT * FROM lists' }).differentialWatch();
export const ContentComponent = () => {
// Subscribes to the `listsQuery` instance. The subscription is automatically
// cleaned up when the component unmounts. The `data` value always reflects
// the latest state of the query.
const { data: lists } = useWatchedQuerySubscription(listsQuery);
return (
{lists.map((l) => (
{JSON.stringify(l)}
))}
);
};
```