├── .eslintignore
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── config.yml
├── dependabot.yaml
└── workflows
│ ├── main.yaml
│ └── size.yaml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .lintstagedrc
├── .npmignore
├── .prettierignore
├── .prettierrc
├── .size-limit.json
├── .vim
└── coc-settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs
├── .gitignore
├── README.md
├── bin
│ └── stork.bin
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── documentation
│ │ ├── auth
│ │ │ ├── meta.json
│ │ │ ├── use-auth-state-change.md
│ │ │ ├── use-reset-password.md
│ │ │ ├── use-signin.md
│ │ │ ├── use-signout.md
│ │ │ └── use-signup.md
│ │ ├── data
│ │ │ ├── meta.json
│ │ │ ├── use-delete.md
│ │ │ ├── use-filter.md
│ │ │ ├── use-insert.md
│ │ │ ├── use-select.md
│ │ │ ├── use-update.md
│ │ │ └── use-upsert.md
│ │ ├── meta.json
│ │ ├── provider.md
│ │ ├── realtime
│ │ │ ├── meta.json
│ │ │ ├── use-realtime.md
│ │ │ └── use-subscription.md
│ │ ├── storage
│ │ │ └── meta.json
│ │ └── use-client.md
│ ├── getting-started.md
│ ├── index.md
│ ├── meta.json
│ └── recipes
│ │ ├── meta.json
│ │ └── use-auth.md
├── public
│ └── stork.wasm
├── theme.config.js
├── tsconfig.json
└── yarn.lock
├── jest.config.ts
├── package.json
├── src
├── context.ts
├── hooks
│ ├── auth
│ │ ├── index.ts
│ │ ├── state.ts
│ │ ├── use-auth-state-change.ts
│ │ ├── use-reset-password.ts
│ │ ├── use-signin.ts
│ │ ├── use-signout.ts
│ │ └── use-signup.ts
│ ├── data
│ │ ├── index.ts
│ │ ├── state.ts
│ │ ├── use-delete.ts
│ │ ├── use-filter.ts
│ │ ├── use-insert.ts
│ │ ├── use-select.ts
│ │ ├── use-update.ts
│ │ └── use-upsert.ts
│ ├── index.ts
│ ├── realtime
│ │ ├── index.ts
│ │ ├── use-realtime.ts
│ │ └── use-subscription.ts
│ └── use-client.ts
├── index.ts
└── types.ts
├── test
├── hooks
│ ├── __snapshots__
│ │ └── use-client.test.tsx.snap
│ ├── auth
│ │ ├── __snapshots__
│ │ │ ├── use-auth-state-change.test.tsx.snap
│ │ │ ├── use-reset-password.test.tsx.snap
│ │ │ ├── use-signin.test.tsx.snap
│ │ │ ├── use-signout.test.tsx.snap
│ │ │ └── use-signup.test.tsx.snap
│ │ ├── use-auth-state-change.test.tsx
│ │ ├── use-reset-password.test.tsx
│ │ ├── use-signin.test.tsx
│ │ ├── use-signout.test.tsx
│ │ └── use-signup.test.tsx
│ ├── data
│ │ ├── __snapshots__
│ │ │ ├── use-delete.test.tsx.snap
│ │ │ ├── use-insert.test.tsx.snap
│ │ │ ├── use-select.test.tsx.snap
│ │ │ ├── use-update.test.tsx.snap
│ │ │ └── use-upsert.test.tsx.snap
│ │ ├── use-delete.test.tsx
│ │ ├── use-filter.test.tsx
│ │ ├── use-insert.test.tsx
│ │ ├── use-select.test.tsx
│ │ ├── use-update.test.tsx
│ │ └── use-upsert.test.tsx
│ ├── realtime
│ │ ├── __snapshots__
│ │ │ ├── use-realtime.test.tsx.snap
│ │ │ └── use-subscription.test.tsx.snap
│ │ ├── use-realtime.test.tsx
│ │ └── use-subscription.test.tsx
│ └── use-client.test.tsx
└── utils.tsx
├── tsconfig.json
├── vite.config.ts
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 | docs/
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 8,
6 | "sourceType": "module",
7 | "ecmaFeatures": {
8 | "impliedStrict": true,
9 | "experimentalObjectRestSpread": true
10 | },
11 | "allowImportExportEverywhere": true
12 | },
13 | "plugins": ["@typescript-eslint", "import", "react", "testing-library"],
14 | "extends": [
15 | "eslint:recommended",
16 | "plugin:@typescript-eslint/recommended",
17 | "plugin:import/errors",
18 | "plugin:import/warnings",
19 | "plugin:import/typescript",
20 | "plugin:jest-dom/recommended",
21 | "plugin:testing-library/react",
22 | "plugin:react/recommended",
23 | "plugin:react-hooks/recommended",
24 | "plugin:prettier/recommended",
25 | "prettier"
26 | ],
27 | "rules": {
28 | // `@typescript-eslint`
29 | // https://github.com/typescript-eslint/typescript-eslint
30 | "@typescript-eslint/explicit-module-boundary-types": "off",
31 | "@typescript-eslint/no-explicit-any": "off",
32 | "@typescript-eslint/no-unused-vars": [
33 | 2,
34 | {
35 | "argsIgnorePattern": "^_"
36 | }
37 | ],
38 | "@typescript-eslint/no-var-requires": "off",
39 | // `eslint-plugin-import`
40 | // https://github.com/benmosher/eslint-plugin-import
41 | "import/order": [
42 | "error",
43 | {
44 | "groups": ["external", "internal"],
45 | "newlines-between": "always-and-inside-groups"
46 | }
47 | ],
48 | "sort-imports": [
49 | "warn",
50 | {
51 | "ignoreCase": false,
52 | "ignoreDeclarationSort": true,
53 | "ignoreMemberSort": false
54 | }
55 | ],
56 | // `eslint-plugin-react`
57 | // https://github.com/yannickcr/eslint-plugin-react
58 | "react/display-name": "off",
59 | "react/jsx-boolean-value": ["warn", "never"],
60 | "react/jsx-sort-props": [
61 | "error",
62 | {
63 | "callbacksLast": true
64 | }
65 | ],
66 | "react/jsx-wrap-multilines": "error",
67 | "react/no-array-index-key": "error",
68 | "react/no-multi-comp": "off",
69 | "react/prop-types": "off",
70 | "react/self-closing-comp": "warn"
71 | },
72 | "settings": {
73 | "import/parsers": {
74 | "@typescript-eslint/parser": [".ts", ".tsx"]
75 | },
76 | "import/resolver": {
77 | "typescript": {
78 | "alwaysTryTypes": true
79 | }
80 | },
81 | "react": {
82 | "version": "detect"
83 | }
84 | },
85 | "env": {
86 | "es6": true,
87 | "browser": true,
88 | "node": true,
89 | "jest": true
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a bug report for the `react-supabase` library
4 | ---
5 |
6 | # Bug report
7 |
8 | ## Description / Observed Behavior
9 |
10 | What kind of issues did you encounter?
11 |
12 | ## Expected Behavior
13 |
14 | How did you expect it to behave?
15 |
16 | ## Repro Steps / Code Example
17 |
18 | Or share your code snippet or a [CodeSandbox](https://codesandbox.io) link is also appreciated!
19 |
20 | ## Additional Context
21 |
22 | `react-supabase` version.
23 | Add any other context about the problem here.
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Ask Question
4 | url: https://github.com/tmm/react-supabase/discussions
5 | about: Ask questions and discuss with other community members
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'npm'
4 | directory: '/'
5 | schedule:
6 | interval: 'monthly'
7 | open-pull-requests-limit: 0
8 |
9 | - package-ecosystem: 'github-actions'
10 | directory: '/'
11 | schedule:
12 | interval: 'monthly'
13 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: Main
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | lint:
10 | name: Lint
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | node-version: [14]
15 | steps:
16 | - name: Check out
17 | uses: actions/checkout@v2
18 |
19 | - name: Set up Node ${{ matrix.node-version }}
20 | uses: actions/setup-node@v2
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 |
24 | - name: Load yarn cache
25 | uses: actions/cache@v2.1.7
26 | id: yarn-cache
27 | with:
28 | path: ./node_modules
29 | key: ${{ runner.os }}-yarn-cache-${{ hashFiles('**/yarn.lock') }}
30 | restore-keys: ${{ runner.os }}-yarn-cache-
31 |
32 | - name: Install dependencies
33 | if: steps.yarn-cache.outputs.cache-hit != 'true'
34 | run: yarn --frozen-lockfile
35 |
36 | - name: Lint code
37 | run: yarn lint
38 |
39 | - name: Check types
40 | run: yarn lint:types
41 |
42 | - name: Format other files
43 | run: yarn lint:format --check
44 |
45 | test:
46 | name: Test
47 | runs-on: ubuntu-latest
48 | strategy:
49 | matrix:
50 | node-version: [14]
51 | steps:
52 | - name: Check out
53 | uses: actions/checkout@v2
54 |
55 | - name: Set up Node ${{ matrix.node-version }}
56 | uses: actions/setup-node@v2
57 | with:
58 | node-version: ${{ matrix.node-version }}
59 |
60 | - name: Load yarn cache
61 | uses: actions/cache@v2.1.7
62 | id: yarn-cache
63 | with:
64 | path: ./node_modules
65 | key: ${{ runner.os }}-yarn-cache-${{ hashFiles('**/yarn.lock') }}
66 | restore-keys: ${{ runner.os }}-yarn-cache-
67 |
68 | - name: Install dependencies
69 | if: steps.yarn-cache.outputs.cache-hit != 'true'
70 | run: yarn --frozen-lockfile
71 |
72 | - name: Run tests
73 | run: yarn test
74 |
--------------------------------------------------------------------------------
/.github/workflows/size.yaml:
--------------------------------------------------------------------------------
1 | name: Size
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | size:
8 | name: Size
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | node-version: [14]
13 | steps:
14 | - name: Check out
15 | uses: actions/checkout@v2
16 |
17 | - name: Set up Node ${{ matrix.node-version }}
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: ${{ matrix.node-version }}
21 |
22 | - name: Load yarn cache
23 | uses: actions/cache@v2.1.7
24 | id: yarn-cache
25 | with:
26 | path: ./node_modules
27 | key: ${{ runner.os }}-yarn-cache-${{ hashFiles('**/yarn.lock') }}
28 | restore-keys: ${{ runner.os }}-yarn-cache-
29 |
30 | - name: Install dependencies
31 | if: steps.yarn-cache.outputs.cache-hit != 'true'
32 | run: yarn --frozen-lockfile
33 |
34 | - uses: andresz1/size-limit-action@v1.5.1
35 | with:
36 | github_token: ${{ secrets.GITHUB_TOKEN }}
37 | skip_step: install
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .eslintcache
3 | dist/
4 | node_modules/
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{js,ts,tsx}": ["yarn lint:fix"],
3 | "*.{json,md,yaml}": ["yarn lint:format"],
4 | }
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.tgz
3 | .env
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | yarn.lock
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "endOfLine": "lf",
4 | "printWidth": 80,
5 | "semi": false,
6 | "singleQuote": true,
7 | "tabWidth": 4,
8 | "trailingComma": "all",
9 | "overrides": [
10 | {
11 | "files": "*.md",
12 | "options": {
13 | "tabWidth": 2
14 | }
15 | },
16 | {
17 | "files": "*.yaml",
18 | "options": {
19 | "tabWidth": 2
20 | }
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/.size-limit.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "path": "dist/index.cjs.js",
4 | "limit": "5 KB"
5 | },
6 | {
7 | "path": "dist/index.es.js",
8 | "limit": "5 KB"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/.vim/coc-settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.autoFixOnSave": true,
3 | "eslint.probe": [
4 | "css",
5 | "javascript",
6 | "javascriptreact",
7 | "json",
8 | "markdown",
9 | "typescript",
10 | "typescriptreact"
11 | ],
12 | "eslint.packageManager": "yarn"
13 | }
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Guidelines
2 |
3 | Thank you for reading this guide and we appreciate any contribution.
4 |
5 | ## Ask a Question
6 |
7 | You can use the repository's [Discussions](https://github.com/tmm/react-supabase/discussions) page to ask any questions, post feedback or share your experience on how you use this library.
8 |
9 | ## Report a Bug
10 |
11 | Whenever you find something which is not working properly, please first search the repository's [Issues](https://github.com/tmm/react-supabase/issues) page and make sure it's not reported by someone else already.
12 |
13 | If not, feel free to open an issue with the detailed description of the problem and the expected behavior. And reproduction (for example a [CodeSandbox](https://codesandbox.io) link) will be extremely helpful.
14 |
15 | ## Request for a New Feature
16 |
17 | For new features, it would be great to have some discussions from the community before starting working on it. You can either create an issue (if there isn't one) or post a thread in the [Discussions](https://github.com/tmm/react-supabase/discussions) page to describe the feature that you want to have.
18 |
19 | If possible, you can add other additional context like how this feature can be implemented technically, what other alternative solutions we can have, etc.
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tom Meagher
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 |
3 | `react-supabase` is a React Hooks library for [Supabase](https://supabase.io).
4 |
5 | Visit the [docs site](https://react-supabase.vercel.app) for more info.
6 |
7 |
8 |
9 | ## Installation
10 |
11 | ```
12 | yarn add react-supabase @supabase/supabase-js
13 | # or
14 | npm install --save react-supabase @supabase/supabase-js
15 | ```
16 |
17 |
18 |
19 | ## Quick Start
20 |
21 | Create a Supabase client and pass it to the `Provider`:
22 |
23 | ```tsx
24 | import { createClient } from '@supabase/supabase-js'
25 | import { Provider } from 'react-supabase'
26 |
27 | const client = createClient('https://xyzcompany.supabase.co', 'public-anon-key')
28 |
29 | const App = () => (
30 |
31 |
32 |
33 | )
34 | ```
35 |
36 | Now every component inside and under the `Provider` can use the Supabase client and hooks:
37 |
38 | ```tsx
39 | import { useRealtime } from 'react-supabase'
40 |
41 | const Todos = () => {
42 | const [result, reexecute] = useRealtime('todos')
43 |
44 | const { data, fetching, error } = result
45 |
46 | if (fetching) return
Loading...
47 | if (error) return Oh no... {error.message}
48 |
49 | return (
50 |
51 | {data.map((todo) => (
52 | - {todo.title}
53 | ))}
54 |
55 | )
56 | }
57 | ```
58 |
59 |
60 |
61 | ## License
62 |
63 | The MIT License.
64 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | .next/*
2 | public/*.st
3 | public/*.toml
4 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | yarn
3 | yarn dev
4 | ```
5 |
--------------------------------------------------------------------------------
/docs/bin/stork.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/react-supabase/fcc4eac38a6426e03863bd159d00c8cf0b5cb31d/docs/bin/stork.bin
--------------------------------------------------------------------------------
/docs/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/docs/next.config.js:
--------------------------------------------------------------------------------
1 | const withNextra = require('nextra')({
2 | theme: 'nextra-theme-docs',
3 | themeConfig: './theme.config.js',
4 | stork: true,
5 | })
6 | module.exports = withNextra()
7 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "private": true,
7 | "scripts": {
8 | "dev": "next dev",
9 | "build": "STORK_PATH=$(pwd)/bin/stork.bin next build",
10 | "start": "next start"
11 | },
12 | "dependencies": {
13 | "next": "^10.2.0",
14 | "nextra": "^0.4.4",
15 | "nextra-theme-docs": "^1.1.6",
16 | "react": "^17.0.2",
17 | "react-dom": "^17.0.2"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^17.0.4",
21 | "@types/react-dom": "^17.0.3",
22 | "typescript": "^4.2.4"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/docs/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import 'nextra-theme-docs/style.css'
2 |
3 | export default function Nextra({ Component, pageProps }) {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/docs/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Head,
3 | Html,
4 | Main,
5 | default as NextDocument,
6 | NextScript,
7 | } from 'next/document'
8 |
9 | class Document extends NextDocument {
10 | render() {
11 | return (
12 |
13 |
15 |
16 |
17 |
18 |
23 |
30 |
31 |
32 | )
33 | }
34 | }
35 |
36 | export default Document
37 |
--------------------------------------------------------------------------------
/docs/pages/documentation/auth/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "use-auth-state-change": "useAuthStateChange",
3 | "use-reset-password": "useResetPassword",
4 | "use-signin": "useSignIn",
5 | "use-signout": "useSignOut",
6 | "use-signup": "useSignUp"
7 | }
8 |
--------------------------------------------------------------------------------
/docs/pages/documentation/auth/use-auth-state-change.md:
--------------------------------------------------------------------------------
1 | # useAuthStateChange
2 |
3 | Receive a notification every time an auth event happens. Composed in the [`useAuth` recipe](/recipes/use-auth).
4 |
5 | ```tsx highlight=4,5,6
6 | import { useAuthStateChange } from 'react-supabase'
7 |
8 | function Page() {
9 | useAuthStateChange((event, session) => {
10 | console.log(`Supabase auth event: ${event}`, session)
11 | })
12 |
13 | return ...
14 | }
15 | ```
16 |
17 | Note: Auth listener is automatically cleaned up during the hook’s [cleanup phase](https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup).
18 |
--------------------------------------------------------------------------------
/docs/pages/documentation/auth/use-reset-password.md:
--------------------------------------------------------------------------------
1 | # useResetPassword
2 |
3 | Sends reset request to email address.
4 |
5 | ```tsx highlight=4
6 | import { useResetPassword } from 'react-supabase'
7 |
8 | function Page() {
9 | const [{ error, fetching }, resetPassword] = useResetPassword()
10 |
11 | async function onClickResetPassword() {
12 | const { error } = await resetPassword('user@example.com')
13 | }
14 |
15 | if (error) return Error sending email
16 | if (fetching) return Sending reset email
17 |
18 | return ...
19 | }
20 | ```
21 |
22 | ## Passing options
23 |
24 | During hook initialization:
25 |
26 | ```tsx
27 | const [{ error, fetching }, resetPassword] = useResetPassword({
28 | options: {
29 | redirectTo: 'https://example.com/welcome',
30 | },
31 | })
32 | ```
33 |
34 | Or execute function:
35 |
36 | ```tsx
37 | const { error } = await resetPassword('user@example.com', {
38 | redirectTo: 'https://example.com/reset',
39 | })
40 | ```
41 |
--------------------------------------------------------------------------------
/docs/pages/documentation/auth/use-signin.md:
--------------------------------------------------------------------------------
1 | # useSignIn
2 |
3 | Log in existing user, or login via a third-party provider.
4 |
5 | ```tsx highlight=4
6 | import { useSignIn } from 'react-supabase'
7 |
8 | function Page() {
9 | const [{ error, fetching, session, user }, signIn] = useSignIn()
10 |
11 | async function onClickSignIn() {
12 | const { error, session, user } = await signIn({
13 | email: 'user@example.com',
14 | password: 'foobarbaz',
15 | })
16 | }
17 |
18 | if (error) return Error signing in
19 | if (fetching) return Signing in
20 | if (user) return Logged in
21 |
22 | return ...
23 | }
24 | ```
25 |
26 | ## Passing options
27 |
28 | During hook initialization:
29 |
30 | ```tsx
31 | const [{ error, fetching, session, user }, signIn] = useSignIn({
32 | options: {
33 | redirectTo: 'https://example.com/dashboard',
34 | },
35 | })
36 | ```
37 |
38 | Or the execute function:
39 |
40 | ```tsx
41 | const { error, session, user } = await signIn(
42 | {
43 | email: 'user@example.com',
44 | password: 'foobarbaz',
45 | },
46 | {
47 | redirectTo: 'https://example.com/account',
48 | },
49 | )
50 | ```
51 |
52 | ## Magic links
53 |
54 | Omit password from the execute function:
55 |
56 | ```tsx
57 | const { error, session, user } = await signIn({ email: 'user@example.com' })
58 | ```
59 |
60 | ## Third-party providers
61 |
62 | Either pass a provider (and scopes) during hook initialization:
63 |
64 | ```tsx
65 | const [{ error, fetching, user, session }, signIn] = useSignIn({
66 | provider: 'github',
67 | options: {
68 | scopes: 'repo gist notifications',
69 | },
70 | })
71 | ```
72 |
73 | Or execute function:
74 |
75 | ```tsx
76 | const { error, session, user } = await signIn(
77 | { provider: 'github' },
78 | { scopes: 'repo gist notifications' },
79 | )
80 | ```
81 |
--------------------------------------------------------------------------------
/docs/pages/documentation/auth/use-signout.md:
--------------------------------------------------------------------------------
1 | # useSignOut
2 |
3 | Remove logged in user and trigger a `SIGNED_OUT` event.
4 |
5 | ```tsx highlight=4
6 | import { useSignOut } from 'react-supabase'
7 |
8 | function Page() {
9 | const [{ error, fetching }, signOut] = useSignOut()
10 |
11 | async function onClickSignOut() {
12 | const { error } = await signOut()
13 | }
14 |
15 | if (error) return Error signing out
16 | if (fetching) return Signing out
17 |
18 | return ...
19 | }
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/pages/documentation/auth/use-signup.md:
--------------------------------------------------------------------------------
1 | # useSignUp
2 |
3 | Creates new user.
4 |
5 | ```tsx highlight=4
6 | import { useSignUp } from 'react-supabase'
7 |
8 | function Page() {
9 | const [{ error, fetching, session, user }, signUp] = useSignUp()
10 |
11 | async function onClickSignUp() {
12 | const { error, session, user } = await signUp({
13 | email: 'user@example.com',
14 | password: 'foobarbaz',
15 | })
16 | }
17 |
18 | if (error) return Error signing up
19 | if (fetching) return Signing up
20 | if (user) return Welcome user
21 |
22 | return ...
23 | }
24 | ```
25 |
26 | ## Passing options
27 |
28 | During hook initialization:
29 |
30 | ```tsx
31 | const [{ error, fetching, session, user }, signUp] = useSignUp({
32 | options: {
33 | redirectTo: 'https://example.com/dashboard',
34 | },
35 | })
36 | ```
37 |
38 | Or execute function:
39 |
40 | ```tsx
41 | const { error, session, user } = await signUp(
42 | {
43 | email: 'user@example.com',
44 | password: 'foobarbaz',
45 | },
46 | {
47 | redirectTo: 'https://example.com/welcome',
48 | },
49 | )
50 | ```
51 |
--------------------------------------------------------------------------------
/docs/pages/documentation/data/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "use-delete": "useDelete",
3 | "use-filter": "useFilter",
4 | "use-insert": "useInsert",
5 | "use-select": "useSelect",
6 | "use-update": "useUpdate",
7 | "use-upsert": "useUpsert"
8 | }
9 |
--------------------------------------------------------------------------------
/docs/pages/documentation/data/use-delete.md:
--------------------------------------------------------------------------------
1 | # useDelete
2 |
3 | Performs DELETE on table.
4 |
5 | ```tsx highlight=4
6 | import { useDelete } from 'react-supabase'
7 |
8 | function Page() {
9 | const [{ count, data, error, fetching }, execute] = useDelete('todos')
10 |
11 | async function onClickDelete(id) {
12 | const { count, data, error } = await deleteTodos(
13 | (query) => query.eq('id', id),
14 | )
15 | }
16 |
17 | return ...
18 | }
19 | ```
20 |
21 | Throws error during execute if a filter is not passed during hook initialization or execute method.
22 |
23 | ## Passing options
24 |
25 | During hook initialization:
26 |
27 | ```tsx
28 | const [{ count, data, error, fetching }, execute] = useDelete('todos', {
29 | filter: (query) => query.eq('status', 'completed'),
30 | options: {
31 | returning: 'represenation',
32 | count: 'exact',
33 | },
34 | })
35 | ```
36 |
37 | Or execute function:
38 |
39 | ```tsx
40 | const { count, data, error } = await execute((query) => query.eq('id', id), {
41 | returning: 'minimal',
42 | count: 'estimated',
43 | })
44 | ```
45 |
--------------------------------------------------------------------------------
/docs/pages/documentation/data/use-filter.md:
--------------------------------------------------------------------------------
1 | # useFilter
2 |
3 | Creates dynamic filter for using with other hooks.
4 |
5 | ```tsx highlight=4,5,6,7,8,9,10,11
6 | import { useFilter, useSelect } from 'react-supabase'
7 |
8 | function Page() {
9 | const filter = useFilter(
10 | (query) =>
11 | query
12 | .eq('status', status)
13 | .textSearch('name', `'exercise' & 'shopping'`)
14 | .limit(10),
15 | [status],
16 | )
17 | // Pass filter to other hooks
18 | const [{ data }] = useSelect('todos', { filter })
19 |
20 | return ...
21 | }
22 | ```
23 |
24 | For an example, see [`useSelect` "Dynamic Filtering"](/documentation/data/use-select#dynamic-filtering).
25 |
--------------------------------------------------------------------------------
/docs/pages/documentation/data/use-insert.md:
--------------------------------------------------------------------------------
1 | # useInsert
2 |
3 | Performs INSERT into table.
4 |
5 | ```tsx highlight=4
6 | import { useInsert } from 'react-supabase'
7 |
8 | function Page() {
9 | const [{ count, data, error, fetching }, execute] = useInsert('todos')
10 |
11 | async function onClickInsert(name) {
12 | const { count, data, error } = await insertTodos({
13 | name,
14 | })
15 | }
16 |
17 | return ...
18 | }
19 | ```
20 |
21 | ## Passing options
22 |
23 | During hook initialization:
24 |
25 | ```tsx
26 | const [{ count, data, error, fetching }, execute] = useInsert('todos', {
27 | options: {
28 | returning: 'represenation',
29 | count: 'exact',
30 | },
31 | })
32 | ```
33 |
34 | Or execute function:
35 |
36 | ```tsx
37 | const { count, data, error } = await execute(
38 | { name: 'Buy more cheese' },
39 | {
40 | count: 'estimated',
41 | returning: 'minimal',
42 | },
43 | )
44 | ```
45 |
--------------------------------------------------------------------------------
/docs/pages/documentation/data/use-select.md:
--------------------------------------------------------------------------------
1 | # useSelect
2 |
3 | Performs vertical filtering with SELECT.
4 |
5 | ```tsx highlight=4
6 | import { useSelect } from 'react-supabase'
7 |
8 | function Page() {
9 | const [{ count, data, error, fetching }, reexecute] = useSelect('todos')
10 |
11 | if (error) return {error.message}
12 | if (fetching) return Loading todos
13 | if (data?.length === 0) return No todos
14 |
15 | return ...
16 | }
17 | ```
18 |
19 | ## Passing options
20 |
21 | During hook initialization:
22 |
23 | ```tsx
24 | const [{ count, data, error, fetching }, reexecute] = useSelect('todos', {
25 | columns: 'id, name, description',
26 | filter: (query) => query.eq('status', 'completed'),
27 | options: {
28 | count: 'exact',
29 | head: false,
30 | },
31 | pause: false,
32 | })
33 | ```
34 |
35 | ## Dynamic filtering
36 |
37 | When using dynamic filters, you must make sure filters aren’t recreated every render. Otherwise, your request can get stuck in an infinite loop.
38 |
39 | The easiest way to avoid this is to create dynamic filters with the [`useFilter`](/documentation/data/use-filter) hook:
40 |
41 | ```tsx
42 | import { useState } from 'react'
43 | import { useFilter, useSelect } from 'react-supabase'
44 |
45 | function Page() {
46 | const [status, setStatus] = useState('completed')
47 | const filter = useFilter(
48 | (query) => query.eq('status', status),
49 | [status],
50 | )
51 | const [result, reexecute] = useSelect('todos', { filter })
52 |
53 | return ...
54 | }
55 | ```
56 |
57 | ## Pausing `useSelect`
58 |
59 | In some cases, we may want `useSelect` to execute when a pre-condition has been met, and not execute otherwise. For instance, we may be building a form and want validation to only take place when a field has been filled out.
60 |
61 | We can do this by setting the `pause` option to `true`:
62 |
63 | ```tsx
64 | import { useState } from 'react'
65 | import { useFilter, useSelect } from 'react-supabase'
66 |
67 | function Page() {
68 | const [username, setUsername] = useState(null)
69 | const filter = useFilter(
70 | (query) => query.eq('username', username),
71 | [username],
72 | )
73 | const [result, reexecute] = useSelect('users', {
74 | filter,
75 | pause: !username,
76 | })
77 |
78 | return (
79 |
84 | )
85 | }
86 | ```
87 |
--------------------------------------------------------------------------------
/docs/pages/documentation/data/use-update.md:
--------------------------------------------------------------------------------
1 | # useUpdate
2 |
3 | Performs UPDATE on table.
4 |
5 | ```tsx highlight=4
6 | import { useUpdate } from 'react-supabase'
7 |
8 | function Page() {
9 | const [{ count, data, error, fetching }, execute] = useUpdate('todos')
10 |
11 | async function onClickMarkAllComplete() {
12 | const { count, data, error } = await execute(
13 | { completed: true },
14 | (query) => query.eq('completed', false),
15 | )
16 | }
17 |
18 | return ...
19 | }
20 | ```
21 |
22 | Throws error during execute if a filter is not passed during hook initialization or execute method.
23 |
24 | ## Passing options
25 |
26 | During hook initialization:
27 |
28 | ```tsx
29 | const [{ count, data, error, fetching }, execute] = useUpdate('todos', {
30 | filter: (query) => query.eq('completed', false),
31 | options: {
32 | returning: 'represenation',
33 | count: 'exact',
34 | },
35 | })
36 | ```
37 |
38 | Or execute function:
39 |
40 | ```tsx
41 | const { count, data, error } = await execute(
42 | { completed: true },
43 | (query) => query.eq('completed', false),
44 | {
45 | count: 'estimated',
46 | returning: 'minimal',
47 | },
48 | )
49 | ```
50 |
--------------------------------------------------------------------------------
/docs/pages/documentation/data/use-upsert.md:
--------------------------------------------------------------------------------
1 | # useUpsert
2 |
3 | Performs `INSERT` or `UPDATE` on table.
4 |
5 | ```tsx 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 | ```tsx
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 | ```tsx
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 | ```
59 |
--------------------------------------------------------------------------------
/docs/pages/documentation/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "provider": "Provider",
3 | "use-client": "useClient",
4 | "auth": "Auth",
5 | "data": "Data",
6 | "realtime": "Realtime",
7 | "storage": "Storage"
8 | }
9 |
--------------------------------------------------------------------------------
/docs/pages/documentation/provider.md:
--------------------------------------------------------------------------------
1 | # Provider
2 |
3 | In order to use a Supabase client, you need to provide it via the [Context API](https://reactjs.org/docs/context.html). This may be done with the help of the Provider export.
4 |
5 | ```tsx
6 | import { createClient } from '@supabase/supabase-js'
7 | import { Provider } from 'react-supabase'
8 |
9 | const client = createClient('https://xyzcompany.supabase.co', 'public-anon-key')
10 |
11 | const App = () => (
12 |
13 |
14 |
15 | )
16 | ```
17 |
18 | All examples and code snippets from now on assume that they are wrapped in a `Provider`.
19 |
--------------------------------------------------------------------------------
/docs/pages/documentation/realtime/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "use-subscription": "useSubscription",
3 | "use-realtime": "useRealtime"
4 | }
5 |
--------------------------------------------------------------------------------
/docs/pages/documentation/realtime/use-realtime.md:
--------------------------------------------------------------------------------
1 | # useRealtime
2 |
3 | Fetch table and listen for changes.
4 |
5 | ```tsx highlight=4
6 | import { useRealtime } from 'react-supabase'
7 |
8 | function Page() {
9 | const [{ data, error, fetching }, reexecute] = 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 | When using your own compare function, you typically want to compare unique values:
20 |
21 | ```tsx highlight=7
22 | import { useRealtime } from 'react-supabase'
23 |
24 | function Page() {
25 | const [result, reexecute] = useRealtime(
26 | 'todos',
27 | { select: { columns:'id, username' } },
28 | (data, payload) => data.username === payload.username,
29 | )
30 |
31 | return ...
32 | }
33 | ```
34 |
35 | ## Initial selection of records
36 |
37 | When initializing the component you might need to filter your data appropriately. You can pass the options directly to the `useSelect` hook.
38 |
39 | First argument can be either a `string` table name or `useSelect` options with table property.
40 |
41 | ```tsx highlight=7,8,9,10
42 | import { useRealtime } from 'react-supabase'
43 |
44 | function Page() {
45 | const [result, reexecute] = useRealtime(
46 | 'todos',
47 | {
48 | select: {
49 | columns: 'id, username, description',
50 | filter: (query) => query.eq('completed', false),
51 | }
52 | },
53 | (data, payload) => data.username === payload.username,
54 | )
55 |
56 | return ...
57 | }
58 | ```
59 |
--------------------------------------------------------------------------------
/docs/pages/documentation/realtime/use-subscription.md:
--------------------------------------------------------------------------------
1 | # useSubscription
2 |
3 | Subscribe to database changes in realtime.
4 |
5 | ```tsx highlight=4,5,6
6 | import { useSubscription } from 'react-supabase'
7 |
8 | function Page() {
9 | useSubscription((payload) => {
10 | console.log('Change received!', payload)
11 | })
12 |
13 | return ...
14 | }
15 | ```
16 |
17 | ## Passing options
18 |
19 | During hook initialization:
20 |
21 | ```tsx
22 | useSubscription(
23 | (payload) => {
24 | console.log('Change received!', payload)
25 | },
26 | {
27 | event: 'INSERT',
28 | table: 'todos',
29 | },
30 | )
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/pages/documentation/storage/meta.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/docs/pages/documentation/use-client.md:
--------------------------------------------------------------------------------
1 | # useClient
2 |
3 | Allows you to access the Supabase client directly, which is useful for use cases not covered by existing hooks or other customer behavoir.
4 |
5 | ```tsx highlight=4
6 | import { useClient } from 'react-supabase'
7 |
8 | function Page() {
9 | const client = useClient()
10 |
11 | // Interact with client normally
12 | // client.from('todos').filter(...)
13 |
14 | return ...
15 | }
16 | ```
17 |
18 | Most of the time, you probably want to use the existing hooks for auth, data fetching/mutation, subscriptions, and storage.
19 |
--------------------------------------------------------------------------------
/docs/pages/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Installation
4 |
5 | ```
6 | yarn add react-supabase @supabase/supabase-js
7 | # or
8 | npm install --save react-supabase @supabase/supabase-js
9 | ```
10 |
11 | ## Quick Start
12 |
13 | Create a Supabase client and pass it to the `Provider`:
14 |
15 | ```tsx
16 | import { createClient } from '@supabase/supabase-js'
17 | import { Provider } from 'react-supabase'
18 |
19 | const client = createClient('https://xyzcompany.supabase.co', 'public-anon-key')
20 |
21 | const App = () => (
22 |
23 |
24 |
25 | )
26 | ```
27 |
28 | Now every component inside and under the `Provider` can use the Supabase client and hooks:
29 |
30 | ```tsx
31 | import { useRealtime } from 'react-supabase'
32 |
33 | const Todos = () => {
34 | const [result, reexecute] = useRealtime('todos')
35 |
36 | const { data, fetching, error } = result
37 |
38 | if (fetching) return Loading...
39 | if (error) return Oh no... {error.message}
40 |
41 | return (
42 |
43 | {data.map((todo) => (
44 | - {todo.title}
45 | ))}
46 |
47 | )
48 | }
49 | ```
50 |
--------------------------------------------------------------------------------
/docs/pages/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | ---
4 |
5 | # react-supabase
6 |
7 | `react-supabase` is a React Hooks library for [Supabase](https://supabase.io).
8 |
9 | ## Features
10 |
11 | - Support for auth, data, realtime, and storage
12 | - Automatically manages internal state
13 | - TypeScript ready
14 | - Zero-dependencies
15 |
16 | ## Community
17 |
18 | Feel free to join the [discussions on GitHub](https://github.com/tmm/react-supabase/discussions)!
19 |
--------------------------------------------------------------------------------
/docs/pages/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": "Introduction",
3 | "getting-started": "Getting Started",
4 | "documentation": "Documentation",
5 | "recipes": "Recipes"
6 | }
7 |
--------------------------------------------------------------------------------
/docs/pages/recipes/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "use-auth": "useAuth"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/recipes/use-auth.md:
--------------------------------------------------------------------------------
1 | # useAuth
2 |
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:
4 |
5 | ```tsx
6 | import { createContext, useEffect, useState } from 'react'
7 | import { useAuthStateChange, useClient } from 'react-supabase'
8 |
9 | const initialState = { session: null, user: null }
10 | export const AuthContext = createContext(initialState)
11 |
12 | export function AuthProvider({ children }) {
13 | const client = useClient()
14 | const [state, setState] = useState(initialState)
15 |
16 | useEffect(() => {
17 | const session = client.auth.session()
18 | setState({ session, user: session?.user ?? null })
19 | }, [])
20 |
21 | useAuthStateChange((event, session) => {
22 | console.log(`Supabase auth event: ${event}`, session)
23 | setState({ session, user: session?.user ?? null })
24 | })
25 |
26 | return {children}
27 | }
28 | ```
29 |
30 | And auth hook:
31 |
32 | ```ts
33 | import { AuthContext } from 'path/to/auth/context'
34 |
35 | export function useAuth() {
36 | const context = useContext(AuthContext)
37 | if (context === undefined)
38 | throw Error('useAuth must be used within AuthProvider')
39 | return context
40 | }
41 | ```
42 |
43 | Then, wrap your app in `AuthProvider` and use the `useAuth` hook in your components:
44 |
45 | ```tsx highlight=4
46 | import { useAuth } from 'path/to/auth/hook'
47 |
48 | function Page() {
49 | const { session, user } = useAuth()
50 |
51 | if (!session) return Hello, stranger
52 |
53 | return Welcome back, {user.email}
54 | }
55 | ```
56 |
--------------------------------------------------------------------------------
/docs/public/stork.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/react-supabase/fcc4eac38a6426e03863bd159d00c8cf0b5cb31d/docs/public/stork.wasm
--------------------------------------------------------------------------------
/docs/theme.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | repository: 'https://github.com/tmm/react-supabase',
3 | docsRepository: 'https://github.com/tmm/react-supabase',
4 | branch: 'main',
5 | path: '/docs/',
6 | titleSuffix: ' – react-supabase',
7 | nextLinks: true,
8 | prevLinks: true,
9 | search: true,
10 | UNSTABLE_stork: true,
11 | customSearch: null,
12 | defaultMenuCollapsed: true,
13 | darkMode: true,
14 | footer: true,
15 | footerText: 'MIT 2020 © Tom Meagher.',
16 | footerEditOnGitHubLink: true,
17 | logo: react-supabase,
18 | head: (
19 | <>
20 |
24 |
28 |
29 | >
30 | ),
31 | }
32 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve"
16 | },
17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
18 | "exclude": ["node_modules"]
19 | }
20 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types'
2 |
3 | const config: Config.InitialOptions = {
4 | preset: 'ts-jest',
5 | testRegex: '.*\\.test\\.(ts|tsx)$',
6 | watchPlugins: [
7 | 'jest-watch-typeahead/filename',
8 | 'jest-watch-typeahead/testname',
9 | ],
10 | }
11 |
12 | export default config
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-supabase",
3 | "version": "0.2.0",
4 | "description": "React Hooks library for Supabase",
5 | "main": "./dist/index.cjs.js",
6 | "module": "./dist/index.es.js",
7 | "types": "./dist/types/index.d.ts",
8 | "exports": {
9 | "import": "./dist/index.es.js",
10 | "require": "./dist/index.cjs.js"
11 | },
12 | "sideEffects": false,
13 | "files": [
14 | "dist/**"
15 | ],
16 | "repository": "tmm/react-supabase",
17 | "homepage": "https://react-supabase.vercel.app",
18 | "license": "MIT",
19 | "scripts": {
20 | "build": "vite build",
21 | "clean": "rimraf dist",
22 | "lint": "eslint . --cache",
23 | "lint:fix": "yarn lint --fix",
24 | "lint:format": "prettier --write .",
25 | "lint:types": "tsc --noEmit",
26 | "prepare": "husky install",
27 | "prepublishOnly": "yarn clean && yarn build",
28 | "size": "size-limit",
29 | "test": "jest"
30 | },
31 | "peerDependencies": {
32 | "@supabase/supabase-js": "^1.11.6",
33 | "react": "^17.0.0"
34 | },
35 | "dependencies": {},
36 | "devDependencies": {
37 | "@size-limit/preset-small-lib": "^4.12.0",
38 | "@supabase/postgrest-js": "0.34.1",
39 | "@supabase/supabase-js": "1.25.2",
40 | "@testing-library/react-hooks": "^5.1.1",
41 | "@types/jest": "^26.0.24",
42 | "@types/node": "^14.17.32",
43 | "@types/react": "^17.0.34",
44 | "@typescript-eslint/eslint-plugin": "^4.33.0",
45 | "@typescript-eslint/parser": "^4.33.0",
46 | "eslint": "^7.32.0",
47 | "eslint-config-prettier": "^8.2.0",
48 | "eslint-import-resolver-typescript": "^2.5.0",
49 | "eslint-plugin-import": "^2.25.2",
50 | "eslint-plugin-jest-dom": "^3.9.2",
51 | "eslint-plugin-prettier": "^3.4.1",
52 | "eslint-plugin-react": "^7.26.1",
53 | "eslint-plugin-react-hooks": "^4.2.0",
54 | "eslint-plugin-testing-library": "^4.12.4",
55 | "husky": "^6.0.0",
56 | "jest": "^26.6.3",
57 | "jest-watch-typeahead": "^0.6.5",
58 | "lint-staged": "^10.5.4",
59 | "prettier": "^2.4.1",
60 | "react": "^17.0.2",
61 | "react-dom": "^17.0.2",
62 | "rimraf": "^3.0.2",
63 | "size-limit": "^4.12.0",
64 | "ts-jest": "^26.5.5",
65 | "ts-node": "^10.4.0",
66 | "typescript": "^4.4.4",
67 | "vite": "^2.6.13",
68 | "vite-plugin-dts": "^0.5.3"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 | import { SupabaseClient } from '@supabase/supabase-js'
3 |
4 | export const Context = createContext(undefined)
5 | export const Provider = Context.Provider
6 | export const Consumer = Context.Consumer
7 | Context.displayName = 'SupabaseContext'
8 |
--------------------------------------------------------------------------------
/src/hooks/auth/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-auth-state-change'
2 | export * from './use-reset-password'
3 | export * from './use-signin'
4 | export * from './use-signout'
5 | export * from './use-signup'
6 |
--------------------------------------------------------------------------------
/src/hooks/auth/state.ts:
--------------------------------------------------------------------------------
1 | export const initialState = {
2 | error: undefined,
3 | fetching: false,
4 | session: undefined,
5 | user: undefined,
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/auth/use-auth-state-change.ts:
--------------------------------------------------------------------------------
1 | import { AuthChangeEvent, Session } from '@supabase/gotrue-js'
2 | import { useEffect } from 'react'
3 |
4 | import { useClient } from '../use-client'
5 |
6 | export function useAuthStateChange(
7 | callback: (event: AuthChangeEvent, session: Session | null) => void,
8 | ) {
9 | const client = useClient()
10 |
11 | /* eslint-disable react-hooks/exhaustive-deps */
12 | useEffect(() => {
13 | const { data: authListener } = client.auth.onAuthStateChange(callback)
14 | return () => {
15 | authListener?.unsubscribe()
16 | }
17 | }, [])
18 | /* eslint-enable react-hooks/exhaustive-deps */
19 | }
20 |
--------------------------------------------------------------------------------
/src/hooks/auth/use-reset-password.ts:
--------------------------------------------------------------------------------
1 | import { ApiError } from '@supabase/gotrue-js/dist/main/GoTrueApi'
2 | import { useCallback, useState } from 'react'
3 |
4 | import { useClient } from '../use-client'
5 |
6 | export type UseResetPasswordState = {
7 | error?: ApiError | null
8 | fetching: boolean
9 | }
10 |
11 | export type UseResetPasswordResponse = [
12 | UseResetPasswordState,
13 | (
14 | email: string,
15 | options?: UseResetPasswordOptions,
16 | ) => Promise>,
17 | ]
18 |
19 | export type UseResetPasswordOptions = {
20 | redirectTo?: string
21 | }
22 |
23 | export type UseResetPasswordConfig = {
24 | options?: UseResetPasswordOptions
25 | }
26 |
27 | const initialState = {
28 | error: undefined,
29 | fetching: false,
30 | }
31 |
32 | export function useResetPassword(
33 | config: UseResetPasswordConfig = {},
34 | ): UseResetPasswordResponse {
35 | const client = useClient()
36 | const [state, setState] = useState(initialState)
37 |
38 | const execute = useCallback(
39 | async (email: string, options?: UseResetPasswordOptions) => {
40 | setState({ ...initialState, fetching: true })
41 | const { error } = await client.auth.api.resetPasswordForEmail(
42 | email,
43 | options ?? config.options,
44 | )
45 | const res = { error }
46 | setState({ ...res, fetching: false })
47 | return res
48 | },
49 | [client, config],
50 | )
51 |
52 | return [state, execute]
53 | }
54 |
--------------------------------------------------------------------------------
/src/hooks/auth/use-signin.ts:
--------------------------------------------------------------------------------
1 | import { Provider, Session, User, UserCredentials } from '@supabase/gotrue-js'
2 | import { ApiError } from '@supabase/gotrue-js/dist/main/GoTrueApi'
3 | import { useCallback, useState } from 'react'
4 |
5 | import { useClient } from '../use-client'
6 | import { initialState } from './state'
7 |
8 | export type UseSignInState = {
9 | error?: ApiError | null
10 | fetching: boolean
11 | session?: Session | null
12 | user?: User | null
13 | }
14 |
15 | export type UseSignInResponse = [
16 | UseSignInState,
17 | (
18 | credentials: UserCredentials,
19 | options?: UseSignInOptions,
20 | ) => Promise>,
21 | ]
22 |
23 | export type UseSignInOptions = {
24 | redirectTo?: string
25 | scopes?: string
26 | }
27 |
28 | export type UseSignInConfig = {
29 | provider?: Provider
30 | options?: UseSignInOptions
31 | }
32 |
33 | export function useSignIn(config: UseSignInConfig = {}): UseSignInResponse {
34 | const client = useClient()
35 | const [state, setState] = useState(initialState)
36 |
37 | const execute = useCallback(
38 | async (credentials: UserCredentials, options?: UseSignInOptions) => {
39 | setState({ ...initialState, fetching: true })
40 | const { error, session, user } = await client.auth.signIn(
41 | {
42 | provider: config.provider,
43 | ...credentials,
44 | },
45 | options ?? config.options,
46 | )
47 | const res = { error, session, user }
48 | setState({ ...res, fetching: false })
49 | return res
50 | },
51 | [client, config],
52 | )
53 |
54 | return [state, execute]
55 | }
56 |
--------------------------------------------------------------------------------
/src/hooks/auth/use-signout.ts:
--------------------------------------------------------------------------------
1 | import { ApiError } from '@supabase/gotrue-js/dist/main/GoTrueApi'
2 | import { useCallback, useState } from 'react'
3 |
4 | import { useClient } from '../use-client'
5 |
6 | export type UseSignOutState = {
7 | error?: ApiError | null
8 | fetching: boolean
9 | }
10 |
11 | export type UseSignOutResponse = [
12 | UseSignOutState,
13 | () => Promise>,
14 | ]
15 |
16 | const initialState = {
17 | error: undefined,
18 | fetching: false,
19 | }
20 |
21 | export function useSignOut(): UseSignOutResponse {
22 | const client = useClient()
23 | const [state, setState] = useState(initialState)
24 |
25 | const execute = useCallback(async () => {
26 | setState({ ...initialState, fetching: true })
27 | const { error } = await client.auth.signOut()
28 | const res = { error }
29 | setState({ ...res, fetching: false })
30 | return res
31 | }, [client])
32 |
33 | return [state, execute]
34 | }
35 |
--------------------------------------------------------------------------------
/src/hooks/auth/use-signup.ts:
--------------------------------------------------------------------------------
1 | import { Session, User, UserCredentials } from '@supabase/gotrue-js'
2 | import { ApiError } from '@supabase/gotrue-js/dist/main/GoTrueApi'
3 | import { useCallback, useState } from 'react'
4 |
5 | import { useClient } from '../use-client'
6 | import { initialState } from './state'
7 |
8 | export type UseSignUpState = {
9 | error?: ApiError | null
10 | fetching: boolean
11 | session?: Session | null
12 | user?: User | null
13 | }
14 |
15 | export type UseSignUpResponse = [
16 | UseSignUpState,
17 | (
18 | credentials: UserCredentials,
19 | options?: UseSignUpOptions,
20 | ) => Promise>,
21 | ]
22 |
23 | export type UseSignUpOptions = {
24 | redirectTo?: string
25 | }
26 |
27 | export type UseSignUpConfig = {
28 | options?: UseSignUpOptions
29 | }
30 |
31 | export function useSignUp(config: UseSignUpConfig = {}): UseSignUpResponse {
32 | const client = useClient()
33 | const [state, setState] = useState(initialState)
34 |
35 | const execute = useCallback(
36 | async (credentials: UserCredentials, options?: UseSignUpOptions) => {
37 | setState({ ...initialState, fetching: true })
38 | const { error, session, user } = await client.auth.signUp(
39 | credentials,
40 | options ?? config.options,
41 | )
42 | const res = { error, session, user }
43 | setState({ ...res, fetching: false })
44 | return res
45 | },
46 | [client, config],
47 | )
48 |
49 | return [state, execute]
50 | }
51 |
--------------------------------------------------------------------------------
/src/hooks/data/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-delete'
2 | export * from './use-filter'
3 | export * from './use-insert'
4 | export * from './use-select'
5 | export * from './use-update'
6 | export * from './use-upsert'
7 |
--------------------------------------------------------------------------------
/src/hooks/data/state.ts:
--------------------------------------------------------------------------------
1 | export const initialState = {
2 | count: undefined,
3 | data: undefined,
4 | error: undefined,
5 | fetching: false,
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/data/use-delete.ts:
--------------------------------------------------------------------------------
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 UseDeleteState = {
8 | count?: number | null
9 | data?: Data | Data[] | null
10 | error?: PostgrestError | null
11 | fetching: boolean
12 | }
13 |
14 | export type UseDeleteResponse = [
15 | UseDeleteState,
16 | (
17 | filter?: Filter,
18 | options?: UseDeleteOptions,
19 | ) => Promise, 'count' | 'data' | 'error'>>,
20 | ]
21 |
22 | export type UseDeleteOptions = {
23 | count?: null | Count
24 | returning?: Returning
25 | }
26 |
27 | export type UseDeleteConfig = {
28 | filter?: Filter
29 | options?: UseDeleteOptions
30 | }
31 |
32 | export function useDelete(
33 | table: string,
34 | config: UseDeleteConfig = { options: {} },
35 | ): UseDeleteResponse {
36 | const client = useClient()
37 | const isMounted = useRef(false)
38 | const [state, setState] = useState(initialState)
39 |
40 | /* eslint-disable react-hooks/exhaustive-deps */
41 | const execute = useCallback(
42 | async (filter?: Filter, options?: UseDeleteOptions) => {
43 | const refine = filter ?? config.filter
44 | if (refine === undefined)
45 | throw Error('delete() should always be combined with `filter`')
46 |
47 | setState({ ...initialState, fetching: true })
48 | const source = client
49 | .from(table)
50 | .delete(options ?? config.options)
51 | const { count, data, error } = await refine(source)
52 |
53 | const res = { count, data, error }
54 | if (isMounted.current) setState({ ...res, fetching: false })
55 | return res
56 | },
57 | [client],
58 | )
59 | /* eslint-enable react-hooks/exhaustive-deps */
60 |
61 | useEffect(() => {
62 | isMounted.current = true
63 | return () => {
64 | isMounted.current = false
65 | }
66 | }, [])
67 |
68 | return [state, execute]
69 | }
70 |
--------------------------------------------------------------------------------
/src/hooks/data/use-filter.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 |
3 | import { Filter } from '../../types'
4 |
5 | export function useFilter(filter: Filter, deps: any[] = []) {
6 | /* eslint-disable react-hooks/exhaustive-deps */
7 | const callback = useCallback(filter, deps)
8 | /* eslint-enable react-hooks/exhaustive-deps */
9 | return callback
10 | }
11 |
--------------------------------------------------------------------------------
/src/hooks/data/use-insert.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from 'react'
2 |
3 | import { Count, PostgrestError, Returning } from '../../types'
4 | import { useClient } from '../use-client'
5 | import { initialState } from './state'
6 |
7 | export type UseInsertState = {
8 | count?: number | null
9 | data?: Data | Data[] | null
10 | error?: PostgrestError | null
11 | fetching: boolean
12 | }
13 |
14 | export type UseInsertResponse = [
15 | UseInsertState,
16 | (
17 | values: Partial | Partial[],
18 | options?: UseInsertOptions,
19 | ) => Promise, 'count' | 'data' | 'error'>>,
20 | ]
21 |
22 | export type UseInsertOptions = {
23 | count?: null | Count
24 | returning?: Returning
25 | }
26 |
27 | export type UseInsertConfig = {
28 | options?: UseInsertOptions
29 | }
30 |
31 | export function useInsert(
32 | table: string,
33 | config: UseInsertConfig = { options: {} },
34 | ): UseInsertResponse {
35 | const client = useClient()
36 | const isMounted = useRef(false)
37 | const [state, setState] = useState(initialState)
38 |
39 | /* eslint-disable react-hooks/exhaustive-deps */
40 | const execute = useCallback(
41 | async (
42 | values: Partial | Partial[],
43 | options?: UseInsertOptions,
44 | ) => {
45 | setState({ ...initialState, fetching: true })
46 | const { count, data, error } = await client
47 | .from(table)
48 | .insert(values, options ?? config.options)
49 | const res = { count, data, error }
50 | if (isMounted.current) setState({ ...res, fetching: false })
51 | return res
52 | },
53 | [client],
54 | )
55 | /* eslint-enable react-hooks/exhaustive-deps */
56 |
57 | useEffect(() => {
58 | isMounted.current = true
59 | return () => {
60 | isMounted.current = false
61 | }
62 | }, [])
63 |
64 | return [state, execute]
65 | }
66 |
--------------------------------------------------------------------------------
/src/hooks/data/use-select.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from 'react'
2 |
3 | import { Count, Filter, PostgrestError } from '../../types'
4 | import { useClient } from '../use-client'
5 | import { initialState } from './state'
6 |
7 | export type UseSelectState = {
8 | count?: number | null
9 | data?: Data[] | null
10 | error?: PostgrestError | null
11 | fetching: boolean
12 | stale: boolean
13 | }
14 |
15 | export type UseSelectResponse = [
16 | UseSelectState,
17 | () => Promise,
19 | 'count' | 'data' | 'error'
20 | > | null>,
21 | ]
22 |
23 | export type UseSelectOptions = {
24 | count?: null | Count
25 | head?: boolean
26 | }
27 |
28 | export type UseSelectConfig = {
29 | columns?: string
30 | filter?: Filter | false | null
31 | options?: UseSelectOptions
32 | pause?: boolean
33 | }
34 |
35 | export function useSelect(
36 | table: string,
37 | config: UseSelectConfig = { columns: '*', options: {} },
38 | ): UseSelectResponse {
39 | const client = useClient()
40 | const isMounted = useRef(false)
41 | const [state, setState] = useState({
42 | ...initialState,
43 | stale: false,
44 | })
45 |
46 | const execute = useCallback(async () => {
47 | if (config.pause) return null
48 | setState((x) => ({
49 | ...initialState,
50 | data: x.data,
51 | stale: true,
52 | fetching: true,
53 | }))
54 | const source = client
55 | .from(table)
56 | .select(config.columns, config.options)
57 | const { count, data, error } = await (config.filter
58 | ? config.filter(source)
59 | : source)
60 | const res = { count, data, error }
61 | if (isMounted.current)
62 | setState({ ...res, stale: false, fetching: false })
63 | return res
64 | }, [client, config, table])
65 |
66 | /* eslint-disable react-hooks/exhaustive-deps */
67 | useEffect(() => {
68 | isMounted.current = true
69 | execute()
70 | return () => {
71 | isMounted.current = false
72 | }
73 | }, [config?.filter])
74 | /* eslint-enable react-hooks/exhaustive-deps */
75 |
76 | return [state, execute]
77 | }
78 |
--------------------------------------------------------------------------------
/src/hooks/data/use-update.ts:
--------------------------------------------------------------------------------
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 UseUpdateState = {
8 | count?: number | null
9 | data?: Data | Data[] | null
10 | error?: PostgrestError | null
11 | fetching: boolean
12 | }
13 |
14 | export type UseUpdateResponse = [
15 | UseUpdateState,
16 | (
17 | values: Partial,
18 | filter?: Filter,
19 | options?: UseUpdateOptions,
20 | ) => Promise, 'count' | 'data' | 'error'>>,
21 | ]
22 |
23 | export type UseUpdateOptions = {
24 | count?: null | Count
25 | returning?: Returning
26 | }
27 |
28 | export type UseUpdateConfig = {
29 | filter?: Filter
30 | options?: UseUpdateOptions
31 | }
32 |
33 | export function useUpdate(
34 | table: string,
35 | config: UseUpdateConfig = { options: {} },
36 | ): UseUpdateResponse {
37 | const client = useClient()
38 | const isMounted = useRef(false)
39 | const [state, setState] = useState(initialState)
40 |
41 | /* eslint-disable react-hooks/exhaustive-deps */
42 | const execute = useCallback(
43 | async (
44 | values: Partial,
45 | filter?: Filter,
46 | options?: UseUpdateOptions,
47 | ) => {
48 | const refine = filter ?? config.filter
49 | if (refine === undefined)
50 | throw Error('update() should always be combined with `filter`')
51 |
52 | setState({ ...initialState, fetching: true })
53 | const source = client
54 | .from(table)
55 | .update(values, options ?? config.options)
56 | const { count, data, error } = await refine(source)
57 |
58 | const res = { count, data, error }
59 | if (isMounted.current) setState({ ...res, fetching: false })
60 | return res
61 | },
62 | [client],
63 | )
64 | /* eslint-enable react-hooks/exhaustive-deps */
65 |
66 | useEffect(() => {
67 | isMounted.current = true
68 | return () => {
69 | isMounted.current = false
70 | }
71 | }, [])
72 |
73 | return [state, execute]
74 | }
75 |
--------------------------------------------------------------------------------
/src/hooks/data/use-upsert.ts:
--------------------------------------------------------------------------------
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 = {
8 | count?: number | null
9 | data?: Data | Data[] | null
10 | error?: PostgrestError | null
11 | fetching: boolean
12 | }
13 |
14 | export type UseUpsertResponse = [
15 | UseUpsertState,
16 | (
17 | values: Partial | Partial[],
18 | options?: UseUpsertOptions,
19 | filter?: Filter,
20 | ) => Promise, '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 = {
30 | filter?: Filter
31 | options?: UseUpsertOptions
32 | }
33 |
34 | export function useUpsert(
35 | table: string,
36 | config: UseUpsertConfig = { options: {} },
37 | ): UseUpsertResponse {
38 | const client = useClient()
39 | const isMounted = useRef(false)
40 | const [state, setState] = useState(initialState)
41 |
42 | /* eslint-disable react-hooks/exhaustive-deps */
43 | const execute = useCallback(
44 | async (
45 | values: Partial | Partial[],
46 | options?: UseUpsertOptions,
47 | filter?: Filter,
48 | ) => {
49 | const refine = filter ?? config.filter
50 | setState({ ...initialState, fetching: true })
51 | const source = client
52 | .from(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 | }
76 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auth'
2 | export * from './data'
3 | export * from './realtime'
4 | export * from './use-client'
5 |
--------------------------------------------------------------------------------
/src/hooks/realtime/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-subscription'
2 | export * from './use-realtime'
3 |
--------------------------------------------------------------------------------
/src/hooks/realtime/use-realtime.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useReducer } from 'react'
2 | import { SupabaseRealtimePayload } from '@supabase/supabase-js'
3 |
4 | import { UseSelectConfig, UseSelectState, useSelect } from '../data'
5 | import { useSubscription } from './use-subscription'
6 |
7 | export type UseRealtimeState = Omit<
8 | UseSelectState,
9 | 'count'
10 | > & {
11 | old?: Data[] | null
12 | }
13 |
14 | export type UseRealtimeResponse = [
15 | UseRealtimeState,
16 | () => Promise,
18 | 'count' | 'data' | 'error'
19 | > | null>,
20 | ]
21 |
22 | export type UseRealtimeAction =
23 | | { type: 'FETCH'; payload: UseSelectState }
24 | | { type: 'SUBSCRIPTION'; payload: SupabaseRealtimePayload }
25 |
26 | export type UseRealtimeConfig = {
27 | select?: Omit, 'pause'>
28 | }
29 |
30 | export type UseRealtimeCompareFn = (
31 | data: Data,
32 | payload: Data,
33 | ) => boolean
34 |
35 | type CompareFnDefaultData = Data & { id: any }
36 |
37 | export function useRealtime(
38 | table: string,
39 | config?: UseRealtimeConfig,
40 | compareFn: UseRealtimeCompareFn = (a, b) =>
41 | (>a).id ===
42 | (>b).id,
43 | ): UseRealtimeResponse {
44 | if (table === '*')
45 | throw Error(
46 | 'Must specify table or row. Cannot listen for all database changes.',
47 | )
48 |
49 | const [result, reexecute] = useSelect(table, config?.select)
50 | const [state, dispatch] = useReducer<
51 | React.Reducer, UseRealtimeAction>
52 | >(reducer(compareFn), result)
53 |
54 | useSubscription((payload) => dispatch({ type: 'SUBSCRIPTION', payload }), {
55 | table,
56 | })
57 |
58 | useEffect(() => {
59 | dispatch({ type: 'FETCH', payload: result })
60 | }, [result])
61 |
62 | return [state, reexecute]
63 | }
64 |
65 | const reducer =
66 | (compareFn: UseRealtimeCompareFn) =>
67 | (
68 | state: UseRealtimeState,
69 | action: UseRealtimeAction,
70 | ): UseRealtimeState => {
71 | const old = state.data
72 | switch (action.type) {
73 | case 'FETCH':
74 | return { ...state, old, ...action.payload }
75 | case 'SUBSCRIPTION':
76 | switch (action.payload.eventType) {
77 | case 'DELETE':
78 | return {
79 | ...state,
80 | data: state.data?.filter(
81 | (x) => !compareFn(x, action.payload.old),
82 | ),
83 | fetching: false,
84 | old,
85 | }
86 | case 'INSERT':
87 | return {
88 | ...state,
89 | data: [...(old ?? []), action.payload.new],
90 | fetching: false,
91 | old,
92 | }
93 | case 'UPDATE': {
94 | const data = old ?? []
95 | const index = data.findIndex((x) =>
96 | compareFn(x, action.payload.new),
97 | )
98 | return {
99 | ...state,
100 | data: [
101 | ...data.slice(0, index),
102 | action.payload.new,
103 | ...data.slice(index + 1),
104 | ],
105 | fetching: false,
106 | old,
107 | }
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/hooks/realtime/use-subscription.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import {
3 | SupabaseEventTypes,
4 | SupabaseRealtimePayload,
5 | } from '@supabase/supabase-js/dist/main/lib/types'
6 |
7 | import { useClient } from '../use-client'
8 |
9 | export type UseSubscriptionConfig = {
10 | event?: SupabaseEventTypes
11 | table?: string
12 | }
13 |
14 | export function useSubscription(
15 | callback: (payload: SupabaseRealtimePayload) => void,
16 | config: UseSubscriptionConfig = { event: '*', table: '*' },
17 | ) {
18 | const client = useClient()
19 |
20 | /* eslint-disable react-hooks/exhaustive-deps */
21 | useEffect(() => {
22 | const subscription = client
23 | .from(config.table ?? '*')
24 | .on(config.event ?? '*', callback)
25 | .subscribe()
26 | return () => {
27 | subscription.unsubscribe()
28 | }
29 | }, [])
30 | /* eslint-enable react-hooks/exhaustive-deps */
31 | }
32 |
--------------------------------------------------------------------------------
/src/hooks/use-client.ts:
--------------------------------------------------------------------------------
1 | import { SupabaseClient } from '@supabase/supabase-js'
2 | import { useContext } from 'react'
3 |
4 | import { Context } from '../context'
5 |
6 | export function useClient(): SupabaseClient {
7 | const client = useContext(Context)
8 | if (client === undefined)
9 | throw Error('No client has been specified using Provider.')
10 | return client
11 | }
12 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './context'
2 | export * from './hooks'
3 | export * from './types'
4 |
--------------------------------------------------------------------------------
/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 Returning = 'minimal' | 'representation'
17 |
--------------------------------------------------------------------------------
/test/hooks/__snapshots__/use-client.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useClient should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/auth/__snapshots__/use-auth-state-change.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useAuthStateChange should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/auth/__snapshots__/use-reset-password.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useResetPassword should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/auth/__snapshots__/use-signin.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useSignIn should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/auth/__snapshots__/use-signout.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useSignOut should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/auth/__snapshots__/use-signup.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useSignUp should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/auth/use-auth-state-change.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useAuthStateChange } from '../../../src'
4 |
5 | describe('useAuthStateChange', () => {
6 | it('should throw when not inside Provider', () => {
7 | const { result } = renderHook(() => useAuthStateChange(jest.fn()))
8 | expect(() => result.current).toThrowErrorMatchingSnapshot()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/test/hooks/auth/use-reset-password.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useResetPassword } from '../../../src'
4 |
5 | describe('useResetPassword', () => {
6 | it('should throw when not inside Provider', () => {
7 | const { result } = renderHook(() => useResetPassword())
8 | expect(() => result.current).toThrowErrorMatchingSnapshot()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/test/hooks/auth/use-signin.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useSignIn } from '../../../src'
4 |
5 | describe('useSignIn', () => {
6 | it('should throw when not inside Provider', () => {
7 | const { result } = renderHook(() => useSignIn())
8 | expect(() => result.current).toThrowErrorMatchingSnapshot()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/test/hooks/auth/use-signout.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useSignOut } from '../../../src'
4 |
5 | describe('useSignOut', () => {
6 | it('should throw when not inside Provider', () => {
7 | const { result } = renderHook(() => useSignOut())
8 | expect(() => result.current).toThrowErrorMatchingSnapshot()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/test/hooks/auth/use-signup.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useSignUp } from '../../../src'
4 |
5 | describe('useSignUp', () => {
6 | it('should throw when not inside Provider', () => {
7 | const { result } = renderHook(() => useSignUp())
8 | expect(() => result.current).toThrowErrorMatchingSnapshot()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/test/hooks/data/__snapshots__/use-delete.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useDelete should throw when filter not provided to execute 1`] = `"delete() should always be combined with \`filter\`"`;
4 |
5 | exports[`useDelete should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
6 |
--------------------------------------------------------------------------------
/test/hooks/data/__snapshots__/use-insert.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useInsert should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/data/__snapshots__/use-select.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useSelect should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/data/__snapshots__/use-update.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useUpdate should throw when filter not provided to execute 1`] = `"update() should always be combined with \`filter\`"`;
4 |
5 | exports[`useUpdate should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
6 |
--------------------------------------------------------------------------------
/test/hooks/data/__snapshots__/use-upsert.test.tsx.snap:
--------------------------------------------------------------------------------
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."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/data/use-delete.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useDelete } from '../../../src'
4 | import { initialState } from '../../../src/hooks/data/state'
5 | import { Wrapper as wrapper } from '../../utils'
6 |
7 | describe('useDelete', () => {
8 | it('should throw when not inside Provider', () => {
9 | const { result } = renderHook(() => useDelete('todos'))
10 | expect(() => result.current).toThrowErrorMatchingSnapshot()
11 | })
12 |
13 | it('should throw when filter not provided to execute', async () => {
14 | const { result } = renderHook(() => useDelete('todos'), { wrapper })
15 | await expect(result.current[1]()).rejects.toThrowErrorMatchingSnapshot()
16 | })
17 |
18 | it('should have correct initial state', async () => {
19 | const { result } = renderHook(() => useDelete('todos'), { wrapper })
20 | expect(result.current[0]).toEqual(initialState)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/test/hooks/data/use-filter.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useFilter } from '../../../src'
4 |
5 | describe('useFilter,', () => {
6 | it('should return filter', () => {
7 | const { result } = renderHook(() =>
8 | useFilter((query) => query.limit(10)),
9 | )
10 | expect(typeof result.current).toBe('function')
11 | })
12 |
13 | it('should return filter with dependencies', () => {
14 | const { result } = renderHook(() =>
15 | useFilter((query) => query.limit(10), [true]),
16 | )
17 | expect(typeof result.current).toBe('function')
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/test/hooks/data/use-insert.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useInsert } from '../../../src'
4 | import { initialState } from '../../../src/hooks/data/state'
5 | import { Wrapper as wrapper } from '../../utils'
6 |
7 | describe('useInsert', () => {
8 | it('should throw when not inside Provider', () => {
9 | const { result } = renderHook(() => useInsert('todos'))
10 | expect(() => result.current).toThrowErrorMatchingSnapshot()
11 | })
12 |
13 | it('should have correct initial state', async () => {
14 | const { result } = renderHook(() => useInsert('todos'), { wrapper })
15 | expect(result.current[0]).toEqual(initialState)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/test/hooks/data/use-select.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useSelect } from '../../../src'
4 | import { initialState } from '../../../src/hooks/data/state'
5 | import { Wrapper as wrapper } from '../../utils'
6 |
7 | describe('useSelect', () => {
8 | it('should throw when not inside Provider', () => {
9 | const { result } = renderHook(() => useSelect('todos'))
10 | expect(() => result.current).toThrowErrorMatchingSnapshot()
11 | })
12 |
13 | it('should have correct initial state', async () => {
14 | const { result } = renderHook(
15 | () => useSelect('todos', { pause: true }),
16 | { wrapper },
17 | )
18 | expect(result.current[0]).toEqual({ ...initialState, stale: false })
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/test/hooks/data/use-update.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useUpdate } from '../../../src'
4 | import { initialState } from '../../../src/hooks/data/state'
5 | import { Wrapper as wrapper } from '../../utils'
6 |
7 | describe('useUpdate', () => {
8 | it('should throw when not inside Provider', () => {
9 | const { result } = renderHook(() => useUpdate('todos'))
10 | expect(() => result.current).toThrowErrorMatchingSnapshot()
11 | })
12 |
13 | it('should throw when filter not provided to execute', async () => {
14 | const { result } = renderHook(() => useUpdate('todos'), { wrapper })
15 | await expect(
16 | result.current[1]({ status: 'complete' }),
17 | ).rejects.toThrowErrorMatchingSnapshot()
18 | })
19 |
20 | it('should have correct initial state', async () => {
21 | const { result } = renderHook(() => useUpdate('todos'), { wrapper })
22 | expect(result.current[0]).toEqual(initialState)
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/test/hooks/data/use-upsert.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useUpsert } from '../../../src'
4 | import { initialState } from '../../../src/hooks/data/state'
5 | import { Wrapper as wrapper } from '../../utils'
6 |
7 | describe('useUpsert', () => {
8 | it('should throw when not inside Provider', () => {
9 | const { result } = renderHook(() => useUpsert('todos'))
10 | expect(() => result.current).toThrowErrorMatchingSnapshot()
11 | })
12 |
13 | it('should have correct initial state', async () => {
14 | const { result } = renderHook(() => useUpsert('todos'), { wrapper })
15 | expect(result.current[0]).toEqual(initialState)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/test/hooks/realtime/__snapshots__/use-realtime.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useRealtime should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
5 | exports[`useRealtime should throw when trying to listen all database changes 1`] = `"Must specify table or row. Cannot listen for all database changes."`;
6 |
7 | exports[`useRealtime should throw when trying to listen all database changes via options 1`] = `"Must specify table or row. Cannot listen for all database changes."`;
8 |
--------------------------------------------------------------------------------
/test/hooks/realtime/__snapshots__/use-subscription.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useSubscription should throw when not inside Provider 1`] = `"No client has been specified using Provider."`;
4 |
--------------------------------------------------------------------------------
/test/hooks/realtime/use-realtime.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useRealtime } from '../../../src'
4 | import { Wrapper as wrapper } from '../../utils'
5 |
6 | describe('useRealtime', () => {
7 | it('should throw when not inside Provider', () => {
8 | const { result } = renderHook(() => useRealtime('todos'))
9 | expect(() => result.current).toThrowErrorMatchingSnapshot()
10 | })
11 |
12 | it('should throw when trying to listen all database changes', () => {
13 | const { result } = renderHook(() => useRealtime('*'), { wrapper })
14 | expect(() => result.current).toThrowErrorMatchingSnapshot()
15 | })
16 |
17 | it('should throw when trying to listen all database changes via options', () => {
18 | const { result } = renderHook(
19 | () =>
20 | useRealtime('*', {
21 | select: {
22 | columns: 'id, username, completed',
23 | filter: (query) => query.eq('completed', false),
24 | },
25 | }),
26 | { wrapper },
27 | )
28 | expect(() => result.current).toThrowErrorMatchingSnapshot()
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/test/hooks/realtime/use-subscription.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useSubscription } from '../../../src'
4 |
5 | describe('useSubscription', () => {
6 | it('should throw when not inside Provider', () => {
7 | const { result } = renderHook(() => useSubscription(jest.fn()))
8 | expect(() => result.current).toThrowErrorMatchingSnapshot()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/test/hooks/use-client.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks'
2 |
3 | import { useClient } from '../../src'
4 | import { Wrapper as wrapper } from '../utils'
5 |
6 | describe('useClient', () => {
7 | it('should throw when not inside Provider', () => {
8 | const { result } = renderHook(() => useClient())
9 | expect(() => result.current).toThrowErrorMatchingSnapshot()
10 | })
11 |
12 | it('should return client', () => {
13 | const { result } = renderHook(() => useClient(), { wrapper })
14 | expect(Object.keys(result.current)).toEqual([
15 | 'supabaseUrl',
16 | 'supabaseKey',
17 | 'restUrl',
18 | 'realtimeUrl',
19 | 'authUrl',
20 | 'storageUrl',
21 | 'schema',
22 | 'auth',
23 | 'realtime',
24 | ])
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/test/utils.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createClient } from '@supabase/supabase-js'
3 |
4 | import { Provider } from '../src'
5 |
6 | export const client = createClient('https://example.com', 'some.fake.key')
7 |
8 | export const Wrapper: React.FC<{ children: any }> = ({ children }) => (
9 | {children}
10 | )
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": false,
4 | "declaration": true,
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "isolatedModules": true,
8 | "jsx": "react",
9 | "lib": ["dom", "esnext"],
10 | "module": "commonjs",
11 | "moduleResolution": "node",
12 | "noFallthroughCasesInSwitch": true,
13 | "noImplicitAny": false,
14 | "noImplicitReturns": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "outDir": "./dist/types",
18 | "resolveJsonModule": true,
19 | "skipLibCheck": false,
20 | "strict": true,
21 | "target": "esnext",
22 | "types": ["node", "jest"]
23 | },
24 | "include": ["src/**/*"],
25 | "watchOptions": {
26 | "watchFile": "useFsEvents",
27 | "watchDirectory": "useFsEvents",
28 | "fallbackPolling": "dynamicPriority"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import dts from 'vite-plugin-dts'
3 |
4 | import pkg from './package.json'
5 |
6 | export default defineConfig({
7 | build: {
8 | lib: {
9 | entry: 'src/index.ts',
10 | fileName: 'index',
11 | formats: ['cjs', 'es'],
12 | },
13 | rollupOptions: {
14 | external: [
15 | ...Object.keys(pkg.dependencies ?? {}),
16 | ...Object.keys(pkg.devDependencies ?? {}),
17 | ],
18 | },
19 | },
20 | plugins: [
21 | dts({
22 | exclude: ['src/**test.ts*'],
23 | beforeWriteFile: (filePath, content) => ({
24 | content,
25 | filePath: filePath.replace('src', ''),
26 | }),
27 | compilerOptions: {
28 | emitDeclarationOnly: true,
29 | noEmit: false,
30 | },
31 | outputDir: 'dist/types',
32 | }),
33 | ],
34 | })
35 |
--------------------------------------------------------------------------------