├── .eslintrc.cjs ├── .github └── workflows │ ├── documentation.yml │ └── test.yml ├── .gitignore ├── .lintignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── mono-docs.yml ├── package-lock.json ├── package.json ├── packages ├── redux-dynamic-modules │ ├── README.md │ ├── docs │ │ └── setup.md │ ├── mono-docs.yml │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── redux │ ├── README.md │ ├── docs │ │ ├── hooks.md │ │ ├── migrating-from-react-redux.md │ │ ├── provider.md │ │ └── setup.md │ ├── mono-docs.yml │ ├── package.json │ ├── src │ │ ├── index.tsx │ │ └── use-sync-external-store.d.ts │ └── tsconfig.json ├── router │ ├── README.md │ ├── docs │ │ ├── hooks.md │ │ ├── links.md │ │ ├── router.md │ │ ├── routes.md │ │ └── setup.md │ ├── mono-docs.yml │ ├── package.json │ ├── src │ │ ├── Link.tsx │ │ ├── Route.tsx │ │ ├── Router.tsx │ │ ├── RouterContext.ts │ │ ├── Switch.tsx │ │ ├── basename.ts │ │ ├── history.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── routeMatcher.ts │ │ └── simpleRouteMatcherFactory.ts │ └── tsconfig.json ├── tsrux │ ├── README.md │ ├── docs │ │ ├── action-creators.md │ │ ├── further-examples.md │ │ ├── reducers.md │ │ ├── setup.md │ │ └── types.md │ ├── jest.config.cjs │ ├── mono-docs.yml │ ├── package.json │ ├── src │ │ ├── actionCreator.spec-d.ts │ │ ├── actionCreator.spec.ts │ │ ├── actionCreator.ts │ │ ├── index.ts │ │ ├── mapReducers.spec-d.ts │ │ ├── mapReducers.spec.ts │ │ └── mapReducers.ts │ ├── tsconfig-build.json │ └── tsconfig.json ├── use-event-source │ ├── README.md │ ├── docs │ │ ├── redux.md │ │ ├── setup.md │ │ └── usage.md │ ├── mono-docs.yml │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── use-fetch │ ├── README.md │ ├── docs │ │ ├── api.md │ │ ├── config.md │ │ ├── get.md │ │ ├── helpers.md │ │ ├── modify.md │ │ └── setup.md │ ├── mono-docs.yml │ ├── package.json │ ├── src │ │ ├── helpers.ts │ │ └── index.ts │ └── tsconfig.json └── use-graphql │ ├── README.md │ ├── docs │ ├── builder.md │ ├── config.md │ ├── hook.md │ └── setup.md │ ├── mono-docs.yml │ ├── package.json │ ├── src │ ├── builder.ts │ ├── config.ts │ ├── index.ts │ ├── state.ts │ └── types.ts │ ├── tests │ ├── builder-complex.spec-d.ts │ ├── builder-primitive.spec-d.ts │ ├── hook-with-vars.spec-d.ts │ ├── hook-without-vars.spec-d.ts │ ├── types.ts │ └── variable-types.spec-d.ts │ ├── tsconfig-build.json │ └── tsconfig.json └── tsconfig.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@lusito/eslint-config-react", "plugin:jest/recommended"], 3 | rules: { 4 | "react-hooks/exhaustive-deps": "error", 5 | }, 6 | env: { 7 | browser: true, 8 | "jest/globals": true, 9 | }, 10 | overrides: [ 11 | { 12 | files: ["./packages/*/tests/*.ts"], 13 | rules: { 14 | "react-hooks/rules-of-hooks": "off", 15 | "import/no-extraneous-dependencies": "off", 16 | "@typescript-eslint/ban-ts-comment": "off", 17 | }, 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | build: 7 | name: Documentation 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - uses: volta-cli/action@v4 12 | - name: Install 13 | run: npm ci 14 | - name: Build 15 | run: npm run docs:build 16 | - name: Deploy 17 | uses: peaceiris/actions-gh-pages@v3 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | publish_dir: ./docs-dist 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: 3 | push: 4 | branches: ["*"] 5 | jobs: 6 | build: 7 | name: Build and Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - uses: volta-cli/action@v4 12 | - name: Install 13 | run: npm ci 14 | - name: Build 15 | run: npm run build 16 | - name: Test 17 | run: npm test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | docs-dist/ 3 | .vscode 4 | node_modules/ 5 | coverage/ 6 | *.tgz 7 | -------------------------------------------------------------------------------- /.lintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | docs-dist 5 | coverage -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@lusito/prettier-config" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021 Santo Pfingsten 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @react-nano 2 | 3 | Tiny, powerful and type-safe React libraries. All released under a liberal license: [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) 4 | 5 | ## Libraries 6 | 7 | - [@react-nano/redux](https://lusito.github.io/react-nano/redux/index.html)\ 8 | Lightweight alternative to react-redux. 9 | - [@react-nano/redux-dynamic-modules](https://lusito.github.io/react-nano/redux-dynamic-modules/index.html)\ 10 | Making redux-dynamic-modules more lightweight by using @react-nano/redux instead of react-redux. 11 | - [@react-nano/router](https://lusito.github.io/react-nano/router/index.html)\ 12 | Lightweight alternative to react-router. 13 | - [@react-nano/tsrux](https://lusito.github.io/react-nano/tsrux/index.html)\ 14 | Lightweight alternative to redux-actions, deox, etc. 15 | - [@react-nano/use-event-source](https://lusito.github.io/react-nano/use-event-source/index.html)\ 16 | Hook for using [Server-Sent-Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). 17 | - [@react-nano/use-fetch](https://lusito.github.io/react-nano/use-fetch/index.html)\ 18 | Hook for using `fetch()` to GET, POST, etc. requests. 19 | - [@react-nano/use-graphql](https://lusito.github.io/react-nano/use-graphql/index.html)\ 20 | Hook for using GraphQL queries and mutations. 21 | 22 | ## Report Issues 23 | 24 | Something not working quite as expected? Do you need a feature that has not been implemented yet? Check the [issue tracker](https://github.com/Lusito/react-nano/issues) and add a new one if your problem is not already listed. Please try to provide a detailed description of your problem, including the steps to reproduce it. 25 | 26 | ## Contribute 27 | 28 | Awesome! If you would like to contribute with a new feature or submit a bugfix, fork this repo and send a pull request. Please, make sure all the unit tests are passing before submitting and add new ones in case you introduced new features. 29 | 30 | ## License 31 | 32 | @react-nano has been released under the [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) license, meaning you 33 | can use it free of charge, without strings attached in commercial and non-commercial projects. Credits are appreciated but not mandatory. 34 | -------------------------------------------------------------------------------- /mono-docs.yml: -------------------------------------------------------------------------------- 1 | siteName: "@react-nano" 2 | title: "" 3 | description: Tiny, powerful and type-safe React libraries 4 | footer: 5 | - Zlib/Libpng License | https://github.com/Lusito/react-nano/blob/master/LICENSE 6 | - Copyright © 2022 Santo Pfingsten 7 | keywords: 8 | - react 9 | links: 10 | - Github | https://github.com/lusito/react-nano 11 | adjustPaths: 12 | - ^\/packages\/([^/]+)(\/docs)?/([^/]+\.html)$|/$1/$3 13 | - ^\/packages\/([^/]+)(\/docs)?/([^/]+/)?$|/$1/$3 14 | projects: 15 | - packages/redux 16 | - packages/redux-dynamic-modules 17 | - packages/router 18 | - packages/tsrux 19 | - packages/use-event-source 20 | - packages/use-graphql 21 | - packages/use-fetch 22 | buildOptions: 23 | out: docs-dist 24 | siteUrl: https://lusito.github.io/react-nano 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/redux", 5 | "packages/redux-dynamic-modules", 6 | "packages/router", 7 | "packages/tsrux", 8 | "packages/use-event-source", 9 | "packages/use-graphql", 10 | "packages/use-fetch" 11 | ], 12 | "scripts": { 13 | "build": "npm run build --workspaces", 14 | "docs:build": "npm run docs:build:base -- build", 15 | "docs:build:base": "rimraf docs-dist && mono-docs .", 16 | "docs:dev": "nodemon --ignore \"docs-dist\" --ignore node_modules --ignore dist -e ts,tsx,md,scss,png,webp --exec \"npm run docs:build:base -- serve\"", 17 | "lint": "mono-lint", 18 | "lint:fix": "mono-lint --fix", 19 | "release": "mono-release", 20 | "test": "npm test --workspaces --if-present" 21 | }, 22 | "devDependencies": { 23 | "@lusito/eslint-config-react": "^4.0.0", 24 | "@lusito/mono": "^0.20.0", 25 | "@lusito/mono-docs": "^0.21.0", 26 | "@lusito/prettier-config": "^3.2.0", 27 | "@lusito/tsconfig": "^1.0.5", 28 | "eslint-plugin-jest": "^28.11.0", 29 | "nodemon": "^3.1.9", 30 | "sort-package-json": "^2.15.1" 31 | }, 32 | "volta": { 33 | "node": "23.8.0" 34 | }, 35 | "monoLint": { 36 | "lintMarkdownLinks": { 37 | "warnOnlyPatterns": [ 38 | "^https:\\/\\/lusito\\.github\\.io\\/react-nano\\/" 39 | ] 40 | }, 41 | "lintMarkdownTitles": { 42 | "ignorePatterns": [ 43 | "@react-nano[/a-z0-9-]*", 44 | "react-redux" 45 | ] 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/redux-dynamic-modules/README.md: -------------------------------------------------------------------------------- 1 | # @react-nano/redux-dynamic-modules 2 | 3 | [![License](https://flat.badgen.net/github/license/lusito/react-nano?icon=github)](https://github.com/Lusito/react-nano/blob/master/LICENSE) 4 | [![Minified + gzipped size](https://flat.badgen.net/bundlephobia/minzip/@react-nano/redux-dynamic-modules?icon=dockbit)](https://bundlephobia.com/result?p=@react-nano/redux-dynamic-modules) 5 | [![NPM version](https://flat.badgen.net/npm/v/@react-nano/redux-dynamic-modules?icon=npm)](https://www.npmjs.com/package/@react-nano/redux-dynamic-modules) 6 | [![Stars](https://flat.badgen.net/github/stars/lusito/react-nano?icon=github)](https://github.com/lusito/react-nano) 7 | [![Watchers](https://flat.badgen.net/github/watchers/lusito/react-nano?icon=github)](https://github.com/lusito/react-nano) 8 | 9 | Making [redux-dynamic-modules](https://github.com/microsoft/redux-dynamic-modules) more lightweight by using `@react-nano/redux` instead of `react-redux`. 10 | Written in TypeScript. 11 | 12 | ## Why Use @react-nano/redux-dynamic-modules? 13 | 14 | - Very lightweight (see the badges above for the latest size). 15 | - It still uses redux-dynamic-modules(-core) under the hood (as a peer dependency), so you'll stay up to date with the latest features and bugfixes! 16 | - All it does is supply a different `DynamicModuleLoader` component which leverages the power of hooks in combination with `@react-nano/redux`. 17 | - All other imports can be taken from `redux-dynamic-modules-core` instead of `redux-dynamic-modules`. 18 | - Only has four peer dependencies: 19 | - React 17.0.0 or higher 20 | - Redux 4.0.0 or higher 21 | - @react-nano/redux in the same version 22 | - redux-dynamic-modules-core 5.0.0 or higher 23 | - Liberal license: [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) 24 | 25 | Note: Since this library uses `@react-nano/redux`, your code also needs to be using `@react-nano/redux` (otherwise you'd be using `redux-dynamic-modules`). 26 | 27 | ## How to Use 28 | 29 | Check out the [documentation](https://lusito.github.io/react-nano/redux-dynamic-modules/setup.html) 30 | 31 | ## Report Issues 32 | 33 | Something not working quite as expected? Do you need a feature that has not been implemented yet? Check the [issue tracker](https://github.com/Lusito/react-nano/issues) and add a new one if your problem is not already listed. Please try to provide a detailed description of your problem, including the steps to reproduce it. 34 | 35 | ## Contribute 36 | 37 | Awesome! If you would like to contribute with a new feature or submit a bugfix, fork this repo and send a pull request. Please, make sure all the unit tests are passing before submitting and add new ones in case you introduced new features. 38 | 39 | ## License 40 | 41 | @react-nano has been released under the [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) license, meaning you 42 | can use it free of charge, without strings attached in commercial and non-commercial projects. Credits are appreciated but not mandatory. 43 | -------------------------------------------------------------------------------- /packages/redux-dynamic-modules/docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | This library is shipped as es2015 modules. To use them in browsers, you'll have to transpile them using webpack or similar, which you probably already do. 4 | 5 | ## Install via NPM 6 | 7 | ```bash 8 | npm i @react-nano/redux-dynamic-modules 9 | ``` 10 | 11 | ## DynamicModuleLoader 12 | 13 | It works just like the original. You only need to adjust the import statement: 14 | 15 | ```tsx 16 | import { DynamicModuleLoader } from "@react-nano/redux-dynamic-modules"; 17 | export const MyComponent = () => ....; 18 | ``` 19 | -------------------------------------------------------------------------------- /packages/redux-dynamic-modules/mono-docs.yml: -------------------------------------------------------------------------------- 1 | title: redux-dynamic-modules 2 | description: Making redux-dynamic-modules more lightweight by using @react-nano/redux instead of react-redux. 3 | keywords: 4 | - react 5 | - redux 6 | - redux-dynamic-modules 7 | sidebar: 8 | - "setup" 9 | -------------------------------------------------------------------------------- /packages/redux-dynamic-modules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-nano/redux-dynamic-modules", 3 | "version": "0.16.0", 4 | "homepage": "https://lusito.github.io/react-nano/", 5 | "bugs": { 6 | "url": "https://github.com/Lusito/react-nano/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Lusito/react-nano.git" 11 | }, 12 | "license": "Zlib", 13 | "type": "module", 14 | "main": "dist/index.js", 15 | "types": "dist/index.d.ts", 16 | "scripts": { 17 | "build": "rimraf dist && tsc" 18 | }, 19 | "devDependencies": { 20 | "@react-nano/redux": "^0.16.0", 21 | "@types/react": "^19.0.10", 22 | "react": "^19.0.0", 23 | "redux": "^5.0.1", 24 | "redux-dynamic-modules-core": "^5.2.3", 25 | "rimraf": "^6.0.1", 26 | "typescript": "^5.8.2" 27 | }, 28 | "peerDependencies": { 29 | "@react-nano/redux": "^0.16.0", 30 | "react": "^17.0.0 || ^18.0.0 || ^19.0.0", 31 | "redux": "^4.0.0 || ^5.0.0", 32 | "redux-dynamic-modules-core": "^5.0.0" 33 | }, 34 | "publishConfig": { 35 | "access": "public" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/redux-dynamic-modules/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, PropsWithChildren, useState, FC } from "react"; 2 | import { IModuleStore, IModuleTuple } from "redux-dynamic-modules-core"; 3 | import { useStore } from "@react-nano/redux"; 4 | 5 | /** Provide modules to load */ 6 | export interface DynamicModuleLoaderProps { 7 | /** Modules that need to be dynamically registered */ 8 | modules: IModuleTuple; 9 | } 10 | 11 | /** 12 | * The DynamicModuleLoader adds a way to register a module on mount 13 | * When this component is initialized, the reducer and saga from the module passed as props will be registered with the system 14 | * On unmount, they will be unregistered 15 | */ 16 | export const DynamicModuleLoader: FC> = ({ modules, children }) => { 17 | const store = useStore() as IModuleStore; 18 | const [active, setActive] = useState(false); 19 | useEffect(() => { 20 | const added = store.addModules(modules); 21 | setActive(true); 22 | return () => added.remove(); 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, [store]); 25 | // eslint-disable-next-line react/jsx-no-useless-fragment 26 | return active ? <>{children} : null; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/redux-dynamic-modules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@lusito/tsconfig/base", "@lusito/tsconfig/react"], 3 | "compilerOptions": { 4 | "outDir": "./dist/" 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/redux/README.md: -------------------------------------------------------------------------------- 1 | # @react-nano/redux 2 | 3 | [![License](https://flat.badgen.net/github/license/lusito/react-nano?icon=github)](https://github.com/Lusito/react-nano/blob/master/LICENSE) 4 | [![Minified + gzipped size](https://flat.badgen.net/bundlephobia/minzip/@react-nano/redux?icon=dockbit)](https://bundlephobia.com/result?p=@react-nano/redux) 5 | [![NPM version](https://flat.badgen.net/npm/v/@react-nano/redux?icon=npm)](https://www.npmjs.com/package/@react-nano/redux) 6 | [![Stars](https://flat.badgen.net/github/stars/lusito/react-nano?icon=github)](https://github.com/lusito/react-nano) 7 | [![Watchers](https://flat.badgen.net/github/watchers/lusito/react-nano?icon=github)](https://github.com/lusito/react-nano) 8 | 9 | A simple, lightweight react-redux alternative, written in TypeScript. 10 | 11 | ## Why Use @react-nano/redux? 12 | 13 | - Very lightweight (see the badges above for the latest size). 14 | - All hooks are compatible to react-redux 15 | - Only has two peer dependencies: 16 | - React 17.0.0 or higher 17 | - Redux 4.0.0 or higher 18 | - Using hooks to access redux in react is soo much cleaner than using react-redux's `connect` higher order component. 19 | - Liberal license: [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) 20 | 21 | ## How to Use 22 | 23 | Check out the [documentation](https://lusito.github.io/react-nano/redux/setup.html) 24 | 25 | ## Report Issues 26 | 27 | Something not working quite as expected? Do you need a feature that has not been implemented yet? Check the [issue tracker](https://github.com/Lusito/react-nano/issues) and add a new one if your problem is not already listed. Please try to provide a detailed description of your problem, including the steps to reproduce it. 28 | 29 | ## Contribute 30 | 31 | Awesome! If you would like to contribute with a new feature or submit a bugfix, fork this repo and send a pull request. Please, make sure all the unit tests are passing before submitting and add new ones in case you introduced new features. 32 | 33 | ## License 34 | 35 | @react-nano has been released under the [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) license, meaning you 36 | can use it free of charge, without strings attached in commercial and non-commercial projects. Credits are appreciated but not mandatory. 37 | -------------------------------------------------------------------------------- /packages/redux/docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | ## `useDispatch` 4 | 5 | Use the hook to dispatch actions like this: 6 | 7 | ```tsx 8 | import { useDispatch } from "@react-nano/redux"; 9 | export const MyComponent = () => { 10 | const dispatch = useDispatch(); 11 | 12 | return ; 13 | }; 14 | ``` 15 | 16 | ## `useSelector` 17 | 18 | Use the hook to get a state property: 19 | 20 | ```tsx 21 | import { useSelector } from "@react-nano/redux"; 22 | 23 | const selectTitle = (state: State) => state.title; 24 | 25 | export const MyComponent = () => { 26 | const title = useSelector(selectTitle); 27 | 28 | return

{title}

; 29 | }; 30 | ``` 31 | 32 | ### Custom Comparison Function 33 | 34 | useSelector will detect changes in the value returned by the selector function by comparing the old value and the new value by reference. Only if they differ, the component will be re-rendered. 35 | 36 | If you want more control, you can pass in a comparison function: 37 | 38 | ```tsx 39 | import { useSelector } from "@react-nano/redux"; 40 | 41 | const selectUsers = (state: State) => state.users; 42 | 43 | const sameMembersInArray = (a: User[], b: User[]) => { 44 | if (a.length !== b.length) return false; 45 | return a.every((value, index) => value === b[index]); 46 | }; 47 | 48 | export const MyComponent = () => { 49 | const users = useSelector(selectUsers, sameMembersInArray); 50 | 51 | return ( 52 | 57 | ); 58 | }; 59 | ``` 60 | 61 | ## `useStore` 62 | 63 | In some rare occasions, you might want to access the store object itself: 64 | 65 | ```tsx 66 | import { useStore } from "@react-nano/redux"; 67 | export const MyComponent = () => { 68 | const store = useStore(); 69 | // ... 70 | }; 71 | ``` 72 | -------------------------------------------------------------------------------- /packages/redux/docs/migrating-from-react-redux.md: -------------------------------------------------------------------------------- 1 | # Migrating From react-redux 2 | 3 | This library defines a different provider, which works the same way, but it does not provide the redux store to `react-redux`. 4 | So using the original hooks and connect functions from `react-redux` won't work. 5 | 6 | That is easily fixed though: If you want to gradually move code from `react-redux` to `@react-nano/redux`, simply add one `Provider` for each library: 7 | 8 | ```tsx 9 | import { Provider } from "@react-nano/redux"; 10 | import { Provider as LegacyProvider } from "react-redux"; 11 | export const App = () => ( 12 | 13 | ...your app content... 14 | 15 | ); 16 | ``` 17 | 18 | Now all you need to do is migrate your components to use hooks instead of `connect()`. If you are already using hooks, then it's just a matter of replacing the import from react-redux to @react-nano/redux! 19 | -------------------------------------------------------------------------------- /packages/redux/docs/provider.md: -------------------------------------------------------------------------------- 1 | # Provider 2 | 3 | ## Adding a Provider 4 | 5 | In order to get access to your redux store, you'll need to wrap your app in a (single) provider like this: 6 | 7 | ```tsx 8 | import { Provider } from "@react-nano/redux"; 9 | export const App = () => ...; 10 | ``` 11 | -------------------------------------------------------------------------------- /packages/redux/docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | This library is shipped as es2015 modules. To use them in browsers, you'll have to transpile them using webpack or similar, which you probably already do. 4 | 5 | ## Install via NPM 6 | 7 | ```bash 8 | npm i @react-nano/redux 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/redux/mono-docs.yml: -------------------------------------------------------------------------------- 1 | title: redux 2 | description: A simple, lightweight react-redux alternative, written in TypeScript. 3 | keywords: 4 | - react 5 | - redux 6 | - hooks 7 | sidebar: 8 | - "setup" 9 | - "provider" 10 | - "hooks" 11 | - "migrating-from-react-redux" 12 | -------------------------------------------------------------------------------- /packages/redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-nano/redux", 3 | "version": "0.16.0", 4 | "homepage": "https://lusito.github.io/react-nano/", 5 | "bugs": { 6 | "url": "https://github.com/Lusito/react-nano/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Lusito/react-nano.git" 11 | }, 12 | "license": "Zlib", 13 | "type": "module", 14 | "main": "dist/index.js", 15 | "types": "dist/index.d.ts", 16 | "scripts": { 17 | "build": "rimraf dist && tsc" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^19.0.10", 21 | "react": "^19.0.0", 22 | "redux": "^5.0.1", 23 | "rimraf": "^6.0.1", 24 | "typescript": "^5.8.2", 25 | "use-sync-external-store": "^1.4.0" 26 | }, 27 | "peerDependencies": { 28 | "react": "^17.0.0 || ^18.0.0 || ^19.0.0", 29 | "redux": "^4.0.0 || ^5.0.0", 30 | "use-sync-external-store": "^1.2.0" 31 | }, 32 | "publishConfig": { 33 | "access": "public" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/redux/src/index.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { useSyncExternalStore } from "use-sync-external-store/shim"; 3 | import { PropsWithChildren, createContext, useContext, useRef, FC } from "react"; 4 | import { Store, AnyAction, Action } from "redux"; 5 | 6 | const ReduxContext = createContext(null); 7 | /** A provider receives a store and children */ 8 | export type ProviderProps = PropsWithChildren<{ 9 | /** The store object to provide to all nested components */ 10 | store: Store; 11 | }>; 12 | 13 | /** 14 | * The redux store provider 15 | * @param props store and children 16 | */ 17 | export const Provider: FC = ({ store, children }) => ( 18 | {children} 19 | ); 20 | 21 | /** 22 | * A hook to access the redux store 23 | * @throws When a `Provider` is missing. 24 | */ 25 | export function useStore() { 26 | const store = useContext(ReduxContext); 27 | if (!store) { 28 | throw new Error("Could not find react redux context. Make sure you've added a Provider."); 29 | } 30 | return store as unknown as Store; 31 | } 32 | 33 | /** Compare by reference */ 34 | export function compareRef(a: T, b: T) { 35 | return a === b; 36 | } 37 | 38 | /** 39 | * A hook to use a selector function to access redux state. 40 | * @param selector The selector function to retrieve a value from the redux store. It receives the current redux state. 41 | * @param compare The comparison function to use in order to only trigger a fresh render when something changed. Default compare by reference. 42 | * @throws When a `Provider` is missing. 43 | */ 44 | export function useSelector( 45 | selector: (state: TState) => TResult, 46 | compare: (a: TResult, b: TResult) => boolean = compareRef, 47 | ) { 48 | const store = useStore(); 49 | const cache = useRef<{ value: TResult } | null>(null); 50 | 51 | return useSyncExternalStore(store.subscribe, () => { 52 | const value = selector(store.getState()); 53 | if (!cache.current || !compare(cache.current.value, value)) { 54 | cache.current = { value }; 55 | } 56 | 57 | return cache.current.value; 58 | }); 59 | } 60 | 61 | /** 62 | * A hook to get the redux stores dispatch function. 63 | * @throws When a `Provider` is missing. 64 | */ 65 | export function useDispatch() { 66 | return useStore().dispatch; 67 | } 68 | -------------------------------------------------------------------------------- /packages/redux/src/use-sync-external-store.d.ts: -------------------------------------------------------------------------------- 1 | declare module "use-sync-external-store/shim" { 2 | export function useSyncExternalStore( 3 | subscribe: (onStoreChange: () => void) => () => void, 4 | getSnapshot: () => T, 5 | getServerSnapshot?: () => T, 6 | ): T; 7 | } 8 | -------------------------------------------------------------------------------- /packages/redux/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@lusito/tsconfig/base", "@lusito/tsconfig/react"], 3 | "compilerOptions": { 4 | "outDir": "./dist/" 5 | }, 6 | "include": ["./src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/router/README.md: -------------------------------------------------------------------------------- 1 | # @react-nano/router 2 | 3 | [![License](https://flat.badgen.net/github/license/lusito/react-nano?icon=github)](https://github.com/Lusito/react-nano/blob/master/LICENSE) 4 | [![Minified + gzipped size](https://flat.badgen.net/bundlephobia/minzip/@react-nano/router?icon=dockbit)](https://bundlephobia.com/result?p=@react-nano/router) 5 | [![NPM version](https://flat.badgen.net/npm/v/@react-nano/router?icon=npm)](https://www.npmjs.com/package/@react-nano/router) 6 | [![Stars](https://flat.badgen.net/github/stars/lusito/react-nano?icon=github)](https://github.com/lusito/react-nano) 7 | [![Watchers](https://flat.badgen.net/github/watchers/lusito/react-nano?icon=github)](https://github.com/lusito/react-nano) 8 | 9 | A simple, lightweight react router using hooks, written in TypeScript. 10 | 11 | ## Why Use @react-nano/router? 12 | 13 | - Very lightweight (see the badges above for the latest size). 14 | - Flexible and dead simple to use. 15 | - Uses the browsers history API (no bulky polyfill). 16 | - Does not force a matching algorithm on you. It's up to you! 17 | - Comes with a simple (one-liner) matching algorithm built-in for simple use-cases. 18 | - Written with [hooks](https://reactjs.org/docs/hooks-intro.html) in TypeScript 19 | - Only has one peer dependency: React 17.0.0 or higher. 20 | - Liberal license: [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) 21 | 22 | ## Example 23 | 24 | A small example might look like this: 25 | 26 | ```tsx 27 | import { Router } from "@react-nano/router"; 28 | export const App = () => ( 29 | 30 | 31 | 32 | 33 | {/* use "(.*)" instead of "*" if you use path-to-regexp */} 34 | 35 | 36 | 37 | ); 38 | ``` 39 | 40 | ## How to Use 41 | 42 | Check out the [documentation](https://lusito.github.io/react-nano/router/setup.html) 43 | 44 | ## Report Issues 45 | 46 | Something not working quite as expected? Do you need a feature that has not been implemented yet? Check the [issue tracker](https://github.com/Lusito/react-nano/issues) and add a new one if your problem is not already listed. Please try to provide a detailed description of your problem, including the steps to reproduce it. 47 | 48 | ## Contribute 49 | 50 | Awesome! If you would like to contribute with a new feature or submit a bugfix, fork this repo and send a pull request. Please, make sure all the unit tests are passing before submitting and add new ones in case you introduced new features. 51 | 52 | ## License 53 | 54 | @react-nano has been released under the [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) license, meaning you 55 | can use it free of charge, without strings attached in commercial and non-commercial projects. Credits are appreciated but not mandatory. 56 | -------------------------------------------------------------------------------- /packages/router/docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | ## `useParams` 4 | 5 | In some cases you want to extract parameters without being in a `Route` component. 6 | You can get a memoized parameters object for the given path like this: 7 | 8 | ```tsx 9 | export const Component = () => { 10 | const params = useParams<{ id: string }>("/news/:id"); 11 | //... 12 | }; 13 | ``` 14 | 15 | ## `useRouter` 16 | 17 | `Router` internally adds a RouterContext to your application, which you can access using the `useRouter()` hook: 18 | 19 | ```tsx 20 | import { useRouter } from "@react-nano/router"; 21 | ... 22 | export function Component() { 23 | // router is of type RouterContextValue (see below) 24 | const router = useRouter(); 25 | .... 26 | } 27 | ``` 28 | 29 | `RouterContextValue` is defined as: 30 | 31 | ```tsx 32 | export interface RouterContextValue { 33 | basename: string; 34 | path: string; 35 | history: RouterHistory; 36 | matchRoute: CachedRouteMatcher; 37 | urlTo: (path: string) => string; 38 | } 39 | // with: 40 | export interface RouterHistory { 41 | push: (path: string) => void; 42 | replace: (path: string) => void; 43 | stop: () => void; // for internal use, do not call. 44 | urlTo: (path: string) => string; 45 | } 46 | // and: 47 | export type CachedRouteMatcher = (pattern: string, path: string) => RouteParams | null; 48 | ``` 49 | 50 | `urlTo()` can be used to create a new url, which respects `basename` and `mode`. It's the same for both `RouterContextValue` and `RouterHistory`. 51 | -------------------------------------------------------------------------------- /packages/router/docs/links.md: -------------------------------------------------------------------------------- 1 | # Links, Etc. 2 | 3 | ## Link 4 | 5 | The `Link` component can be used to change the url and still act as a normal `` tag, so you can open the link in a new tab. 6 | 7 | ```tsx 8 | export const Component = () => Test; 9 | ``` 10 | 11 | Any extra props you pass in will be forwarded to the `` element. If you specify an `onClick` property and it calls `preventDefault()`, then the history change will not happen, as would be the case with any normal link. 12 | 13 | ## LinkButton, Etc. 14 | 15 | If you want to create a LinkButton or similar, you can do that easily. This is the implementation of Link: 16 | 17 | ```tsx 18 | export function Link(props: React.PropsWithChildren) { 19 | const routeLink = useRouteLink(props.href, props.onClick); 20 | return ; 21 | } 22 | ``` 23 | 24 | Creating a `LinkButton` is as simple as this: 25 | 26 | ```tsx 27 | import { useRouteLink } from "@react-nano/router"; 28 | ... 29 | export function LinkButton(props: React.PropsWithChildren) { 30 | const routeLink = useRouteLink(props.href, props.onClick); 31 | return 54 | 55 | ); 56 | } 57 | ``` 58 | 59 | There's a lot more going on here: 60 | 61 | - In addition to getting the user, which we already did in the first example, 62 | - We're also using the `useUpdateUserFetch` hook. No `autoSubmit` config means we need to call it manually. 63 | - The second entry in the returned array is a submit function, which you can call to manually (re-)submit the request. 64 | - The server returns a validation hashmap in case of an error (implementation is up to you). 65 | - We're using some pseudo UI library to define our user form: 66 | - onSubmit is passed on to the `
` element, so we get notified of submits. 67 | - On submit, we create a new FormData object from the `` element. 68 | - The biggest advantage of this is that you don't need to connect all of your input elements to your components state. 69 | - When an error happened, we try to show some information about it. See [API](./api.md) for more information on the state values. 70 | 71 | In case you are wondering about the implementations of `ErrorMessageForState` and `getValidationErrors`, here they are: 72 | 73 | ```tsx 74 | interface ErrorMessageForStateProps { 75 | state: FetchState; 76 | } 77 | 78 | export function ErrorMessageForState({ state }: ErrorMessageForStateProps) { 79 | switch (state.state) { 80 | case "error": 81 | return
Error {state.error.error}
; 82 | case "exception": 83 | return
Error {state.error.message}
; 84 | default: 85 | return null; 86 | } 87 | } 88 | 89 | export function getValidationErrors(state: FetchState) { 90 | return (state.state === "error" && state.error.validation_errors) || {}; 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /packages/use-fetch/docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | This library is shipped as es2017 modules. To use them in browsers, you'll have to transpile them using webpack or similar, which you probably already do. 4 | 5 | ## Install via NPM 6 | 7 | ```bash 8 | npm i @react-nano/use-fetch 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/use-fetch/mono-docs.yml: -------------------------------------------------------------------------------- 1 | title: "use-fetch" 2 | description: Lightweight fetching hooks for react, written in TypeScript. 3 | keywords: 4 | - react 5 | - hooks 6 | - fetch 7 | sidebar: 8 | - "setup" 9 | - "get" 10 | - "modify" 11 | - "api" 12 | - "config" 13 | - "helpers" 14 | -------------------------------------------------------------------------------- /packages/use-fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-nano/use-fetch", 3 | "version": "0.16.0", 4 | "description": "A lightweight fetching hook for react, written in TypeScript", 5 | "keywords": [ 6 | "TypeScript", 7 | "react", 8 | "fetch", 9 | "hooks", 10 | "react-hooks" 11 | ], 12 | "homepage": "https://lusito.github.io/react-nano/", 13 | "bugs": { 14 | "url": "https://github.com/Lusito/react-nano/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/Lusito/react-nano.git" 19 | }, 20 | "license": "Zlib", 21 | "author": "Santo Pfingsten", 22 | "main": "dist/index.js", 23 | "types": "dist/index.d.ts", 24 | "files": [ 25 | "dist/" 26 | ], 27 | "scripts": { 28 | "build": "rimraf dist && tsc" 29 | }, 30 | "devDependencies": { 31 | "@types/react": "^19.0.10", 32 | "react": "^19.0.0", 33 | "rimraf": "^6.0.1", 34 | "typescript": "^5.8.2" 35 | }, 36 | "peerDependencies": { 37 | "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 38 | }, 39 | "publishConfig": { 40 | "access": "public" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/use-fetch/src/helpers.ts: -------------------------------------------------------------------------------- 1 | export interface FetchRequestInit extends RequestInit { 2 | credentials: RequestCredentials; 3 | readonly headers: Headers; 4 | readonly signal: AbortSignal; 5 | } 6 | 7 | export type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE" | "HEAD" | "CONNECT" | "OPTIONS" | "TRACE"; 8 | 9 | export function prepareInit(init: FetchRequestInit, method: HttpMethod) { 10 | init.method = method; 11 | init.credentials = "include"; 12 | init.headers.set("Accept", "application/json"); 13 | } 14 | 15 | export const prepareGet = (init: FetchRequestInit) => prepareInit(init, "GET"); 16 | 17 | export const preparePost = (init: FetchRequestInit) => prepareInit(init, "POST"); 18 | 19 | export const preparePatch = (init: FetchRequestInit) => prepareInit(init, "PATCH"); 20 | 21 | export const preparePut = (init: FetchRequestInit) => prepareInit(init, "PUT"); 22 | 23 | export const prepareDelete = (init: FetchRequestInit) => prepareInit(init, "DELETE"); 24 | 25 | export function preparePostUrlEncoded(init: FetchRequestInit) { 26 | preparePost(init); 27 | init.headers.set("Content-Type", "application/x-www-form-urlencoded"); 28 | } 29 | 30 | export function prepareFormDataPost(init: FetchRequestInit, formData: FormData) { 31 | const entries = Array.from(formData.entries()); 32 | if (entries.some((entry) => entry[1] instanceof File)) { 33 | preparePost(init); 34 | init.body = formData; 35 | } else { 36 | preparePostUrlEncoded(init); 37 | init.body = entries.map(([key, value]) => `${key}=${encodeURIComponent(value.toString())}`).join("&"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/use-fetch/src/index.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useContext, useMemo, useReducer, createContext } from "react"; 2 | 3 | import { FetchRequestInit } from "./helpers"; 4 | 5 | export * from "./helpers"; 6 | 7 | const FetchGlobalConfigContext = createContext, unknown>>({}); 8 | export const FetchGlobalConfigProvider = FetchGlobalConfigContext.Provider; 9 | 10 | export interface FetchResponseInfo { 11 | /** The status code of the response */ 12 | responseStatus: number; 13 | /** The headers of the response */ 14 | responseHeaders: Headers; 15 | } 16 | 17 | export interface FetchStateBase { 18 | /** Request is currently in progress */ 19 | loading: boolean; 20 | /** Either an exception occurred or the request returned an error */ 21 | failed: boolean; 22 | /** Request was successful */ 23 | success: boolean; 24 | } 25 | 26 | export interface FetchStateEmpty extends FetchStateBase { 27 | state: "empty"; 28 | failed: false; 29 | success: false; 30 | } 31 | 32 | export interface FetchStateDone extends FetchStateBase, FetchResponseInfo {} 33 | 34 | export interface FetchStateDoneSuccess extends FetchStateDone { 35 | failed: false; 36 | success: true; 37 | /** Data is present */ 38 | state: "success"; 39 | /** The response data in case of success */ 40 | data: TData; 41 | } 42 | 43 | export interface FetchStateDoneError extends FetchStateDone { 44 | failed: true; 45 | success: false; 46 | /** Errors is present */ 47 | state: "error"; 48 | /** The server result data. */ 49 | error: TError; 50 | } 51 | 52 | export interface FetchStateDoneException extends FetchStateBase { 53 | failed: true; 54 | success: false; 55 | /** Errors is present */ 56 | state: "exception"; 57 | /** The cause of the exception. */ 58 | error: Error; 59 | } 60 | 61 | export type FetchState = 62 | | FetchStateEmpty 63 | | FetchStateDoneSuccess 64 | | FetchStateDoneError 65 | | FetchStateDoneException; 66 | 67 | export interface CallbackContext { 68 | /** The data you used to submit the request */ 69 | inputData: TVars; 70 | } 71 | 72 | export interface CallbackContextWithResponse extends CallbackContext { 73 | /** The status code of the request */ 74 | status: number; 75 | /** The response headers headers of the request */ 76 | responseHeaders: Headers; 77 | } 78 | 79 | export interface OnSuccessContext extends CallbackContextWithResponse { 80 | /** The result of the fetch */ 81 | data: TData; 82 | } 83 | 84 | export interface OnErrorContext extends CallbackContextWithResponse { 85 | /** The error data the server returned for the fetch */ 86 | error: TError; 87 | } 88 | 89 | export interface OnExceptionContext extends CallbackContext { 90 | /** The error that was thrown. */ 91 | error: Error; 92 | } 93 | 94 | export interface FetchConfig { 95 | /** 96 | * Called right before a request will be made. Use it to extend the request with additional information like authorization headers. 97 | * 98 | * @param init The request data to be send. 99 | */ 100 | onInit?(init: FetchRequestInit): void; 101 | 102 | /** 103 | * Called on successful request with the result 104 | * 105 | * @param context Information about the request 106 | */ 107 | onSuccess?(context: OnSuccessContext): void; 108 | 109 | /** 110 | * Called on server error 111 | * 112 | * @param context Information about the request 113 | */ 114 | onError?(context: OnErrorContext): void; 115 | 116 | /** 117 | * Called when an exception happened in the frontend 118 | * 119 | * @param context Information about the request 120 | */ 121 | onException?(context: OnExceptionContext): void; 122 | } 123 | 124 | export type VariableType = null | Record; 125 | 126 | export interface FetchLocalConfig extends FetchConfig { 127 | /** Specify to cause the request to be submitted automatically */ 128 | autoSubmit?: TVars extends null ? true : TVars; 129 | } 130 | 131 | export interface FetchInitializerBase { 132 | /** 133 | * Called on successful request with the result. Use to specify the result type 134 | * 135 | * @param response The response 136 | */ 137 | getResult: (response: Response) => Promise; 138 | 139 | /** 140 | * Called on server error 141 | * 142 | * @param response The response 143 | */ 144 | getError: (response: Response) => Promise; 145 | } 146 | 147 | export interface FetchInitializerNoData extends FetchInitializerBase { 148 | /** 149 | * Called right before a request will be made. Use it to extend the request with additional information like authorization headers. 150 | * 151 | * @param init The request data to be send. 152 | * @returns The url to fetch 153 | */ 154 | prepare: (init: FetchRequestInit) => string; 155 | } 156 | 157 | export interface FetchInitializerWithData> 158 | extends FetchInitializerBase { 159 | /** 160 | * Called right before a request will be made. Use it to extend the request with additional information like authorization headers. 161 | * 162 | * @param init The request data to be send. 163 | * @param data The data passed in via submit or autoSubmit 164 | * @returns The url to fetch 165 | */ 166 | prepare: (init: FetchRequestInit, data: TVars) => string; 167 | } 168 | 169 | interface FetchActionLoading { 170 | type: "loading"; 171 | value: boolean; 172 | } 173 | interface FetchActionSuccess extends FetchResponseInfo { 174 | type: "success"; 175 | data: TData; 176 | } 177 | interface FetchActionError extends FetchResponseInfo { 178 | type: "error"; 179 | error: TError; 180 | } 181 | interface FetchActionException { 182 | type: "exception"; 183 | error: Error; 184 | } 185 | 186 | type FetchAction = 187 | | FetchActionLoading 188 | | FetchActionSuccess 189 | | FetchActionError 190 | | FetchActionException; 191 | 192 | function stateReducer( 193 | state: FetchState, 194 | action: FetchAction, 195 | ): FetchState { 196 | switch (action.type) { 197 | case "loading": 198 | return { 199 | ...state, 200 | loading: action.value, 201 | }; 202 | case "success": 203 | return { 204 | failed: false, 205 | success: true, 206 | state: "success", 207 | loading: false, 208 | data: action.data, 209 | responseHeaders: action.responseHeaders, 210 | responseStatus: action.responseStatus, 211 | }; 212 | case "error": 213 | return { 214 | failed: true, 215 | success: false, 216 | state: "error", 217 | loading: false, 218 | error: action.error, 219 | responseHeaders: action.responseHeaders, 220 | responseStatus: action.responseStatus, 221 | }; 222 | case "exception": 223 | return { 224 | failed: true, 225 | success: false, 226 | state: "exception", 227 | loading: false, 228 | error: action.error, 229 | }; 230 | } 231 | return state; 232 | } 233 | 234 | class FetchInstance { 235 | public globalConfig?: FetchConfig; 236 | 237 | public config?: FetchConfig; 238 | 239 | public mounted = true; 240 | 241 | private initializer: TVars extends null 242 | ? FetchInitializerNoData 243 | : FetchInitializerWithData; 244 | 245 | private controller?: AbortController; 246 | 247 | private updateState: (action: FetchAction) => void; 248 | 249 | public constructor( 250 | initializer: TVars extends null 251 | ? FetchInitializerNoData 252 | : FetchInitializerWithData, 253 | updateState: (action: FetchAction) => void, 254 | ) { 255 | this.initializer = initializer; 256 | this.updateState = updateState; 257 | } 258 | 259 | public abort = () => { 260 | if (this.controller) { 261 | this.controller.abort(); 262 | this.controller = undefined; 263 | this.mounted && this.updateState({ type: "loading", value: false }); 264 | } 265 | }; 266 | 267 | public submit = (requestData: TVars) => { 268 | this.submitAsync(requestData); 269 | }; 270 | 271 | private async submitAsync(requestData: TVars) { 272 | if (!this.mounted) return; 273 | 274 | const globalConfig = this.globalConfig ?? {}; 275 | const config = this.config ?? {}; 276 | const { initializer } = this; 277 | 278 | let responseStatus = -1; 279 | try { 280 | this.controller?.abort(); 281 | this.controller = new AbortController(); 282 | this.updateState({ type: "loading", value: true }); 283 | const init: FetchRequestInit = { 284 | credentials: "include", 285 | headers: new Headers(), 286 | signal: this.controller.signal, 287 | }; 288 | const url = initializer.prepare(init, requestData); 289 | const response = await fetch(url, init); 290 | 291 | responseStatus = response.status; 292 | 293 | if (response.ok) { 294 | const data = await initializer.getResult(response); 295 | if (!this.mounted) return; 296 | const context = { 297 | inputData: requestData, 298 | data, 299 | status: responseStatus, 300 | responseHeaders: response.headers, 301 | }; 302 | globalConfig.onSuccess?.(context); 303 | if (!this.mounted) return; 304 | config.onSuccess?.(context); 305 | if (!this.mounted) return; 306 | this.updateState({ 307 | type: "success", 308 | responseStatus: response.status, 309 | responseHeaders: response.headers, 310 | data, 311 | }); 312 | } else { 313 | const error = await initializer.getError(response); 314 | if (!this.mounted) return; 315 | const context = { 316 | inputData: requestData, 317 | error, 318 | status: responseStatus, 319 | responseHeaders: response.headers, 320 | }; 321 | globalConfig.onError?.(context); 322 | if (!this.mounted) return; 323 | config.onError?.(context); 324 | if (!this.mounted) return; 325 | this.updateState({ 326 | type: "error", 327 | responseStatus: response.status, 328 | responseHeaders: response.headers, 329 | error, 330 | }); 331 | } 332 | } catch (error: any) { 333 | if (error.name !== "AbortError") { 334 | console.log(error); 335 | if (!this.mounted) return; 336 | const context = { 337 | inputData: requestData, 338 | error, 339 | }; 340 | globalConfig.onException?.(context); 341 | if (!this.mounted) return; 342 | config.onException?.(context); 343 | if (!this.mounted) return; 344 | this.updateState({ 345 | type: "exception", 346 | error, 347 | }); 348 | } 349 | } 350 | } 351 | } 352 | 353 | export type FetchSubmit = TVars extends null ? () => void : (vars: TVars) => void; 354 | 355 | export type FetchHook = ( 356 | config?: FetchLocalConfig, 357 | ) => [FetchState, FetchSubmit, () => void]; 358 | 359 | export function createFetchHook( 360 | initializer: FetchInitializerNoData, 361 | ): FetchHook; 362 | export function createFetchHook>( 363 | initializer: FetchInitializerWithData, 364 | ): FetchHook; 365 | export function createFetchHook(initializer: any) { 366 | return (config?: FetchLocalConfig) => { 367 | const autoSubmit = config?.autoSubmit; 368 | const [state, updateState] = useReducer(stateReducer, { 369 | failed: false, 370 | success: false, 371 | state: "empty", 372 | loading: !!autoSubmit, 373 | }); 374 | const instance = useMemo(() => new FetchInstance(initializer, updateState), []); 375 | instance.globalConfig = useContext(FetchGlobalConfigContext) as FetchConfig; 376 | instance.config = config; 377 | useLayoutEffect(() => { 378 | instance.mounted = true; 379 | if (autoSubmit === true) instance.submit(null); 380 | else if (autoSubmit) instance.submit(autoSubmit as TVars); 381 | 382 | return () => { 383 | instance.mounted = false; 384 | instance.abort(); 385 | }; 386 | // eslint-disable-next-line react-hooks/exhaustive-deps 387 | }, []); 388 | return [state, instance.submit, instance.abort] as any; 389 | }; 390 | } 391 | -------------------------------------------------------------------------------- /packages/use-fetch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@lusito/tsconfig/base", "@lusito/tsconfig/react"], 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/use-graphql/README.md: -------------------------------------------------------------------------------- 1 | # @react-nano/use-graphql 2 | 3 | [![License](https://flat.badgen.net/github/license/lusito/react-nano?icon=github)](https://github.com/Lusito/react-nano/blob/master/LICENSE) 4 | [![Minified + gzipped size](https://flat.badgen.net/bundlephobia/minzip/@react-nano/use-graphql?icon=dockbit)](https://bundlephobia.com/result?p=@react-nano/use-graphql) 5 | [![NPM version](https://flat.badgen.net/npm/v/@react-nano/use-graphql?icon=npm)](https://www.npmjs.com/package/@react-nano/use-graphql) 6 | [![Stars](https://flat.badgen.net/github/stars/lusito/react-nano?icon=github)](https://github.com/lusito/react-nano) 7 | [![Watchers](https://flat.badgen.net/github/watchers/lusito/react-nano?icon=github)](https://github.com/lusito/react-nano) 8 | 9 | A lightweight, type-safe graphql hook for react, written in TypeScript. 10 | 11 | ## Why Use @react-nano/use-graphql? 12 | 13 | - Very lightweight (see the badges above for the latest size). 14 | - Flexible and dead simple to use. 15 | - Written in TypeScript 16 | - Type-safe results (tested with [tsd](https://github.com/SamVerschueren/tsd)) 17 | - Autocompletion while writing query definitions 18 | - Only has one required peer dependency: React 17.0.0 or higher. 19 | - Liberal license: [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) 20 | 21 | This is no code-generator. It works purely by using **TypeScript 4.1** features. 22 | 23 | - **No Query Strings**\ 24 | Don't write query strings manually. Write TypeScript and get autocompletion for free! 25 | - **Type-Safe**\ 26 | Instead of getting the full interface as a result type from a query/mutation, you only get those fields you actually selected in your hook definition! 27 | - **Easy to Use**\ 28 | Write your types, define queries/mutations, use the hook, display data => done! 29 | 30 | This is a **Work In Progress**! The API might change before version 1.0 is released. 31 | 32 | ## Simple Example 33 | 34 | In your component file, define your customized GraphQL hook and use it in the component: 35 | 36 | ```tsx 37 | import React from "react"; 38 | import { graphQL } from "@react-nano/use-graphql"; 39 | import { UserDTO, ErrorDTO, queryUserVariableTypes } from "../types"; 40 | 41 | // No need to write the query as string. Write it in TypeScript and get autocompletion for free! 42 | const useUserQuery = graphQL 43 | .query("user") 44 | .with(queryUserVariableTypes) 45 | .createHook({ 46 | // These properties will be autocompleted based on the first type argument above 47 | name: true, 48 | icon: true, 49 | posts: { 50 | id: true, 51 | title: true, 52 | hits: true, 53 | }, 54 | }); 55 | 56 | export function UserSummary({ id }: UserSummaryProps) { 57 | // It is possible to supply the url globally using a provider 58 | // autoSubmit results in the request being send instantly. You can trigger it manually as well. 59 | const [userState] = useUserQuery({ url: "/graphql", autoSubmit: { id } }); 60 | 61 | // There is more state information available. This is just kept short for an overview! 62 | if (!userState.success) return
Loading
; 63 | 64 | // Unless you checked for userState.state === "success" (or userState.success), userState.data will not exist on the type. 65 | const user = userState.data; 66 | return ( 67 |
    68 |
  • Name: {user.name}
  • 69 |
  • 70 | Icon: User Icon 71 |
  • 72 |
  • Age: {user.age /* Error: No property 'age' on user! */}
  • 73 |
  • 74 | Posts: 75 |
      76 | {user.posts.map((post) => ( 77 |
    • 78 | {post.title} with {post.hits} hits 79 |
    • 80 | ))} 81 |
    82 |
  • 83 |
84 | ); 85 | } 86 | ``` 87 | 88 | In the above example, the type of `userState.data` is automatically created by inspecting the attribute choices specified in the fields definition of your hook. 89 | 90 | So, even though `UserDTO` specifies the properties `id` and `age` and `PostDTO` specifies `message` and `user`, they will not end up in the returned data type and will lead to a compile-time error when you try to access them. For all other properties you will get autocompletion and type-safety. 91 | 92 | To use the above example, you'll need to define your full types somewhere (i.e. all types and attributes that could possibly be requested): 93 | 94 | ```TypeScript 95 | import { GraphGLVariableTypes } from "@react-nano/use-graphql"; 96 | 97 | export interface ErrorDTO { 98 | message: string; 99 | } 100 | 101 | export interface PostDTO { 102 | id: number; 103 | title: string; 104 | message: string; 105 | hits: number; 106 | user: UserDTO; 107 | } 108 | 109 | export interface UserDTO { 110 | id: string; 111 | name: string; 112 | icon: string; 113 | age: number; 114 | posts: PostDTO[]; 115 | } 116 | 117 | export interface QueryUserVariables { 118 | id: string; 119 | } 120 | 121 | // Also specify GraphQL variable types as a constant like this: 122 | const queryUserVariableTypes: GraphGLVariableTypes = { 123 | // These will be autocompleted (and are required) based on the type argument above 124 | // The values here are the only place where you still need to write GraphQL types. 125 | id: "String!", 126 | }; 127 | 128 | ``` 129 | 130 | ## How to Use 131 | 132 | Check out the [documentation](https://lusito.github.io/react-nano/use-graphql/setup.html) 133 | 134 | ## Report Issues 135 | 136 | Something not working quite as expected? Do you need a feature that has not been implemented yet? Check the [issue tracker](https://github.com/Lusito/react-nano/issues) and add a new one if your problem is not already listed. Please try to provide a detailed description of your problem, including the steps to reproduce it. 137 | 138 | ## Contribute 139 | 140 | Awesome! If you would like to contribute with a new feature or submit a bugfix, fork this repo and send a pull request. Please, make sure all the unit tests are passing before submitting and add new ones in case you introduced new features. 141 | 142 | ## License 143 | 144 | @react-nano has been released under the [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) license, meaning you 145 | can use it free of charge, without strings attached in commercial and non-commercial projects. Credits are appreciated but not mandatory. 146 | -------------------------------------------------------------------------------- /packages/use-graphql/docs/builder.md: -------------------------------------------------------------------------------- 1 | # Hook Builder 2 | 3 | ## Specifying a Query or Mutation 4 | 5 | The `graphQL` builder pattern helps you to define a query or mutation [hook](./hook.md), which can then be used in your component. 6 | 7 | The first thing you do is define the type (query or mutation) and its name like this: 8 | 9 | ### Query 10 | 11 | ```typescript 12 | const useUserQuery = graphQL.query("user"); 13 | // ... see next steps 14 | ``` 15 | 16 | ### Mutation 17 | 18 | ```typescript 19 | const useUpdateUserMutation = graphQL.mutation, ErrorDTO>("updateUser"); 20 | // ...see next steps 21 | ``` 22 | 23 | As you can see, `graphQL.query` and `graphQL.mutation` also require two type arguments to be specified: 24 | 25 | - The full type that could be returned by the server if all fields had been selected 26 | - The error type that would be returned by the server in case of an error (the non-array form). 27 | 28 | ## Specifying Variable Types 29 | 30 | After that you can optionally specify variable types for this query/mutation: 31 | 32 | ```typescript 33 | // ...see previous step 34 | .with(queryUserVariableTypes) 35 | // ...see next step 36 | ``` 37 | 38 | See [setup](./setup.md) for the definition of `queryUserVariableTypes`. 39 | 40 | ## Creating the Hook 41 | 42 | The final step is to create a hook, which can then be used in your components. 43 | 44 | - If your request returns a primitive or an array of primitives like `number`, `string` or `string[]`, you obviously can't select any fields, so createHook takes no arguments. 45 | - Otherwise the first (and only) argument is an object with `true` for each attribute and an object for the relations you want to get returned (similarly to a GraphQL query string): 46 | 47 | ### Non-Primitive Return Type 48 | 49 | ```typescript 50 | // ...see previous steps 51 | .createHook({ 52 | // These properties will be autocompleted based on the first type argument of the query call above 53 | name: true, 54 | icon: true, 55 | posts: { 56 | id: true, 57 | title: true, 58 | hits: true, 59 | }, 60 | }); 61 | ``` 62 | 63 | ### Primitive Return Type 64 | 65 | ```typescript 66 | // ...see previous steps 67 | .createHook(); 68 | ``` 69 | 70 | ## Examples 71 | 72 | Here are two full examples: 73 | 74 | ### Complex Type With Variables 75 | 76 | ```TypeScript 77 | import { graphQL } from "@react-nano/use-graphql"; 78 | // See "Setup" section for these types 79 | import { UserDTO, ErrorDTO, queryUserVariableTypes } from '../types'; 80 | 81 | // No need to write the query as string. Write it in TypeScript and get autocompletion for free! 82 | const useUserQuery = graphQL 83 | .query("user") 84 | .with(queryUserVariableTypes) 85 | .createHook({ 86 | // These properties will be autocompleted based on the first type argument of the query call above 87 | name: true, 88 | icon: true, 89 | posts: { 90 | id: true, 91 | title: true, 92 | hits: true, 93 | } 94 | }); 95 | 96 | ``` 97 | 98 | ### Primitive Type Without Variables 99 | 100 | ```TypeScript 101 | import { graphQL } from "@react-nano/use-graphql"; 102 | 103 | const useUserNamesQuery = graphQL.query("userNames").createHook(); 104 | ``` 105 | -------------------------------------------------------------------------------- /packages/use-graphql/docs/config.md: -------------------------------------------------------------------------------- 1 | # Configurations 2 | 3 | It's possible to supply configurations both globally (via provider) and locally (as argument to your query/mutation hook). 4 | Both configurations have the same attributes, except that the local config has one additional property `autoSubmit`. 5 | 6 | ## Local Configuration 7 | 8 | Let's take a look at the local configuration first: 9 | 10 | ```TypeScript 11 | export interface GraphQLConfig, TVars> { 12 | /** The url to use. Defaults to "/graphql" if neither global nor local config specifies it */ 13 | url?: string; 14 | 15 | /** 16 | * Called right before a request will be made. Use it to extend the request with additional information like authorization headers. 17 | * 18 | * @param init The request data to be send. 19 | */ 20 | onInit?(init: RequestInit & GraphQLRequestInit): void; 21 | 22 | /** 23 | * Called on successful request with the result 24 | * 25 | * @param context Information about the request 26 | */ 27 | onSuccess?(context: OnSuccessContext): void; 28 | 29 | /** 30 | * Called on server error 31 | * 32 | * @param context Information about the request 33 | */ 34 | onError?(context: OnErrorContext): void; 35 | 36 | /** 37 | * Called when an exception happened in the frontend 38 | * 39 | * @param context Information about the request 40 | */ 41 | onException?(context: OnExceptionContext): void; 42 | } 43 | 44 | export interface GraphQLLocalConfig, TVars extends VariableType> 45 | extends GraphQLConfig { 46 | /** Specify to cause the request to be submitted automatically */ 47 | autoSubmit?: TVars extends null ? true : TVars; 48 | } 49 | ``` 50 | 51 | - All of the properties are optional. 52 | - Specify `autoSubmit` if you want to send the request on component mount without having to call its submit function manually. 53 | - The value is expected to be `true` for requests without variables and a variable object otherwise. 54 | - Specify callbacks for certain events instead of watching the state object in a useEffect Hook. 55 | - The hook will always use the latest version of the callbacks. 56 | - If the component making the request has been unmounted, the callbacks will not be called. 57 | - Take a look at the [hook description](hook.md) hook to see how to specify this config. 58 | 59 | ### Callback Context 60 | 61 | The context parameter has different properties depending on the callback: 62 | 63 | ```TypeScript 64 | export interface CallbackContext { 65 | /** The data you used to submit the request */ 66 | inputData: TVars; 67 | } 68 | 69 | export interface CallbackContextWithResponse extends CallbackContext { 70 | /** The status code of the request */ 71 | status: number; 72 | /** The response headers headers of the request */ 73 | responseHeaders: Headers; 74 | } 75 | 76 | export interface OnSuccessContext extends CallbackContextWithResponse { 77 | /** The result of the query/mutation */ 78 | data: TData; 79 | } 80 | 81 | export interface OnErrorContext extends CallbackContextWithResponse { 82 | /** The errors the server returned for the query/mutation */ 83 | errors: TError[]; 84 | } 85 | 86 | export interface OnExceptionContext extends CallbackContext { 87 | /** The error that was thrown. */ 88 | error: Error; 89 | } 90 | ``` 91 | 92 | ## Global Configuration 93 | 94 | You can specify a global configuration by wrapping your app content in a provider: 95 | 96 | ```tsx 97 | function MyApp { 98 | return ( 99 | 108 | ...The app content 109 | 110 | ); 111 | } 112 | ``` 113 | 114 | As said, the only difference between local and global configurations is the autoSubmit property in local configurations. 115 | 116 | - This means, that you can specify the url globally, but you can override it locally if you need to. 117 | - Callbacks on the other hand will run both globally and locally. 118 | - Keep in mind, that some of the types are different for each query/mutation, so you'll have to manually cast the data if you need access to it. 119 | - Most common scenarios for using the callbacks globally: 120 | - Adding authorization headers to the request 121 | - Logging and/or showing toast messages on success/error/exception events 122 | - Just as with local callbacks: If the component making the request has been unmounted, the callbacks will not be called. 123 | -------------------------------------------------------------------------------- /packages/use-graphql/docs/hook.md: -------------------------------------------------------------------------------- 1 | # Your GraphQL Hook 2 | 3 | ## Using the Hook 4 | 5 | The hook you build using [the hook builder](./builder.md) allows you to perform a GraphQL request from within a react component. 6 | 7 | It takes an optional [config](config.md) object and returns a tuple with 3 elements: 8 | 9 | - A state object containing information about the request and possibly the results or errors 10 | - A submit function if you want to run the request manually 11 | - An abort function if you want to abort the request manually (it will be aborted automatically when the component gets unmounted). 12 | 13 | All of this completely type-safe! 14 | 15 | ### Example 16 | 17 | ```tsx 18 | import { graphQL } from "@react-nano/use-graphql"; 19 | 20 | // See the examples in the "Hook Builder" section. 21 | const useUserQuery = graphQL.query("user")...; 22 | 23 | export function UserSummary({ id }: UserSummaryProps) { 24 | const [userState] = useUserQuery({ url: "/graphql", autoSubmit: { id } }); 25 | 26 | if (!userState.success) return
Loading
; 27 | 28 | const user = userState.data; 29 | return ( 30 |
    31 |
  • Name: {user.name}
  • 32 |
  • Icon: User Icon
  • 33 |
  • Posts: 34 |
      35 | {user.posts.map((post) => ( 36 |
    • {post.title} with {post.hits} hits
    • 37 | ))} 38 |
    39 |
  • 40 |
41 | ); 42 | } 43 | ``` 44 | 45 | ## The State Object 46 | 47 | The state object always has these properties: 48 | 49 | - `loading: boolean` => Request is currently in progress 50 | - `failed: boolean;` => Either an exception occurred or the request returned an error 51 | - `success: boolean;` => Request was successful 52 | - `type: "empty" | "success" | "error" | "exception"` => The last known state of the request (a new request might be in progress) 53 | 54 | Depending on `type`, additional properties might be available: 55 | 56 | - `"empty"` => This is the initial state if no request has returned yet 57 | - `failed` will always be `false` 58 | - `success` will always be `false` 59 | - `"success` => This is the state when a request returned successful. 60 | - `failed` will always be `false` 61 | - `success` will always be `true` 62 | - `responseStatus: number;` => The status code of the response 63 | - `responseHeaders: Headers;` => The headers of the response 64 | - `data: ResultType` => The server result 65 | - `"error"` => The server responded with an error. 66 | - `failed` will always be `true` 67 | - `success` will always be `false` 68 | - `responseStatus: number;` => The status code of the response 69 | - `responseHeaders: Headers;` => The headers of the response 70 | - `errors: ErrorType[];` => The list of errors returned by the server 71 | - `"exception"` => An exception has been thrown in JavaScript 72 | - `failed` will always be `true` 73 | - `success` will always be `false` 74 | - `errors: Error;` => The error that has been thrown 75 | 76 | ## The Submit Function 77 | 78 | The submit function arguments depend on wether you defined variables in your hook: 79 | 80 | - If you defined variables, you'll need to pass them as an object to the submit function. 81 | - E.g. `submit({ id: "hello" });` 82 | - Otherwise, call the submit function without arguments. 83 | 84 | ## The Abort Function 85 | 86 | This function is simple. It takes no arguments and stops the request. 87 | -------------------------------------------------------------------------------- /packages/use-graphql/docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | This library is shipped as es2015 modules. To use them in browsers, you'll have to transpile them using webpack or similar, which you probably already do. 4 | 5 | ## TypeScript 6 | 7 | Ensure you have at least TypeScript 4.1 in your setup. 8 | 9 | ## Install via NPM 10 | 11 | ```bash 12 | npm i @react-nano/use-graphql 13 | ``` 14 | 15 | ## Define Types 16 | 17 | Define your full types somewhere (i.e. all types and attributes that could possibly be requested, including their relations), for example: 18 | 19 | ```TypeScript 20 | import { GraphGLVariableTypes } from "@react-nano/use-graphql"; 21 | 22 | export interface ErrorDTO { 23 | message: string; 24 | } 25 | 26 | export interface PostDTO { 27 | id: number; 28 | title: string; 29 | message: string; 30 | hits: number; 31 | user: UserDTO; 32 | } 33 | 34 | export interface UserDTO { 35 | id: string; 36 | name: string; 37 | icon: string; 38 | age: number; 39 | posts: PostDTO[]; 40 | } 41 | 42 | export interface QueryUserVariables { 43 | id: string; 44 | } 45 | 46 | // Also specify GraphQL variable types as a constant like this: 47 | const queryUserVariableTypes: GraphGLVariableTypes = { 48 | // These will be autocompleted (and are required) based on the type argument above 49 | // The values here are the only place where you still need to write GraphQL types. 50 | id: "String!", 51 | }; 52 | 53 | ``` 54 | -------------------------------------------------------------------------------- /packages/use-graphql/mono-docs.yml: -------------------------------------------------------------------------------- 1 | title: "use-graphql" 2 | description: A lightweight, type-safe graphql hook builder for react, written in TypeScript. 3 | keywords: 4 | - react 5 | - hooks 6 | - graphql 7 | sidebar: 8 | - "setup" 9 | - "builder" 10 | - "hook" 11 | - "config" 12 | -------------------------------------------------------------------------------- /packages/use-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-nano/use-graphql", 3 | "version": "0.16.0", 4 | "description": "A lightweight, type-safe graphql hook for react, written in TypeScript.", 5 | "keywords": [ 6 | "TypeScript", 7 | "react", 8 | "graphql", 9 | "hooks", 10 | "react-hooks" 11 | ], 12 | "homepage": "https://lusito.github.io/react-nano/", 13 | "bugs": { 14 | "url": "https://github.com/Lusito/react-nano/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/Lusito/react-nano.git" 19 | }, 20 | "license": "Zlib", 21 | "author": "Santo Pfingsten", 22 | "main": "dist/index.js", 23 | "types": "dist/index.d.ts", 24 | "files": [ 25 | "dist/" 26 | ], 27 | "scripts": { 28 | "build": "rimraf dist && tsc -p tsconfig-build.json", 29 | "test": "tsd" 30 | }, 31 | "devDependencies": { 32 | "@types/react": "^19.0.10", 33 | "react": "^18.3.1", 34 | "rimraf": "^6.0.1", 35 | "tsd": "^0.31.2", 36 | "typescript": "^5.8.2" 37 | }, 38 | "peerDependencies": { 39 | "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 40 | }, 41 | "publishConfig": { 42 | "access": "public" 43 | }, 44 | "tsd": { 45 | "directory": "tests" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/use-graphql/src/builder.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useLayoutEffect, useMemo, useReducer } from "react"; 2 | 3 | import { GraphQLConfig, GraphQLGlobalConfigContext, GraphQLLocalConfig } from "./config"; 4 | import { GraphQLState, GraphQLStateManager, stateReducer } from "./state"; 5 | import { ErrorType, JsonPrimitive, ResultType, VariableType } from "./types"; 6 | 7 | /* eslint-disable @typescript-eslint/ban-types */ 8 | function toVariableDef(variableTypes?: Record) { 9 | if (!variableTypes) return ""; 10 | const keys = Object.keys(variableTypes); 11 | if (keys.length === 0) return ""; 12 | const parts = keys.map((key) => `$${key}: ${variableTypes[key]}`); 13 | return `(${parts.join(", ")})`; 14 | } 15 | 16 | function toVariablePass(variableTypes?: Record) { 17 | if (!variableTypes) return ""; 18 | const keys = Object.keys(variableTypes); 19 | if (keys.length === 0) return ""; 20 | const parts = keys.map((key) => `${key}: $${key}`); 21 | return `(${parts.join(", ")})`; 22 | } 23 | 24 | type AttribMap = { [s: string]: undefined | boolean | AttribMap }; 25 | 26 | function toFieldsDef(flags?: null | AttribMap): string { 27 | if (!flags) return ""; 28 | const keys = Object.keys(flags); 29 | if (keys.length === 0) return ""; 30 | const result = ["{"]; 31 | for (const key of keys) { 32 | const val = flags[key]; 33 | if (val) { 34 | result.push(key); 35 | if (val !== true) { 36 | result.push(toFieldsDef(val)); 37 | } 38 | } 39 | } 40 | result.push("}"); 41 | return result.join(" "); 42 | } 43 | 44 | export type ChoicesDeep2 = [T] extends [JsonPrimitive] 45 | ? Partial 46 | : T extends Array 47 | ? ChoicesDeep2 48 | : T extends Record 49 | ? ChoicesDeep 50 | : never; 51 | 52 | export type ChoicesDeep> = { 53 | [KeyType in keyof T]?: ChoicesDeep2; 54 | }; 55 | 56 | export type KeepField = TOpt extends object ? 1 : TOpt extends true ? 1 : 0; 57 | export type TypeForChoice = [T] extends [JsonPrimitive] 58 | ? T 59 | : T extends Array 60 | ? Array> 61 | : T extends Record 62 | ? TOpt extends ChoicesDeep 63 | ? ChoicesToResult 64 | : never 65 | : never; 66 | export type ChoicesToResult, TOpt extends ChoicesDeep> = { 67 | [P in keyof T as KeepField extends 1 ? P : never]: TypeForChoice; 68 | }; 69 | 70 | export type GraphQLResultOf = T extends GraphQLHook ? TResultData : never; 71 | 72 | export type FieldChoicesFor = [T] extends [JsonPrimitive] 73 | ? never 74 | : T extends Array 75 | ? T2 extends ResultType 76 | ? FieldChoicesFor 77 | : never 78 | : T extends Record 79 | ? ChoicesDeep 80 | : never; 81 | 82 | export type ReducedResult = [T] extends [JsonPrimitive] 83 | ? T 84 | : T extends Array 85 | ? T2 extends ResultType 86 | ? Array> 87 | : never 88 | : T extends Record 89 | ? TFieldChoices extends ChoicesDeep 90 | ? ChoicesToResult 91 | : never 92 | : never; 93 | 94 | export type GraphQLHook = ( 95 | config?: GraphQLLocalConfig, 96 | ) => [GraphQLState, TVars extends null ? () => void : (vars: TVars) => void, () => void]; 97 | 98 | function buildHook( 99 | type: "query" | "mutation", 100 | name: string, 101 | fields?: AttribMap, 102 | variableTypes?: Record, 103 | ): GraphQLHook { 104 | const varsDef = toVariableDef(variableTypes); 105 | const varsPass = toVariablePass(variableTypes); 106 | const fieldsDef = toFieldsDef(fields); 107 | const query = `${type}${varsDef} { ${name}${varsPass} ${fieldsDef} }`; 108 | 109 | return (config) => { 110 | const autoSubmit = config?.autoSubmit; 111 | const [state, updateState] = useReducer(stateReducer, { 112 | failed: false, 113 | success: false, 114 | state: "empty", 115 | loading: !!autoSubmit, 116 | }); 117 | const manager = useMemo(() => new GraphQLStateManager(query, name, updateState), []); 118 | manager.globalConfig = useContext(GraphQLGlobalConfigContext) as GraphQLConfig; 119 | manager.config = config; 120 | useLayoutEffect(() => { 121 | manager.mounted = true; 122 | if (autoSubmit === true) manager.submit(); 123 | else if (autoSubmit) manager.submit(autoSubmit as Record); 124 | 125 | return () => { 126 | manager.mounted = false; 127 | manager.abort(); 128 | }; 129 | // eslint-disable-next-line react-hooks/exhaustive-deps 130 | }, []); 131 | return [state, manager.submit, manager.abort]; 132 | }; 133 | } 134 | 135 | export type CreateHook = [ 136 | TFullResult, 137 | ] extends [JsonPrimitive | JsonPrimitive[]] 138 | ? () => GraphQLHook, TError, TVars> 139 | : >( 140 | fields: TFieldChoices, 141 | ) => GraphQLHook, TError, TVars>; 142 | 143 | export type HookBuilder = { 144 | createHook: CreateHook; 145 | with: >( 146 | variableTypes: GraphGLVariableTypes, 147 | ) => { 148 | createHook: CreateHook; 149 | }; 150 | }; 151 | 152 | function hookBuilder( 153 | type: "query" | "mutation", 154 | name: string, 155 | ) { 156 | return { 157 | createHook: (fields) => buildHook(type, name, fields), 158 | with: (variableTypes: { [s: string]: string }) => ({ 159 | createHook: (fields) => buildHook(type, name, fields, variableTypes), 160 | }), 161 | } as HookBuilder; 162 | } 163 | 164 | export type GraphQLBuilder = ( 165 | name: string, 166 | ) => HookBuilder; 167 | 168 | export type GraphQL = { 169 | query: GraphQLBuilder; 170 | mutation: GraphQLBuilder; 171 | }; 172 | 173 | export const graphQL: GraphQL = { 174 | query: (name) => hookBuilder("query", name), 175 | mutation: (name) => hookBuilder("mutation", name), 176 | }; 177 | 178 | export type GraphGLVariableTypes> = { 179 | [P in keyof T]: string; 180 | }; 181 | -------------------------------------------------------------------------------- /packages/use-graphql/src/config.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | import { GraphQLRequestInit, VariableType } from "./types"; 4 | 5 | export interface CallbackContext { 6 | /** The data you used to submit the request */ 7 | inputData: TVars; 8 | } 9 | 10 | export interface CallbackContextWithResponse extends CallbackContext { 11 | /** The status code of the request */ 12 | status: number; 13 | /** The response headers headers of the request */ 14 | responseHeaders: Headers; 15 | } 16 | 17 | export interface OnSuccessContext extends CallbackContextWithResponse { 18 | /** The result of the query/mutation */ 19 | data: TData; 20 | } 21 | 22 | export interface OnErrorContext extends CallbackContextWithResponse { 23 | /** The errors the server returned for the query/mutation */ 24 | errors: TError[]; 25 | } 26 | 27 | export interface OnExceptionContext extends CallbackContext { 28 | /** The error that was thrown. */ 29 | error: Error; 30 | } 31 | 32 | export interface GraphQLConfig, TVars> { 33 | /** The url to use. Defaults to "/graphql" if neither global nor local config specifies it */ 34 | url?: string; 35 | 36 | /** 37 | * Called right before a request will be made. Use it to extend the request with additional information like authorization headers. 38 | * 39 | * @param init The request data to be send. 40 | */ 41 | onInit?(init: RequestInit & GraphQLRequestInit): void; 42 | 43 | /** 44 | * Called on successful request with the result 45 | * 46 | * @param context Information about the request 47 | */ 48 | onSuccess?(context: OnSuccessContext): void; 49 | 50 | /** 51 | * Called on server error 52 | * 53 | * @param context Information about the request 54 | */ 55 | onError?(context: OnErrorContext): void; 56 | 57 | /** 58 | * Called when an exception happened in the frontend 59 | * 60 | * @param context Information about the request 61 | */ 62 | onException?(context: OnExceptionContext): void; 63 | } 64 | 65 | export interface GraphQLLocalConfig, TVars extends VariableType> 66 | extends GraphQLConfig { 67 | /** Specify to cause the request to be submitted automatically */ 68 | autoSubmit?: TVars extends null ? boolean : TVars; 69 | } 70 | 71 | export const defaultGraphQLConfig = { url: "/graphql" }; 72 | 73 | export const GraphQLGlobalConfigContext = 74 | createContext, unknown>>(defaultGraphQLConfig); 75 | export const GraphQLGlobalConfigProvider = GraphQLGlobalConfigContext.Provider; 76 | -------------------------------------------------------------------------------- /packages/use-graphql/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export { 3 | GraphQLStateBase, 4 | GraphQLStateEmpty, 5 | GraphQLStateDone, 6 | GraphQLStateDoneSuccess, 7 | GraphQLStateDoneError, 8 | GraphQLStateDoneException, 9 | GraphQLState, 10 | } from "./state"; 11 | export * from "./config"; 12 | export * from "./builder"; 13 | -------------------------------------------------------------------------------- /packages/use-graphql/src/state.ts: -------------------------------------------------------------------------------- 1 | import { defaultGraphQLConfig, GraphQLConfig } from "./config"; 2 | import { ErrorType, GraphQLRequestInit, GraphQLResponseInfo } from "./types"; 3 | 4 | export interface GraphQLStateBase { 5 | /** Request is currently in progress */ 6 | loading: boolean; 7 | /** Either an exception occurred or the request returned an error */ 8 | failed: boolean; 9 | /** Request was successful */ 10 | success: boolean; 11 | } 12 | 13 | export interface GraphQLStateEmpty extends GraphQLStateBase { 14 | state: "empty"; 15 | failed: false; 16 | success: false; 17 | } 18 | 19 | export interface GraphQLStateDone extends GraphQLStateBase, GraphQLResponseInfo {} 20 | 21 | export interface GraphQLStateDoneSuccess extends GraphQLStateDone { 22 | failed: false; 23 | success: true; 24 | /** Data is present */ 25 | state: "success"; 26 | /** The response data in case of success */ 27 | data: TData; 28 | } 29 | 30 | export interface GraphQLStateDoneError extends GraphQLStateDone { 31 | failed: true; 32 | success: false; 33 | /** Errors is present */ 34 | state: "error"; 35 | /** Request has finished with either an error or an exception. */ 36 | errors: TError[]; 37 | } 38 | 39 | export interface GraphQLStateDoneException extends GraphQLStateBase { 40 | failed: true; 41 | success: false; 42 | /** Errors is present */ 43 | state: "exception"; 44 | /** Request has finished with either an error or an exception. */ 45 | error: Error; 46 | } 47 | 48 | export type GraphQLState = 49 | | GraphQLStateEmpty 50 | | GraphQLStateDoneSuccess 51 | | GraphQLStateDoneError 52 | | GraphQLStateDoneException; 53 | 54 | interface GraphQLActionLoading { 55 | type: "loading"; 56 | value: boolean; 57 | } 58 | interface GraphQLActionSuccess extends GraphQLResponseInfo { 59 | type: "success"; 60 | data: TData; 61 | } 62 | interface GraphQLActionError extends GraphQLResponseInfo { 63 | type: "error"; 64 | errors: TError[]; 65 | } 66 | interface GraphQLActionException { 67 | type: "exception"; 68 | error: Error; 69 | } 70 | 71 | type GraphQLAction = 72 | | GraphQLActionLoading 73 | | GraphQLActionSuccess 74 | | GraphQLActionError 75 | | GraphQLActionException; 76 | 77 | export function stateReducer( 78 | state: GraphQLState, 79 | action: GraphQLAction, 80 | ): GraphQLState { 81 | switch (action.type) { 82 | case "loading": 83 | return { 84 | ...state, 85 | loading: action.value, 86 | }; 87 | case "success": 88 | return { 89 | failed: false, 90 | state: "success", 91 | success: true, 92 | loading: false, 93 | data: action.data, 94 | responseHeaders: action.responseHeaders, 95 | responseStatus: action.responseStatus, 96 | }; 97 | case "error": 98 | return { 99 | failed: true, 100 | success: false, 101 | state: "error", 102 | loading: false, 103 | errors: action.errors, 104 | responseHeaders: action.responseHeaders, 105 | responseStatus: action.responseStatus, 106 | }; 107 | case "exception": 108 | return { 109 | failed: true, 110 | success: false, 111 | state: "exception", 112 | loading: false, 113 | error: action.error, 114 | }; 115 | } 116 | return state; 117 | } 118 | 119 | export class GraphQLStateManager { 120 | public globalConfig?: GraphQLConfig; 121 | 122 | public config?: GraphQLConfig; 123 | 124 | public mounted = true; 125 | 126 | private query: string; 127 | 128 | private queryName: string; 129 | 130 | private controller?: AbortController; 131 | 132 | private updateState: (action: GraphQLAction) => void; 133 | 134 | public constructor( 135 | query: string, 136 | queryName: string, 137 | updateState: (action: GraphQLAction) => void, 138 | ) { 139 | this.query = query; 140 | this.queryName = queryName; 141 | this.updateState = updateState; 142 | } 143 | 144 | public abort = () => { 145 | if (this.controller) { 146 | this.controller.abort(); 147 | this.controller = undefined; 148 | this.mounted && this.updateState({ type: "loading", value: false }); 149 | } 150 | }; 151 | 152 | public submit = (variables?: Record) => { 153 | this.submitAsync(variables); 154 | }; 155 | 156 | private async submitAsync(variables?: Record) { 157 | if (!this.mounted) return; 158 | 159 | const globalConfig = this.globalConfig ?? (defaultGraphQLConfig as GraphQLConfig); 160 | const config = this.config ?? (defaultGraphQLConfig as GraphQLConfig); 161 | let responseStatus = -1; 162 | try { 163 | this.controller?.abort(); 164 | this.controller = new AbortController(); 165 | this.updateState({ type: "loading", value: true }); 166 | const init: GraphQLRequestInit = { 167 | method: "POST", 168 | credentials: "include", 169 | headers: new Headers({ 170 | "Content-Type": "application/json", 171 | }), 172 | body: JSON.stringify({ 173 | query: this.query, 174 | variables, 175 | }), 176 | signal: this.controller.signal, 177 | }; 178 | globalConfig.onInit?.(init); 179 | if (!this.mounted) return; 180 | config.onInit?.(init); 181 | if (!this.mounted) return; 182 | const url = config.url ?? globalConfig.url ?? defaultGraphQLConfig.url; 183 | const response = await fetch(url, init); 184 | 185 | responseStatus = response.status; 186 | 187 | const json = await response.json(); 188 | if (!this.mounted) return; 189 | 190 | if (response.ok && !json.errors) { 191 | const data: TResultData = json.data[this.queryName]; 192 | const context = { 193 | inputData: variables, 194 | data, 195 | status: responseStatus, 196 | responseHeaders: response.headers, 197 | }; 198 | globalConfig.onSuccess?.(context); 199 | if (!this.mounted) return; 200 | config.onSuccess?.(context); 201 | if (!this.mounted) return; 202 | this.updateState({ 203 | type: "success", 204 | responseStatus: response.status, 205 | responseHeaders: response.headers, 206 | data, 207 | }); 208 | } else { 209 | const { errors } = json; 210 | const context = { 211 | inputData: variables, 212 | errors, 213 | status: responseStatus, 214 | responseHeaders: response.headers, 215 | }; 216 | globalConfig.onError?.(context); 217 | if (!this.mounted) return; 218 | config.onError?.(context); 219 | if (!this.mounted) return; 220 | this.updateState({ 221 | type: "error", 222 | responseStatus: response.status, 223 | responseHeaders: response.headers, 224 | errors, 225 | }); 226 | } 227 | } catch (error: any) { 228 | if (error.name !== "AbortError") { 229 | console.log(error); 230 | if (!this.mounted) return; 231 | const context = { 232 | inputData: variables, 233 | error, 234 | }; 235 | globalConfig.onException?.(context); 236 | if (!this.mounted) return; 237 | config.onException?.(context); 238 | if (!this.mounted) return; 239 | this.updateState({ 240 | type: "exception", 241 | error, 242 | }); 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /packages/use-graphql/src/types.ts: -------------------------------------------------------------------------------- 1 | export type JsonPrimitive = null | string | number | boolean; 2 | export type ResultType = JsonPrimitive | Record; 3 | export type VariableType = null | Record; 4 | export type ErrorType = Record; 5 | 6 | export interface GraphQLResponseInfo { 7 | /** The status code of the response */ 8 | responseStatus: number; 9 | /** The headers of the response */ 10 | responseHeaders: Headers; 11 | } 12 | 13 | export interface GraphQLRequestInit { 14 | readonly method: "POST"; 15 | credentials: RequestCredentials; 16 | readonly headers: Headers; 17 | readonly body: string; 18 | readonly signal: AbortSignal; 19 | } 20 | -------------------------------------------------------------------------------- /packages/use-graphql/tests/builder-complex.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { graphQL } from "../dist"; 2 | import { ErrorDTO, UserDTO } from "./types"; 3 | 4 | const query = graphQL.query("user"); 5 | const mutation = graphQL.mutation("user"); 6 | 7 | /// Variables may not be left out 8 | // @ts-expect-error 9 | query.with(); 10 | // @ts-expect-error 11 | mutation.with(); 12 | 13 | /// Objects are not allowed to be selected via true 14 | // @ts-expect-error 15 | query.createHook({ posts: true }); 16 | // @ts-expect-error 17 | mutation.createHook({ posts: true }); 18 | 19 | /// Attributes are not allowed to be selected via object 20 | // @ts-expect-error 21 | query.createHook({ id: {} }); 22 | // @ts-expect-error 23 | mutation.createHook({ id: {} }); 24 | 25 | /// Allowed calls: 26 | query.createHook({ id: true }); 27 | query.createHook({ id: true, posts: { hits: true } }); 28 | mutation.createHook({ id: true }); 29 | mutation.createHook({ id: true, posts: { hits: true } }); 30 | -------------------------------------------------------------------------------- /packages/use-graphql/tests/builder-primitive.spec-d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { expectType } from "tsd"; 3 | 4 | import { graphQL } from "../dist"; 5 | import { ErrorDTO, QueryUserVariables, queryUserVariableTypes } from "./types"; 6 | 7 | const query = graphQL.query("time"); 8 | const mutation = graphQL.mutation("time"); 9 | 10 | /// Fields may not be specified 11 | // @ts-expect-error 12 | query.createHook({}); 13 | // @ts-expect-error 14 | mutation.createHook({}); 15 | 16 | /// Allowed calls: 17 | query.createHook(); 18 | mutation.createHook(); 19 | 20 | /// Fields may not be specified 21 | // @ts-expect-error 22 | query.with(queryUserVariableTypes).createHook({}); 23 | // @ts-expect-error 24 | mutation.with(queryUserVariableTypes).createHook({}); 25 | 26 | /// Variables may not be left out 27 | // @ts-expect-error 28 | query.with(); 29 | // @ts-expect-error 30 | mutation.with(); 31 | 32 | /// Allowed calls: 33 | query.with(queryUserVariableTypes).createHook(); 34 | mutation.with(queryUserVariableTypes).createHook(); 35 | 36 | // callback types must be correct 37 | const useBooleanQuery = graphQL.query("boolean").with(queryUserVariableTypes).createHook(); 38 | useBooleanQuery({ 39 | onSuccess(context) { 40 | expectType<{ 41 | data: boolean; 42 | inputData: QueryUserVariables; 43 | status: number; 44 | responseHeaders: Headers; 45 | }>(context); 46 | }, 47 | onError(context) { 48 | expectType<{ 49 | errors: ErrorDTO[]; 50 | inputData: QueryUserVariables; 51 | status: number; 52 | responseHeaders: Headers; 53 | }>(context); 54 | }, 55 | onException(context) { 56 | expectType<{ 57 | error: Error; 58 | inputData: QueryUserVariables; 59 | }>(context); 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /packages/use-graphql/tests/hook-with-vars.spec-d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { expectType } from "tsd"; 3 | 4 | import { graphQL } from "../dist"; 5 | import { ErrorDTO, QueryUserVariables, queryUserVariableTypes, UserDTO } from "./types"; 6 | 7 | const useUserQuery = graphQL 8 | .query("user") 9 | .with(queryUserVariableTypes) 10 | .createHook({ 11 | name: true, 12 | icon: true, 13 | posts: { 14 | id: true, 15 | title: true, 16 | hits: true, 17 | }, 18 | }); 19 | 20 | const [state, submit, abort] = useUserQuery({ url: "/graphql", autoSubmit: { id: "hello" } }); 21 | 22 | expectType<(vars: { id: string }) => void>(submit); 23 | expectType<() => void>(abort); 24 | 25 | if (state.state === "success") { 26 | expectType<{ 27 | loading: boolean; 28 | failed: false; 29 | success: true; 30 | state: "success"; 31 | responseHeaders: Headers; 32 | responseStatus: number; 33 | data: { 34 | name: string; 35 | icon: string; 36 | posts: Array<{ 37 | id: number; 38 | title: string; 39 | hits: number; 40 | }>; 41 | }; 42 | }>(state); 43 | } else if (state.state === "empty") { 44 | expectType<{ 45 | loading: boolean; 46 | success: false; 47 | failed: false; 48 | state: "empty"; 49 | }>(state); 50 | } else if (state.state === "error") { 51 | expectType<{ 52 | loading: boolean; 53 | success: false; 54 | failed: true; 55 | state: "error"; 56 | responseHeaders: Headers; 57 | responseStatus: number; 58 | errors: ErrorDTO[]; 59 | }>(state); 60 | } else if (state.state === "exception") { 61 | expectType<{ 62 | loading: boolean; 63 | success: false; 64 | failed: true; 65 | state: "exception"; 66 | error: Error; 67 | }>(state); 68 | } 69 | 70 | // autoSubmit may not be true 71 | // @ts-expect-error 72 | useUserQuery({ url: "/graphql", autoSubmit: true }); 73 | 74 | // autoSubmit may not specify unknown attributes 75 | // @ts-expect-error 76 | useUserQuery({ url: "/graphql", autoSubmit: { foo: "bar" } }); 77 | 78 | // autoSubmit may not specify extra attributes 79 | // @ts-expect-error 80 | useUserQuery({ url: "/graphql", autoSubmit: { id: "some-id", foo: "bar" } }); 81 | 82 | // autoSubmit must be object 83 | useUserQuery({ url: "/graphql", autoSubmit: { id: "some-id" } }); 84 | 85 | // callback types must be correct 86 | useUserQuery({ 87 | url: "/graphql", 88 | onSuccess(context) { 89 | expectType<{ 90 | data: { 91 | name: string; 92 | icon: string; 93 | posts: Array<{ 94 | id: number; 95 | title: string; 96 | hits: number; 97 | }>; 98 | }; 99 | inputData: QueryUserVariables; 100 | status: number; 101 | responseHeaders: Headers; 102 | }>(context); 103 | }, 104 | onError(context) { 105 | expectType<{ 106 | errors: ErrorDTO[]; 107 | inputData: QueryUserVariables; 108 | status: number; 109 | responseHeaders: Headers; 110 | }>(context); 111 | }, 112 | onException(context) { 113 | expectType<{ 114 | error: Error; 115 | inputData: QueryUserVariables; 116 | }>(context); 117 | }, 118 | }); 119 | -------------------------------------------------------------------------------- /packages/use-graphql/tests/hook-without-vars.spec-d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { expectType } from "tsd"; 3 | 4 | import { graphQL } from "../dist"; 5 | import { ErrorDTO, UserDTO } from "./types"; 6 | 7 | const useUserQuery = graphQL.query("user").createHook({ 8 | name: true, 9 | icon: true, 10 | posts: { 11 | id: true, 12 | title: true, 13 | hits: true, 14 | }, 15 | }); 16 | 17 | const [state, submit, abort] = useUserQuery({ url: "/graphql" }); 18 | 19 | expectType<() => void>(submit); 20 | expectType<() => void>(abort); 21 | 22 | if (state.state === "success") { 23 | expectType<{ 24 | loading: boolean; 25 | failed: false; 26 | success: true; 27 | state: "success"; 28 | responseHeaders: Headers; 29 | responseStatus: number; 30 | data: { 31 | name: string; 32 | icon: string; 33 | posts: Array<{ 34 | id: number; 35 | title: string; 36 | hits: number; 37 | }>; 38 | }; 39 | }>(state); 40 | } else if (state.state === "empty") { 41 | expectType<{ 42 | loading: boolean; 43 | success: false; 44 | failed: false; 45 | state: "empty"; 46 | }>(state); 47 | } else if (state.state === "error") { 48 | expectType<{ 49 | loading: boolean; 50 | success: false; 51 | failed: true; 52 | state: "error"; 53 | responseHeaders: Headers; 54 | responseStatus: number; 55 | errors: ErrorDTO[]; 56 | }>(state); 57 | } else if (state.state === "exception") { 58 | expectType<{ 59 | loading: boolean; 60 | success: false; 61 | failed: true; 62 | state: "exception"; 63 | error: Error; 64 | }>(state); 65 | } 66 | 67 | // autoSubmit may not be object 68 | // @ts-expect-error 69 | useUserQuery({ url: "/graphql", autoSubmit: {} }); 70 | 71 | // autoSubmit must be true 72 | useUserQuery({ url: "/graphql", autoSubmit: true }); 73 | -------------------------------------------------------------------------------- /packages/use-graphql/tests/types.ts: -------------------------------------------------------------------------------- 1 | import { GraphGLVariableTypes } from "../dist"; 2 | 3 | export interface ErrorDTO { 4 | message: string; 5 | } 6 | 7 | export interface PostDTO { 8 | id: number; 9 | title: string; 10 | message: string; 11 | hits: number; 12 | user: UserDTO; 13 | } 14 | 15 | export interface UserDTO { 16 | id: string; 17 | name: string; 18 | icon: string; 19 | age: number; 20 | posts: PostDTO[]; 21 | } 22 | 23 | export interface QueryUserVariables { 24 | id: string; 25 | } 26 | 27 | export const queryUserVariableTypes: GraphGLVariableTypes = { 28 | id: "String!", 29 | }; 30 | -------------------------------------------------------------------------------- /packages/use-graphql/tests/variable-types.spec-d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { expectType } from "tsd"; 3 | 4 | import { GraphGLVariableTypes } from "../dist"; 5 | import { PostDTO, QueryUserVariables } from "./types"; 6 | 7 | expectType<{ 8 | id: string; 9 | }>(0 as any as GraphGLVariableTypes); 10 | 11 | // Only top-level attributes need to be specified: 12 | expectType<{ 13 | id: string; 14 | title: string; 15 | message: string; 16 | hits: string; 17 | user: string; 18 | }>(0 as any as GraphGLVariableTypes); 19 | 20 | /// Missing variables not allowed 21 | // @ts-expect-error 22 | export const missingVars: GraphGLVariableTypes = {}; 23 | 24 | /// Wrong variable not allowed 25 | // @ts-expect-error 26 | export const wrongVars: GraphGLVariableTypes = { foo: "String!" }; 27 | 28 | /// Additional variables not allowed 29 | // @ts-expect-error 30 | export const additionalVars: GraphGLVariableTypes = { id: "String!", foo: "String!" }; 31 | -------------------------------------------------------------------------------- /packages/use-graphql/tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/use-graphql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@lusito/tsconfig/base", "@lusito/tsconfig/react"], 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["./src", "./tests"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lusito/tsconfig/base" 3 | } 4 | --------------------------------------------------------------------------------