├── .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 | 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 |
199 | setTodoName(e.target.value)} /> 200 | 203 |
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 | --------------------------------------------------------------------------------