├── .prettierrc.json ├── tsconfig.json ├── .lintignore ├── .gitignore ├── packages ├── tsrux │ ├── src │ │ ├── index.ts │ │ ├── mapReducers.ts │ │ ├── actionCreator.ts │ │ ├── mapReducers.spec.ts │ │ ├── actionCreator.spec.ts │ │ ├── actionCreator.spec-d.ts │ │ └── mapReducers.spec-d.ts │ ├── tsconfig-build.json │ ├── tsconfig.json │ ├── jest.config.cjs │ ├── mono-docs.yml │ ├── docs │ │ ├── setup.md │ │ ├── types.md │ │ ├── further-examples.md │ │ ├── reducers.md │ │ └── action-creators.md │ ├── package.json │ └── README.md ├── use-graphql │ ├── tsconfig-build.json │ ├── tsconfig.json │ ├── mono-docs.yml │ ├── src │ │ ├── index.ts │ │ ├── types.ts │ │ ├── config.ts │ │ ├── builder.ts │ │ └── state.ts │ ├── tests │ │ ├── types.ts │ │ ├── builder-complex.spec-d.ts │ │ ├── variable-types.spec-d.ts │ │ ├── builder-primitive.spec-d.ts │ │ ├── hook-without-vars.spec-d.ts │ │ └── hook-with-vars.spec-d.ts │ ├── package.json │ ├── docs │ │ ├── setup.md │ │ ├── builder.md │ │ ├── hook.md │ │ └── config.md │ └── README.md ├── router │ ├── src │ │ ├── simpleRouteMatcherFactory.ts │ │ ├── index.ts │ │ ├── basename.ts │ │ ├── hooks.ts │ │ ├── Switch.tsx │ │ ├── routeMatcher.ts │ │ ├── RouterContext.ts │ │ ├── Link.tsx │ │ ├── Route.tsx │ │ ├── Router.tsx │ │ └── history.ts │ ├── tsconfig.json │ ├── docs │ │ ├── setup.md │ │ ├── links.md │ │ ├── hooks.md │ │ ├── routes.md │ │ └── router.md │ ├── mono-docs.yml │ ├── package.json │ └── README.md ├── use-fetch │ ├── tsconfig.json │ ├── docs │ │ ├── setup.md │ │ ├── helpers.md │ │ ├── get.md │ │ ├── config.md │ │ ├── api.md │ │ └── modify.md │ ├── mono-docs.yml │ ├── package.json │ ├── src │ │ ├── helpers.ts │ │ └── index.ts │ └── README.md ├── redux │ ├── tsconfig.json │ ├── docs │ │ ├── setup.md │ │ ├── provider.md │ │ ├── migrating-from-react-redux.md │ │ └── hooks.md │ ├── mono-docs.yml │ ├── src │ │ ├── use-sync-external-store.d.ts │ │ └── index.tsx │ ├── package.json │ └── README.md ├── use-event-source │ ├── tsconfig.json │ ├── docs │ │ ├── setup.md │ │ ├── redux.md │ │ └── usage.md │ ├── mono-docs.yml │ ├── package.json │ ├── src │ │ └── index.ts │ └── README.md └── redux-dynamic-modules │ ├── tsconfig.json │ ├── mono-docs.yml │ ├── docs │ └── setup.md │ ├── package.json │ ├── src │ └── index.tsx │ └── README.md ├── .github └── workflows │ ├── test.yml │ └── documentation.yml ├── .eslintrc.cjs ├── mono-docs.yml ├── LICENSE ├── package.json └── README.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@lusito/prettier-config" 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lusito/tsconfig/base" 3 | } 4 | -------------------------------------------------------------------------------- /.lintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | docs-dist 5 | coverage -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | docs-dist/ 3 | .vscode 4 | node_modules/ 5 | coverage/ 6 | *.tgz 7 | -------------------------------------------------------------------------------- /packages/tsrux/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./actionCreator"; 2 | export * from "./mapReducers"; 3 | -------------------------------------------------------------------------------- /packages/use-graphql/tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/tsrux/tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["./src/*.spec.ts", "./src/*.spec-d.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/tsrux/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lusito/tsconfig/base", 3 | "compilerOptions": { 4 | "outDir": "./dist/" 5 | }, 6 | "include": ["./src/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/router/src/simpleRouteMatcherFactory.ts: -------------------------------------------------------------------------------- 1 | export function simpleRouteMatcherFactory(pattern: string) { 2 | return (path: string) => (path === pattern || pattern === "*" ? {} : null); 3 | } 4 | -------------------------------------------------------------------------------- /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/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/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@lusito/tsconfig/base", "@lusito/tsconfig/react"], 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "exclude": ["test", "dist"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/use-event-source/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@lusito/tsconfig/base", "@lusito/tsconfig/react"], 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "exclude": ["dist"] 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/tsrux/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | ".+\\.ts$": "ts-jest", 4 | }, 5 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.ts$", 6 | moduleFileExtensions: ["ts", "js"], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/tsrux/mono-docs.yml: -------------------------------------------------------------------------------- 1 | title: "tsrux" 2 | description: Typesafe and painless action creators and reducers for redux. 3 | keywords: 4 | - redux 5 | sidebar: 6 | - "setup" 7 | - "action-creators" 8 | - "reducers" 9 | - "types" 10 | - "further-examples" 11 | -------------------------------------------------------------------------------- /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/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/router/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/router 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/router/mono-docs.yml: -------------------------------------------------------------------------------- 1 | title: router 2 | description: A simple, lightweight react router using hooks, written in TypeScript. 3 | keywords: 4 | - react 5 | - router 6 | - hooks 7 | sidebar: 8 | - "setup" 9 | - "router" 10 | - "routes" 11 | - "links" 12 | - "hooks" 13 | -------------------------------------------------------------------------------- /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-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/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/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-event-source/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/use-event-source 9 | ``` 10 | -------------------------------------------------------------------------------- /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/use-event-source/mono-docs.yml: -------------------------------------------------------------------------------- 1 | title: "use-event-source" 2 | description: A lightweight EventSource (server-sent-events) hook for react, written in TypeScript. 3 | keywords: 4 | - react 5 | - hooks 6 | - event-source 7 | - server-sent-events 8 | - sse 9 | sidebar: 10 | - "setup" 11 | - "usage" 12 | - "redux" 13 | -------------------------------------------------------------------------------- /packages/router/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./basename"; 2 | export * from "./history"; 3 | export * from "./hooks"; 4 | export * from "./Link"; 5 | export * from "./Route"; 6 | export * from "./routeMatcher"; 7 | export * from "./Router"; 8 | export * from "./RouterContext"; 9 | export * from "./simpleRouteMatcherFactory"; 10 | export * from "./Switch"; 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/router/src/basename.ts: -------------------------------------------------------------------------------- 1 | export const getPathWithoutBasename = (basename: string) => document.location.pathname.substring(basename.length); 2 | 3 | export function getBasename() { 4 | const base = document.querySelector("base"); 5 | if (!base) return ""; 6 | const basename = `/${base.href.split("/").slice(3).join("/")}`; 7 | if (basename.endsWith("/")) return basename.substr(0, basename.length - 1); 8 | return basename; 9 | } 10 | -------------------------------------------------------------------------------- /packages/router/src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from "react"; 2 | 3 | import { RouterContext } from "./RouterContext"; 4 | import type { RouteParams } from "./Route"; 5 | 6 | export const useRouter = () => useContext(RouterContext); 7 | 8 | export function useParams(path: string): T { 9 | const router = useRouter(); 10 | return useMemo(() => router.matchRoute(path, router.path), [path, router]) as T; 11 | } 12 | -------------------------------------------------------------------------------- /packages/router/src/Switch.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, isValidElement } from "react"; 2 | 3 | import { RouteProps } from "./Route"; 4 | import { useRouter } from "./hooks"; 5 | 6 | export interface SwitchProps { 7 | children: Array>; 8 | } 9 | 10 | export const Switch: FC = (props: SwitchProps) => { 11 | const { matchRoute, path } = useRouter(); 12 | 13 | return ( 14 | props.children.find((child) => child && isValidElement(child) && !!matchRoute(child.props.path, path)) ?? null 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/tsrux/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/tsrux 9 | ``` 10 | 11 | ## Redux 12 | 13 | You will need [redux](https://redux.js.org) to use @react-nano/tsrux, but since you got here, you most likely already use redux. 14 | 15 | Actually, you don't need redux specifically, since @react-nano/tsrux has no dependency on redux. You could use any other library that uses the same API for actions and reducers! 16 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/router/src/routeMatcher.ts: -------------------------------------------------------------------------------- 1 | import { RouteParams } from "./Route"; 2 | 3 | export type CachedRouteMatcher = (pattern: string, path: string) => RouteParams | null; 4 | 5 | export type RouteMatcher = (path: string) => RouteParams | null; 6 | 7 | export type RouteMatcherFactory = (pattern: string) => RouteMatcher; 8 | 9 | export function createRouteMatcher(routeMatcherFactory: RouteMatcherFactory): CachedRouteMatcher { 10 | const cache: { [s: string]: RouteMatcher } = {}; 11 | 12 | return (pattern: string, path: string) => { 13 | const matcher = cache[pattern] || (cache[pattern] = routeMatcherFactory(pattern)); 14 | return matcher(path); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/router/src/RouterContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | import { RouterHistory } from "./history"; 4 | import type { CachedRouteMatcher } from "./routeMatcher"; 5 | 6 | export interface RouterContextValue { 7 | basename: string; 8 | path: string; 9 | history: RouterHistory; 10 | matchRoute: CachedRouteMatcher; 11 | urlTo: (path: string) => string; 12 | } 13 | 14 | const throwMissingDefault = () => { 15 | throw new Error("You forgot to add a Router element to your app."); 16 | }; 17 | 18 | export const RouterContext = createContext({ 19 | basename: "", 20 | path: "", 21 | history: { 22 | push: throwMissingDefault, 23 | replace: throwMissingDefault, 24 | onChange: throwMissingDefault, 25 | urlTo: throwMissingDefault, 26 | }, 27 | matchRoute: throwMissingDefault, 28 | urlTo: throwMissingDefault, 29 | }); 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/use-fetch/docs/helpers.md: -------------------------------------------------------------------------------- 1 | # Helper Functions 2 | 3 | ## Preparing Init 4 | 5 | The following functions will initialize the `RequestInit` object for specific use-cases. 6 | All of them will set`credentials` to `"include"` and a header `Accept` with value of `"application/json"` 7 | 8 | - `prepareGet` prepares a GET request 9 | - `preparePost/Patch/Put/Delete` prepares a form-data POST/PATCH/PUT/DELETE request. 10 | - `preparePostUrlEncoded` prepares a form url-encoded POST request 11 | - `prepareFormDataPost` prepares a POST request with a `FormData` object and detects if it contains files. 12 | - if it contains files, it will call `preparePost` and set the body to the formData object. 13 | - otherwise `preparePostUrlEncoded` will be called and the properties of the formData will be set accordingly. 14 | 15 | You don't need to use them. You can write your own code to initialize the `RequestInit` object. These are just here for convenience. 16 | -------------------------------------------------------------------------------- /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/use-event-source/docs/redux.md: -------------------------------------------------------------------------------- 1 | # Usage with Redux 2 | 3 | ## Example 4 | 5 | You can just as easily combine it with redux: 6 | 7 | ```tsx 8 | import { useEventSource, useEventSourceListener } from "@react-nano/use-event-source"; 9 | import { Action, Store } from "redux"; 10 | import { useStore } from "@react-nano/redux"; // or: "react-redux"; 11 | 12 | function MyComponent() { 13 | const messages = useSelector(getMessages); 14 | const dispatch = useDispatch(); 15 | const [eventSource, eventSourceStatus] = useEventSource("api/events", true); 16 | useEventSourceListener( 17 | eventSource, 18 | ["update"], 19 | (evt) => { 20 | dispatch(addMessages(JSON.parse(evt.data))); 21 | }, 22 | [dispatch], 23 | ); 24 | 25 | return ( 26 |
27 | {eventSourceStatus === "open" ? null : } 28 | {messages.map((msg) => ( 29 |
{msg.text}
30 | ))} 31 |
32 | ); 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /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/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/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/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/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-nano/router", 3 | "version": "0.16.0", 4 | "description": "A lightweight react router using hooks, written in TypeScript", 5 | "keywords": [ 6 | "TypeScript", 7 | "react", 8 | "router", 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/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-event-source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-nano/use-event-source", 3 | "version": "0.16.0", 4 | "description": "A lightweight EventSource (server-sent-events) hook for react, written in TypeScript", 5 | "keywords": [ 6 | "TypeScript", 7 | "react", 8 | "EventSource", 9 | "sse", 10 | "server-sent-events", 11 | "hooks", 12 | "react-hooks" 13 | ], 14 | "homepage": "https://lusito.github.io/react-nano/", 15 | "bugs": { 16 | "url": "https://github.com/Lusito/react-nano/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Lusito/react-nano.git" 21 | }, 22 | "license": "Zlib", 23 | "author": "Santo Pfingsten", 24 | "main": "dist/index.js", 25 | "types": "dist/index.d.ts", 26 | "files": [ 27 | "dist/" 28 | ], 29 | "scripts": { 30 | "build": "rimraf dist && tsc" 31 | }, 32 | "devDependencies": { 33 | "@types/react": "^19.0.10", 34 | "react": "^19.0.0", 35 | "rimraf": "^6.0.1", 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 | } 45 | -------------------------------------------------------------------------------- /packages/router/src/Link.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | 3 | import { useRouter } from "./hooks"; 4 | 5 | export function useRouteLink(href: string, onClick?: React.EventHandler>) { 6 | const { history } = useRouter(); 7 | 8 | return useMemo( 9 | () => ({ 10 | onClick(e: React.MouseEvent) { 11 | try { 12 | onClick?.(e); 13 | } catch (error) { 14 | console.error(error); 15 | } 16 | if (!e.defaultPrevented) { 17 | e.preventDefault(); 18 | history.push(href); 19 | } 20 | }, 21 | href: history.urlTo(href), 22 | }), 23 | [href, onClick, history], 24 | ); 25 | } 26 | 27 | export interface LinkProps extends React.AnchorHTMLAttributes { 28 | href: string; 29 | } 30 | 31 | export function Link(props: React.PropsWithChildren) { 32 | const routeLink = useRouteLink(props.href, props.onClick); 33 | // eslint-disable-next-line jsx-a11y/anchor-has-content 34 | return ; 35 | } 36 | -------------------------------------------------------------------------------- /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/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 ; 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 |
    53 | {users.map((user) => ( 54 |
  • {user.name}
  • 55 | ))} 56 |
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/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/tsrux/docs/reducers.md: -------------------------------------------------------------------------------- 1 | # Reducers 2 | 3 | Reducers apply your actions to the state. 4 | 5 | ## Initial State 6 | 7 | First, you'll need to create an initial state. Some typing might be required here, some can be infered: 8 | 9 | ```typescript 10 | import { mapReducers } from "@react-nano/tsrux"; 11 | 12 | // Some types, you obviously still need 13 | interface TodoEntry { 14 | id: number; 15 | label: string; 16 | checked: boolean; 17 | } 18 | 19 | const initialState = { 20 | list: [] as TodoEntry[], 21 | nextId: 0, 22 | }; 23 | 24 | // Some types you can infer from a variable 25 | export type TodoState = typeof initialState; 26 | ``` 27 | 28 | ## Handle Actions 29 | 30 | When using `mapReducers()`, you don't need to define any more types! 31 | 32 | - This function is used to create a reducer for multiple actions. 33 | - It receives the initial state and a callback. 34 | - The callback is used to set up action handlers and returns an array of action handlers. 35 | 36 | ```typescript 37 | export const todosReducer = mapReducers(initialState, (handle) => [ 38 | // handle(actionCreator, reducer) helps you define a reducer responsible for one single action. 39 | // Both state and action know their types without needing to manually specify them! 40 | handle(addTodo, (state, action) => ({ 41 | ...state, 42 | list: [...state.list, { id: state.nextId, label: action.payload.label, checked: false }], 43 | nextId: state.nextId + 1, 44 | })), 45 | handle(setTodoChecked, (state, action) => ({ 46 | ...state, 47 | list: state.list.map((todo) => { 48 | if (todo.id === action.payload.id) return { ...todo, checked: action.payload.checked }; 49 | return todo; 50 | }), 51 | })), 52 | handle(removeTodo, (state, action) => ({ 53 | ...state, 54 | // Complains about next line: payload does not have an attribute named "ID" 55 | list: state.list.filter((todo) => todo.id !== action.payload.ID), 56 | })), 57 | ]); 58 | ``` 59 | -------------------------------------------------------------------------------- /packages/router/src/Router.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useMemo, PropsWithChildren, FC } from "react"; 2 | 3 | import { RouterContext, RouterContextValue } from "./RouterContext"; 4 | import { createHistory, getHashPath, OnNavigateFn } from "./history"; 5 | import { createRouteMatcher, RouteMatcherFactory } from "./routeMatcher"; 6 | import { getPathWithoutBasename } from "./basename"; 7 | import { simpleRouteMatcherFactory } from "./simpleRouteMatcherFactory"; 8 | 9 | export interface RouterProps { 10 | basename?: string; 11 | mode?: "hash" | "path"; 12 | routeMatcherFactory?: RouteMatcherFactory; 13 | /** 14 | * An async callback that will be called on a navigation event. 15 | * If it resolves to false, the navigation event will be aborted 16 | */ 17 | onNavigate?: OnNavigateFn; 18 | } 19 | 20 | export const Router: FC> = ({ 21 | basename = "", 22 | mode, 23 | routeMatcherFactory = simpleRouteMatcherFactory, 24 | children, 25 | onNavigate, 26 | }) => { 27 | const hashMode = mode === "hash"; 28 | const [path, setPath] = useState(() => (hashMode ? getHashPath() : getPathWithoutBasename(basename))); 29 | const history = useMemo( 30 | () => createHistory({ hashMode, basename, setPath, onNavigate }), 31 | [setPath, basename, hashMode, onNavigate], 32 | ); 33 | const matchRoute = useMemo(() => createRouteMatcher(routeMatcherFactory), [routeMatcherFactory]); 34 | const router = useMemo( 35 | () => ({ 36 | basename, 37 | path, 38 | history, 39 | matchRoute, 40 | urlTo: history.urlTo, 41 | }), 42 | [basename, path, history, matchRoute], 43 | ); 44 | useEffect(() => { 45 | window.addEventListener("popstate", history.onChange); 46 | return () => window.removeEventListener("popstate", history.onChange); 47 | }, [history]); 48 | return {children}; 49 | }; 50 | -------------------------------------------------------------------------------- /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/router/src/history.ts: -------------------------------------------------------------------------------- 1 | import { getPathWithoutBasename } from "./basename"; 2 | 3 | export type OnNavigateData = { 4 | from: string; 5 | to: string; 6 | type: "push" | "replace"; 7 | }; 8 | export type OnNavigateFn = (data: OnNavigateData) => Promise | boolean; 9 | 10 | export interface RouterHistory { 11 | /** Resolves to false if onNavigate resolved to false */ 12 | push: (path: string) => Promise; 13 | /** Resolves to false if onNavigate resolved to false */ 14 | replace: (path: string) => Promise; 15 | onChange: () => void; 16 | urlTo: (path: string) => string; 17 | } 18 | 19 | export const getHashPath = () => window.location.hash.substr(1); 20 | 21 | type CreateHistoryOptions = { 22 | hashMode: boolean; 23 | basename: string; 24 | setPath: (path: string) => void; 25 | onNavigate?: OnNavigateFn; 26 | }; 27 | 28 | export function createHistory({ hashMode, basename, setPath, onNavigate }: CreateHistoryOptions): RouterHistory { 29 | const { location, history } = window; 30 | const onChange = hashMode ? () => setPath(getHashPath()) : () => setPath(getPathWithoutBasename(basename)); 31 | 32 | const urlTo = hashMode 33 | ? (path: string) => `${location.pathname}${location.search}#${path}` 34 | : (path: string) => `${basename}${path}`; 35 | 36 | async function historyAction(path: string, type: "push" | "replace") { 37 | const to = urlTo(path); 38 | const from = location.pathname + location.search + location.hash; 39 | // Only push if something changed. 40 | if (to !== from) { 41 | if (onNavigate) { 42 | const allowed = await onNavigate({ from, to, type: "push" }); 43 | if (!allowed) return false; 44 | } 45 | 46 | if (type === "push") history.pushState({}, "", to); 47 | else history.replaceState({}, "", to); 48 | 49 | onChange(); 50 | } 51 | 52 | return true; 53 | } 54 | 55 | return { 56 | push: (path: string) => historyAction(path, "push"), 57 | replace: (path: string) => historyAction(path, "replace"), 58 | onChange, 59 | urlTo, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/tsrux/src/mapReducers.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, ActionCreator } from "./actionCreator"; 2 | 3 | /** 4 | * Definition of a reducer 5 | * 6 | * @param state The previous state 7 | * @param action The action to handle 8 | * @returns The new state 9 | */ 10 | export type Reducer = (state: TState, action: TAction) => TState; 11 | 12 | /** 13 | * Definition of a reducer map.. For internal use. 14 | */ 15 | export type ReducerMap = { 16 | [TType in TAction["type"]]: Reducer; 17 | }; 18 | 19 | /** 20 | * Infer actions of a reducer map.. For internal use. 21 | */ 22 | export type ReducerMapActions = 23 | TReducerMap extends ReducerMap ? TAction | AnyAction : never; 24 | 25 | /** 26 | * A callback used to create a reducer for one single action. 27 | * 28 | * @param actionCreator The action creator is used to determine the action type. 29 | * @param reducer The reducer works the usual way, except, that you don't need to specify types anymore. And it only handles one action. 30 | */ 31 | export type ReducerMapHandler = ( 32 | actionCreator: ActionCreator, 33 | reducer: Reducer, 34 | ) => ReducerMap; 35 | 36 | /** 37 | * Creates a reducer, which is capable of infering the type of action for each reducer based on the action-creators. 38 | * 39 | * @param defaultState The initial state value 40 | * @param setup This function is used to set up the contained reducers. 41 | */ 42 | export function mapReducers>( 43 | defaultState: TState | undefined, 44 | setup: (handle: ReducerMapHandler) => TReducerMap[], 45 | ): (state: TState | undefined, action: ReducerMapActions) => TState { 46 | const map = Object.assign({}, ...setup((actionCreator, reducer) => ({ [actionCreator.type]: reducer }) as any)); 47 | // eslint-disable-next-line default-param-last, @typescript-eslint/default-param-last 48 | return (state = defaultState, action) => { 49 | const reducer = map[action.type]; 50 | return reducer ? reducer(state, action) : state; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /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/use-event-source/docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Example 4 | 5 | In order to subscribe to an SSE endpoint, you need to call useEventSource. 6 | After you have it, you can add listeners to it. Here's a simple example: 7 | 8 | ```tsx 9 | import { useEventSource, useEventSourceListener } from "@react-nano/use-event-source"; 10 | 11 | function MyComponent() { 12 | const [messages, addMessages] = useReducer(messageReducer, []); 13 | 14 | const [eventSource, eventSourceStatus] = useEventSource("api/events", true); 15 | useEventSourceListener( 16 | eventSource, 17 | ["update"], 18 | (evt) => { 19 | addMessages(JSON.parse(evt.data)); 20 | }, 21 | [addMessages], 22 | ); 23 | 24 | return ( 25 |
26 | {eventSourceStatus === "open" ? null : } 27 | {messages.map((msg) => ( 28 |
{msg.text}
29 | ))} 30 |
31 | ); 32 | } 33 | ``` 34 | 35 | ## `useEventSource` 36 | 37 | This describes the entire API of the useEventSource hook: 38 | 39 | ```tsx 40 | // Create an EventSource 41 | function useEventSource( 42 | // The url to fetch from 43 | url: string, 44 | // Send credentials or not 45 | withCredentials?: boolean, 46 | // Optionally override the EventSource class (for example with a polyfill) 47 | ESClass: EventSourceConstructor = EventSource 48 | ) => [ 49 | // The generated EventSource.. on first call, it will be null. 50 | EventSource | null, 51 | // The status of the connection can be used to display a busy indicator, error indicator, etc. 52 | EventSourceStatus 53 | ]; 54 | 55 | type EventSourceStatus = "init" | "open" | "closed" | "error"; 56 | ``` 57 | 58 | ## `useEventSourceListener` 59 | 60 | This describes the entire API of the useEventSourceListener hook: 61 | 62 | ```tsx 63 | // Add a listener to the EventSource 64 | function useEventSourceListener( 65 | // The EventSource from the above hook 66 | source: EventSource | null, 67 | // The event types to add the listener to 68 | types: string[], 69 | // A listener callback (use e.type to get the event type) 70 | listener: (e: EventSourceEvent) => void, 71 | // If one of the dependencies changes, the listener will be re-added to the event types. 72 | dependencies: any[] = [] 73 | ) => void; 74 | 75 | ``` 76 | -------------------------------------------------------------------------------- /packages/use-event-source/README.md: -------------------------------------------------------------------------------- 1 | # @react-nano/use-event-source 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-event-source?icon=dockbit)](https://bundlephobia.com/result?p=@react-nano/use-event-source) 5 | [![NPM version](https://flat.badgen.net/npm/v/@react-nano/use-event-source?icon=npm)](https://www.npmjs.com/package/@react-nano/use-event-source) 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 EventSource (server-sent-events) hook for react, written in TypeScript. 10 | 11 | ## Why Use @react-nano/use-event-source? 12 | 13 | - Very lightweight (see the badges above for the latest size). 14 | - Flexible and dead simple to use. 15 | - Written in TypeScript 16 | - Only has one required peer dependency: React 17.0.0 or higher. 17 | - Liberal license: [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) 18 | 19 | **Beware**: This is currently work in progress. The API might change. 20 | 21 | ## How to Use 22 | 23 | Check out the [documentation](https://lusito.github.io/react-nano/use-event-source/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/use-fetch/README.md: -------------------------------------------------------------------------------- 1 | # @react-nano/use-fetch 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-fetch?icon=dockbit)](https://bundlephobia.com/result?p=@react-nano/use-fetch) 5 | [![NPM version](https://flat.badgen.net/npm/v/@react-nano/use-fetch?icon=npm)](https://www.npmjs.com/package/@react-nano/use-fetch) 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 | Lightweight fetching hooks for react, written in TypeScript. 10 | 11 | ### Why Use @react-nano/use-fetch? 12 | 13 | - Very lightweight (see the badges above for the latest size). 14 | - Flexible and dead simple to use. 15 | - Written in TypeScript 16 | - Only has one required peer dependency: React 17.0.0 or higher. 17 | - Liberal license: [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) 18 | 19 | **Beware**: This is currently work in progress. The API might change. 20 | 21 | There are a lot of similar hooks out there, but they either lacked something I needed or seemed overly complicated to use. 22 | 23 | ## How to Use 24 | 25 | Check out the [documentation](https://lusito.github.io/react-nano/use-fetch/setup.html) 26 | 27 | ## Report Issues 28 | 29 | 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. 30 | 31 | ## Contribute 32 | 33 | 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. 34 | 35 | ## License 36 | 37 | @react-nano has been released under the [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) license, meaning you 38 | can use it free of charge, without strings attached in commercial and non-commercial projects. Credits are appreciated but not mandatory. 39 | -------------------------------------------------------------------------------- /packages/tsrux/docs/action-creators.md: -------------------------------------------------------------------------------- 1 | # Action Creators 2 | 3 | Action creators are used to easily create actions. Defining action creators enables us to use their types in reducers! 4 | 5 | ## Without Payload and Metadata 6 | 7 | The first parameter of `actionCreator()` is the type of the action. 8 | If you don't specify further parameters, the returned function receives no arguments: 9 | 10 | ```typescript 11 | import { actionCreator } from "@react-nano/tsrux"; 12 | 13 | const fetchTodos = actionCreator("TODOS/FETCH"); 14 | 15 | // When called returns: { type: "TODOS/FETCH" } 16 | console.log(fetchTodos()); 17 | 18 | // Has a static property type="TODOS/FETCH" 19 | console.log(fetchTodos.type); 20 | ``` 21 | 22 | ## With Payload, but Without Metadata 23 | 24 | The second, optional parameter of `actionCreator()` is a factory function to create the payload of the action. 25 | If you specify it, the returned function receives the same arguments as your factory function: 26 | 27 | ```typescript 28 | import { actionCreator } from "@react-nano/tsrux"; 29 | 30 | const setTodoChecked = actionCreator("TODOS/SET_CHECKED", (id: number, checked: boolean) => ({ id, checked })); 31 | 32 | // When called returns: { type: "TODOS/SET_CHECKED", payload: { id: 42, checked: true } } 33 | console.log(setTodoChecked(42, true)); 34 | ``` 35 | 36 | ## With Payload and Metadata 37 | 38 | The third, optional parameter of `actionCreator()` is a factory function to create the metadata of the action. 39 | If you specify it, the returned function receives the same arguments as your factory function. As such, it must have the same signature as your payload factory: 40 | 41 | ```typescript 42 | import { actionCreator } from "@react-nano/tsrux"; 43 | 44 | const removeTodo = actionCreator( 45 | "TODOS/REMOVE", 46 | (id: number) => ({ id }), 47 | (id: number) => ({ metaId: id, foo: "bar" }), 48 | ); 49 | 50 | // When called returns: { type: "TODOS/REMOVE", payload: { id: 42 }, meta: { metaId: id, foo: "bar" } } 51 | console.log(removeTodo(42)); 52 | ``` 53 | 54 | ## Without Payload, but With Metadata 55 | 56 | Of course, you can also leave out the payload factory: 57 | 58 | ```typescript 59 | import { actionCreator } from "@react-nano/tsrux"; 60 | 61 | const removeTodo = actionCreator("TODOS/FOO", undefined, (id: number) => ({ metaId: id, foo: "bar" })); 62 | 63 | // When called returns: { type: "TODOS/FOO", meta: { metaId: id, foo: "bar" } } 64 | console.log(removeTodo(42)); 65 | ``` 66 | -------------------------------------------------------------------------------- /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/router/docs/routes.md: -------------------------------------------------------------------------------- 1 | # Routes 2 | 3 | ## Route 4 | 5 | Showing a component if the location matches a certain path is done with a `Route` component. It takes a `path` prop and either a `component` prop or children. 6 | 7 | ```tsx 8 | export const Component = () => ( 9 |
10 | 11 | Drumpf 12 |
13 | ); 14 | ``` 15 | 16 | **Beware:** If multiple routes have a matching path, all will be shown. Use a `Switch` component if that's not desired. 17 | 18 | As you can see, it's possible to specify a component to render or normal children. 19 | 20 | You can even use a callback instead of children like this: 21 | 22 | ```tsx 23 | export const Component = () => ( 24 | {(params: { id: string }) =>
Bar {params.id}
}
; 25 | ); 26 | ``` 27 | 28 | See further below for the the possibility of using parameters. 29 | 30 | ## Switch 31 | 32 | If you only want the first `Route` that has a matching path to be shown, you can use a `Switch`: 33 | 34 | ```tsx 35 | export const Component = () => ( 36 | 37 | 38 | 39 | {/* use "(.*)" instead of "*" if you use path-to-regexp */} 40 | 41 | 42 | ); 43 | ``` 44 | 45 | **Note:** The path pattern for the "Otherwise" Route differs depending on your [route matching algorithm](./router.md). With the built-in `simpleRouteMatcherFactory` you would use `"*"`, while you would use `"(.*)"` or `"/:fallback"` for `path-to-regexp`. 46 | 47 | ## Adding Parameters 48 | 49 | When you use a custom matching algorithm like `path-to-regexp`, you can extract values from the path. Let's say you have this route: 50 | 51 | ```tsx 52 | export const Component = () => ; 53 | ``` 54 | 55 | You defined a parameter :id in your path. Now you can access it in your `News` component: 56 | 57 | ```tsx 58 | export const News = (props: RouteComponentProps<{ id: string }>) =>
News ID: {props.params.id}
; 59 | ``` 60 | 61 | ## Fresh Rendering 62 | 63 | Let's say you have this route: 64 | 65 | ```tsx 66 | export const Component = () => ; 67 | ``` 68 | 69 | Moving from `/news/1` to `/news/2` will only update the components properties. State will be preserved. 70 | If you want to force the component to be created from scratch in this situation, you can do so by setting the property `addKey` (boolean). 71 | This will add the `key` property to the component with a value of the current path. 72 | -------------------------------------------------------------------------------- /packages/tsrux/src/actionCreator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definition of an Action, possibly including a payload and/or a meta attribute. 3 | * 4 | * @property type he type used to identify this action 5 | * @property payload The payload 6 | * @property meta The metadata 7 | */ 8 | export type Action = TPayload extends undefined 9 | ? TMeta extends undefined 10 | ? { type: TType } 11 | : { type: TType; meta: TMeta } 12 | : TMeta extends undefined 13 | ? { type: TType; payload: TPayload } 14 | : { type: TType; payload: TPayload; meta: TMeta }; 15 | 16 | export type AnyAction = Action; 17 | 18 | /** 19 | * An Action creator is used to create actions. 20 | * 21 | * @param ...args Parameters required to create the action 22 | * @property type The type, which will be set in the action. 23 | */ 24 | export type ActionCreator< 25 | TType extends string, 26 | TAction extends Action, 27 | TParams extends any[] = any[], 28 | > = ((...args: TParams) => TAction) & { type: TType }; 29 | 30 | export type ActionOf> = ReturnType; 31 | 32 | /** 33 | * Redux Action Creator Factory 34 | * 35 | * @param type The type to be used in created types. 36 | * @param getPayload Optional. A function used to create the payload. The parameters are up to you! 37 | * @param getMeta Optional. A function used to create the metadata. The parameters must match getPayload! 38 | * @returns An ActionCreator. 39 | */ 40 | export function actionCreator(type: TType): (() => Action) & { type: TType }; 41 | export function actionCreator( 42 | type: TType, 43 | getPayload?: (...args: TParams) => TPayload, 44 | getMeta?: (...args: TParams) => TMeta, 45 | ): ((...args: TParams) => Action) & { type: TType }; 46 | export function actionCreator( 47 | type: TType, 48 | getPayload?: (...args: TParams) => TPayload, 49 | getMeta?: (...args: TParams) => TMeta, 50 | ): ActionCreator, TParams> { 51 | const creator = (...args: TParams) => { 52 | const action: { type: TType; payload?: TPayload; meta?: TMeta } = { type }; 53 | if (getPayload) action.payload = getPayload(...args); 54 | if (getMeta) action.meta = getMeta(...args); 55 | return action; 56 | }; 57 | return Object.assign(creator as any, { type }); 58 | } 59 | -------------------------------------------------------------------------------- /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/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/router.md: -------------------------------------------------------------------------------- 1 | # Router 2 | 3 | You'll need to add a `Router` component in your app (just one). Any other components and hooks from this library need to be children of this `Router` (doesn't matter how deeply nested). 4 | 5 | ```tsx 6 | import { Router } from "@react-nano/router"; 7 | export const App = () => ....; 8 | ``` 9 | 10 | ## Storing the Route in the Hash 11 | 12 | In some situations, it's easier to store the route in the hash part of the URL, as it avoids the server having to be aware of the single page application behavior. You can enable the "hash" mode on the `Router` component: 13 | 14 | ```tsx 15 | import { Router } from "@react-nano/router"; 16 | export const App = () => ....; 17 | ``` 18 | 19 | This will result in a url like `https://some-domain.com/#/News` instead of `https://some-domain.com/News`. 20 | 21 | This approach will still use the [history](https://caniuse.com/history) API internally! 22 | 23 | ## Using a Basename 24 | 25 | If your app is not located at the root directory of a server, but instead in a sub-directory, you'll want to specify that sub-directory. Basename will then automatically be prefixed on [Link](./links.md) components. 26 | 27 | ```tsx 28 | import { Router } from "@react-nano/router"; 29 | export const App = () => ....; 30 | ``` 31 | 32 | If you have a `` tag in your HTML, this can be easily detected using the `getBasename()` helper. That way you don't have to hard-code it: 33 | 34 | ```tsx 35 | import { Router, getBasename } from "@react-nano/router"; 36 | export const App = () => ....; 37 | ``` 38 | 39 | ## Custom Route Matching 40 | 41 | This library doesn't force a route matching algorithm on you, but it comes with a lightweight one built-in. 42 | The built-in route matching algorithm only allows exact matches and a "match everything" ("\*") though. 43 | 44 | If you need something more sophisticated, you'll have to supply a factory. Here is a simple example using the popular [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) library: 45 | 46 | ```tsx 47 | import { pathToRegexp, Key } from "path-to-regexp"; 48 | import { Router, RouteParams } from "@react-nano/router"; 49 | 50 | function routeMatcherFactory(pattern: string) { 51 | const keys: Key[] = []; 52 | const regex = pathToRegexp(pattern, keys); 53 | 54 | return (path: string) => { 55 | const out = regex.exec(path); 56 | 57 | if (!out) return null; 58 | 59 | return keys.reduce((params, key, i) => { 60 | params[key.name] = out[i + 1]; 61 | return params; 62 | }, {} as RouteParams); 63 | }; 64 | } 65 | 66 | export const App = () => ....; 67 | ``` 68 | 69 | Using pathToRegexp allows to extract named parameters from a pattern like "/users/:name". 70 | I.e. if the path is "/users/Zaphod", then the param with the key "name" would have the value "Zaphod". 71 | -------------------------------------------------------------------------------- /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/tsrux/src/mapReducers.spec.ts: -------------------------------------------------------------------------------- 1 | import { actionCreator } from "./actionCreator"; 2 | import { mapReducers } from "./mapReducers"; 3 | 4 | const actionCreator1 = actionCreator("foo/action1"); 5 | const actionCreator2 = actionCreator( 6 | "bar/action2", 7 | (hello: string, world: number) => ({ hello, world }), 8 | (foo: string, bar: number) => ({ foo, bar }), 9 | ); 10 | 11 | const action1 = actionCreator1(); 12 | const action2 = actionCreator2("ultimate answer", 42); 13 | 14 | const initialState = { 15 | foo: "woot", 16 | bar: 13, 17 | wat: true, 18 | }; 19 | 20 | describe("mapReducers", () => { 21 | describe("with state=undefined", () => { 22 | it("passes the initialState to the handler and returns the value returned by the handler", () => { 23 | const handler1 = jest.fn().mockReturnValueOnce("newState1"); 24 | const handler2 = jest.fn().mockReturnValueOnce("newState2"); 25 | const reducer = mapReducers(initialState, (handle) => [ 26 | handle(actionCreator1, handler1), 27 | handle(actionCreator2, handler2), 28 | ]); 29 | expect(reducer(undefined, action1)).toBe("newState1"); 30 | expect(handler1).toHaveBeenCalledWith(initialState, action1); 31 | expect(handler2).not.toHaveBeenCalled(); 32 | expect(reducer(undefined, action2)).toBe("newState2"); 33 | expect(handler2).toHaveBeenCalledWith(initialState, action2); 34 | }); 35 | 36 | it("ignores unhandled actions", () => { 37 | const handler1 = jest.fn(); 38 | const reducer = mapReducers(initialState, (handle) => [handle(actionCreator1, handler1)]); 39 | expect(reducer(undefined, action2)).toBe(initialState); 40 | expect(handler1).not.toHaveBeenCalled(); 41 | }); 42 | }); 43 | 44 | describe("with state!=undefined", () => { 45 | it("passes the state to the handler and returns the value returned by the handler", () => { 46 | const handler1 = jest.fn().mockReturnValueOnce("newState1"); 47 | const handler2 = jest.fn().mockReturnValueOnce("newState2"); 48 | const reducer = mapReducers(initialState, (handle) => [ 49 | handle(actionCreator1, handler1), 50 | handle(actionCreator2, handler2), 51 | ]); 52 | expect(reducer("oldState1" as any, action1)).toBe("newState1"); 53 | expect(handler1).toHaveBeenCalledWith("oldState1", action1); 54 | expect(handler2).not.toHaveBeenCalled(); 55 | expect(reducer("oldState2" as any, action2)).toBe("newState2"); 56 | expect(handler2).toHaveBeenCalledWith("oldState2", action2); 57 | }); 58 | 59 | it("ignores unhandled actions", () => { 60 | const handler1 = jest.fn(); 61 | const reducer = mapReducers(initialState, (handle) => [handle(actionCreator1, handler1)]); 62 | expect(reducer("oldState" as any, action2)).toBe("oldState"); 63 | expect(handler1).not.toHaveBeenCalled(); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/tsrux/src/actionCreator.spec.ts: -------------------------------------------------------------------------------- 1 | import { actionCreator } from "./actionCreator"; 2 | 3 | const ACTION = "hello/world"; 4 | 5 | describe("actionCreator", () => { 6 | describe("without payload and without meta", () => { 7 | const myAction = actionCreator(ACTION); 8 | 9 | it("should have the type set correctly", () => { 10 | expect(myAction.type).toBe(ACTION); 11 | }); 12 | 13 | it("should create an action with only the type set", () => { 14 | const action = myAction(); 15 | expect(Object.keys(action)).toEqual(["type"]); 16 | expect(action.type).toBe(ACTION); 17 | }); 18 | }); 19 | describe("with payload, but without meta", () => { 20 | const myAction = actionCreator(ACTION, (foo: string, bar: number) => ({ foo, bar })); 21 | 22 | it("should have the type set correctly", () => { 23 | expect(myAction.type).toBe(ACTION); 24 | }); 25 | 26 | it("should create an action with only the type and payload set", () => { 27 | const action = myAction("ultimate answer", 42); 28 | expect(Object.keys(action).sort()).toEqual(["payload", "type"]); 29 | expect(action.type).toBe(ACTION); 30 | expect(action.payload).toEqual({ 31 | foo: "ultimate answer", 32 | bar: 42, 33 | }); 34 | }); 35 | }); 36 | describe("without payload, but with meta", () => { 37 | const myAction = actionCreator(ACTION, undefined, (foo: string, bar: number) => ({ foo, bar })); 38 | 39 | it("should have the type set correctly", () => { 40 | expect(myAction.type).toBe(ACTION); 41 | }); 42 | 43 | it("should create an action with only the type and meta set", () => { 44 | const action = myAction("ultimate answer", 42); 45 | expect(Object.keys(action).sort()).toEqual(["meta", "type"]); 46 | expect(action.type).toBe(ACTION); 47 | expect(action.meta).toEqual({ 48 | foo: "ultimate answer", 49 | bar: 42, 50 | }); 51 | }); 52 | }); 53 | 54 | describe("with payload and meta", () => { 55 | const myAction = actionCreator( 56 | ACTION, 57 | (hello: string, world: number) => ({ hello, world }), 58 | (foo: string, bar: number) => ({ foo, bar }), 59 | ); 60 | 61 | it("should have the type set correctly", () => { 62 | expect(myAction.type).toBe(ACTION); 63 | }); 64 | 65 | it("should create an action with type, payload and meta set", () => { 66 | const action = myAction("ultimate answer", 42); 67 | expect(Object.keys(action).sort()).toEqual(["meta", "payload", "type"]); 68 | expect(action.type).toBe(ACTION); 69 | expect(action.payload).toEqual({ 70 | hello: "ultimate answer", 71 | world: 42, 72 | }); 73 | expect(action.meta).toEqual({ 74 | foo: "ultimate answer", 75 | bar: 42, 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /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/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/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/tsrux/README.md: -------------------------------------------------------------------------------- 1 | # @react-nano/tsrux 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/tsrux?icon=dockbit)](https://bundlephobia.com/result?p=@react-nano/tsrux) 5 | [![NPM version](https://flat.badgen.net/npm/v/@react-nano/tsrux?icon=npm)](https://www.npmjs.com/package/@react-nano/tsrux) 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 | @react-nano/tsrux enables you to reduce the [redux](https://redux.js.org/) boilerplate code you usually write to define your action creators, reducers, etc. and even gain type-safety in the process! 10 | 11 | The name stands for type-safe [redux](https://redux.js.org/), aside from the bloody obvious: TypeScript Rocks! 12 | 13 | ## Why Use @react-nano/tsrux? 14 | 15 | - Extremely lightweight: 300 byte vs 7.7 kilobyte for [deox](https://bundlephobia.com/result?p=deox). 16 | - Deadsimple to use 17 | - No dependencies! 18 | - [Fully documented](https://lusito.github.io/react-nano/tsrux/setup.html) 19 | - Automated unit- and type tests 20 | - Liberal license: [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) 21 | 22 | ## Example: Actions Creators 23 | 24 | ```typescript 25 | // No payload: 26 | export const fetchTodos = actionCreator("TODOS/FETCH"); 27 | // With payload: 28 | export const addTodo = actionCreator("TODOS/ADD", (label: string) => ({ label })); 29 | // With payload and metadata: 30 | export const removeTodo = actionCreator( 31 | "TODOS/REMOVE", 32 | (id: number) => ({ id }), 33 | (id: number) => ({ metaId: id, foo: "bar" }), 34 | ); 35 | ``` 36 | 37 | [find out more](https://lusito.github.io/react-nano/tsrux/action-creators.html) 38 | 39 | ## Example: Reducers 40 | 41 | ```typescript 42 | export const todosReducer = mapReducers(initialState, (handle) => [ 43 | handle(addTodo, (state, action) => ({ 44 | ...state, 45 | list: [...state.list, { id: state.nextId, label: action.payload.label, checked: false }], 46 | nextId: state.nextId + 1, 47 | })), 48 | ... 49 | ]); 50 | ``` 51 | 52 | [find out more](https://lusito.github.io/react-nano/tsrux/reducers.html) 53 | 54 | ## How to Use 55 | 56 | Check out the [documentation](https://lusito.github.io/react-nano/tsrux/setup.html) 57 | 58 | ## Similar Projects 59 | 60 | This package is heavily inspired by [deox](https://github.com/thebrodmann/deox), but uses a more lightweight approach. 61 | 62 | Aside from that, there are [redux-actions](https://github.com/redux-utilities/redux-actions) and [typesafe-actions](https://github.com/piotrwitek/typesafe-actions). 63 | 64 | ## Report Issues 65 | 66 | 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. 67 | 68 | ## Contribute 69 | 70 | 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. 71 | 72 | ## License 73 | 74 | @react-nano has been released under the [zlib/libpng](https://github.com/Lusito/react-nano/blob/master/LICENSE) license, meaning you 75 | can use it free of charge, without strings attached in commercial and non-commercial projects. Credits are appreciated but not mandatory. 76 | -------------------------------------------------------------------------------- /packages/use-fetch/docs/config.md: -------------------------------------------------------------------------------- 1 | # Configurations 2 | 3 | It's possible to supply configurations both globally (via provider) and locally (as argument to your custom 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 | ```tsx 11 | function UserComponent(props: { id: number }) { 12 | const [getUser] = useGetUser({ 13 | onInit(init) { 14 | init.headers.set("Authorization", "..."); 15 | } 16 | onSuccess(context) { 17 | console.log('success', context.data, context.status, context.responseHeaders); 18 | }, 19 | onError(context) { 20 | console.log('error', context.error, context.status, context.responseHeaders); 21 | }, 22 | onException(context) { 23 | console.log('exception', context.error); 24 | }, 25 | autoSubmit: { id: data.id }, 26 | }); 27 | // ... 28 | } 29 | ``` 30 | 31 | - All of the properties are optional. 32 | - Specify `autoSubmit` if you want to send the request on component mount without having to call its submit function manually. 33 | - Set this to true if your `prepare` function does not take a data parameter 34 | - Or set this to the data object your `prepare` function will receive 35 | - Specify callbacks for certain events instead of watching the state object in a useEffect Hook. 36 | - The hook will always use the latest version of the callbacks. 37 | - If the component making the request has been unmounted, the callbacks will not be called. 38 | 39 | ### Callback Context 40 | 41 | The context parameter has different properties depending on the callback: 42 | 43 | ```TypeScript 44 | export interface CallbackContext { 45 | /** The data you used to submit the request */ 46 | inputData: TVars; 47 | } 48 | 49 | export interface CallbackContextWithResponse extends CallbackContext { 50 | /** The status code of the request */ 51 | status: number; 52 | /** The response headers headers of the request */ 53 | responseHeaders: Headers; 54 | } 55 | 56 | export interface OnSuccessContext extends CallbackContextWithResponse { 57 | /** The result of the fetch */ 58 | data: TData; 59 | } 60 | 61 | export interface OnErrorContext extends CallbackContextWithResponse { 62 | /** The error data the server returned for the fetch */ 63 | error: TError; 64 | } 65 | 66 | export interface OnExceptionContext extends CallbackContext { 67 | /** The error that was thrown. */ 68 | error: Error; 69 | } 70 | ``` 71 | 72 | ## Global Configuration 73 | 74 | You can specify a global configuration by wrapping your app content in a provider: 75 | 76 | ```tsx 77 | function MyApp { 78 | return ( 79 | 88 | ...The app content 89 | 90 | ); 91 | } 92 | ``` 93 | 94 | As said, the only difference between local and global configurations is the autoSubmit property in local configurations. 95 | 96 | - Callbacks will run both globally and locally. 97 | - Keep in mind, that some of the types are different for each hook, so you'll have to manually cast the data if you need access to it. 98 | - Most common scenarios for using the callbacks globally: 99 | - Adding authorization headers to the request 100 | - Logging and/or showing toast messages on success/error/exception events 101 | - Just as with local callbacks: If the component making the request has been unmounted, the callbacks will not be called. 102 | -------------------------------------------------------------------------------- /packages/use-fetch/docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## `createFetchHook` 4 | 5 | - `createFetchHook` creates a type-safe hook that you can use to perform the fetch. 6 | - it takes an object with 3 attributes: 7 | - `prepare` is a function used to prepare the init object you would pass to a fetch call. 8 | - the first parameter is the init object you can modify. 9 | - its (optional) second parameter can be an object of your liking 10 | - the return value should be the URL you want to run this fetch against. 11 | - `getResult` is a function called to get the result of a response and also to specify the type of the data. Always add a `as Promise` at the end to define your type. 12 | - `getError` is essentially the same, but for the case where `response.ok === false`. I.e. you can have a different type for non-ok responses. 13 | 14 | ## Your Custom Hooks 15 | 16 | - The hook created by `createFetchHook` gets an optional config parameter with these optional properties: 17 | - One or more of these callbacks: `onInit`, `onSuccess`, `onError`, `onException`. 18 | - See [config](./config.md) for more details 19 | - A parameter `autoSubmit`, which can be used to automatically submit the request on component mount 20 | - Set this to true if your `prepare` function does not take a data parameter 21 | - Or set this to the data object your `prepare` function will receive 22 | - `useFetch` returns an array containing 3 items: 23 | 1. The current state of the fetch request, containing the result or error data when it's done. See below for more details. 24 | 2. A submit function, which you can call to manually (re-)submit the request. 25 | 3. An abort function to cancel the active request (it will be automatically called upon unmount). 26 | 27 | ## `FetchState` 28 | 29 | The first entry of the array returned by your custom hook is a state object. Depending on its `state` property, it can have more properties: 30 | 31 | ```tsx 32 | // These properties are always available 33 | export interface FetchStateBase { 34 | /** Request is currently in progress */ 35 | loading: boolean; 36 | /** Either an exception occurred or the request returned an error */ 37 | failed: boolean; 38 | /** Request was successful */ 39 | success: boolean; 40 | } 41 | 42 | // These are only available when the request has not finished yet 43 | export interface FetchStateEmpty extends FetchStateBase { 44 | state: "empty"; 45 | failed: false; 46 | success: false; 47 | } 48 | 49 | // These are available in case of success or error 50 | export interface FetchStateDone extends FetchStateBase { 51 | /** The status code of the response */ 52 | responseStatus: number; 53 | /** The headers of the response */ 54 | responseHeaders: Headers; 55 | } 56 | 57 | // These are available in case of success 58 | export interface FetchStateDoneSuccess extends FetchStateDone { 59 | failed: false; 60 | success: true; 61 | /** Data is present */ 62 | state: "success"; 63 | /** The response data in case of success */ 64 | data: TData; 65 | } 66 | 67 | // These are available in case of an error 68 | export interface FetchStateDoneError> extends FetchStateDone { 69 | failed: true; 70 | success: false; 71 | /** Errors is present */ 72 | state: "error"; 73 | /** The server result data. */ 74 | error: TError; 75 | } 76 | 77 | // These are available in case of an exception 78 | export interface FetchStateDoneException extends FetchStateBase { 79 | failed: true; 80 | success: false; 81 | /** Errors is present */ 82 | state: "exception"; 83 | /** The cause of the exception. */ 84 | error: Error; 85 | } 86 | 87 | // FetchState can be either of the above: 88 | export type FetchState> = 89 | | FetchStateEmpty 90 | | FetchStateDoneSuccess 91 | | FetchStateDoneError 92 | | FetchStateDoneException; 93 | ``` 94 | 95 | As you can see, you will only be able to access `state.data` if you checked for `state.success` or `state.state === "success"` (or if you ruled out the other possibilities first) 96 | -------------------------------------------------------------------------------- /packages/use-fetch/docs/modify.md: -------------------------------------------------------------------------------- 1 | # Example: Modify Data 2 | 3 | This builds upon what you've learned in the [Request Data](./get.md) section. 4 | 5 | ## Creating a Custom Fetch Hook 6 | 7 | Let's say you have a form to submit updates on a user object. 8 | 9 | Again, we'll need to create a custom hook. This time it will take a FormData object in addition to the id. 10 | 11 | ```tsx 12 | import { setupFetch, preparePost } from "@react-nano/use-fetch"; 13 | 14 | export const useUpdateUser = createFetchHook({ 15 | prepare: (init: FetchRequestInit, data: { id: number; formData: FormData }) => { 16 | prepareFormDataPost(init, data.formData); 17 | init.method = "PUT"; 18 | return `api/user${data.id}`; 19 | }, 20 | getResult: (response: Response) => response.json() as Promise, 21 | getError: (response: Response) => response.json() as Promise, 22 | }); 23 | ``` 24 | 25 | - `prepareFormDataPost` is a helper function, which will prepare the init object with a FormData object. See [helper functions](./helpers.md) for more details. 26 | - Additionally, since `prepareFormDataPost` sets `init.method` to "POST", we override this here with a "PUT". 27 | - In this case, we expect the server to return `true` on success, so the result type is `boolean`. 28 | - Aside from that there is nothing special going on here. 29 | 30 | ## Using the Custom Fetch Hook 31 | 32 | Here's how you might use the hook (in addition to `useGetUser` from the previous example): 33 | 34 | ```tsx 35 | function EditUserComponent(props: { id: number }) { 36 | const [getUser] = useGetUser({ autoSubmit: { id: props.id } }); 37 | const [updateUser, submitUpdateUser] = useUpdateUserFetch(); 38 | 39 | if (getUser.failed) return
Error fetching user
; 40 | if (!getUser.success) return
Loading..
; 41 | 42 | const user = getUser.data; 43 | const validationErrors = getValidationErrors(updateUser); 44 | 45 | return ( 46 |
submitUpdateUser({ id: props.id, formData: new FormData(e.currentTarget) })} 48 | loading={updateUser.loading} 49 | > 50 | 51 | ... 52 | 53 | 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/tsrux/src/actionCreator.spec-d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved 2 | import { expectType, expectAssignable } from "tsd"; 3 | 4 | import { actionCreator, Action, ActionCreator, AnyAction, ActionOf } from "./actionCreator"; 5 | 6 | export interface TestPayload { 7 | foo: string; 8 | bar: number; 9 | } 10 | 11 | export interface TestMeta { 12 | hello: string; 13 | world: number; 14 | } 15 | 16 | export const placeholder = {} as any; 17 | 18 | /** 19 | * Type Action should work correctly 20 | */ 21 | export type Action1 = Action<"foo/bar/1">; 22 | export type Action2 = Action<"foo/bar/2", TestPayload>; 23 | export type Action3 = Action<"foo/bar/3", undefined, TestMeta>; 24 | export type Action4 = Action<"foo/bar/4", TestPayload, TestMeta>; 25 | 26 | expectType<{ type: "foo/bar/1" }>(placeholder as Action1); 27 | expectType<{ type: "foo/bar/2"; payload: TestPayload }>(placeholder as Action2); 28 | expectType<{ type: "foo/bar/3"; meta: TestMeta }>(placeholder as Action3); 29 | expectType<{ type: "foo/bar/4"; payload: TestPayload; meta: TestMeta }>(placeholder as Action4); 30 | 31 | /** 32 | * Type AnyAction should work correctly 33 | */ 34 | expectType | Action | Action | Action>( 35 | placeholder as AnyAction, 36 | ); 37 | expectAssignable(placeholder as Action1); 38 | expectAssignable(placeholder as Action2); 39 | expectAssignable(placeholder as Action3); 40 | expectAssignable(placeholder as Action4); 41 | 42 | /** 43 | * Type ActionCreator should work correctly 44 | */ 45 | export type ActionCreator1 = ActionCreator<"foo/bar/1", Action1, []>; 46 | export type ActionCreator2 = ActionCreator<"foo/bar/2", Action2, [string, number]>; 47 | export type ActionCreator3 = ActionCreator<"foo/bar/3", Action3, [string, number]>; 48 | export type ActionCreator4 = ActionCreator<"foo/bar/4", Action4, [string, number]>; 49 | 50 | expectType<{ type: "foo/bar/1" } & (() => Action1)>(placeholder as ActionCreator1); 51 | expectType<{ type: "foo/bar/2" } & ((x: string, y: number) => Action2)>(placeholder as ActionCreator2); 52 | expectType<{ type: "foo/bar/3" } & ((x: string, y: number) => Action3)>(placeholder as ActionCreator3); 53 | expectType<{ type: "foo/bar/4" } & ((x: string, y: number) => Action4)>(placeholder as ActionCreator4); 54 | 55 | /** 56 | * Type ActionOf should work correctly 57 | */ 58 | expectType(placeholder as ActionOf); 59 | expectType(placeholder as ActionOf); 60 | expectType(placeholder as ActionOf); 61 | expectType(placeholder as ActionOf); 62 | 63 | /** 64 | * Function actionCreator should work 65 | */ 66 | export const actionCreator1 = actionCreator("foo/bar/1"); 67 | export const actionCreator2 = actionCreator("foo/bar/2", (foo: string, bar: number) => ({ foo, bar })); 68 | export const actionCreator3 = actionCreator("foo/bar/3", undefined, (hello: string, world: number) => ({ 69 | hello, 70 | world, 71 | })); 72 | export const actionCreator4 = actionCreator( 73 | "foo/bar/4", 74 | (foo: string, bar: number) => ({ foo, bar }), 75 | (hello: string, world: number) => ({ hello, world }), 76 | ); 77 | 78 | /** 79 | * Should return the correct action creator type 80 | */ 81 | expectType(actionCreator1); 82 | expectType(actionCreator2); 83 | expectType(actionCreator3); 84 | expectType(actionCreator4); 85 | 86 | /** 87 | * Action creators should have a type property 88 | */ 89 | expectType<"foo/bar/1">(actionCreator1.type); 90 | expectType<"foo/bar/2">(actionCreator2.type); 91 | expectType<"foo/bar/3">(actionCreator3.type); 92 | expectType<"foo/bar/4">(actionCreator4.type); 93 | 94 | /** 95 | * Action creators should return correct action type when called 96 | */ 97 | expectType(actionCreator1()); 98 | expectType(actionCreator2("foo", 13)); 99 | expectType(actionCreator3("bar", 12)); 100 | expectType(actionCreator4("2k", 11)); 101 | -------------------------------------------------------------------------------- /packages/tsrux/src/mapReducers.spec-d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved 2 | import { expectType, expectAssignable } from "tsd"; 3 | 4 | import { Reducer, ReducerMap, ReducerMapActions, ReducerMapHandler, mapReducers } from "./mapReducers"; 5 | import { 6 | TestPayload, 7 | TestMeta, 8 | Action1, 9 | Action2, 10 | Action3, 11 | Action4, 12 | placeholder, 13 | actionCreator1, 14 | actionCreator4, 15 | actionCreator3, 16 | actionCreator2, 17 | } from "./actionCreator.spec-d"; 18 | import { AnyAction } from "./actionCreator"; 19 | 20 | interface State { 21 | part1: TestPayload; 22 | part2: TestMeta; 23 | } 24 | 25 | const initialState: State = { 26 | part1: { 27 | foo: "woot", 28 | bar: 12, 29 | }, 30 | part2: { 31 | hello: "woop", 32 | world: 13, 33 | }, 34 | }; 35 | 36 | /** 37 | * Type Reducer should work correctly 38 | */ 39 | type Reducer1 = Reducer; 40 | type Reducer2 = Reducer; 41 | type Reducer3 = Reducer; 42 | type Reducer4 = Reducer; 43 | 44 | expectType<(state: State, action: Action1) => State>(placeholder as Reducer1); 45 | expectType<(state: State, action: Action2) => State>(placeholder as Reducer2); 46 | expectType<(state: State, action: Action3) => State>(placeholder as Reducer3); 47 | expectType<(state: State, action: Action4) => State>(placeholder as Reducer4); 48 | 49 | /** 50 | * Type ReducerMap should work correctly 51 | */ 52 | expectAssignable<{ 53 | "foo/bar/1": Reducer1; 54 | "foo/bar/2": Reducer2; 55 | "foo/bar/3": Reducer3; 56 | "foo/bar/4": Reducer4; 57 | }>(placeholder as ReducerMap); 58 | 59 | /** 60 | * Type ReducerMapActions should work correctly 61 | */ 62 | expectType( 63 | placeholder as ReducerMapActions>, 64 | ); 65 | 66 | /** 67 | * Type ReducerMapHandler should work correctly 68 | */ 69 | expectType>( 70 | (placeholder as ReducerMapHandler)(actionCreator1, (state, action) => { 71 | expectType(state); 72 | expectType(action); 73 | return state; 74 | }), 75 | ); 76 | expectType>( 77 | (placeholder as ReducerMapHandler)(actionCreator2, (state, action) => { 78 | expectType(state); 79 | expectType(action); 80 | return state; 81 | }), 82 | ); 83 | expectType>( 84 | (placeholder as ReducerMapHandler)(actionCreator3, (state, action) => { 85 | expectType(state); 86 | expectType(action); 87 | return state; 88 | }), 89 | ); 90 | expectType>( 91 | (placeholder as ReducerMapHandler)(actionCreator4, (state, action) => { 92 | expectType(state); 93 | expectType(action); 94 | return state; 95 | }), 96 | ); 97 | 98 | /** 99 | * mapReducers should work correctly 100 | */ 101 | type ExpectedMappedReducerType = ( 102 | state: State | undefined, 103 | action: ReducerMapActions>, 104 | ) => State; 105 | 106 | expectType( 107 | mapReducers(initialState, (handle) => [ 108 | handle(actionCreator1, (state, action) => { 109 | expectType(state); 110 | expectType(action); 111 | return state; 112 | }), 113 | handle(actionCreator2, (state, action) => { 114 | expectType(state); 115 | expectType(action); 116 | return state; 117 | }), 118 | handle(actionCreator3, (state, action) => { 119 | expectType(state); 120 | expectType(action); 121 | return state; 122 | }), 123 | handle(actionCreator4, (state, action) => { 124 | expectType(state); 125 | expectType(action); 126 | return state; 127 | }), 128 | ]), 129 | ); 130 | -------------------------------------------------------------------------------- /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/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/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/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-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 | --------------------------------------------------------------------------------