├── .eslintrc.cjs
├── .github
└── FUNDING.yml
├── .gitignore
├── README.md
├── jest.config.js
├── package.json
├── src
├── context.tsx
├── hooks
│ ├── useClient.ts
│ ├── useFetcher.ts
│ ├── useQuery.ts
│ ├── useSelect.ts
│ ├── useSelectMaybeSingle.ts
│ ├── useSelectSingle.ts
│ └── useSession.ts
├── index.ts
├── query.ts
└── types.ts
├── tsconfig.eslint.json
├── tsconfig.json
└── yarn.lock
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['airbnb-typescript'],
3 | parserOptions: {
4 | project: './tsconfig.eslint.json',
5 | },
6 | rules: {
7 | 'react/jsx-props-no-spreading': 0,
8 | 'react/prop-types': 0,
9 | '@typescript-eslint/comma-dangle': 0,
10 | 'import/no-extraneous-dependencies': 0,
11 | },
12 | overrides: [{
13 | files: ['.eslintrc.cjs'],
14 | rules: {
15 | 'import/no-extraneous-dependencies': 0,
16 | },
17 | }],
18 | };
19 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [alfredosalzillo]
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | /yarn.lock
4 | /dist/
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # supabase-swr
2 |
3 | A React library to use [supabase-js](https://github.com/supabase/supabase-js) with [swr](https://github.com/vercel/swr).
4 |
5 | ## Install
6 |
7 | Using npm.
8 |
9 | ```shell
10 | npm install supabase-swr supabase-js swr
11 | ```
12 |
13 | Using yarn.
14 |
15 | ```shell
16 | yarn add supabase-swr supabase-js swr
17 | ```
18 |
19 | ## Usage
20 |
21 | Crate a supabase client and pass it to the `SwrSupabaseContext.Provider`.
22 |
23 | ```typescript jsx
24 | import { createClient } from 'supabase-js';
25 | import { SwrSupabaseContext } from 'supabase-swr';
26 |
27 | const client = createClient('https://xyzcompany.supabase.co', 'public-anon-key');
28 |
29 | function App() {
30 | return (
31 |
32 |
33 |
34 | )
35 | }
36 | ```
37 |
38 | Now you can use in any component the api of supabase-swr.
39 |
40 | ```typescript jsx
41 | import React from 'react';
42 | import { useClient, useSelect, useQuery } from 'supabase-swr';
43 |
44 | type Todo = {
45 | id: string,
46 | name: string,
47 | created_at: string,
48 | };
49 |
50 | const Todos = () => {
51 | const todosQuery = useQuery('todos', {
52 | filter: (query) => query.order('created_at', { ascending: false }),
53 | }, []);
54 | const {
55 | data: {
56 | data: todos,
57 | },
58 | mutate,
59 | } = useSelect(todosQuery, {
60 | // any swr config
61 | revalidateOnMount: true,
62 | suspense: true,
63 | });
64 | return (
65 |
66 | {todos.map((todo: Todo) => (
67 | -
68 | {todo.name}
69 |
70 | ))}
71 |
72 | );
73 | }
74 | ```
75 |
76 | ## References
77 |
78 | - [supabase-js](https://github.com/supabase/supabase-js)
79 | - [swr](https://github.com/vercel/swr)
80 |
81 | ## API
82 |
83 | ### hooks
84 |
85 | #### useClient
86 |
87 | Retrieve the supabase-js client instance provided to `SwrSupabaseContext.Provider`.
88 |
89 | ```typescript jsx
90 | export default function (props) {
91 | const client = useClient();
92 | // ...
93 | return (<>...>)
94 | }
95 | ```
96 |
97 | #### useQuery
98 |
99 | Create a `Query` to use with `useSelect` and other hooks.
100 | The created query is also a [swr](https://github.com/vercel/swr) key and can be used with [mutate](https://swr.vercel.app/docs/mutation).
101 |
102 | ```typescript jsx
103 | type Todo = {}
104 |
105 | export default function (props) {
106 | const query = useQuery('todos', {
107 | // the filter ro apply to the query
108 | filter: (q) => q.order('created_at', { ascending: false }),
109 | head: false,
110 | count: 'exact',
111 | });
112 | const {
113 | data
114 | } = useSelect(selectKey, {
115 | // swr config here
116 | });
117 | // ...
118 | return (<>>)
119 | }
120 | ```
121 |
122 | #### useSelect
123 |
124 | Retrieve the `table` data requested.
125 | Return and SwrResponse.
126 |
127 | ```typescript jsx
128 | type Todo = {}
129 |
130 | export default function (props) {
131 | const query = useQuery('todos', {
132 | // the filter ro apply to the query
133 | filter: (q) => q.order('created_at', { ascending: false }),
134 | head: false,
135 | count: 'exact',
136 | });
137 | const {
138 | data
139 | } = useSelect(query, {
140 | // swr config here
141 | });
142 | // ...
143 | return (<>>)
144 | }
145 | ```
146 |
147 | ### useSession
148 |
149 | Subscribe to `authStateChange` event and always return the current session.
150 | Useful to use inside component that need to change when the user sign-in or sign-out.
151 |
152 | ```typescript jsx
153 | export default function (props) {
154 | const session = useSession();
155 | if (!session) return <>Need to sign-in to access this feature>
156 | return (<>...>)
157 | }
158 | ```
159 | ## createQuery
160 |
161 | Create a global `Query` to use with `useSelect` and other hooks.
162 | The created query is also a [swr](https://github.com/vercel/swr) key and can be used with [mutate](https://swr.vercel.app/docs/mutation).
163 |
164 | ```typescript jsx
165 | import { useSWRConfig } from 'swr';
166 | import { useState } from 'react';
167 | import { createClient } from 'supabase-js';
168 | import { SwrSupabaseContext, useSelect, createQuery } from 'supabase-swr';
169 |
170 | const client = createClient('https://xyzcompany.supabase.co', 'public-anon-key');
171 |
172 | type Todo = {
173 | id: string,
174 | name: string,
175 | created_at: string,
176 | }
177 |
178 | const todosQuery = createQuery('todos', {
179 | columns: '*',
180 | // the filter ro apply to the query
181 | filter: (q) => q.order('created_at', { ascending: false }),
182 | })
183 |
184 | function AddTodoForm() {
185 | const { mutate } = useSWRConfig()
186 | const client = useClient()
187 | const [todoName, setTodoName] = useState('')
188 | const addTodo = () => {
189 | client.from('todos').insert({
190 | name: todoName,
191 | }).then(() => {
192 | // update the todosQuery inside the TodosList
193 | mutate(todosQuery)
194 | setTodoName('')
195 | })
196 | }
197 | return (
198 |
204 | )
205 | }
206 |
207 | function TodosList() {
208 | const {
209 | data: {
210 | data: todos,
211 | },
212 | } = useSelect(todosQuery, {
213 | // swr config here
214 | });
215 | // ...
216 | return (
217 |
218 | {todos.map((todo: Todo) => (
219 | -
220 | {todo.name}
221 |
222 | ))}
223 |
224 | );
225 | }
226 |
227 | export default function App() {
228 | return (
229 |
230 |
231 |
232 |
233 | )
234 | }
235 | ```
236 |
237 | ---
238 |
239 | Inspired by [react-supabase](https://github.com/tmm/react-supabase).
240 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | transform: { '.+\\.ts$': 'ts-jest', '.+\\.tsx$': 'ts-jest' },
3 | testEnvironment: 'jsdom',
4 | testRegex: '/.*\\.(test|spec)?\\.(ts|tsx)$',
5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "supabase-swr",
3 | "version": "1.0.0-alpha.5",
4 | "description": "A component library to use https://github.com/supabase/supabase-js with https://github.com/vercel/swr",
5 | "keywords": [
6 | "supabase",
7 | "react",
8 | "swr"
9 | ],
10 | "repository": "https://github.com/alfredosalzillo/supabase-swr",
11 | "author": "alfredo.salzillo ",
12 | "license": "MIT",
13 | "private": false,
14 | "type": "module",
15 | "source": "src/index.ts",
16 | "main": "dist/index.cjs",
17 | "module": "dist/index.es.js",
18 | "umd:main": "dist/index.umd.js",
19 | "types": "dist/index.d.ts",
20 | "files": [
21 | "dist",
22 | "README.md",
23 | "package.json"
24 | ],
25 | "devDependencies": {
26 | "@babel/preset-env": "7.5.5",
27 | "@supabase/supabase-js": "1.35.7",
28 | "@testing-library/jest-dom": "^5.14.1",
29 | "@testing-library/react": "^12.0.0",
30 | "@testing-library/react-hooks": "^7.0.1",
31 | "@types/jest": "^27.0.1",
32 | "@types/react": "^17.0.17",
33 | "@types/react-dom": "^17.0.9",
34 | "@typescript-eslint/eslint-plugin": "4.17.0",
35 | "@typescript-eslint/parser": "^4.29.3",
36 | "babel-eslint": "10.0.2",
37 | "babel-preset-env": "1.7.0",
38 | "bundlesize": "0.17.2",
39 | "documentation": "12.0.3",
40 | "eslint": "^7.32.0",
41 | "eslint-config-airbnb-typescript": "^12.3.1",
42 | "eslint-plugin-import": "^2.22.1",
43 | "eslint-plugin-jsx-a11y": "^6.3.1",
44 | "eslint-plugin-react": "^7.20.3",
45 | "eslint-plugin-react-hooks": "^4.0.8",
46 | "jest": "^27.0.6",
47 | "jest-fetch-mock": "^3.0.3",
48 | "microbundle": "^0.13.3",
49 | "react": "17.0.2",
50 | "react-dom": "16.9.0",
51 | "swr": "1.3.0",
52 | "ts-jest": "^27.0.5",
53 | "typescript": "^4.3.5"
54 | },
55 | "peerDependencies": {
56 | "@supabase/supabase-js": "^1.30.0",
57 | "react": "^17.0.2 | ^18.0.0",
58 | "swr": "^1.3.0"
59 | },
60 | "scripts": {
61 | "dev": "microbundle watch --jsx React.createElement --tsconfig tsconfig.json",
62 | "build": "microbundle --jsx React.createElement --tsconfig tsconfig.json",
63 | "lint": "eslint -c .eslintrc.cjs ./src/*.ts* --fix",
64 | "prepare": "rm -rf dist && yarn build",
65 | "test": "jest --config jest.config.js",
66 | "test:watch": "jest --watch",
67 | "coverage": "jest --coverage",
68 | "release": "yarn -s prepare && npm publish",
69 | "release:alpha": "yarn -s prepare && npm publish --tag alpha"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | import { SupabaseClient } from '@supabase/supabase-js';
3 |
4 | const Context = createContext(null);
5 |
6 | export default Context;
7 |
--------------------------------------------------------------------------------
/src/hooks/useClient.ts:
--------------------------------------------------------------------------------
1 | import { SupabaseClient } from '@supabase/supabase-js';
2 | import { useContext } from 'react';
3 | import Context from '../context';
4 |
5 | const useClient = (): SupabaseClient => {
6 | const client = useContext(Context);
7 | if (!client) throw new Error('supabase client instance required');
8 | return client;
9 | };
10 |
11 | export default useClient;
12 |
--------------------------------------------------------------------------------
/src/hooks/useFetcher.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { SupabaseClient } from '@supabase/supabase-js';
3 | import { PostgrestSingleSuccessResponse, PostgrestSuccessResponse } from '../types';
4 | import useClient from './useClient';
5 | import { QueryConfig } from '../query';
6 |
7 | export type Fetcher = (
8 | table: string,
9 | config: QueryConfig,
10 | ) => Promise>;
11 | export type FetcherSingle = (
12 | table: string,
13 | config: QueryConfig,
14 | ) => Promise>;
15 |
16 | type FetcherType = 'multiple' | 'single' | 'maybeSingle' | 'csv';
17 |
18 | function createFetcher(client: SupabaseClient, type: 'multiple'): Fetcher;
19 | function createFetcher(client: SupabaseClient, type: 'single'): FetcherSingle;
20 | function createFetcher(client: SupabaseClient, type: 'maybeSingle'): FetcherSingle;
21 | function createFetcher(client: SupabaseClient, type: 'csv'): FetcherSingle;
22 | function createFetcher(client: SupabaseClient, type: FetcherType) {
23 | return async (
24 | table: string, config: QueryConfig,
25 | ) => {
26 | const select = client.from(table).select(config.columns, {
27 | count: config.count,
28 | head: config.head,
29 | });
30 | const hasFilter = typeof config.filter === 'function';
31 | const query = hasFilter ? config.filter(select) : select;
32 | switch (type) {
33 | default:
34 | case 'multiple':
35 | return query.throwOnError();
36 | case 'single':
37 | // @ts-ignore
38 | return query.throwOnError().single();
39 | case 'maybeSingle':
40 | // @ts-ignore
41 | return query.throwOnError().maybeSigle();
42 | case 'csv':
43 | // @ts-ignore
44 | return query.throwOnError().csv();
45 | }
46 | };
47 | }
48 |
49 | function useFetcher(type: 'multiple'): Fetcher;
50 | function useFetcher(type: 'single'): FetcherSingle;
51 | function useFetcher(type: 'maybeSingle'): FetcherSingle;
52 | function useFetcher(type: 'csv'): FetcherSingle;
53 | function useFetcher(type: FetcherType) {
54 | const client = useClient();
55 | return useMemo(() => createFetcher(client, type as any), [client, type]) as any;
56 | }
57 |
58 | export default useFetcher;
59 |
--------------------------------------------------------------------------------
/src/hooks/useQuery.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { createQuery, Query, QueryConfig } from '../query';
3 |
4 | const useQuery = (
5 | table: string,
6 | config: QueryConfig,
7 | deps: any[],
8 | ): Query => useMemo(() => createQuery(table, config), deps);
9 |
10 | export default useQuery;
11 |
--------------------------------------------------------------------------------
/src/hooks/useSelect.ts:
--------------------------------------------------------------------------------
1 | import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
2 | import { PostgrestError, PostgrestSuccessResponse } from '../types';
3 | import useFetcher from './useFetcher';
4 | import { Query } from '../query';
5 |
6 | const useSelect = (
7 | query: Query, swrConfig: Omit,
8 | ): SWRResponse, PostgrestError> => {
9 | const fetcher = useFetcher('multiple');
10 | return useSWR(query, fetcher, swrConfig);
11 | };
12 |
13 | export default useSelect;
14 |
--------------------------------------------------------------------------------
/src/hooks/useSelectMaybeSingle.ts:
--------------------------------------------------------------------------------
1 | import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
2 | import { PostgrestError, PostgrestSuccessResponse } from '../types';
3 | import useFetcher from './useFetcher';
4 | import { Query } from '../query';
5 |
6 | const useSelectMaybeSingle = (
7 | query: Query, swrConfig: Omit,
8 | ): SWRResponse, PostgrestError> => {
9 | const fetcher = useFetcher('maybeSingle');
10 | return useSWR(query, fetcher, swrConfig);
11 | };
12 |
13 | export default useSelectMaybeSingle;
14 |
--------------------------------------------------------------------------------
/src/hooks/useSelectSingle.ts:
--------------------------------------------------------------------------------
1 | import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
2 | import { PostgrestError, PostgrestSingleSuccessResponse } from '../types';
3 | import { Query } from '../query';
4 | import useFetcher from './useFetcher';
5 |
6 | const useSelectSingle = (
7 | query: Query, swrConfig: Omit,
8 | ): SWRResponse, PostgrestError> => {
9 | const fetcher = useFetcher('single');
10 | return useSWR(query, fetcher, swrConfig);
11 | };
12 |
13 | export default useSelectSingle;
14 |
--------------------------------------------------------------------------------
/src/hooks/useSession.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Session } from '@supabase/supabase-js';
3 | import useClient from './useClient';
4 |
5 | const useSession = (): Session | null => {
6 | const client = useClient();
7 | const [session, setSession] = useState(client.auth.session());
8 | useEffect(() => {
9 | const {
10 | data: subscription,
11 | } = client.auth.onAuthStateChange((_, newSession) => {
12 | setSession(newSession);
13 | });
14 | return () => subscription.unsubscribe();
15 | }, [setSession, client]);
16 | return session;
17 | };
18 |
19 | export default useSession;
20 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './query';
3 | export { default as SwrSupabaseContext } from './context';
4 | export { default as useClient } from './hooks/useClient';
5 | export { default as useQuery } from './hooks/useQuery';
6 | export { default as useSelect } from './hooks/useSelect';
7 | export { default as useSelectSingle } from './hooks/useSelectSingle';
8 | export { default as useSelectMaybeSingle } from './hooks/useSelectMaybeSingle';
9 | export { default as useSession } from './hooks/useSession';
10 |
--------------------------------------------------------------------------------
/src/query.ts:
--------------------------------------------------------------------------------
1 | import { Count, Filter } from './types';
2 |
3 | export type QueryConfig = {
4 | columns?: string,
5 | filter?: Filter,
6 | count?: Count,
7 | head?: boolean,
8 | };
9 | export type Query = [string, QueryConfig];
10 | export const createQuery = (
11 | table: string,
12 | config: QueryConfig,
13 | ): Query => [table, config];
14 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { PostgrestFilterBuilder } from '@supabase/postgrest-js';
2 |
3 | export type Count = 'exact' | 'planned' | 'estimated';
4 |
5 | export type Filter = (
6 | query: PostgrestFilterBuilder,
7 | ) => PostgrestFilterBuilder;
8 |
9 | export type PostgrestError = {
10 | message: string
11 | details: string
12 | hint: string
13 | code: string
14 | };
15 |
16 | export type PostgrestSuccessResponse = {
17 | status?: number
18 | statusText?: string
19 | data: Data[],
20 | count: number | null
21 | };
22 |
23 | export type PostgrestSingleSuccessResponse = {
24 | status?: number
25 | statusText?: string
26 | data: Data,
27 | count: number | null
28 | };
29 | export type Returning = 'minimal' | 'representation';
30 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2019",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "strictNullChecks": false,
22 | "jsx": "react"
23 | },
24 | "include": [
25 | "src",
26 | "test",
27 | ".eslintrc.cjs",
28 | "jest.config.js"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2019",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "strictNullChecks": false,
22 | "jsx": "react"
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------