# 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 } ``` ## 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...
); }; ``` ### 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 ( ); }; 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)} ))} ); }; ```