Skip to content

Commit 800a190

Browse files
authored
Add useUpsert hook (#6)
1 parent 3e7a1ca commit 800a190

File tree

6 files changed

+149
-1
lines changed

6 files changed

+149
-1
lines changed

docs/pages/documentation/data/meta.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"use-filter": "useFilter",
44
"use-insert": "useInsert",
55
"use-select": "useSelect",
6-
"use-update": "useUpdate"
6+
"use-update": "useUpdate",
7+
"use-upsert": "useUpsert"
78
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# useUpsert
2+
3+
Performs `INSERT` or `UPDATE` on table.
4+
5+
```js highlight=4
6+
import { useUpsert } from 'react-supabase'
7+
8+
function Page() {
9+
const [{ count, data, error, fetching }, execute] = useUpsert('users')
10+
11+
async function onClickMarkAllComplete() {
12+
const { count, data, error } = await execute(
13+
{ completed: true },
14+
{ onConflict: 'username' },
15+
(query) => query.eq('completed', false),
16+
)
17+
}
18+
19+
return ...
20+
}
21+
```
22+
23+
## Notes
24+
25+
- By specifying the `onConflict` option, you can make `UPSERT` work on a column(s) that has a `UNIQUE` constraint.
26+
- Primary keys should to be included in the data payload in order for an update to work correctly.
27+
- Primary keys must be natural, not surrogate. There are however, workarounds for surrogate primary keys.
28+
- Param `filter` makes sense only when operation is update
29+
- Upsert supports sending array of elements, just like `useInsert`
30+
31+
## Passing options
32+
33+
During hook initialization:
34+
35+
```js
36+
const [{ count, data, error, fetching }, execute] = useUpsert('users', {
37+
filter: (query) => query.eq('completed', false),
38+
options: {
39+
returning: 'represenation',
40+
onConflict: 'username',
41+
count: 'exact',
42+
},
43+
})
44+
```
45+
46+
Or execute function:
47+
48+
```js
49+
const { count, data, error } = await execute(
50+
{ completed: true },
51+
{
52+
count: 'estimated',
53+
onConflict: 'username',
54+
returning: 'minimal',
55+
},
56+
(query) => query.eq('completed', false),
57+
)
58+
```

src/hooks/data/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './use-filter'
33
export * from './use-insert'
44
export * from './use-select'
55
export * from './use-update'
6+
export * from './use-upsert'

src/hooks/data/use-upsert.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useCallback, useEffect, useRef, useState } from 'react'
2+
3+
import { Count, Filter, PostgrestError, Returning } from '../../types'
4+
import { useClient } from '../use-client'
5+
import { initialState } from './state'
6+
7+
export type UseUpsertState<Data = any> = {
8+
count?: number | null
9+
data?: Data | Data[] | null
10+
error?: PostgrestError | null
11+
fetching: boolean
12+
}
13+
14+
export type UseUpsertResponse<Data = any> = [
15+
UseUpsertState<Data>,
16+
(
17+
values: Partial<Data> | Partial<Data>[],
18+
options?: UseUpsertOptions,
19+
filter?: Filter<Data>,
20+
) => Promise<Pick<UseUpsertState<Data>, 'count' | 'data' | 'error'>>,
21+
]
22+
23+
export type UseUpsertOptions = {
24+
count?: null | Count
25+
onConflict?: string
26+
returning?: Returning
27+
}
28+
29+
export type UseUpsertConfig<Data = any> = {
30+
filter?: Filter<Data>
31+
options?: UseUpsertOptions
32+
}
33+
34+
export function useUpsert<Data = any>(
35+
table: string,
36+
config: UseUpsertConfig<Data> = { options: {} },
37+
): UseUpsertResponse<Data> {
38+
const client = useClient()
39+
const isMounted = useRef(false)
40+
const [state, setState] = useState<UseUpsertState>(initialState)
41+
42+
/* eslint-disable react-hooks/exhaustive-deps */
43+
const execute = useCallback(
44+
async (
45+
values: Partial<Data> | Partial<Data>[],
46+
options?: UseUpsertOptions,
47+
filter?: Filter<Data>,
48+
) => {
49+
const refine = filter ?? config.filter
50+
setState({ ...initialState, fetching: true })
51+
const source = client
52+
.from<Data>(table)
53+
.upsert(values, options ?? config.options)
54+
55+
const { count, data, error } = await (refine
56+
? refine(source)
57+
: source)
58+
59+
const res = { count, data, error }
60+
if (isMounted.current) setState({ ...res, fetching: false })
61+
return res
62+
},
63+
[client],
64+
)
65+
/* eslint-enable react-hooks/exhaustive-deps */
66+
67+
useEffect(() => {
68+
isMounted.current = true
69+
return () => {
70+
isMounted.current = false
71+
}
72+
}, [])
73+
74+
return [state, execute]
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`useUpsert should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;

test/hooks/data/use-upsert.test.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { renderHook } from '@testing-library/react-hooks'
2+
3+
import { useUpsert } from '../../../src'
4+
5+
describe('useUpsert', () => {
6+
it('should throw when not inside Provider', () => {
7+
const { result } = renderHook(() => useUpsert('todos'))
8+
expect(() => result.current).toThrowErrorMatchingSnapshot()
9+
})
10+
})

0 commit comments

Comments
 (0)