Skip to content

Commit 3208be6

Browse files
committed
feat: add realtime hook
1 parent 9c7d047 commit 3208be6

File tree

12 files changed

+174
-20
lines changed

12 files changed

+174
-20
lines changed

docs/pages/documentation/auth/use-auth-state-change.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# useAuthStateChange
22

3-
Receive a notification every time an auth event happens. Composed in the [User Context example](/documentation/recipes/user-context).
3+
Receive a notification every time an auth event happens. Composed in the [`useAuth` recipe](/recipes/use-auth).
44

55
```js highlight=4,5,6
66
import { useAuthStateChange } from 'react-supabase'

docs/pages/documentation/data/use-select.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import Link from 'next/link'
2-
31
# useSelect
42

53
Performs vertical filtering with SELECT.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"use-subscription": "useSubscription"
2+
"use-subscription": "useSubscription",
3+
"use-realtime": "useRealtime"
34
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# useRealtime
2+
3+
Fetch table and listen for changes.
4+
5+
```js highlight=4
6+
import { useRealtime } from 'react-supabase'
7+
8+
function Page() {
9+
const [{ data, error, fetching }, refresh] = useRealtime('todos')
10+
11+
return ...
12+
}
13+
```
14+
15+
## Compare function
16+
17+
You can pass a function for comparing subscription event changes. By default, the compare function checks the `id` field.
18+
19+
```js highlight=6
20+
import { useRealtime } from 'react-supabase'
21+
22+
function Page() {
23+
const [{ data, error, fetching }, refresh] = useRealtime(
24+
'todos',
25+
(data, payload) => data.username === payload.username,
26+
)
27+
28+
return ...
29+
}
30+
```

docs/pages/documentation/realtime/use-subscription.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# useSubscription
22

3-
Subscribe to realtime changes to databse.
3+
Subscribe to database changes in realtime.
44

55
```js highlight=4,5,6
66
import { useSubscription } from 'react-supabase'

docs/pages/recipes/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"auth-context": "AuthContext"
2+
"use-auth": "useAuth"
33
}
Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
# Auth Context
1+
# useAuth
22

