React
ElectricSQL integrates with React via a Context provider and Hooks.
The context provider provides your Electric Client to your components. The hooks are used to bind live queries to your components and handle connectivity state. You can see both in action in the Examples -> Basic.
Context
makeElectricContext
In React, Context provides a way to pass data through the component tree without having to pass props down manually at every level. ElectricSQL provides a makeElectricContext
function that constructs an ElectricProvider
Context.Provider and useElectric
hook:
import { makeElectricContext } from 'electric-sql/react'
import { Electric } from './generated/client'
const { ElectricProvider, useElectric } = makeElectricContext<Electric>()
You typically call this once per app as part of your instantiation code. You then use the provider and hook in tandem to pass down and access the client in your components.
We provide this dynamic API rather than static ElectricProvider
and useElectric
imports in order to preserve the type information about your database structure. As you can see from the example above, the context is constructed using the Electric
type argument, which is a generated type containing all of the information about your database structure. This then allows you to write type safe data access code.
ElectricProvider
ElectricProvider
is a Context.Provider that accepts an Electric Client as it's value. Use it to pass down your electrified Client instance to your components, e.g.:
// wrapper.tsx
import React, { ReactNode, useEffect, useState } from 'react'
import { insecureAuthToken } from 'electric-sql/auth'
import { makeElectricContext } from 'electric-sql/react'
import { ElectricDatabase, electrify } from 'electric-sql/wa-sqlite'
import { Electric, schema } from './generated/client'
const { ElectricProvider, useElectric } = makeElectricContext<Electric>()
export const ElectricWrapper = ({ children }) => {
const [ electric, setElectric ] = useState<Electric>()
useEffect(() => {
let isMounted = true
const init = async () => {
const conn = await ElectricDatabase.init('electric.db')
const electric = await electrify(conn, schema)
const token = insecureAuthToken({sub: 'dummy'})
await electric.connect(token)
if (!isMounted) {
return
}
setElectric(electric)
}
init()
return () => {
isMounted = false
}
}, [])
if (electric === undefined) {
return null
}
return (
<ElectricProvider db={electric}>
{ children }
</ElectricProvider>
)
}
With an ElectricProvider
in place, you can then access the electric
client instance using the useElectric
hook.
Hooks
useElectric
The useElectric
hook returns the client instance registered with the ElectricProvider
, e.g.:
import React, { useState } from 'react'
import { useElectric } from './wrapper'
const ExampleComponent = () => {
const { db } = useElectric()!
const [ value, setValue ] = useState()
const generate = async () => {
const { newValue } = await db.rawQuery({
sql: 'select random() as newValue'
})
setValue(newValue)
}
return (
<div>
<p>
{value}
</p>
<p>
<a onClick={generate}>
Generate ↺
</a>
</div>
)
}
useLiveQuery
useLiveQuery
sets up a live query (aka a dynamic or reactive query). This takes query function returned by one of the db.live*
methods and keeps the results in sync whenever the relevant data changes.
import React from 'react'
import { useLiveQuery } from 'electric-sql/react'
import { useElectric } from './wrapper'
const Component = () => {
const { db } = useElectric()!
// Use the query builder API.
const { results } = useLiveQuery(
db.items.liveMany()
)
// Use the raw SQL API.
const { results: countResults } = useLiveQuery(
db.liveRawQuery({
sql: 'select count(*) from items'
})
)
const items: Item[] = results ?? []
const count: number =
countResults !== undefined
? countResults[0].count
: items.length
return (
<div>
<p>
{ count }
{ count === 1 ? 'item' : 'items' }
</p>
<ul>
{items.map((item, index) => (
<li key={ index }>
Item: { item.value }
</li>
))}
</ul>
</div>
)
}
The full return value of the hook is:
const { results, error, updatedAt } = useLiveQuery(runQuery)
With a signature of:
import { LiveResultContext } from 'electric-sql/client/model/model'
export interface LiveResultUpdate<T> {
results?: T
error?: unknown
updatedAt?: Date
}
function successResult<T>(results: T): LiveResultUpdate<T> {
return {
results: results,
error: undefined,
updatedAt: new Date(),
}
}
function errorResult<T>(error: unknown): LiveResultUpdate<T> {
return {
results: undefined,
error: error,
updatedAt: new Date(),
}
}
function useLiveQuery<Res>(
runQuery: LiveResultContext<Res>
): LiveResultUpdate<Res>
Running the query successfully will assign a new array of rows to the results
and error
will be undefined
. Or if the query errors, the error will be assigned to the error
variable and results
will be undefined
. The updatedAt
variable is a Date instance set when the return value last changed. Which is either when the query is first run or whenever it's re-run following a data change event.
See the implementation in frameworks/react/hooks.ts for more details.
Query dependencies
The live query is re-run:
- when any of the data in any of the tables it depends on changes
- when any of the query parameters change
By default, useLiveQuery
detects query parameter changes by comparing an ohash of the whole query parameter object. So in this case, an hash of the {where: {status: ...}}
object.
const Component = () => {
const [ status, setStatus ] = useState()
const { results } = useLiveQuery(
db.projects.liveMany({
where: {
status: status
}
})
)
// ...
}
With this API, the hash needs to be generated on every render cycle. This can be expensive. You can avoid this and optimise the comparison by passing an explicit dependency list as a second argument to useLiveQuery
:
const Component = ({ isActive }) => {
const [ status, setStatus ] = useState()
const { results } = useLiveQuery(
() => db.projects.liveMany({
where: {
status: status
}
}),
[status]
)
// ...
}
Note that with this usage, the first argument wraps the db.projects.liveMany()
call in a function. The alternative signature is:
function useLiveQuery<Res>(
runQueryFn: () => LiveResultContext<Res>,
dependencies: DependencyList
): LiveResultUpdate<Res>
useConnectivityState
useConnectivityState
binds the current connectivity status of the Satellite replication process for the electrified database client to a state variable:
import React from 'react'
import { useConnectivityState } from 'electric-sql/react'
const ConnectivityMonitor = () => {
const connectivityState = useConnectivityState()
return (
<span className="capitalize">
{ connectivityState }
</span>
)
}