3-
Keep track of the authenticated user using the [Context API](https://reactjs.org/docs/context.html) and `useAuthStateChange` hook.
4-
5-
First, create a context and user hook:
3+
Keep track of the authenticated session with the [Context API](https://reactjs.org/docs/context.html) and [`useAuthStateChange`](/documentation/auth/use-auth-state-change) hook. First, create a new React Context:
64

75
```js
86
import { createContext, useEffect, useState } from 'react'
@@ -11,7 +9,7 @@ import { useAuthStateChange, useClient } from 'react-supabase'
119
const initialState = { session: null, user: null }
1210
export const AuthContext = createContext(initialState)
1311

14-
export const AuthProvider = ({ children }) => {
12+
export function AuthProvider({ children }) {
1513
const client = useClient()
1614
const [state, setState] = useState(initialState)
1715

@@ -25,25 +23,27 @@ export const AuthProvider = ({ children }) => {
2523
setState({ session, user: session?.user ?? null })
2624
})
2725

28-
return (
29-
<AuthContext.Provider value={state}>
30-
{children}
31-
</AuthContext.Provider>
32-
)
26+
return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>
3327
}
28+
```
29+
30+
And auth hook:
31+
32+
```js
33+
import { AuthContext } from 'path/to/auth/context'
3434

35-
export const useAuth = () => {
35+
export function useAuth() {
3636
const context = useContext(AuthContext)
3737
if (context === undefined) {
3838
throw Error('useAuth must be used within AuthProvider')
3939
return context
4040
}
4141
```
4242
43-
Wrap your app in `AuthProvider` and use the `useAuth` hook in your components:
43+
Then, wrap your app in `AuthProvider` and use the `useAuth` hook in your components:
4444
4545
```js highlight=4
46-
import { useAuth } from 'path/to/auth'
46+
import { useAuth } from 'path/to/auth/hook'
4747

4848
function Page() {
4949
const { session, user } = useAuth()

src/hooks/realtime/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './use-subscription'
2+
export * from './use-realtime'

src/hooks/realtime/use-realtime.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { useEffect, useReducer } from 'react'
2+
import { SupabaseRealtimePayload } from '@supabase/supabase-js'
3+
4+
import { UseSelectState, useSelect } from '../data'
5+
import { useSubscription } from './use-subscription'
6+
7+
export type UseRealtimeState<Data = any> = Omit<
8+
UseSelectState<Data>,
9+
'count'
10+
> & {
11+
old?: Data[] | null
12+
}
13+
14+
export type UseRealtimeResponse<Data = any> = [
15+
UseRealtimeState<Data>,
16+
() => Promise<Pick<UseSelectState<Data>, 'count' | 'data' | 'error'>>,
17+
]
18+
19+
export type UseRealtimeAction<Data = any> =
20+
| { type: 'FETCH'; payload: UseSelectState<Data> }
21+
| { type: 'SUBSCRIPTION'; payload: SupabaseRealtimePayload<Data> }
22+
23+
export type UseRealtimeCompareFn<Data = any> = (
24+
data: Data,
25+
payload: Data,
26+
) => boolean
27+
28+
export function useRealtime<Data = any>(
29+
table: string,
30+
compareFn: UseRealtimeCompareFn = (a, b) => a.id === b.id,
31+
): UseRealtimeResponse<Data> {
32+
if (table === '*')
33+
throw Error(
34+
'Must specify table or row. Cannot listen for all database changes.',
35+
)
36+
37+
const [result, reexecute] = useSelect<Data>(table)
38+
const [state, dispatch] = useReducer<
39+
React.Reducer<UseRealtimeState<Data>, UseRealtimeAction<Data>>
40+
>(reducer(compareFn), result)
41+
42+
useSubscription((payload) => dispatch({ type: 'SUBSCRIPTION', payload }), {
43+
table,
44+
})
45+
46+
useEffect(() => {
47+
dispatch({ type: 'FETCH', payload: result })
48+
}, [result])
49+
50+
return [state, reexecute]
51+
}
52+
53+
const reducer = <Data = any>(compareFn: UseRealtimeCompareFn) => (
54+
state: UseRealtimeState<Data>,
55+
action: UseRealtimeAction<Data>,
56+
): UseRealtimeState<Data> => {
57+
const old = state.data
58+
switch (action.type) {
59+
case 'FETCH':
60+
return { ...state, old, ...action.payload }
61+
case 'SUBSCRIPTION':
62+
switch (action.payload.eventType) {
63+
case 'DELETE':
64+
return {
65+
...state,
66+
data: state.data?.filter(
67+
(x) => !compareFn(x, action.payload.old),
68+
),
69+
fetching: false,
70+
old,
71+
}
72+
case 'INSERT':
73+
return {
74+
...state,
75+
data: [...(old ?? []), action.payload.new],
76+
fetching: false,
77+
old,
78+
}
79+
case 'UPDATE': {
80+
const data = old ?? []
81+
const index = data.findIndex((x) =>
82+
compareFn(x, action.payload.new),
83+
)
84+
return {
85+
...state,
86+
data: [
87+
...data.slice(0, index),
88+
action.payload.new,
89+
...data.slice(index + 1),
90+
],
91+
fetching: false,
92+
old,
93+
}
94+
}
95+
default:
96+
throw Error(
97+
`eventType "${action.payload.eventType}" does not exist.`,
98+
)
99+
}
100+
default:
101+
throw Error('Action type does not exist.')
102+
}
103+
}

src/hooks/use-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useContext } from 'react'
33

44
import { Context } from '../context'
55

6-
export const useClient = (): SupabaseClient => {
6+
export function useClient(): SupabaseClient {
77
const client = useContext(Context)
88
if (client === undefined)
99
throw Error('No client has been specified using Provider.')

0 commit comments

Comments
 (0)