├── .changeset └── config.json ├── .coveralls.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .yarnrc ├── LICENSE ├── README.md ├── examples ├── basic │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ └── hello.ts │ │ └── index.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json ├── example-rest │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ └── hello.ts │ │ ├── custom.tsx │ │ ├── deps.tsx │ │ ├── index.tsx │ │ └── update.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json ├── stook-graphql │ ├── .editorconfig │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── .umirc.ts │ ├── README.md │ ├── mock │ │ └── .gitkeep │ ├── package.json │ ├── src │ │ └── pages │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── others.tsx │ ├── tsconfig.json │ └── typings.d.ts ├── stook-localstorage │ ├── .editorconfig │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── .umirc.ts │ ├── README.md │ ├── mock │ │ └── .gitkeep │ ├── package.json │ ├── src │ │ └── pages │ │ │ ├── index.less │ │ │ └── index.tsx │ ├── tsconfig.json │ └── typings.d.ts ├── stook-rest-todos │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── manifest.json │ │ └── now.json │ ├── src │ │ ├── components │ │ │ └── App.tsx │ │ ├── hooks │ │ │ └── useFetchTodos.hooks.ts │ │ ├── index.tsx │ │ └── react-app-env.d.ts │ └── tsconfig.json └── stook-todomvc │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── now.json │ ├── src │ ├── components │ │ ├── App.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── Link.tsx │ │ ├── MainSection.tsx │ │ ├── TodoItem.tsx │ │ ├── TodoList.tsx │ │ └── TodoTextInput.tsx │ ├── constants │ │ ├── TodoFilters.tsx │ │ └── index.tsx │ ├── hooks │ │ ├── todos.hooks.ts │ │ └── visibilityFilter.hooks.ts │ ├── index.tsx │ └── react-app-env.d.ts │ └── tsconfig.json ├── lerna.json ├── package.json ├── packages ├── stook-async-storage │ ├── .gitignore │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── stook-devtools │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── stook-graphql │ ├── .gitignore │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── Client.ts │ │ ├── fetcher.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ └── tsconfig.json ├── stook-localstorage │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── test │ │ └── blah.test.ts │ └── tsconfig.json ├── stook-rest │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── Client.ts │ │ ├── fetcher.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ └── tsconfig.json ├── stook-toggle │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── test │ │ └── blah.test.ts │ └── tsconfig.json └── stook │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── Storage.ts │ ├── Store.ts │ ├── emitter.ts │ ├── getState.ts │ ├── index.ts │ ├── mutate.ts │ ├── types.ts │ └── useStore.ts │ ├── test │ ├── getState.test.tsx │ ├── mutate.test.tsx │ └── useStore.test.tsx │ └── tsconfig.json └── website ├── .gitignore ├── README.md ├── babel.config.js ├── blog ├── 2019-05-28-hola.md ├── 2019-05-29-hello-world.md └── 2019-05-30-welcome.md ├── docs ├── devtools │ └── intro.md ├── graphql │ ├── config.md │ ├── intro.md │ ├── middleware.md │ ├── query.md │ ├── quick-start.md │ ├── useMutate.md │ └── useQuery.md ├── intro.md ├── rest │ ├── config.md │ ├── custom-hooks.md │ ├── dependent.md │ ├── fetch.md │ ├── interceptor.md │ ├── intro.md │ ├── middleware.md │ ├── quick-start.md │ ├── refetch.md │ ├── share-state.md │ ├── useFetch.md │ └── useUpdate.md └── stook │ ├── custom-hooks.md │ ├── faq.md │ ├── get-state.md │ ├── intro.md │ ├── mutate.md │ ├── quick-start.md │ ├── share-state.md │ ├── test.md │ ├── typescript.md │ └── use-store.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src ├── components │ └── UserForm.tsx ├── css │ └── custom.css └── pages │ ├── ecosystem.js │ ├── index.js │ └── styles.module.css └── static ├── fonts ├── calligraffitti-regular-webfont.eot ├── calligraffitti-regular-webfont.svg ├── calligraffitti-regular-webfont.ttf ├── calligraffitti-regular-webfont.woff └── calligraffitti-regular-webfont.woff2 ├── img ├── favicon.ico ├── github-brands.svg ├── logo.png ├── logo.svg ├── stook-devtools.png ├── undraw_docusaurus_mountain.svg ├── undraw_docusaurus_react.svg ├── undraw_docusaurus_tree.svg └── use-store.png └── vercel.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [ 11 | "basic", 12 | "example-rest", 13 | "stook-graphql-example", 14 | "example-stook-localstorage", 15 | "stook-rest-todos", 16 | "stook-todomvc", 17 | "website" 18 | ] 19 | } -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: ZOGZQzU1FurMOifUdHahADN4lmlTv90JF -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | 4 | node_modules/ 5 | build/ 6 | 7 | yarn-error.log 8 | npm-debug.log 9 | lerna-debug.log 10 | 11 | coverage/ 12 | .cache/ 13 | 14 | 15 | yarn.lock 16 | *package-lock.json 17 | package-lock.json 18 | 19 | dist/ 20 | 21 | .cache 22 | .rts2_cache_cjs 23 | .rts2_cache_esm 24 | .rts2_cache_umd 25 | .rts2_cache_system 26 | .turbo -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .docusaurus 4 | .cache 5 | *.html 6 | build 7 | *.json 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | "registry" "https://registry.npmjs.org " 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 forsigner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stook 2 | 3 | [![npm](https://img.shields.io/npm/v/stook.svg)](https://www.npmjs.com/package/stook) [![Coverage Status](https://coveralls.io/repos/github/forsigner/stook/badge.svg?branch=master)](https://coveralls.io/github/forsigner/stook?branch=master) [![Minzipped size](https://img.shields.io/bundlephobia/minzip/stook.svg)](https://bundlephobia.com/result?p=stook) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 4 | 5 | A minimalist design state management library for React. 6 | 7 | ## Documentation 8 | 9 | The documentation site of stook is hosted at [https://stook.vercel.app](https://stook.vercel.app). 10 | 11 | ## Quick start 12 | 13 | **simplest** 14 | 15 | ```tsx 16 | import React from 'react' 17 | import { useStore } from 'stook' 18 | 19 | function Counter() { 20 | const [count, setCount] = useStore('Counter', 0) 21 | return ( 22 |
23 |

You clicked {count} times

24 | 25 |
26 | ) 27 | } 28 | ``` 29 | 30 | **share state** 31 | 32 | ```jsx 33 | import React from 'react' 34 | import { useStore } from 'stook' 35 | 36 | function Counter() { 37 | const [count, setCount] = useStore('Counter', 0) 38 | return ( 39 |
40 |

You clicked {count} times

41 | 42 |
43 | ) 44 | } 45 | 46 | function Display() { 47 | const [count] = useStore('Counter') 48 | return

{count}

49 | } 50 | 51 | function App() { 52 | return ( 53 |
54 | 55 | 56 |
57 | ) 58 | } 59 | ``` 60 | 61 | ## License 62 | 63 | [MIT License](https://github.com/forsigner/stook/blob/master/LICENSE) 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/basic/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.tsx`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /examples/basic/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /examples/basic/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.15.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "echo", 8 | "build:prod": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "next": "^11.0.1", 14 | "stook": "^1.17.0" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^7.29.0", 18 | "eslint-config-next": "^11.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/basic/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /examples/basic/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /examples/basic/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { useStore, mutate } from 'stook' 3 | import { produce, original } from 'immer' 4 | import styles from '../styles/Home.module.css' 5 | 6 | const Counter = () => { 7 | const [value] = useStore('count') 8 | return
count: {value.count}
9 | } 10 | 11 | const List = () => { 12 | const [list, setList] = useStore('list', [0]) 13 | return ( 14 |
15 |
len: {list.length}
16 | 28 |
29 | ) 30 | } 31 | 32 | export default function Home() { 33 | const [value, setValue] = useStore('count', { count: 0 }) 34 | 35 | return ( 36 |
37 | 38 | Create Next App 39 | 40 | 41 | 42 | 43 |
44 |
{value.count}
45 | 62 | 63 | 64 |
65 |
66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /examples/basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forsigner/stook/04e494ca2cd84f1fe5f7b48aa40b3047175ab9b0/examples/basic/public/favicon.ico -------------------------------------------------------------------------------- /examples/basic/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/basic/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .card { 82 | margin: 1rem; 83 | padding: 1.5rem; 84 | text-align: left; 85 | color: inherit; 86 | text-decoration: none; 87 | border: 1px solid #eaeaea; 88 | border-radius: 10px; 89 | transition: color 0.15s ease, border-color 0.15s ease; 90 | width: 45%; 91 | } 92 | 93 | .card:hover, 94 | .card:focus, 95 | .card:active { 96 | color: #0070f3; 97 | border-color: #0070f3; 98 | } 99 | 100 | .card h2 { 101 | margin: 0 0 1rem 0; 102 | font-size: 1.5rem; 103 | } 104 | 105 | .card p { 106 | margin: 0; 107 | font-size: 1.25rem; 108 | line-height: 1.5; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | @media (max-width: 600px) { 117 | .grid { 118 | width: 100%; 119 | flex-direction: column; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/basic/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/example-rest/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/example-rest/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /examples/example-rest/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.tsx`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /examples/example-rest/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /examples/example-rest/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /examples/example-rest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-rest", 3 | "version": "1.15.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "echo", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "11.0.1", 13 | "react": "17.0.2", 14 | "react-dom": "17.0.2", 15 | "stook-rest": "^1.17.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "17.0.17", 19 | "eslint": "7.32.0", 20 | "eslint-config-next": "11.0.1", 21 | "typescript": "4.3.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/example-rest/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /examples/example-rest/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /examples/example-rest/pages/custom.tsx: -------------------------------------------------------------------------------- 1 | import { config, useFetch } from 'stook-rest' 2 | 3 | config({ 4 | baseURL: 'https://jsonplaceholder.typicode.com', 5 | }) 6 | 7 | interface Todo { 8 | id: number 9 | title: string 10 | completed: boolean 11 | } 12 | 13 | const useFetchTodos = () => { 14 | const { loading, data: todos = [], error } = useFetch('/todos') 15 | const count = todos.length 16 | const completedCount = todos.filter((i) => i.completed).length 17 | return { loading, todos, count, completedCount, error } 18 | } 19 | 20 | const TodoList = () => { 21 | const { loading, todos, count, completedCount } = useFetchTodos() 22 | if (loading) return
loading....
23 | return ( 24 |
25 |
TodoList:
26 |
todos count: {count}
27 |
completed count: {completedCount}
28 |
{JSON.stringify(todos, null, 2)}
29 |
30 | ) 31 | } 32 | 33 | const ReuseTodoList = () => { 34 | const { loading, todos, count, completedCount } = useFetchTodos() 35 | if (loading) return
loading....
36 | return ( 37 |
38 |
ReuseTodoList:
39 |
todos count: {count}
40 |
completed count: {completedCount}
41 |
{JSON.stringify(todos, null, 2)}
42 |
43 | ) 44 | } 45 | 46 | export default function Page() { 47 | return ( 48 |
49 | 50 | 51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /examples/example-rest/pages/deps.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { config, useFetch } from 'stook-rest' 3 | 4 | config({ 5 | baseURL: 'https://jsonplaceholder.typicode.com', 6 | }) 7 | 8 | export default function Page() { 9 | const [count, setCount] = React.useState(1) 10 | const { loading, data, error } = useFetch('/todos', { 11 | deps: [count], 12 | }) 13 | 14 | if (loading) return loading... 15 | if (error) return error! 16 | 17 | const update = () => { 18 | setCount(count + 1) 19 | } 20 | 21 | return ( 22 |
23 | 24 |
    25 | {data.map((item) => ( 26 |
  • {item.title}
  • 27 | ))} 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /examples/example-rest/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { config, useFetch, applyMiddleware } from 'stook-rest' 2 | 3 | config({ 4 | baseURL: 'https://jsonplaceholder.typicode.com', 5 | }) 6 | 7 | applyMiddleware(async (ctx, next) => { 8 | await next() 9 | ctx.valid = false 10 | console.log('ctx', ctx) 11 | }) 12 | 13 | const TodoItem = () => { 14 | const { 15 | loading, 16 | called, 17 | data: todo, 18 | start, 19 | error, 20 | } = useFetch('/todos/1', { 21 | // lazy: true, 22 | }) 23 | 24 | console.log('loading:', loading, 'called:', called, 'error:', error) 25 | 26 | if (loading) 27 | return ( 28 |
29 | 30 | {loading && called &&
loading....
} 31 |
32 | ) 33 | 34 | return ( 35 |
36 |
{JSON.stringify(todo, null, 2)}
37 |
38 | ) 39 | } 40 | 41 | const ReuseTodoItem = () => { 42 | const { loading, data: todo } = useFetch('/todos/1') 43 | if (loading) return
loading....
44 | 45 | return ( 46 |
47 |
ReuseTodoItem:
48 |
{JSON.stringify(todo, null, 2)}
49 |
50 | ) 51 | } 52 | 53 | export default function Page() { 54 | return ( 55 |
56 | 57 | {/* */} 58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /examples/example-rest/pages/update.tsx: -------------------------------------------------------------------------------- 1 | import { config, useUpdate } from 'stook-rest' 2 | 3 | config({ 4 | baseURL: 'https://jsonplaceholder.typicode.com', 5 | }) 6 | 7 | export default function Page() { 8 | const [addTodo, { loading, called, data, error }] = useUpdate('/todos') 9 | 10 | return ( 11 |
12 | 22 | 23 | {error &&
{JSON.stringify(error, null, 2)}
} 24 | {data &&
{JSON.stringify(data, null, 2)}
} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /examples/example-rest/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forsigner/stook/04e494ca2cd84f1fe5f7b48aa40b3047175ab9b0/examples/example-rest/public/favicon.ico -------------------------------------------------------------------------------- /examples/example-rest/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/example-rest/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .card { 82 | margin: 1rem; 83 | padding: 1.5rem; 84 | text-align: left; 85 | color: inherit; 86 | text-decoration: none; 87 | border: 1px solid #eaeaea; 88 | border-radius: 10px; 89 | transition: color 0.15s ease, border-color 0.15s ease; 90 | width: 45%; 91 | } 92 | 93 | .card:hover, 94 | .card:focus, 95 | .card:active { 96 | color: #0070f3; 97 | border-color: #0070f3; 98 | } 99 | 100 | .card h2 { 101 | margin: 0 0 1rem 0; 102 | font-size: 1.5rem; 103 | } 104 | 105 | .card p { 106 | margin: 0; 107 | font-size: 1.25rem; 108 | line-height: 1.5; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | @media (max-width: 600px) { 117 | .grid { 118 | width: 100%; 119 | flex-direction: column; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/example-rest/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /examples/example-rest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/stook-graphql/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /examples/stook-graphql/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | /package-lock.json 9 | 10 | # production 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | 16 | # umi 17 | /src/.umi 18 | /src/.umi-production 19 | /src/.umi-test 20 | /.env.local 21 | -------------------------------------------------------------------------------- /examples/stook-graphql/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | .umi-test 9 | -------------------------------------------------------------------------------- /examples/stook-graphql/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/stook-graphql/.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | 3 | export default defineConfig({ 4 | nodeModulesTransform: { 5 | type: 'none', 6 | }, 7 | routes: [ 8 | { path: '/', component: '@/pages/index' }, 9 | ], 10 | }); 11 | -------------------------------------------------------------------------------- /examples/stook-graphql/README.md: -------------------------------------------------------------------------------- 1 | # umi project 2 | 3 | ## Getting Started 4 | 5 | Install dependencies, 6 | 7 | ```bash 8 | $ yarn 9 | ``` 10 | 11 | Start the dev server, 12 | 13 | ```bash 14 | $ yarn start 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/stook-graphql/mock/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forsigner/stook/04e494ca2cd84f1fe5f7b48aa40b3047175ab9b0/examples/stook-graphql/mock/.gitkeep -------------------------------------------------------------------------------- /examples/stook-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-graphql-example", 3 | "version": "1.15.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "umi dev", 7 | "build": "echo", 8 | "postinstall": "umi generate tmp", 9 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 10 | "test": "umi-test", 11 | "test:coverage": "umi-test --coverage" 12 | }, 13 | "gitHooks": { 14 | "pre-commit": "lint-staged" 15 | }, 16 | "lint-staged": { 17 | "*.{js,jsx,less,md,json}": [ 18 | "prettier --write" 19 | ], 20 | "*.ts?(x)": [ 21 | "prettier --parser=typescript --write" 22 | ] 23 | }, 24 | "dependencies": { 25 | "@ant-design/pro-layout": "^6.10.7", 26 | "@umijs/preset-react": "1.7.10", 27 | "stook-graphql": "^1.17.0", 28 | "umi": "^3.3.4" 29 | }, 30 | "devDependencies": { 31 | "@umijs/test": "^3.4.22", 32 | "lint-staged": "^11.0.0", 33 | "prettier": "^2.3.0", 34 | "yorkie": "^2.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/stook-graphql/src/pages/index.less: -------------------------------------------------------------------------------- 1 | .title { 2 | background: rgb(121, 242, 157); 3 | } 4 | -------------------------------------------------------------------------------- /examples/stook-graphql/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getState, mutate } from 'stook'; 3 | import gql from 'gql-tag'; 4 | 5 | import { 6 | config, 7 | query, 8 | useQuery, 9 | useMutation, 10 | fetcher, 11 | useSubscription, 12 | fromSubscription, 13 | applyMiddleware, 14 | Client, 15 | Result, 16 | } from 'stook-graphql'; 17 | 18 | applyMiddleware(async (ctx, next) => { 19 | await next(); 20 | if (typeof ctx.body !== 'object') return; 21 | if (Object.keys(ctx.body).length === 1) { 22 | ctx.body = ctx.body[Object.keys(ctx.body)[0]]; 23 | } 24 | }); 25 | 26 | const endpoint = 'https://graphql.anilist.co'; 27 | 28 | config({ endpoint }); 29 | 30 | const User = gql` 31 | { 32 | User(id: 1) { 33 | id 34 | name 35 | } 36 | } 37 | `; 38 | 39 | const MediaTagCollection = gql` 40 | { 41 | MediaTagCollection { 42 | id 43 | description 44 | } 45 | } 46 | `; 47 | 48 | export default () => { 49 | const { loading, data } = useQuery(User); 50 | const { data: list } = useQuery(MediaTagCollection); 51 | 52 | function updateUser() { 53 | mutate(User, (state: Result) => { 54 | state.data = { 55 | id: 10, 56 | name: 'foo', 57 | }; 58 | }); 59 | } 60 | 61 | function updateList() { 62 | mutate(MediaTagCollection, (state: Result) => { 63 | state.data[0].description = 'fofoo'; 64 | }); 65 | } 66 | 67 | console.log('loading:', loading, data); 68 | if (loading) return null; 69 | 70 | return ( 71 |
72 |

Result

73 | 74 | 75 |
{JSON.stringify(data, null, 2)}
76 |
{JSON.stringify(list, null, 2)}
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /examples/stook-graphql/src/pages/others.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import gql from 'gql-tag'; 3 | 4 | import { 5 | config, 6 | query, 7 | useQuery, 8 | useMutation, 9 | fetcher, 10 | useSubscription, 11 | fromSubscription, 12 | applyMiddleware, 13 | Client, 14 | } from 'stook-graphql'; 15 | 16 | applyMiddleware(async (ctx, next) => { 17 | ctx.headers.Authorization = `bearer token...`; 18 | await next(); 19 | if (typeof ctx.body !== 'object') return; 20 | if (Object.keys(ctx.body).length === 1) { 21 | ctx.body = ctx.body[Object.keys(ctx.body)[0]]; 22 | } 23 | }); 24 | 25 | export const GET_USER = gql` 26 | { 27 | userMany { 28 | _id 29 | name 30 | } 31 | } 32 | `; 33 | 34 | config({ 35 | // endpoint: 'http://localhost:7001/graphql', 36 | endpoint: 'https://graphql-compose.herokuapp.com/user', 37 | // endpoint: 'http://localhost:5001/graphql', 38 | // subscriptionsEndpoint: 'ws://localhost:5001/graphql', 39 | }); 40 | 41 | const client = new Client({ 42 | endpoint: 'http://localhost:5001/graphql', 43 | subscriptionsEndpoint: 'ws://localhost:5001/graphql', 44 | }); 45 | 46 | export const GET_PROJECT = gql` 47 | query plot($scriptId: Int) { 48 | plots(scriptId: $scriptId) { 49 | scriptId 50 | id 51 | title 52 | } 53 | } 54 | # query getProject($slug: String!) { 55 | # project(slug: $slug) { 56 | # _id 57 | # name 58 | # } 59 | # } 60 | # { 61 | # message { 62 | # content 63 | # id 64 | # } 65 | # } 66 | 67 | # { 68 | # users { 69 | # name 70 | # age 71 | # } 72 | # user(name: "Rose") { 73 | # name 74 | # } 75 | # } 76 | `; 77 | 78 | const GET_USER_BY_ID = gql` 79 | query User($_id: MongoID!) { 80 | userById(_id: $_id) { 81 | _id 82 | name 83 | gender 84 | age 85 | } 86 | } 87 | `; 88 | 89 | const SUB = gql` 90 | subscription msg { 91 | messageSubscription { 92 | id 93 | content 94 | } 95 | } 96 | `; 97 | 98 | const GET_NOTICE = gql` 99 | { 100 | message { 101 | content 102 | id 103 | } 104 | } 105 | `; 106 | 107 | const SubApp = () => { 108 | const { data = {} } = client.useSubscription(SUB, { 109 | initialQuery: { 110 | query: GET_NOTICE, 111 | }, 112 | }); 113 | return ( 114 |
115 |
{JSON.stringify(data, null, 2)}
116 |
117 | ); 118 | }; 119 | 120 | const QueryApp = () => { 121 | const [data, setData] = useState(); 122 | useEffect(() => { 123 | async function queryData() { 124 | // const res = await query(GET_USER) 125 | const res = await query(GET_PROJECT, { 126 | variables: { slug: 'foo' }, 127 | }); 128 | 129 | setData(res); 130 | } 131 | queryData(); 132 | }, []); 133 | 134 | return ( 135 |
136 |
{JSON.stringify(data, null, 2)}
137 |
138 | ); 139 | }; 140 | 141 | const UseQueryApp = () => { 142 | const { loading, data, error, refetch } = useQuery(GET_USER, { 143 | // pollInterval: 3000, 144 | variables: { slug: 'foo' }, 145 | }); 146 | 147 | console.log('loading:', loading); 148 | console.log('data:', data); 149 | 150 | if (loading) return
loading....
; 151 | if (error) return
{JSON.stringify(error, null, 2)}
; 152 | 153 | return ( 154 |
155 | 156 | 157 |
{JSON.stringify(data, null, 2)}
158 |
159 | ); 160 | }; 161 | 162 | // setTimeout(() => { 163 | // store.setId() 164 | // }, 2000) 165 | 166 | interface Project { 167 | name: string; 168 | age: string; 169 | } 170 | 171 | const UseQueryById = () => { 172 | // const { data: user } = useQuery(GET_USER, { 173 | // variables: { login: 'forsigner' }, 174 | // }) 175 | 176 | const { data: script, loading: loading1 } = client.useQuery(` 177 | { 178 | script(id: 32){ 179 | id 180 | limitNum 181 | title 182 | } 183 | } 184 | `); 185 | 186 | const { loading, data, error, refetch } = client.useQuery( 187 | GET_PROJECT, 188 | { 189 | // name: 'getUserById', 190 | variables: () => { 191 | return { scriptId: script.script.id }; 192 | }, 193 | }, 194 | ); 195 | 196 | console.log('render---------data:', loading1, loading, script, data); 197 | if (loading) return
loading....
; 198 | if (error) return
{JSON.stringify(error, null, 2)}
; 199 | 200 | return ( 201 |
202 | 205 | 212 |
213 |
{JSON.stringify(data, null, 2)}
214 |
215 |
216 | ); 217 | }; 218 | 219 | const UseMutateApp = () => { 220 | const [addTodo, { loading, data, error }] = useMutation(GET_USER); 221 | console.log('loading:', loading); 222 | 223 | return ( 224 |
225 | 229 | 230 | {error &&
{JSON.stringify(error, null, 2)}
} 231 | {data &&
{JSON.stringify(data, null, 2)}
} 232 |
233 | ); 234 | }; 235 | 236 | export default () => ( 237 |
238 | {/* */} 239 | {/* */} 240 | {/* */} 241 | 242 | {/* */} 243 |
244 | ); 245 | 246 | // fromSubscription(SUB).subscribe({ 247 | // next(data) { 248 | // console.log('data---------:', data) 249 | // }, 250 | // }) 251 | -------------------------------------------------------------------------------- /examples/stook-graphql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "strict": true, 12 | "paths": { 13 | "@/*": ["src/*"], 14 | "@@/*": ["src/.umi/*"] 15 | }, 16 | "allowSyntheticDefaultImports": true 17 | }, 18 | "include": [ 19 | "mock/**/*", 20 | "src/**/*", 21 | "config/**/*", 22 | ".umirc.ts", 23 | "typings.d.ts" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "es", 29 | "dist", 30 | "typings", 31 | "**/__test__", 32 | "test", 33 | "docs", 34 | "tests" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/stook-graphql/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | declare module '*.png'; 4 | declare module '*.svg' { 5 | export function ReactComponent(props: React.SVGProps): React.ReactElement 6 | const url: string 7 | export default url 8 | } 9 | -------------------------------------------------------------------------------- /examples/stook-localstorage/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /examples/stook-localstorage/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | /package-lock.json 9 | 10 | # production 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | 16 | # umi 17 | /src/.umi 18 | /src/.umi-production 19 | /src/.umi-test 20 | /.env.local 21 | -------------------------------------------------------------------------------- /examples/stook-localstorage/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | .umi-test 9 | -------------------------------------------------------------------------------- /examples/stook-localstorage/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/stook-localstorage/.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | 3 | export default defineConfig({ 4 | nodeModulesTransform: { 5 | type: 'none', 6 | }, 7 | routes: [ 8 | { path: '/', component: '@/pages/index' }, 9 | ], 10 | fastRefresh: {}, 11 | }); 12 | -------------------------------------------------------------------------------- /examples/stook-localstorage/README.md: -------------------------------------------------------------------------------- 1 | # umi project 2 | 3 | ## Getting Started 4 | 5 | Install dependencies, 6 | 7 | ```bash 8 | $ yarn 9 | ``` 10 | 11 | Start the dev server, 12 | 13 | ```bash 14 | $ yarn start 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/stook-localstorage/mock/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forsigner/stook/04e494ca2cd84f1fe5f7b48aa40b3047175ab9b0/examples/stook-localstorage/mock/.gitkeep -------------------------------------------------------------------------------- /examples/stook-localstorage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-stook-localstorage", 3 | "private": true, 4 | "scripts": { 5 | "start": "umi dev", 6 | "build": "echo", 7 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 8 | "test": "umi-test", 9 | "test:coverage": "umi-test --coverage" 10 | }, 11 | "gitHooks": { 12 | "pre-commit": "lint-staged" 13 | }, 14 | "lint-staged": { 15 | "*.{js,jsx,less,md,json}": [ 16 | "prettier --write" 17 | ], 18 | "*.ts?(x)": [ 19 | "prettier --parser=typescript --write" 20 | ] 21 | }, 22 | "dependencies": { 23 | "@ant-design/pro-layout": "^6.5.0", 24 | "@umijs/preset-react": "1.x", 25 | "umi": "^3.4.22" 26 | }, 27 | "devDependencies": { 28 | "@umijs/test": "^3.4.22", 29 | "lint-staged": "^10.0.7", 30 | "stook-localstorage": "^1.17.0", 31 | "yorkie": "^2.0.0" 32 | }, 33 | "version": "1.15.0" 34 | } 35 | -------------------------------------------------------------------------------- /examples/stook-localstorage/src/pages/index.less: -------------------------------------------------------------------------------- 1 | .title { 2 | background: rgb(121, 242, 157); 3 | } 4 | -------------------------------------------------------------------------------- /examples/stook-localstorage/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from 'stook-localstorage'; 2 | 3 | export default function IndexPage() { 4 | const [value, setItem] = useLocalStorage('STORAGE_KEY', true); 5 | return ( 6 |
7 | {value} 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /examples/stook-localstorage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react-jsx", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "strict": true, 12 | "paths": { 13 | "@/*": ["src/*"], 14 | "@@/*": ["src/.umi/*"] 15 | }, 16 | "allowSyntheticDefaultImports": true 17 | }, 18 | "include": [ 19 | "mock/**/*", 20 | "src/**/*", 21 | "config/**/*", 22 | ".umirc.ts", 23 | "typings.d.ts" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "es", 29 | "dist", 30 | "typings", 31 | "**/__test__", 32 | "test", 33 | "docs", 34 | "tests" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/stook-localstorage/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | declare module '*.png'; 4 | declare module '*.svg' { 5 | export function ReactComponent( 6 | props: React.SVGProps, 7 | ): React.ReactElement; 8 | const url: string; 9 | export default url; 10 | } 11 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-rest-todos", 3 | "version": "1.15.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/classnames": "^2.2.11", 7 | "@types/node": "13.1.0", 8 | "classnames": "^2.2.6", 9 | "react-scripts": "4.0.3", 10 | "redux": "^4.0.5", 11 | "stook": "^1.17.0", 12 | "stook-devtools": "^1.17.0", 13 | "stook-rest": "^1.17.0", 14 | "todomvc-app-css": "^2.4.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "echo", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forsigner/stook/04e494ca2cd84f1fe5f7b48aa40b3047175ab9b0/examples/stook-rest-todos/public/favicon.ico -------------------------------------------------------------------------------- /examples/stook-rest-todos/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/public/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-rest-todos", 3 | "version": 2, 4 | "alias": ["stook-rest-todos"] 5 | } 6 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useFetchTodos } from '../hooks/useFetchTodos.hooks' 3 | 4 | export default () => { 5 | const { loading, todos, count, completedCount } = useFetchTodos() 6 | 7 | if (loading) return
loading....
8 | return ( 9 |
10 |

Todos:

11 |
12 | 总计:{count} 13 |    14 | 已完成:{completedCount} 15 |
16 |
    17 | {todos.map(item => ( 18 |
  • {item.title}
  • 19 | ))} 20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/src/hooks/useFetchTodos.hooks.ts: -------------------------------------------------------------------------------- 1 | import { useFetch } from 'stook-rest' 2 | interface Todo { 3 | id: number 4 | title: string 5 | completed: boolean 6 | } 7 | 8 | export const useFetchTodos = () => { 9 | const result = useFetch('/todos') 10 | const { data: todos = [] } = result 11 | const count = todos.length 12 | const completedCount = todos.filter(i => i.completed).length 13 | 14 | return { 15 | ...result, 16 | todos, 17 | count, 18 | completedCount, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { render } from 'react-dom' 3 | import { config } from 'stook-rest' 4 | import App from './components/App' 5 | import { devtools } from 'stook-devtools' 6 | 7 | 8 | devtools.init() 9 | 10 | config({ 11 | baseURL: "https://jsonplaceholder.typicode.com" 12 | }); 13 | 14 | render(, document.getElementById('root')) 15 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/stook-rest-todos/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/stook-todomvc/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/stook-todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/stook-todomvc/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /examples/stook-todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-todomvc", 3 | "version": "1.15.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/classnames": "^2.2.11", 7 | "@types/jest": "26.0.19", 8 | "@types/node": "14.14.14", 9 | "classnames": "^2.2.6", 10 | "react-scripts": "4.0.1", 11 | "redux": "^4.1.0", 12 | "stook": "^1.17.0", 13 | "stook-devtools": "^1.17.0", 14 | "todomvc-app-css": "^2.3.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "echo", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/stook-todomvc/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forsigner/stook/04e494ca2cd84f1fe5f7b48aa40b3047175ab9b0/examples/stook-todomvc/public/favicon.ico -------------------------------------------------------------------------------- /examples/stook-todomvc/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/stook-todomvc/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/stook-todomvc/public/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-todomvc", 3 | "version": 2, 4 | "alias": ["stook-todomvc"] 5 | } 6 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Header from './Header' 3 | import MainSection from './MainSection' 4 | 5 | const App = () => ( 6 |
7 |
8 | 9 |
10 | ) 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Link from './Link' 3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 4 | 5 | const FILTER_TITLES: any = { 6 | [SHOW_ALL]: 'All', 7 | [SHOW_ACTIVE]: 'Active', 8 | [SHOW_COMPLETED]: 'Completed', 9 | } 10 | 11 | interface FooterProps { 12 | completedCount: number 13 | activeCount: number 14 | onClearCompleted: () => void 15 | } 16 | 17 | const Footer: React.SFC = props => { 18 | const { activeCount, completedCount, onClearCompleted } = props 19 | const itemWord = activeCount === 1 ? 'item' : 'items' 20 | return ( 21 |
22 | 23 | {activeCount || 'No'} {itemWord} left 24 | 25 |
    26 | {Object.keys(FILTER_TITLES).map(filter => ( 27 |
  • 28 | {FILTER_TITLES[filter]} 29 |
  • 30 | ))} 31 |
32 | {!!completedCount && ( 33 | 36 | )} 37 |
38 | ) 39 | } 40 | 41 | export default Footer 42 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TodoTextInput from './TodoTextInput' 3 | import { useTodos } from '../hooks/todos.hooks' 4 | 5 | const Header = () => { 6 | const { addTodo } = useTodos() 7 | return ( 8 |
9 |

todos

10 | { 13 | if (text.length !== 0) { 14 | addTodo(text) 15 | } 16 | }} 17 | placeholder="What needs to be done?" 18 | /> 19 |
20 | ) 21 | } 22 | 23 | export default Header 24 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import classnames from 'classnames' 3 | import { useVisibilityFilter } from '../hooks/visibilityFilter.hooks' 4 | 5 | interface LinkProp { 6 | filter: string 7 | active?: boolean 8 | children: React.ReactNode 9 | } 10 | 11 | const Link: React.SFC = ({ children, filter }) => { 12 | const { visibilityFilter, setVisibilityFilter } = useVisibilityFilter() 13 | 14 | return ( 15 | 16 | setVisibilityFilter(filter)} 20 | > 21 | {children} 22 | 23 | 24 | ) 25 | } 26 | 27 | export default Link 28 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Footer from './Footer' 3 | import TodoList from './TodoList' 4 | import { useTodos } from '../hooks/todos.hooks' 5 | 6 | const MainSection = () => { 7 | const { todosCount, completedCount, completeAllTodos, clearCompleted } = useTodos() 8 | 9 | return ( 10 |
11 | 12 | {!!todosCount && ( 13 | 14 | 15 | 17 | )} 18 | 19 | {!!todosCount && ( 20 |
25 | )} 26 | 27 |
28 | ) 29 | } 30 | 31 | export default MainSection 32 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/components/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import classnames from 'classnames' 3 | import TodoTextInput from './TodoTextInput' 4 | import { useTodos } from '../hooks/todos.hooks' 5 | 6 | interface Props { 7 | todo: object | any // TODO 8 | } 9 | 10 | const TodoItem: React.SFC = props => { 11 | const USE_STATE = 'useState' 12 | const useState = React[USE_STATE] 13 | const [editing, setState] = useState(false) 14 | const { todo } = props 15 | const { deleteTodo, editTodo, completeTodo } = useTodos() 16 | 17 | const handleSave = (id: number, text: string) => { 18 | if (text.length === 0) { 19 | deleteTodo(id) 20 | } else { 21 | editTodo({ id, text } as any) 22 | } 23 | setState(false) 24 | } 25 | 26 | const handleDoubleClick = () => { 27 | setState(true) 28 | } 29 | 30 | let element 31 | 32 | if (editing) { 33 | element = ( 34 | handleSave(todo.id, text)} 38 | /> 39 | ) 40 | } else { 41 | element = ( 42 |
43 | completeTodo(todo.id)} 48 | /> 49 | 50 |
57 | ) 58 | } 59 | return ( 60 |
  • 66 | {element} 67 |
  • 68 | ) 69 | } 70 | 71 | export default TodoItem 72 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TodoItem from './TodoItem' 3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 4 | import { useTodos } from '../hooks/todos.hooks' 5 | import { useVisibilityFilter, Todo } from '../hooks/visibilityFilter.hooks' 6 | 7 | function getVisibleTodos(todos: Todo[], visibilityFilter: string) { 8 | let visibleTodos: Todo[] 9 | switch (visibilityFilter) { 10 | case SHOW_ALL: 11 | visibleTodos = todos 12 | break 13 | case SHOW_COMPLETED: 14 | visibleTodos = todos.filter(t => t.completed) 15 | break 16 | case SHOW_ACTIVE: 17 | visibleTodos = todos.filter(t => !t.completed) 18 | break 19 | default: 20 | visibleTodos = todos 21 | } 22 | return visibleTodos 23 | } 24 | 25 | const TodoList: React.SFC = () => { 26 | const { todos } = useTodos() 27 | const { visibilityFilter } = useVisibilityFilter() 28 | const visibleTodos = getVisibleTodos(todos, visibilityFilter) 29 | 30 | return ( 31 |
      32 | 33 | {visibleTodos.map(todo => ( 34 | 35 | ))} 36 | 37 |
    38 | ) 39 | } 40 | 41 | export default TodoList 42 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/components/TodoTextInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import classnames from 'classnames' 3 | 4 | interface Props { 5 | onSave: (text: string) => void 6 | text?: string 7 | placeholder?: string 8 | editing?: boolean 9 | newTodo?: any | boolean 10 | } 11 | 12 | export default class TodoTextInput extends React.Component { 13 | state = { 14 | text: this.props.text || '', 15 | } 16 | 17 | handleSubmit = (e: any | React.FormEvent): void => { 18 | const text = e.currentTarget.value.trim() 19 | if (e.which === 13) { 20 | this.props.onSave(text) 21 | if (this.props.newTodo) { 22 | this.setState({ text: '' }) 23 | } 24 | } 25 | } 26 | 27 | handleChange = (e: React.FormEvent): void => { 28 | this.setState({ text: e.currentTarget.value }) 29 | } 30 | 31 | handleBlur = (e: React.FormEvent): void => { 32 | if (!this.props.newTodo) { 33 | this.props.onSave(e.currentTarget.value) 34 | } 35 | } 36 | 37 | render() { 38 | return ( 39 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/constants/TodoFilters.tsx: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all' 2 | export const SHOW_COMPLETED = 'show_completed' 3 | export const SHOW_ACTIVE = 'show_active' 4 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/constants/index.tsx: -------------------------------------------------------------------------------- 1 | export const TODOS = 'todos' 2 | export const VISIBILITY_FILTER = 'visibilityFilter' 3 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/hooks/todos.hooks.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from 'stook' 2 | import { TODOS } from '../constants' 3 | 4 | export interface Todo { 5 | text: string 6 | completed: boolean 7 | id: number 8 | } 9 | 10 | export const useTodos = () => { 11 | const [todos, setTodos] = useStore(TODOS, [ 12 | { 13 | text: 'Use Stook', 14 | completed: false, 15 | id: 0, 16 | }, 17 | ]) 18 | 19 | const todosCount = todos.length 20 | const completedCount = todos.filter(todo => todo.completed).length 21 | 22 | const addTodo = (text: string) => { 23 | setTodos(todos => { 24 | todos.push({ 25 | id: todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 26 | completed: false, 27 | text, 28 | }) 29 | }) 30 | } 31 | 32 | const deleteTodo = (id: number) => { 33 | const newTodos = todos.filter(todo => todo.id !== id) 34 | setTodos(newTodos) 35 | } 36 | 37 | const editTodo = ({ id, text }: Todo) => { 38 | const newTodos = todos.map(todo => (todo.id === id ? { ...todo, text } : todo)) 39 | setTodos(newTodos) 40 | } 41 | 42 | const completeTodo = (id: number) => { 43 | const newTodos = todos.map(todo => 44 | todo.id === id ? { ...todo, completed: !todo.completed } : todo, 45 | ) 46 | setTodos(newTodos) 47 | } 48 | 49 | const completeAllTodos = () => { 50 | const areAllMarked = todos.every(todo => todo.completed) 51 | const newTodos = todos.map(todo => ({ 52 | ...todo, 53 | completed: !areAllMarked, 54 | })) 55 | setTodos(newTodos) 56 | } 57 | 58 | const clearCompleted = () => { 59 | const newTodos = todos.filter(todo => todo.completed === false) 60 | setTodos(newTodos) 61 | } 62 | 63 | return { 64 | todos, 65 | todosCount, 66 | completedCount, 67 | addTodo, 68 | deleteTodo, 69 | editTodo, 70 | completeTodo, 71 | completeAllTodos, 72 | clearCompleted, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/hooks/visibilityFilter.hooks.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from 'stook' 2 | import { VISIBILITY_FILTER } from '../constants' 3 | 4 | export interface Todo { 5 | text: string 6 | completed: boolean 7 | id: number 8 | } 9 | 10 | export const useVisibilityFilter = () => { 11 | const [visibilityFilter, setVisibilityFilter] = useStore(VISIBILITY_FILTER, 'show_all') 12 | 13 | return { visibilityFilter, setVisibilityFilter } 14 | } 15 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { render } from 'react-dom' 3 | import App from './components/App' 4 | import 'todomvc-app-css/index.css' 5 | import { devtools } from 'stook-devtools' 6 | 7 | devtools.init() 8 | 9 | render(, document.getElementById('root')) 10 | -------------------------------------------------------------------------------- /examples/stook-todomvc/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/stook-todomvc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "useWorkspaces": true, 4 | "command": { 5 | "publish": { 6 | "ignoreChanges": [ 7 | "ignored-file", 8 | "*.md", 9 | "*.tsconfg.json" 10 | ], 11 | "message": "chore(release): publish", 12 | "registry": "https://registry.npmjs.org" 13 | } 14 | }, 15 | "version": "1.15.0" 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-root", 3 | "private": true, 4 | "workspaces": { 5 | "packages": [ 6 | "packages/*", 7 | "examples/*", 8 | "website" 9 | ] 10 | }, 11 | "scripts": { 12 | "format": "prettier --write \"packages/**/src/**/*.{ts,tsx}\"", 13 | "test": "lerna run --parallel --stream test", 14 | "test:watch": "lerna run --parallel --stream test:watch", 15 | "build": "lerna run --stream build", 16 | "coveralls": "jest --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 17 | "changeset": "changeset", 18 | "version": "changeset version", 19 | "release": "changeset publish" 20 | }, 21 | "jest": { 22 | "preset": "ts-jest", 23 | "testPathIgnorePatterns": [ 24 | "/node_modules/", 25 | "/dist/", 26 | "/.history/" 27 | ] 28 | }, 29 | "devDependencies": { 30 | "@testing-library/react-hooks": "^7.0.0", 31 | "@types/jest": "^26.0.23", 32 | "@types/react": "^17.0.11", 33 | "@types/react-dom": "^17.0.8", 34 | "coveralls": "^3.1.0", 35 | "jest": "^27.0.5", 36 | "lerna": "^4.0.0", 37 | "prettier": "^2.3.2", 38 | "react": "^17.0.2", 39 | "react-dom": "^17.0.2", 40 | "react-test-renderer": "^17.0.2", 41 | "ts-jest": "^27.0.3", 42 | "tsdx": "^0.14.1", 43 | "tslib": "^2.3.0", 44 | "typescript": "^4.3.4" 45 | }, 46 | "dependencies": { 47 | "@changesets/cli": "^2.26.1" 48 | }, 49 | "version": "2.0.2" 50 | } 51 | -------------------------------------------------------------------------------- /packages/stook-async-storage/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # JetBrains IDE files 9 | .idea/ 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /dist 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | tsconfig.tsbuildinfo 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | storybook-static 35 | -------------------------------------------------------------------------------- /packages/stook-async-storage/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # stook-async-storage 2 | 3 | ## 1.17.0 4 | 5 | ### Minor Changes 6 | 7 | - add stook-async-storage 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - stook@1.17.0 13 | -------------------------------------------------------------------------------- /packages/stook-async-storage/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /packages/stook-async-storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-async-storage", 3 | "version": "1.17.0", 4 | "description": "", 5 | "author": "forsigner", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "scripts": { 9 | "build": "tsup", 10 | "watch": "tsup --watch", 11 | "type-check": "tsc", 12 | "test": "jest --coverage --passWithNoTests", 13 | "test:watch": "jest --watch" 14 | }, 15 | "types": "./dist/index.d.ts", 16 | "files": [ 17 | "dist" 18 | ], 19 | "engines": { 20 | "node": ">=14.0.0" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/forsigner/stook/issues" 24 | }, 25 | "homepage": "https://github.com/forsigner/stook/tree/master/packages/stook-graphql", 26 | "dependencies": { 27 | "stook": "^1.17.0" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.21.3", 31 | "@types/is-promise": "^2.2.0", 32 | "@types/jest": "^29.5.0", 33 | "@types/node": "^18.15.11", 34 | "@types/react-test-renderer": "^18.0.0", 35 | "babel-loader": "^9.1.2", 36 | "jest": "^29.5.0", 37 | "react-test-renderer": "^18.2.0", 38 | "ts-jest": "^29.0.5", 39 | "graphql": "^16.6.0", 40 | "tsup": "^6.7.0", 41 | "typescript": "^5.0.3" 42 | }, 43 | "tsup": { 44 | "entry": [ 45 | "src/index.ts" 46 | ], 47 | "treeshake": true, 48 | "sourcemap": true, 49 | "minify": false, 50 | "clean": true, 51 | "dts": true, 52 | "splitting": false, 53 | "format": [ 54 | "cjs", 55 | "esm" 56 | ], 57 | "external": [ 58 | "react", 59 | "stook" 60 | ] 61 | } 62 | } -------------------------------------------------------------------------------- /packages/stook-async-storage/src/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback } from 'react' 2 | import { useStore, Action, Dispatch, getState, mutate } from 'stook' 3 | 4 | export interface AsyncStorageResult { 5 | loading: boolean 6 | setLoading: Dispatch> 7 | data: S 8 | setData: Dispatch> 9 | } 10 | 11 | function getLocalValue(key: string) { 12 | if (typeof window === 'undefined') return null 13 | const localValue: any = window.localStorage.getItem(key) 14 | try { 15 | return JSON.parse(localValue) 16 | } catch { 17 | return localValue 18 | } 19 | } 20 | 21 | export function useAsyncStorage(key: string, defaultStorage?: S) { 22 | const [loading, setLoading] = useStore(`${key}_loading`, true) 23 | const [state, setState] = useStore(key, defaultStorage) 24 | 25 | const setItem = useCallback( 26 | (newValue: any) => { 27 | const nextValue = setState(newValue) 28 | const storageValue = typeof nextValue === 'object' ? JSON.stringify(nextValue) : nextValue 29 | window.localStorage.setItem(key, storageValue as any) 30 | }, 31 | [setState, key], 32 | ) 33 | 34 | // init storage 35 | useEffect(() => { 36 | const localValue = getLocalValue(key) 37 | 38 | if (localValue) { 39 | setState(localValue) 40 | } else { 41 | if (defaultStorage) { 42 | setState(defaultStorage) 43 | window.localStorage.setItem(key, JSON.stringify(defaultStorage)) 44 | } 45 | } 46 | setLoading(false) 47 | }, []) 48 | 49 | return { 50 | loading, 51 | setLoading, 52 | data: state, 53 | setData: setItem, 54 | } as AsyncStorageResult 55 | } 56 | 57 | export function getStorage(key: string): S { 58 | const state = getState(key) 59 | return state || getLocalValue(key) 60 | } 61 | 62 | export function mutateStorage(key: string, newValue: S) { 63 | const nextValue = mutate(key, newValue) 64 | const storageValue: any = typeof nextValue === 'object' ? JSON.stringify(nextValue) : nextValue 65 | window.localStorage.setItem(key, storageValue) 66 | } 67 | -------------------------------------------------------------------------------- /packages/stook-async-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true 10 | } 11 | } -------------------------------------------------------------------------------- /packages/stook-devtools/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # stook-devtools 2 | 3 | ## 1.17.0 4 | 5 | ### Minor Changes 6 | 7 | - add stook-async-storage 8 | 9 | ## 1.16.0 10 | 11 | ### Minor Changes 12 | 13 | - b2521ac: update stook-graphql 14 | - update stook-graphql 15 | -------------------------------------------------------------------------------- /packages/stook-devtools/README.md: -------------------------------------------------------------------------------- 1 | # stook-devtools 2 | 3 | [![npm](https://img.shields.io/npm/v/stook-devtools.svg)](https://www.npmjs.com/package/stook-devtools) [![Minzipped size](https://img.shields.io/bundlephobia/minzip/stook-devtools.svg)](https://bundlephobia.com/result?p=stook-devtools) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 4 | 5 | > devtools for stook 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm i stook-devtools 11 | ``` 12 | 13 | ## Documentation 14 | 15 | https://stook.vercel.app/docs/devtools/intro 16 | 17 | ## License 18 | 19 | [MIT License](https://github.com/forsigner/stook/blob/master/LICENSE) 20 | -------------------------------------------------------------------------------- /packages/stook-devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-devtools", 3 | "version": "1.17.0", 4 | "license": "MIT", 5 | "author": "forsigner", 6 | "main": "dist/index.js", 7 | "module": "dist/stook-devtools.esm.js", 8 | "typings": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "start": "tsdx watch", 14 | "build": "tsdx build", 15 | "test": "tsdx test --env=jsdom" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/forsigner/stook/issues" 19 | }, 20 | "homepage": "https://github.com/forsigner/stook/tree/master/packages/stook-devtools", 21 | "devDependencies": { 22 | "stook": "^1.17.0" 23 | }, 24 | "dependencies": { 25 | "redux": "^4.1.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/stook-devtools/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux' 2 | import { getState, onStoreInit, onStoreUpdate } from 'stook' 3 | 4 | export class devtools { 5 | static init() { 6 | // init REDUX_DEVTOOLS_EXTENSION 7 | const INITIAL_STOOK_REDUCER = (state = 0) => state 8 | 9 | const args: any[] = [] 10 | if (typeof window !== 'undefined') { 11 | args.push( 12 | (window as any).__REDUX_DEVTOOLS_EXTENSION__ && 13 | (window as any).__REDUX_DEVTOOLS_EXTENSION__(), 14 | ) 15 | } 16 | 17 | const store = createStore(combineReducers({ INITIAL_STOOK_REDUCER }), ...args) 18 | 19 | const reducers = {} as any 20 | 21 | /** 22 | * 23 | * @param key useStor key 24 | */ 25 | function createReducers(key: any) { 26 | const initialState = getState(key) || null 27 | reducers[key] = (state: any = initialState, action: any) => { 28 | if (action.type !== key) return state 29 | return action.nextState 30 | } 31 | return reducers 32 | } 33 | 34 | onStoreInit(key => { 35 | const reducers = createReducers(key) 36 | const newReducer: any = combineReducers(reducers) 37 | store.replaceReducer(newReducer) 38 | }) 39 | 40 | onStoreUpdate(({ key, nextState }) => { 41 | store.dispatch({ type: key, nextState }) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/stook-devtools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "types" 5 | ], 6 | "compilerOptions": { 7 | "module": "esnext", 8 | "lib": [ 9 | "dom", 10 | "esnext" 11 | ], 12 | "importHelpers": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "rootDir": "./src", 16 | "strict": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "noImplicitThis": true, 20 | "noImplicitAny": true, 21 | "strictNullChecks": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "moduleResolution": "node", 27 | "baseUrl": "./", 28 | "paths": { 29 | "*": [ 30 | "src/*", 31 | "node_modules/*" 32 | ] 33 | }, 34 | "jsx": "react", 35 | "esModuleInterop": true 36 | } 37 | } -------------------------------------------------------------------------------- /packages/stook-graphql/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # JetBrains IDE files 9 | .idea/ 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /dist 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | tsconfig.tsbuildinfo 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | storybook-static 35 | -------------------------------------------------------------------------------- /packages/stook-graphql/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # stook-graphql 2 | 3 | ## 1.17.0 4 | 5 | ### Minor Changes 6 | 7 | - add stook-async-storage 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - stook@1.17.0 13 | 14 | ## 1.16.0 15 | 16 | ### Minor Changes 17 | 18 | - b2521ac: update stook-graphql 19 | - update stook-graphql 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [b2521ac] 24 | - Updated dependencies 25 | - stook@1.16.0 26 | -------------------------------------------------------------------------------- /packages/stook-graphql/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /packages/stook-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-graphql", 3 | "version": "1.17.0", 4 | "description": "", 5 | "author": "forsigner", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "scripts": { 9 | "build": "tsup", 10 | "watch": "tsup --watch", 11 | "type-check": "tsc", 12 | "test": "jest --coverage --passWithNoTests", 13 | "test:watch": "jest --watch" 14 | }, 15 | "types": "./dist/index.d.ts", 16 | "files": [ 17 | "dist" 18 | ], 19 | "engines": { 20 | "node": ">=14.0.0" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/forsigner/stook/issues" 24 | }, 25 | "homepage": "https://github.com/forsigner/stook/tree/master/packages/stook-graphql", 26 | "dependencies": { 27 | "@boter/graphql-client": "^0.8.0", 28 | "gql-tag": "^1.0.1", 29 | "graphql-tag": "^2.12.6", 30 | "graphql-ws": "^5.12.0", 31 | "koa-compose": "^4.1.0", 32 | "react-fast-compare": "^3.2.0", 33 | "stook": "^1.17.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.20.12", 37 | "@types/is-promise": "^2.2.0", 38 | "@types/jest": "^29.4.0", 39 | "@types/node": "^18.11.18", 40 | "@types/react-test-renderer": "^18.0.0", 41 | "babel-loader": "^9.1.2", 42 | "jest": "^29.4.1", 43 | "react-test-renderer": "^18.2.0", 44 | "ts-jest": "^29.0.5", 45 | "graphql": "^16.6.0", 46 | "tsup": "^6.7.0", 47 | "typescript": "^5.0.2" 48 | }, 49 | "tsup": { 50 | "entry": [ 51 | "src/index.ts" 52 | ], 53 | "treeshake": true, 54 | "sourcemap": true, 55 | "minify": false, 56 | "clean": true, 57 | "dts": true, 58 | "splitting": false, 59 | "format": [ 60 | "cjs", 61 | "esm" 62 | ], 63 | "external": [ 64 | "react", 65 | "stook", 66 | "graphql", 67 | "@boter/graphql-client", 68 | "gql-tag", 69 | "graphql-tag", 70 | "immer", 71 | "graphql-ws", 72 | "koa-compose", 73 | "react-fast-compare" 74 | ] 75 | } 76 | } -------------------------------------------------------------------------------- /packages/stook-graphql/src/fetcher.ts: -------------------------------------------------------------------------------- 1 | import { Fetcher, FetcherItem } from './types' 2 | 3 | const NULL: any = null 4 | export class fetcher { 5 | private static store: Fetcher = {} 6 | 7 | static set(name: string, value: FetcherItem) { 8 | fetcher.store[name] = value 9 | } 10 | 11 | static get(name: string) { 12 | if (!fetcher.store[name]) { 13 | // fetcher.store[name] = { 14 | // refetch() { 15 | // const error = new Error(`[stook-graphql]: In fetcher, can not get ${name}`) 16 | // console.warn(error) 17 | // }, 18 | // } as FetcherItem 19 | fetcher.store[name] = NULL 20 | } 21 | 22 | return fetcher.store[name] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/stook-graphql/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Client } from './Client' 2 | 3 | const client = new Client({ endpoint: '/graphql' }) 4 | const { query, useQuery, useMutation, useSubscription, applyMiddleware, config } = client 5 | 6 | export * from './types' 7 | export * from './fetcher' 8 | export { Client, client, query, useQuery, useMutation, useSubscription, applyMiddleware, config } 9 | 10 | export * from './types' 11 | export * from './fetcher' 12 | -------------------------------------------------------------------------------- /packages/stook-graphql/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from './Client' 2 | export interface Variables { 3 | [key: string]: any 4 | } 5 | 6 | export type SetData = (data: T, newData: T) => void 7 | 8 | export interface Options { 9 | key?: string 10 | variables?: V | (() => V) 11 | deps?: Deps 12 | headers?: HeadersInit 13 | initialData?: T 14 | pollInterval?: number 15 | lazy?: boolean 16 | timeout?: number 17 | onUpdate?(result: Result): any 18 | 19 | // TODO: 乐观更新 20 | // optimisticResponse?: any 21 | // retryOn: any 22 | // retryDelay: any 23 | // retryOnError: false 24 | // onError(err) 25 | // errorRetryCount 26 | // errorRetryInterval 27 | 28 | // 覆盖 Client endpoint 29 | endpoint?: string 30 | } 31 | 32 | export interface RefetchOptions { 33 | key?: string 34 | variables?: V | ((prevVariables: V) => V) 35 | headers?: HeadersInit 36 | setData?: SetData 37 | showLoading?: boolean 38 | } 39 | 40 | export type Refetch = (options?: RefetchOptions) => Promise 41 | 42 | export type Start = () => Promise 43 | 44 | export type Deps = ReadonlyArray 45 | 46 | export interface FetcherItem { 47 | refetch: Refetch 48 | start: Start 49 | result: Result 50 | called: boolean 51 | polled: boolean 52 | variables?: V 53 | } 54 | 55 | export interface Fetcher { 56 | [key: string]: FetcherItem 57 | } 58 | 59 | export interface Result { 60 | loading: boolean 61 | called: boolean 62 | data: T 63 | error: any 64 | } 65 | 66 | export interface QueryResult extends Result { 67 | refetch: Refetch 68 | start: Start 69 | called: boolean 70 | } 71 | 72 | export interface MutateResult extends Result {} 73 | 74 | export interface SubscribeResult extends Result { 75 | unsubscribe: () => void 76 | } 77 | 78 | export interface GraphqlOptions { 79 | endpoint: string 80 | subscriptionsEndpoint?: string 81 | headers?: HeadersInit 82 | } 83 | 84 | export interface Observer { 85 | next?: (value: T) => void 86 | error?: (error: Error) => void 87 | complete?: () => void 88 | } 89 | 90 | export interface SubscriptionOption { 91 | key?: string 92 | variables?: Object 93 | operationName?: string 94 | initialQuery?: { 95 | query: string 96 | variables?: Variables 97 | onUpdate?(result: Result): any 98 | } 99 | onUpdate?(result: Result): any 100 | } 101 | 102 | export interface FromSubscriptionOption { 103 | variables?: Object 104 | } 105 | 106 | export type NextFn = () => Promise 107 | 108 | export type Middleware = (ctx: Context, next: NextFn) => any 109 | -------------------------------------------------------------------------------- /packages/stook-graphql/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql' 2 | import { useRef, useCallback, EffectCallback, useEffect } from 'react' 3 | import { Options, Deps } from './types' 4 | 5 | export function isFalsy(value: any) { 6 | if (typeof value === 'boolean') { 7 | return !value 8 | } 9 | 10 | return value === undefined || value === null 11 | } 12 | 13 | export const getDeps = (options?: Options): Deps => { 14 | if (options && Array.isArray(options.deps)) return options.deps 15 | return [] 16 | } 17 | export function isResolve(arg: any) { 18 | if (!arg) return true 19 | if (typeof arg !== 'function') return true 20 | try { 21 | arg() 22 | return true 23 | } catch (error) { 24 | return false 25 | } 26 | } 27 | 28 | export const getVariables = (options: Options): any => { 29 | if (!options.variables) return {} 30 | 31 | if (typeof options.variables !== 'function') return options.variables 32 | try { 33 | return options.variables({}) 34 | } catch (error) { 35 | return false 36 | } 37 | } 38 | 39 | /** 40 | * transfrom deps to object 41 | * ['a', 'b', 'c'] => {0: 'a', 1: 'b', 2: 'c'} 42 | * @param deps 43 | */ 44 | export const getDepsMaps = (deps: Deps) => { 45 | return deps.reduce((result, cur, i) => { 46 | result[i] = cur 47 | return result 48 | }, {} as { [key: string]: any }) 49 | } 50 | 51 | export function useUnmounted(): () => boolean { 52 | const unmountedRef = useRef(false) 53 | const isUnmouted = useCallback(() => unmountedRef.current, []) 54 | 55 | useEffect(() => { 56 | unmountedRef.current = false 57 | return () => { 58 | unmountedRef.current = true 59 | } 60 | }) 61 | 62 | return isUnmouted 63 | } 64 | 65 | export const useEffectOnce = (effect: EffectCallback) => { 66 | useEffect(effect, []) 67 | } 68 | 69 | export const useUnmount = (fn: () => any): void => { 70 | const fnRef = useRef(fn) 71 | 72 | // update the ref each render so if it change the newest callback will be invoked 73 | fnRef.current = fn 74 | 75 | useEffectOnce(() => () => fnRef.current()) 76 | } 77 | 78 | export const getOperationName = (input: string) => { 79 | try { 80 | return (parse(input).definitions[0] as any).name.value 81 | } catch (error) { 82 | return null 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/stook-graphql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true 10 | } 11 | } -------------------------------------------------------------------------------- /packages/stook-localstorage/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # stook-localstorage 2 | 3 | ## 1.17.0 4 | 5 | ### Minor Changes 6 | 7 | - add stook-async-storage 8 | 9 | ## 1.16.0 10 | 11 | ### Minor Changes 12 | 13 | - b2521ac: update stook-graphql 14 | - update stook-graphql 15 | -------------------------------------------------------------------------------- /packages/stook-localstorage/README.md: -------------------------------------------------------------------------------- 1 | # stook-localstorage 2 | 3 | ## Installation 4 | 5 | ```bash 6 | npm i stook-localstorage 7 | ``` 8 | 9 | ## Usage 10 | 11 | **string value** 12 | 13 | ```jsx 14 | const App = () => { 15 | const [value, setItem] = useLocalStorage('STORAGE_KEY', 'initialValue'); 16 | return ( 17 |
    18 | value 19 | 20 |
    21 | ); 22 | }; 23 | ``` 24 | 25 | **number value** 26 | 27 | ```jsx 28 | const App = () => { 29 | const [value, setItem] = useLocalStorage('STORAGE_KEY', 100); 30 | return ( 31 |
    32 | {value} 33 | 34 |
    35 | ); 36 | }; 37 | ``` 38 | 39 | **boolen value** 40 | 41 | ```jsx 42 | const App = () => { 43 | const [value, setItem] = useLocalStorage('STORAGE_KEY', true); 44 | return ( 45 |
    46 | {value} 47 | 48 |
    49 | ); 50 | }; 51 | ``` 52 | 53 | **object value** 54 | 55 | ```jsx 56 | const App = () => { 57 | const [user, setUser] = useLocalStorage('USER', { name: 'foo' }); 58 | return ( 59 |
    60 |
    {JSON.stringify(user, null, 2)}
    61 | 62 |
    63 | ); 64 | }; 65 | ``` 66 | 67 | ## getLocalStorage 68 | 69 | getLocalStorage will return Object, not string 70 | 71 | ```jsx 72 | const data = getLocalStorage('USER'); 73 | ``` 74 | 75 | ## License 76 | 77 | [MIT License](https://github.com/forsigner/stook-localstorage/blob/master/LICENSE) 78 | -------------------------------------------------------------------------------- /packages/stook-localstorage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-localstorage", 3 | "version": "1.17.0", 4 | "license": "MIT", 5 | "author": "forsigner", 6 | "main": "dist/index.js", 7 | "module": "dist/stook-localstorage.esm.js", 8 | "typings": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "start": "tsdx watch", 14 | "build": "tsdx build", 15 | "test": "tsdx test", 16 | "lint": "tsdx lint" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+ssh://git@github.com/forsigner/stook.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/forsigner/stook/issues" 24 | }, 25 | "homepage": "https://github.com/forsigner/stook#README", 26 | "devDependencies": { 27 | "stook": "^1.17.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/stook-localstorage/src/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useStore, Action, Dispatch, getState, mutate } from 'stook' 3 | 4 | function getLocalValue(key: string) { 5 | if (typeof window === 'undefined') return null 6 | const localValue: any = window.localStorage.getItem(key) 7 | try { 8 | return JSON.parse(localValue) 9 | } catch { 10 | return localValue 11 | } 12 | } 13 | 14 | export function useLocalStorage(key: string, initialState?: S): [S, Dispatch>] { 15 | const [state, setState] = useStore(key, initialState) 16 | 17 | const setItem = (newValue: any) => { 18 | const nextValue = setState(newValue) 19 | const storageValue = typeof nextValue === 'object' ? JSON.stringify(nextValue) : nextValue 20 | window.localStorage.setItem(key, storageValue as any) 21 | } 22 | 23 | // init storage 24 | useEffect(() => { 25 | const localValue = getLocalValue(key) 26 | 27 | // 如果 localStorage 有值,优先使用 localStorage 的值 28 | if (localValue) { 29 | setState(localValue) 30 | } else { 31 | if (initialState) { 32 | setState(initialState) 33 | window.localStorage.setItem(key, JSON.stringify(initialState)) 34 | } 35 | } 36 | }, []) 37 | 38 | try { 39 | const objectValue = JSON.parse(state) 40 | return [objectValue, setItem] 41 | } catch (error) { 42 | return [state, setItem] 43 | } 44 | } 45 | 46 | export function getLocalStorage(key: string): S { 47 | const state = getState(key) 48 | return state || getLocalValue(key) 49 | } 50 | 51 | export function mutateLocalStorage(key: string, newValue: S) { 52 | const nextValue = mutate(key, newValue) 53 | const storageValue: any = typeof nextValue === 'object' ? JSON.stringify(nextValue) : nextValue 54 | 55 | window.localStorage.setItem(key, storageValue) 56 | } 57 | -------------------------------------------------------------------------------- /packages/stook-localstorage/test/blah.test.ts: -------------------------------------------------------------------------------- 1 | describe('blah', () => { 2 | it('works', () => { 3 | expect(1 + 1).toEqual(2); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/stook-localstorage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "types" 5 | ], 6 | "compilerOptions": { 7 | "module": "esnext", 8 | "lib": [ 9 | "dom", 10 | "esnext" 11 | ], 12 | "importHelpers": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "rootDir": "./src", 16 | "strict": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "noImplicitThis": true, 20 | "noImplicitAny": true, 21 | "strictNullChecks": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "moduleResolution": "node", 27 | "baseUrl": "./", 28 | "paths": { 29 | "*": [ 30 | "src/*", 31 | "node_modules/*" 32 | ] 33 | }, 34 | "jsx": "react", 35 | "esModuleInterop": true 36 | } 37 | } -------------------------------------------------------------------------------- /packages/stook-rest/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # stook-rest 2 | 3 | ## 1.17.0 4 | 5 | ### Minor Changes 6 | 7 | - add stook-async-storage 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - stook@1.17.0 13 | 14 | ## 1.16.0 15 | 16 | ### Minor Changes 17 | 18 | - b2521ac: update stook-graphql 19 | - update stook-graphql 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [b2521ac] 24 | - Updated dependencies 25 | - stook@1.16.0 26 | -------------------------------------------------------------------------------- /packages/stook-rest/README.md: -------------------------------------------------------------------------------- 1 | # stook-rest 2 | 3 | [![npm](https://img.shields.io/npm/v/stook-rest.svg)](https://www.npmjs.com/package/stook-rest) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 4 | 5 | > Restful && stook 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm i stook-rest 11 | ``` 12 | 13 | ## Documentation 14 | 15 | https://stook.vercel.app/docs/rest/intro 16 | 17 | ## License 18 | 19 | [MIT License](https://github.com/forsigner/stook/blob/master/LICENSE) 20 | -------------------------------------------------------------------------------- /packages/stook-rest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-rest", 3 | "version": "1.17.0", 4 | "license": "MIT", 5 | "author": "forsigner", 6 | "main": "dist/index.js", 7 | "module": "dist/stook-rest.esm.js", 8 | "typings": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "start": "tsdx watch", 14 | "build": "tsdx build", 15 | "test": "tsdx test --env=jsdom", 16 | "test:watch": "tsdx test --watch", 17 | "test:cov": "tsdx test --coverage", 18 | "lint": "tsdx lint" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/forsigner/stook/issues" 22 | }, 23 | "homepage": "https://github.com/forsigner/stook/tree/master/packages/stook-rest", 24 | "devDependencies": { 25 | "@types/koa-compose": "^3.2.5" 26 | }, 27 | "dependencies": { 28 | "@boter/request": "^0.8.0", 29 | "koa-compose": "^4.1.0", 30 | "react-fast-compare": "^3.2.0", 31 | "stook": "^1.17.0" 32 | } 33 | } -------------------------------------------------------------------------------- /packages/stook-rest/src/Client.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react' 2 | import { request, Options as RequestOptions } from '@boter/request' 3 | import { useEffect } from 'react' 4 | import { useStore, Storage } from 'stook' 5 | import compose from 'koa-compose' 6 | import { fetcher } from './fetcher' 7 | import { 8 | FetchResult, 9 | Refetch, 10 | Options, 11 | HooksResult, 12 | FetcherItem, 13 | Update, 14 | UpdateResult, 15 | Middleware, 16 | RestOptions, 17 | } from './types' 18 | import { useUnmount, useUnmounted, isResolve, getArg } from './utils' 19 | import { Start } from '.' 20 | 21 | interface ArgsCurrent { 22 | resolve: boolean 23 | params: any 24 | query: any 25 | body: any 26 | } 27 | 28 | /** 29 | * get final url for http 30 | * @param url 31 | * @param baseURL 32 | */ 33 | function getReqURL(url: string, baseURL: string) { 34 | const isAbsoluteURL = /http:\/\/|https:\/\//.test(url) 35 | 36 | if (isAbsoluteURL) return url 37 | const arr = url.split(/\s+/) 38 | 39 | // handle something like: 'POST: /todos' 40 | if (arr.length === 2) return `${arr[0]} ${baseURL + arr[1]}` 41 | 42 | return baseURL + arr[0] 43 | } 44 | 45 | function last(arr: T[]): T { 46 | return arr[arr.length - 1] 47 | } 48 | 49 | function getMethod(url: string, options: Options = {}) { 50 | const arr = url.split(/\s+/) 51 | if (arr.length === 2) return arr[0] 52 | const { method = 'GET' } = options 53 | return method 54 | } 55 | 56 | /** 57 | * unique key for cache 58 | * @param url api url 59 | * @param options 60 | * @returns 61 | */ 62 | function getFetcherName(url: string, options: Options = {}) { 63 | if (options.key) return options.key 64 | const method = getMethod(url, options) 65 | url = last(url.split(/\s+/)) 66 | return `${method} ${url}` 67 | } 68 | 69 | export class Context { 70 | headers: Record = {} 71 | body: any = {} 72 | query: any = {} 73 | valid: boolean = true 74 | url: string = '' 75 | method: string = '' 76 | } 77 | 78 | export class Client { 79 | restOptions: RestOptions 80 | middleware: Middleware[] = [] 81 | 82 | constructor(config: RestOptions) { 83 | this.restOptions = config 84 | } 85 | 86 | applyMiddleware = (fn: Middleware) => { 87 | this.middleware.push(fn) 88 | } 89 | 90 | config = (opt: RestOptions) => { 91 | this.restOptions = { ...this.restOptions, ...opt } 92 | } 93 | 94 | fetch = async (url: string, options: RequestOptions = {}): Promise => { 95 | const context = new Context() 96 | const action = async (ctx: Context) => { 97 | const { baseURL, headers } = this.restOptions 98 | const reqURL = getReqURL(url, baseURL) 99 | 100 | ctx.url = reqURL 101 | ctx.method = options.method || '' 102 | 103 | // merge global headers, interceptor headers,fetch headers 104 | options.headers = { ...headers, ...ctx.headers, ...options.headers } as any 105 | 106 | if (['PATCH', 'POST', 'PUT'].includes(options.method || '')) { 107 | options.body = { ...ctx.body, ...((options.body as any) || {}) } 108 | } 109 | 110 | options.query = { ...ctx.query, ...(options.query || {}) } 111 | 112 | try { 113 | ctx.body = await request(reqURL, options) 114 | } catch (error) { 115 | ctx.body = error 116 | ctx.valid = false 117 | throw ctx.body 118 | } 119 | } 120 | 121 | await compose([...this.middleware, action])(context) 122 | 123 | if (!context.valid) throw context.body 124 | return context.body 125 | } 126 | 127 | useFetch = (url: string, options: Options = {}) => { 128 | const isUnmouted = useUnmounted() 129 | const { initialData: data, key, onUpdate, lazy = false } = options 130 | const initialState = { loading: true, called: false, data } as FetchResult 131 | const fetcherName = key || getFetcherName(url, options) 132 | const [result, setState] = useStore(fetcherName, initialState) 133 | 134 | // 是否应该立刻开始发送请求 135 | const [shouldStart, setShouldStart] = useState(!lazy) 136 | 137 | const update = (nextState: Partial>) => { 138 | setState(nextState as FetchResult) 139 | onUpdate && onUpdate(nextState as FetchResult) 140 | } 141 | 142 | const makeFetch = async (opt?: Options) => { 143 | try { 144 | update({ loading: true, called: true }) 145 | fetcher.get(fetcherName).called = true 146 | const data: T = await this.fetch(url, opt || {}) 147 | 148 | update({ loading: false, data, called: true }) 149 | return data 150 | } catch (error) { 151 | update({ loading: false, error, called: true }) 152 | throw error 153 | } 154 | } 155 | 156 | const refetch: Refetch = async

    (opt?: Options): Promise

    => { 157 | update({ loading: true, called: true }) 158 | const refetchedData: any = await makeFetch(opt) 159 | return refetchedData as P 160 | } 161 | 162 | const argsRef = useRef({ 163 | resolve: isResolve(options.params) && isResolve(options.query) && isResolve(options.body), 164 | params: getArg(options.params), 165 | query: getArg(options.query), 166 | body: getArg(options.body), 167 | }) 168 | 169 | if ( 170 | !argsRef.current.resolve && 171 | getArg(options.params) && 172 | getArg(options.query) && 173 | getArg(options.body) 174 | ) { 175 | argsRef.current = { 176 | resolve: true, 177 | params: getArg(options.params), 178 | query: getArg(options.query), 179 | body: getArg(options.body), 180 | } 181 | } 182 | 183 | const getOpt = (options: Options): Options => { 184 | if (Object.keys(argsRef.current.params).length) { 185 | options.params = argsRef.current.params 186 | } 187 | if (Object.keys(argsRef.current.query).length) { 188 | options.query = argsRef.current.query 189 | } 190 | if (Object.keys(argsRef.current.body).length) { 191 | options.body = argsRef.current.body 192 | } 193 | 194 | return options 195 | } 196 | 197 | // TODO: 要确保 args resolve 198 | const start: Start = (opt): any => { 199 | if (opt?.params) argsRef.current.params = opt?.params 200 | if (opt?.query) argsRef.current.query = opt?.query 201 | if (opt?.body) argsRef.current.body = opt?.body 202 | setShouldStart(true) 203 | } 204 | 205 | useEffect(() => { 206 | // store refetch fn to fetcher 207 | if (!fetcher.get(fetcherName)) { 208 | fetcher.set(fetcherName, { refetch, called: false } as FetcherItem) 209 | } 210 | 211 | // if resolve, 说明已经拿到最终的 args 212 | const shouldFetch = 213 | argsRef.current.resolve && !fetcher.get(fetcherName).called && !isUnmouted() && shouldStart 214 | 215 | if (shouldFetch) { 216 | const opt = getOpt(options) 217 | makeFetch(opt) 218 | } 219 | }, [argsRef.current, shouldStart]) 220 | 221 | // when unmount 222 | useUnmount(() => { 223 | // 全部 unmount,设置 called false 224 | const store = Storage.get(fetcherName) 225 | 226 | // 对应的 hooks 全部都 unmount 了 227 | if (store && store.setters.length === 0) { 228 | // 重新设置为 false,以便后续调用刷新 229 | fetcher.get(fetcherName).called = false 230 | 231 | // TODO: 要为true ? 还是 undefined 好 232 | update({ loading: true, called: false } as any) 233 | } 234 | }) 235 | 236 | return { ...result, refetch, start } as HooksResult 237 | } 238 | 239 | useUpdate = (url: string, options: RequestOptions = {}) => { 240 | options.method = options.method || 'POST' 241 | const fetcherName = getFetcherName(url, options) 242 | const initialState = { loading: false, called: false } as UpdateResult 243 | const [result, setState] = useStore(fetcherName, initialState) 244 | 245 | const updateData = async (updateOptions: RequestOptions = {}) => { 246 | try { 247 | setState({ loading: true } as UpdateResult) 248 | const data: T = await this.fetch(url, { ...options, ...updateOptions }) 249 | const nextState = { loading: false, called: true, data } as UpdateResult 250 | setState(nextState) 251 | return nextState 252 | } catch (error) { 253 | const nextState = { loading: false, called: true, error } as UpdateResult 254 | setState(nextState) 255 | return nextState 256 | } 257 | } 258 | 259 | const update = async (updateOptions: RequestOptions = {}): Promise => { 260 | return await updateData(updateOptions) 261 | } 262 | 263 | const out: [Update, UpdateResult] = [update, result] 264 | 265 | return out 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /packages/stook-rest/src/fetcher.ts: -------------------------------------------------------------------------------- 1 | import { Fetcher, FetcherItem } from './types' 2 | 3 | const NULL: any = null 4 | export class fetcher { 5 | private static store: Fetcher = {} 6 | 7 | static set(name: string, value: FetcherItem) { 8 | fetcher.store[name] = value 9 | } 10 | 11 | static get(name: string) { 12 | if (!fetcher.store[name]) { 13 | // fetcher.store[name] = { 14 | // refetch() { 15 | // const error = new Error(`[stook-rest]: In fetcher, can not get ${name}`) 16 | // console.warn(error) 17 | // }, 18 | // } as FetcherItem 19 | fetcher.store[name] = NULL 20 | } 21 | return fetcher.store[name] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/stook-rest/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Client } from './Client' 2 | 3 | let baseURL: string = '' 4 | 5 | if (typeof window !== 'undefined') { 6 | baseURL = window.location.protocol + '//' + window.location.host 7 | } 8 | 9 | const client = new Client({ baseURL }) 10 | const { fetch, useFetch, useUpdate, config, applyMiddleware } = client 11 | 12 | export * from './types' 13 | export * from './fetcher' 14 | export { Client, config, fetch, useFetch, useUpdate, applyMiddleware } 15 | -------------------------------------------------------------------------------- /packages/stook-rest/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Options as RequestOptions } from '@boter/request' 2 | import type { Context } from './Client' 3 | 4 | export type Update = (updateOptions?: RequestOptions) => Promise> 5 | 6 | export interface Options extends RequestOptions { 7 | key?: string 8 | initialData?: T 9 | lazy?: boolean 10 | onUpdate?(result: Result): any 11 | } 12 | 13 | export type Refetch = (options?: Options) => Promise 14 | 15 | export type Start = (options?: Options) => Promise 16 | 17 | export interface FetcherItem { 18 | refetch: Refetch 19 | start: Start 20 | result: Result 21 | called: boolean // is request called 22 | } 23 | 24 | export interface Fetcher { 25 | [key: string]: FetcherItem 26 | } 27 | 28 | export interface Result { 29 | loading: boolean 30 | called: boolean 31 | data: T 32 | error: any 33 | } 34 | 35 | export interface FetchResult extends Result {} 36 | export interface UpdateResult extends Result {} 37 | 38 | export interface RestOptions { 39 | baseURL: string 40 | headers?: HeadersInit 41 | middlewares?: Middleware[] 42 | } 43 | 44 | export interface HooksResult extends FetchResult { 45 | start: Start 46 | refetch: Refetch 47 | } 48 | 49 | export type Middleware = (ctx: Context, next: () => Promise) => any 50 | -------------------------------------------------------------------------------- /packages/stook-rest/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback, EffectCallback, useEffect } from 'react' 2 | 3 | export function isFalsy(value: any) { 4 | if (typeof value === 'boolean') { 5 | return !value 6 | } 7 | 8 | return value === undefined || value === null 9 | } 10 | 11 | export function isResolve(arg: any) { 12 | if (!arg) return true 13 | if (typeof arg !== 'function') return true 14 | try { 15 | arg() 16 | return true 17 | } catch (error) { 18 | return false 19 | } 20 | } 21 | 22 | export const getArg = (arg: any) => { 23 | if (!arg) return {} 24 | if (typeof arg !== 'function') return arg 25 | try { 26 | return arg() 27 | } catch (error) { 28 | return false 29 | } 30 | } 31 | 32 | export function useUnmounted(): () => boolean { 33 | const unmountedRef = useRef(false) 34 | const isUnmouted = useCallback(() => unmountedRef.current, []) 35 | 36 | useEffect(() => { 37 | unmountedRef.current = false 38 | return () => { 39 | unmountedRef.current = true 40 | } 41 | }) 42 | 43 | return isUnmouted 44 | } 45 | 46 | export const useEffectOnce = (effect: EffectCallback) => { 47 | useEffect(effect, []) 48 | } 49 | 50 | export const useUnmount = (fn: () => any): void => { 51 | const fnRef = useRef(fn) 52 | 53 | // update the ref each render so if it change the newest callback will be invoked 54 | fnRef.current = fn 55 | 56 | useEffectOnce(() => () => fnRef.current()) 57 | } 58 | -------------------------------------------------------------------------------- /packages/stook-rest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "types" 5 | ], 6 | "compilerOptions": { 7 | "module": "esnext", 8 | "lib": [ 9 | "dom", 10 | "esnext" 11 | ], 12 | "importHelpers": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "rootDir": "./src", 16 | "strict": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "noImplicitThis": true, 20 | "noImplicitAny": true, 21 | "strictNullChecks": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "moduleResolution": "node", 27 | "baseUrl": "./", 28 | "paths": { 29 | "*": [ 30 | "src/*", 31 | "node_modules/*" 32 | ] 33 | }, 34 | "jsx": "react", 35 | "esModuleInterop": true 36 | } 37 | } -------------------------------------------------------------------------------- /packages/stook-toggle/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # stook-toggle 2 | 3 | ## 1.17.0 4 | 5 | ### Minor Changes 6 | 7 | - add stook-async-storage 8 | 9 | ## 1.16.0 10 | 11 | ### Minor Changes 12 | 13 | - b2521ac: update stook-graphql 14 | - update stook-graphql 15 | -------------------------------------------------------------------------------- /packages/stook-toggle/README.md: -------------------------------------------------------------------------------- 1 | # stook-toggle 2 | 3 | ## Installation 4 | 5 | ```bash 6 | npm i stook-toggle 7 | ``` 8 | 9 | ## License 10 | 11 | [MIT License](https://github.com/forsigner/stook-toggle/blob/master/LICENSE) 12 | -------------------------------------------------------------------------------- /packages/stook-toggle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook-toggle", 3 | "version": "1.17.0", 4 | "license": "MIT", 5 | "author": "forsigner", 6 | "main": "dist/index.js", 7 | "module": "dist/stook-toggle.esm.js", 8 | "typings": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "start": "tsdx watch", 14 | "build": "tsdx build", 15 | "test": "tsdx test", 16 | "lint": "tsdx lint" 17 | }, 18 | "devDependencies": { 19 | "stook": "^1.17.0" 20 | } 21 | } -------------------------------------------------------------------------------- /packages/stook-toggle/src/index.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from 'stook'; 2 | import { useRef, useState } from 'react'; 3 | 4 | const FALSE_AS_ANY: any = false; 5 | 6 | export function useToggle( 7 | key: string, 8 | initialState: S | [S, S] = FALSE_AS_ANY 9 | ): [S, () => any] { 10 | let tuple: any[] = []; 11 | 12 | if (typeof initialState === undefined) { 13 | tuple = [false, true]; 14 | } else if (Array.isArray(initialState)) { 15 | tuple = initialState; 16 | } else { 17 | tuple = [initialState, !initialState]; 18 | } 19 | 20 | const tupleContainer = useRef<[S, S]>(tuple as any); 21 | const [state, setState] = useStore(key, tuple[0]); 22 | const [index, setIndex] = useState(1); 23 | 24 | function toggle() { 25 | setState(tupleContainer.current[index]); 26 | setIndex(index => (index === 0 ? 1 : 0)); 27 | } 28 | 29 | return [state, toggle]; 30 | } 31 | -------------------------------------------------------------------------------- /packages/stook-toggle/test/blah.test.ts: -------------------------------------------------------------------------------- 1 | describe('blah', () => { 2 | it('works', () => { 3 | expect(1 + 1).toEqual(2); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/stook-toggle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "types" 5 | ], 6 | "compilerOptions": { 7 | "module": "esnext", 8 | "lib": [ 9 | "dom", 10 | "esnext" 11 | ], 12 | "importHelpers": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "rootDir": "./src", 16 | "strict": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "noImplicitThis": true, 20 | "noImplicitAny": true, 21 | "strictNullChecks": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "moduleResolution": "node", 27 | "baseUrl": "./", 28 | "paths": { 29 | "*": [ 30 | "src/*", 31 | "node_modules/*" 32 | ] 33 | }, 34 | "jsx": "react", 35 | "esModuleInterop": true 36 | } 37 | } -------------------------------------------------------------------------------- /packages/stook/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # stook 2 | 3 | ## 1.17.0 4 | 5 | ### Minor Changes 6 | 7 | - add stook-async-storage 8 | 9 | ## 1.16.0 10 | 11 | ### Minor Changes 12 | 13 | - b2521ac: update stook-graphql 14 | - update stook-graphql 15 | -------------------------------------------------------------------------------- /packages/stook/README.md: -------------------------------------------------------------------------------- 1 | # Stook 2 | 3 | [![npm](https://img.shields.io/npm/v/stook.svg)](https://www.npmjs.com/package/stook) [![Coverage Status](https://coveralls.io/repos/github/forsigner/stook/badge.svg?branch=master)](https://coveralls.io/github/forsigner/stook?branch=master) [![Minzipped size](https://img.shields.io/bundlephobia/minzip/stook.svg)](https://bundlephobia.com/result?p=stook) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 4 | 5 | A minimalist design state management library for React. 6 | 7 | ## Documentation 8 | 9 | The documentation site of stook is hosted at [https://stook.vercel.app](https://stook.vercel.app). 10 | 11 | ## Quick start 12 | 13 | **simplest** 14 | 15 | ```tsx 16 | import React from 'react' 17 | import { useStore } from 'stook' 18 | 19 | function Counter() { 20 | const [count, setCount] = useStore('Counter', 0) 21 | return ( 22 |

    23 |

    You clicked {count} times

    24 | 25 |
    26 | ) 27 | } 28 | ``` 29 | 30 | **share state** 31 | 32 | ```jsx 33 | import React from 'react' 34 | import { useStore } from 'stook' 35 | 36 | function Counter() { 37 | const [count, setCount] = useStore('Counter', 0) 38 | return ( 39 |
    40 |

    You clicked {count} times

    41 | 42 |
    43 | ) 44 | } 45 | 46 | function Display() { 47 | const [count] = useStore('Counter') 48 | return

    {count}

    49 | } 50 | 51 | function App() { 52 | return ( 53 |
    54 | 55 | 56 |
    57 | ) 58 | } 59 | ``` 60 | 61 | ## License 62 | 63 | [MIT License](https://github.com/forsigner/stook/blob/master/LICENSE) 64 | -------------------------------------------------------------------------------- /packages/stook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stook", 3 | "version": "1.17.0", 4 | "description": "A minimalist design state management library for React", 5 | "author": "forsigner", 6 | "main": "dist/index.js", 7 | "module": "dist/stook.esm.js", 8 | "typings": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "start": "tsdx watch", 14 | "build": "tsdx build", 15 | "test": "tsdx test --env=jsdom", 16 | "test:watch": "tsdx test --watch", 17 | "test:cov": "tsdx test --coverage", 18 | "lint": "tsdx lint" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/forsigner/stook/issues" 22 | }, 23 | "homepage": "https://github.com/forsigner/stook", 24 | "license": "MIT", 25 | "dependencies": { 26 | "immer": "^9.0.12", 27 | "mitt": "^3.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/stook/src/Storage.ts: -------------------------------------------------------------------------------- 1 | import { Store } from './Store' 2 | 3 | interface Stores { 4 | [key: string]: Store 5 | } 6 | 7 | /** 8 | * Storage for anything 9 | */ 10 | export class Storage { 11 | static stores: Stores = {} 12 | static set(key: any, value: Store) { 13 | const store = Storage.stores[key] 14 | if (!store || !store.setState) { 15 | Storage.stores[key] = value 16 | } 17 | } 18 | 19 | static get(key: any): Store { 20 | return Storage.stores[key] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/stook/src/Store.ts: -------------------------------------------------------------------------------- 1 | import { produce } from 'immer' 2 | import { Dispatch, SetStateAction } from './types' 3 | import { emitStoreUpdate } from './emitter' 4 | 5 | /** 6 | * store for one key 7 | */ 8 | export class Store { 9 | state: S 10 | setters: Dispatch>[] = [] 11 | constructor(value: any) { 12 | this.state = value 13 | } 14 | 15 | private getNextState(state: S, value: any): any { 16 | let nextState: any 17 | 18 | // not function 19 | if (typeof value !== 'function') return value 20 | 21 | // can not use immer 22 | if (typeof state !== 'object') return value(state) 23 | 24 | let useImmer = true 25 | 26 | const immerState = produce(state, draft => { 27 | const fnValue = value(draft) 28 | if (fnValue === draft) return // do nothing 29 | 30 | // use function return value 31 | if (fnValue && typeof fnValue === 'object') { 32 | nextState = fnValue 33 | useImmer = false 34 | } 35 | }) 36 | 37 | if (useImmer) { 38 | nextState = immerState 39 | } 40 | 41 | return nextState 42 | } 43 | 44 | setState = (key: any, state: any, value: any): any => { 45 | const nextState = this.getNextState(state, value) 46 | 47 | this.state = nextState 48 | 49 | emitStoreUpdate({ key, nextState }) 50 | 51 | this.state = nextState 52 | this.setters.forEach(set => set(nextState)) 53 | return nextState 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/stook/src/emitter.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | interface Data { 4 | key: any 5 | nextState: any 6 | } 7 | 8 | type Events = { 9 | STORE_INITED: string 10 | STORE_UPDATED: Data 11 | } 12 | 13 | enum EventKey { 14 | STORE_INITED = 'STORE_INITED', 15 | STORE_UPDATED = 'STORE_UPDATED', 16 | } 17 | 18 | export const emitter = mitt() 19 | 20 | export function emitStoreInit(key: any) { 21 | emitter.emit(EventKey.STORE_INITED, key) 22 | } 23 | 24 | export function emitStoreUpdate(data: Data) { 25 | emitter.emit(EventKey.STORE_UPDATED, data) 26 | } 27 | 28 | export function onStoreInit(cb: (data: string) => void) { 29 | emitter.on(EventKey.STORE_INITED, key => { 30 | cb(key) 31 | }) 32 | } 33 | 34 | export function onStoreUpdate(cb: (data: Data) => void) { 35 | emitter.on(EventKey.STORE_UPDATED, data => { 36 | cb(data) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /packages/stook/src/getState.ts: -------------------------------------------------------------------------------- 1 | import { Storage } from './Storage' 2 | import { keyType } from './types' 3 | 4 | const undefined_as_any: any = undefined 5 | 6 | /** 7 | * Get store by Key 8 | * @param key 9 | */ 10 | export function getState(key: K | keyType): S { 11 | const store = Storage.get(key) 12 | return store ? store.state : undefined_as_any 13 | } 14 | -------------------------------------------------------------------------------- /packages/stook/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Storage' 2 | export * from './useStore' 3 | export * from './getState' 4 | export * from './mutate' 5 | export * from './types' 6 | export { onStoreInit, onStoreUpdate } from './emitter' 7 | -------------------------------------------------------------------------------- /packages/stook/src/mutate.ts: -------------------------------------------------------------------------------- 1 | import { Storage } from './Storage' 2 | import { keyType } from './types' 3 | 4 | /** 5 | * update store by key 6 | * 7 | * @param key unique store key (唯一key) 8 | * @param nextValue next value 9 | */ 10 | export function mutate(key: K | keyType, nextValue?: S) { 11 | const store = Storage.get(key) 12 | 13 | if (store && store.setState) { 14 | return store.setState(key, store.state, nextValue) 15 | } else { 16 | // init state, if no store exist 17 | Storage.set(key, { state: nextValue } as any) 18 | return nextValue 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/stook/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Action = S | ((prevState: S) => S) | ((prevState: S) => void) 2 | export type SetStateAction = S | ((prevState: S) => S) 3 | export type Dispatch = (value: A) => void 4 | 5 | export interface Key { 6 | } 7 | 8 | export type keyType = keyof Key 9 | -------------------------------------------------------------------------------- /packages/stook/src/useStore.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useLayoutEffect } from 'react' 2 | import { Storage } from './Storage' 3 | import { Store } from './Store' 4 | import { Dispatch, Action, keyType } from './types' 5 | import { emitStoreInit } from './emitter' 6 | 7 | const isBrowser = 8 | typeof window === 'object' && typeof document === 'object' && document.nodeType === 9 9 | 10 | const useSafeLayoutEffect = isBrowser ? useLayoutEffect : useEffect 11 | 12 | /** 13 | * Returns a stateful value, similar to useState, but need a key; 14 | * 15 | * 用法和 useState 几乎一模一样,只是第一个参数是唯一key; 16 | * 17 | * @param key unique store key (唯一key) 18 | * @param initialValue initial value, can not override, use first useStore to init 19 | * @see https://stook.vercel.app/docs/stook/use-store 20 | * 21 | * 需要注意的是,如果调用多个相同key的 useStore, 第一个被调用的 useStore 的 initialValue 才是有效的 initialValue 22 | */ 23 | export function useStore( 24 | key: K | keyType, 25 | initialValue?: S, 26 | ): [S, Dispatch>] { 27 | const storageStore = Storage.get(key) 28 | const initialState = storageStore ? storageStore.state : initialValue 29 | 30 | Storage.set(key, new Store(initialState)) 31 | 32 | const newStore = Storage.get(key) 33 | const [state, set] = useState(initialState) 34 | const { setters } = newStore 35 | 36 | /** 37 | * push setState sync 38 | */ 39 | useSafeLayoutEffect(() => { 40 | setters.push(set) 41 | emitStoreInit(key) 42 | }, []) 43 | 44 | useEffect(() => { 45 | return () => { 46 | setters.splice(setters.indexOf(set), 1) 47 | } 48 | }, []) 49 | 50 | function act(key: any): Dispatch> { 51 | return (value: any) => { 52 | return newStore.setState(key, state, value) 53 | } 54 | } 55 | 56 | return [state, act(key)] 57 | } 58 | -------------------------------------------------------------------------------- /packages/stook/test/getState.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks' 2 | import { useStore, getState } from '../src' 3 | 4 | describe('mutate', () => { 5 | it('none state', () => { 6 | const count = getState('Counter1') 7 | expect(count).toBeUndefined() 8 | }) 9 | 10 | it('can get state', () => { 11 | const {} = renderHook(() => useStore('Counter2', 2)) 12 | const count = getState('Counter2') 13 | expect(count).toBe(2) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/stook/test/mutate.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks' 2 | import { useStore, mutate } from '../src' 3 | 4 | describe('mutate', () => { 5 | it('inited', () => { 6 | const { result } = renderHook(() => useStore('Counter1', 0)) 7 | 8 | expect(result.current[0]).toBe(0) 9 | 10 | act(() => { 11 | mutate('Counter1', 10) 12 | }) 13 | 14 | expect(result.current[0]).toBe(10) 15 | }) 16 | 17 | it('did not inited', () => { 18 | act(() => { 19 | mutate('Counter2', 10) 20 | }) 21 | 22 | const { result } = renderHook(() => useStore('Counter2', 0)) 23 | expect(result.current[0]).toBe(10) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/stook/test/useStore.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks' 2 | import { useStore } from '../src' 3 | 4 | describe('useStore', () => { 5 | it('simplest', () => { 6 | const { result } = renderHook(() => useStore('Counter', 0)) 7 | 8 | expect(result.current[0]).toBe(0) 9 | 10 | act(() => { 11 | result.current[1](1) 12 | }) 13 | expect(result.current[0]).toBe(1) 14 | 15 | act(() => { 16 | result.current[1](() => 2) 17 | }) 18 | expect(result.current[0]).toBe(2) 19 | }) 20 | 21 | it('setState(value)', () => { 22 | const { result } = renderHook(() => useStore('User', { name: 'fo' })) 23 | 24 | expect(result.current[0].name).toBe('fo') 25 | 26 | act(() => { 27 | result.current[1]({ name: 'foo' }) 28 | }) 29 | expect(result.current[0].name).toBe('foo') 30 | 31 | // function 32 | act(() => { 33 | result.current[1]((state: any) => ({ ...state, name: 'fooo' })) 34 | }) 35 | expect(result.current[0].name).toBe('fooo') 36 | 37 | // immer 38 | act(() => { 39 | result.current[1]((state: any) => { 40 | state.name = 'foooo' 41 | }) 42 | }) 43 | expect(result.current[0].name).toBe('foooo') 44 | }) 45 | 46 | it('try init again', () => { 47 | const { result: result1 } = renderHook(() => useStore('User2', { name: 'fo' })) 48 | const { result: result2 } = renderHook(() => useStore('User2', { name: 'bar' })) 49 | 50 | expect(result1.current[0].name).toBe('fo') 51 | expect(result2.current[0].name).toBe('fo') 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/stook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "types" 5 | ], 6 | "compilerOptions": { 7 | "module": "esnext", 8 | "lib": [ 9 | "dom", 10 | "esnext" 11 | ], 12 | "importHelpers": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "rootDir": "./src", 16 | "strict": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "noImplicitThis": true, 20 | "noImplicitAny": true, 21 | "strictNullChecks": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "moduleResolution": "node", 27 | "baseUrl": "./", 28 | "paths": { 29 | "*": [ 30 | "src/*", 31 | "node_modules/*" 32 | ] 33 | }, 34 | "jsx": "react", 35 | "esModuleInterop": true 36 | } 37 | } -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /website/blog/2019-05-28-hola.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: hola 3 | title: Hola 4 | author: Gao Wei 5 | author_title: Docusaurus Core Team 6 | author_url: https://github.com/wgao19 7 | author_image_url: https://avatars1.githubusercontent.com/u/2055384?v=4 8 | tags: [hola, docusaurus] 9 | --- 10 | 11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 12 | -------------------------------------------------------------------------------- /website/blog/2019-05-29-hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: hello-world 3 | title: Hello 4 | author: Endilie Yacop Sucipto 5 | author_title: Maintainer of Docusaurus 6 | author_url: https://github.com/endiliey 7 | author_image_url: https://avatars1.githubusercontent.com/u/17883920?s=460&v=4 8 | tags: [hello, docusaurus] 9 | --- 10 | 11 | Welcome to this blog. This blog is created with [**Docusaurus 2 alpha**](https://docusaurus.io/). 12 | 13 | 14 | 15 | This is a test post. 16 | 17 | A whole bunch of other information. 18 | -------------------------------------------------------------------------------- /website/blog/2019-05-30-welcome.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: welcome 3 | title: Welcome 4 | author: Yangshun Tay 5 | author_title: Front End Engineer @ Facebook 6 | author_url: https://github.com/yangshun 7 | author_image_url: https://avatars0.githubusercontent.com/u/1315101?s=400&v=4 8 | tags: [facebook, hello, docusaurus] 9 | --- 10 | 11 | Blog features are powered by the blog plugin. Simply add files to the `blog` directory. It supports tags as well! 12 | 13 | Delete the whole directory if you don't want the blog features. As simple as that! 14 | -------------------------------------------------------------------------------- /website/docs/devtools/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: intro 3 | title: 简介 4 | sidebar_label: 简介 5 | --- 6 | 7 | 为了更好的编程体验,Stook 支持使用 Redux DevTools 调试。 8 | 9 | ## Install Redux DevTools extension 10 | 11 | 如果你未安装 Redux DevTools extension,请安装相应浏览器的插件:[Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)。 12 | 13 | ## Setup 14 | 15 | 使用 devtools 非常简单,先安装 `stook-devtools` 包: 16 | 17 | ```bash 18 | npm i stook-devtools 19 | ``` 20 | 21 | 然后在项目代码中进入,并初始化: 22 | 23 | ```js 24 | import { devtools } from 'devtools' 25 | 26 | devtools.init() 27 | ``` 28 | 29 | 如果你不想在生产环境引入: 30 | 31 | ```js 32 | import { devtools } from 'devtools' 33 | 34 | if (process.env.NODE_ENV !== 'production') { 35 | devtools.init() 36 | } 37 | ``` 38 | 39 | ## 效果 40 | 41 | 生效后,可以在浏览器的 Redux DevTools extension 看到整个应用的 state 状态: 42 | 43 | ![devtools](/img/stook-devtools.png) 44 | -------------------------------------------------------------------------------- /website/docs/graphql/config.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: config 3 | title: Config 4 | sidebar_label: Config 5 | --- 6 | 7 | ## 全局配置 8 | 9 | 你可以使用 `config` 方法进行全局配置,全局配置将在每个请求生效: 10 | 11 | ```tsx 12 | import { config } from 'stook-graphql' 13 | 14 | config({ 15 | endpoint: 'https://graphql-compose.herokuapp.com/user', 16 | headers: { 17 | foo: 'bar', 18 | }, 19 | }) 20 | ``` 21 | 22 | ## 配置选项 23 | 24 | **`endpoint`**: string 25 | 26 | GraphQL Client 服务器端点, 默认为 /graphql。 27 | 28 | **`headers`**: object 29 | 30 | 每个请求都会带上的请求头,默认为 `{ 'content-type': 'application/json; charset=utf-8' }` 31 | 32 | ## Creating an instance 33 | 34 | 在某些应用场景,你可以能有多个后端服务,这时你需要多个 Client 实例: 35 | 36 | ```js 37 | const client = new Client({ 38 | endpoint: 'https://graphql-compose.herokuapp.com/user', 39 | headers: { 40 | foo: 'bar', 41 | }, 42 | }) 43 | ``` 44 | 45 | 创建实例后,你同样可以调用这些 Api: 46 | 47 | - query 48 | - useQuery 49 | - useMutation 50 | - useSubscription 51 | -------------------------------------------------------------------------------- /website/docs/graphql/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: intro 3 | title: Introduction 4 | sidebar_label: Introduction 5 | --- 6 | 7 | 有人说,GraphQL 是未来,这里不讨论 GraphQL 和 RESTful 谁更优秀。`stook-graphql`和 `stook-rest`一样,推崇使用 hooks 获取并维护异步数据。 8 | 9 | ## 使用 `stook-graphql` 10 | 11 | 我们使用 `stook-rest` 的 `useFetch` 获取数据,可以轻松的拿到数据的状态 `{ loading, data, error }`,然后渲染处理: 12 | 13 | ```jsx 14 | import React from 'react' 15 | import { useQuery } from 'stook-graphql' 16 | 17 | const GET_USER = ` 18 | query User { 19 | userOne { 20 | _id 21 | name 22 | gender 23 | age 24 | } 25 | } 26 | ` 27 | const User = () => { 28 | const { loading, data, error } = useQuery(GET_USER) 29 | 30 | if (loading) return
    loading....
    31 | if (error) return
    error!
    32 | 33 | return
    {JSON.stringify(data, null, 2)}
    34 | } 35 | ``` 36 | 37 | 这是最简单的 GraphQL 用法,下面章节你会学习到如何配置 endpoint、refetch、mutate 等更多详细的用法。 38 | -------------------------------------------------------------------------------- /website/docs/graphql/middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: middleware 3 | title: Middleware 4 | sidebar_label: Middleware 5 | --- 6 | 7 | ## 中间件用法 8 | 9 | 为了方便的方便的拦截请求,stook-graphql 提供了中间件机制。 10 | 11 | ```js 12 | import { applyMiddleware } from 'stook-graphql' 13 | 14 | // add a middleware 15 | applyMiddleware(async (ctx, next) => { 16 | ctx.headers.Authorization = `bearer token...` 17 | await next() 18 | ctx.body = { data: ctx.body } 19 | }) 20 | 21 | // add another middleware 22 | applyMiddleware(async (ctx, next) => { 23 | try { 24 | await next() 25 | } catch (error) { 26 | ctx.body = { error: ctx.body } 27 | } 28 | }) 29 | ``` 30 | 31 | ## 中间件机制 32 | 33 | stook-graphql 的机制和 [koa](https://github.com/koajs/koa) 类似,都是洋葱模型。 34 | 35 | 每个中间件是一个 async 函数,类型定义如下: 36 | 37 | ```ts 38 | type Middleware = (ctx: Ctx, next: () => Promise) => anyjs 39 | 40 | type NextFn = () => Promise 41 | 42 | interface Ctx { 43 | headers: { 44 | [key: string]: string 45 | } 46 | body: any 47 | } 48 | ``` 49 | 50 | ## 使用场景 51 | 52 | 下面是一些实际应用场景的例子: 53 | 54 | **为所有请求添加 token** 55 | 56 | ```js 57 | applyMiddleware(async (ctx, next) => { 58 | ctx.headers.Authorization = `bearer my_token_qazxsw` 59 | await next() 60 | }) 61 | ``` 62 | 63 | **格式化 response** 64 | 65 | ```js 66 | applyMiddleware(async (ctx, next) => { 67 | await next() 68 | ctx.body = { 69 | code: 0, 70 | data: ctx.body, 71 | msg: 'fetch data success', 72 | } 73 | }) 74 | ``` 75 | 76 | **统一错误处理** 77 | 78 | ```js 79 | applyMiddleware(async (ctx, next) => { 80 | try { 81 | await next() 82 | } catch (error) { 83 | throw { 84 | code: 1, 85 | error: error, 86 | msg: 'fetch data error', 87 | } 88 | } 89 | }) 90 | ``` 91 | -------------------------------------------------------------------------------- /website/docs/graphql/query.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: query 3 | title: query 4 | sidebar_label: query 5 | --- 6 | 7 | ```jsx 8 | import React, { useState, useEffect } from 'react' 9 | import { query } from 'stook-graphql' 10 | 11 | const GET_USER = ` 12 | query User { 13 | userById(_id: "57bb44dd21d2befb7ca3f010") { 14 | name 15 | gender 16 | age 17 | } 18 | } 19 | ` 20 | export default () => { 21 | const [data, setData] = useState() 22 | 23 | useEffect(() => { 24 | async function queryData() { 25 | const res = await query(GET_USER) 26 | setData(res) 27 | } 28 | queryData() 29 | }, []) 30 | 31 | return
    {JSON.stringify(data, null, 2)}
    32 | } 33 | 34 | ``` -------------------------------------------------------------------------------- /website/docs/graphql/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: quick-start 3 | title: Quick start 4 | sidebar_label: Quick start 5 | --- 6 | 7 | stook-graphql 一个基于 hooks 实现的 Graphql 数据请求工具。 8 | 9 | ## 安装 10 | 11 | ```bash 12 | npm i stook-graphql 13 | ``` 14 | 15 | ## 获取数据 16 | 17 | 下面展示如何快速获取 GraphQL Api 数据。你就可以使用 `stook-graphql` 提供的一个 hooks `useQuery`来获取远程服务器数据。 18 | 19 | ```tsx 20 | import { useQuery } from 'stook-graphql' 21 | 22 | interface User { 23 | userOne: { 24 | _id: string 25 | name: string 26 | gender: string 27 | age: number 28 | } 29 | } 30 | 31 | const User = () => { 32 | const { loading, data, error } = useQuery(GET_USER) 33 | 34 | if (loading) return
    loading....
    35 | if (error) return
    error!
    36 | return
    {JSON.stringify(data, null, 2)}
    37 | } 38 | ``` 39 | 40 | 当然,这只是 `useQuery` 最基本功能,如果你想深入了解它的其他功能,比如 refetch、retry 等高级功能,你看详情阅读 `useQuery` Api。 41 | 42 | ## 下一步 43 | 44 | 上面就是用获取数据最简单的例子,如果你要深入了解如何使用 `stook-graphql`,建议细看: 45 | 46 | - [获取数据](/docs/graphql/useQuery): 深入了解 `useFetch` 的使用 47 | - [更新数据](/docs/graphql/useMutation): 深入了解 `useMutation` 的使用 48 | - [网络请求](/docs/graphql/query): 深入了解 `query` 的使用 49 | -------------------------------------------------------------------------------- /website/docs/graphql/useMutate.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: useMutation 3 | title: useMutation 4 | sidebar_label: useMutation 5 | --- 6 | 7 | ```jsx 8 | import React from 'react' 9 | import { useMutation } from 'stook-graphql' 10 | 11 | const GET_USER = ` 12 | query User { 13 | userById(_id: "57bb44dd21d2befb7ca3f010") { 14 | name 15 | gender 16 | age 17 | } 18 | } 19 | ` 20 | 21 | export default () => { 22 | const [addTodo, { loading, data, error }] = useMutation(GET_USER) 23 | return ( 24 |
    25 | 29 | 30 | {error &&
    {JSON.stringify(error, null, 2)}
    } 31 | {data &&
    {JSON.stringify(data, null, 2)}
    } 32 |
    33 | ) 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /website/docs/graphql/useQuery.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: useQuery 3 | title: useQuery 4 | sidebar_label: useQuery 5 | --- 6 | 7 | > const result = useQuery(input, options) 8 | 9 | 以简单高效的方式获取和管理异步数据是 stook-query 的核心功能。接下来,你将学习如何通过 `useQuery` 获取数据并渲染成 UI。 10 | 11 | 下面是一个示例,这里假设你已经配置好 client,如果不了解如何配置,请看 [配置](/docs/graphql/config)。 12 | 13 | ## 使用 `useQuery` 14 | 15 | ```jsx 16 | import React from 'react' 17 | import { useQuery } from 'stook-graphql' 18 | 19 | const GET_USER = ` 20 | query User { 21 | userById(_id: "57bb44dd21d2befb7ca3f010") { 22 | name 23 | gender 24 | age 25 | } 26 | } 27 | ` 28 | 29 | export default () => { 30 | const { loading, data, error } = useQuery(GET_USER) 31 | 32 | if (loading) return
    loading....
    33 | if (error) return
    {JSON.stringify(error, null, 2)}
    34 | 35 | return
    {JSON.stringify(data, null, 2)}
    36 | } 37 | ``` 38 | 39 | ## input (string) 40 | 41 | GraphQL 请求的字符串。 42 | 43 | ## options 44 | 45 | **`variables: boolean`** 46 | 47 | GraphQL 变量。 48 | 49 | **`key?: string`** 50 | 51 | 该请求的唯一标识符,因为 stook-graphql 是基于 stook,这个 key 就是 stook 的唯一 key,对于 refetch 非常有用。默认是为 input。 52 | 53 | **`headers?: HeadersInit;`** 54 | 55 | HTTP 请求头,和原生`fetch`的 [`Headers`](https://github.github.io/fetch/#Headers) 一致,但有默认值: `{ 'content-type': 'application/json; charset=utf-8' }` 56 | 57 | **pollInterval?: number** 58 | 59 | 轮询时间间隔 60 | 61 | **lazy?: boolean** 62 | 63 | 默认不发请求,需要手动 start 触发,start 是 result 里面的方法。 64 | 65 | **errRetryCount?: number** 66 | 67 | 错误重试次数 68 | 69 | **timeout?: number** 70 | 71 | 超时时间(单位毫秒) 72 | 73 | ## 结果 (Result) 74 | 75 | **`loading: boolean`** 76 | 77 | 一个布尔值,表示数据是否加载中。 78 | 79 | **`data: T`** 80 | 81 | 服务器返回的数据。 82 | 83 | **`error: GraphqlError`** 84 | 85 | 服务器返回错误。 86 | 87 | **`refetch: (options?: Options) => Promise`** 88 | 89 | 重新发起一个请求获取数据。 90 | 91 | **`start: (options?: Options) => Promise`** 92 | 93 | 手动触发一个请求获取数据,配合 lazy 使用。 94 | -------------------------------------------------------------------------------- /website/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: intro 3 | title: intro 4 | sidebar_label: intro 5 | --- 6 | 7 | .. 8 | . 9 | -------------------------------------------------------------------------------- /website/docs/rest/config.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: config 3 | title: Config 4 | sidebar_label: Config 5 | --- 6 | 7 | ## 全局配置 8 | 9 | 你可以使用 `config` 方法进行全局配置,全局配置将在每个请求生效: 10 | 11 | ```tsx 12 | import { config } from 'stook-rest' 13 | 14 | config({ 15 | baseURL: 'https://jsonplaceholder.typicode.com', 16 | headers: { 17 | foo: 'bar', 18 | }, 19 | }) 20 | ``` 21 | 22 | ## 配置选项 23 | 24 | **`baseURL`**: string 25 | 26 | Restful Api 服务器 baseURL, 默认为当前前端页面 host。 27 | 28 | **`headers`**: object 29 | 30 | 每个请求都会带上的请求头,默认为 `{ 'content-type': 'application/json; charset=utf-8' }` 31 | 32 | ## Creating an instance 33 | 34 | 在某些应用场景,你可以能有多个后端服务,这时你需要多个 Client 实例: 35 | 36 | ```js 37 | const client = new Client({ 38 | baseURL: 'https://jsonplaceholder.typicode.com', 39 | headers: { 40 | foo: 'bar', 41 | }, 42 | }) 43 | 44 | client.fetch('/todos').then((data) => { 45 | console.log(data) 46 | }) 47 | ``` 48 | 49 | 创建实例后,你同样可以调用这些 Api: 50 | 51 | - fetch 52 | - useFetch 53 | - useUpdate 54 | -------------------------------------------------------------------------------- /website/docs/rest/custom-hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: custom-hooks 3 | title: Custom hooks 4 | sidebar_label: Custom hooks 5 | --- 6 | 7 | 在真实的业务开发中,不建议直接在组件中使用 `useFetch`,更推荐是使用使用自定义 hooks 对请求的业务逻辑进行封装。 8 | 9 | ## 如何自定义 hooks ? 10 | 11 | ```jsx 12 | const useFetchTodos = () => { 13 | const { loading, data: todos = [], error } = useFetch('/todos') 14 | return { loading, todos, error } 15 | } 16 | ``` 17 | 18 | ## 为何推荐自定义 hooks ? 19 | 20 | 自定义 hooks 有下面几点好处: 21 | 22 | ### 为 hooks 命名 23 | 24 | 这看上去和直接使用 `useFetch` 没有太大区别,实际上它增加了代码的可读性。 25 | 26 | ### 文件更易管理 27 | 28 | 如果我们我们直接在组件中使用 `useFetch`,我们需要在组件引入非常多文件。这个请求数据只有一个组件使用还好,如果多个组件需要共享此请求数据,文件管理将会非常乱。 29 | 30 | ```tsx 31 | import React from 'react' 32 | import { useFetch } from 'stook-rest' 33 | import { Todo } from '../../typings' 34 | import { GET_TODO } from '../../URL.constant' 35 | 36 | export default () => { 37 | const { data: todos } = useFetch(GET_TODO) 38 | 39 | if (loading) return
    loading....
    40 | return ( 41 |
    42 |
    Todo:
    43 |
    {JSON.stringify(todo, null, 2)}
    44 |
    45 | ) 46 | } 47 | ``` 48 | 49 | 如果使用使用自定义 hooks,我们只需在组件中引入 hooks: 50 | 51 | ```tsx 52 | import React from 'react' 53 | import { useFetchTodos } from '../../useFetchTodos' 54 | 55 | export default () => { 56 | const { loading, todos } = useFetchTodos() 57 | 58 | if (loading) return
    loading....
    59 | return ( 60 |
    61 |
    Todos:
    62 |
    {JSON.stringify(todos, null, 2)}
    63 |
    64 | ) 65 | } 66 | ``` 67 | 68 | ### 更好管理 computed value 69 | 70 | 为了业务逻辑更好的复用,我们经常会使用 computed value: 71 | 72 | ```tsx 73 | const useFetchTodos = () => { 74 | const { loading, data: todos = [], error } = useFetch('/todos') 75 | const count = todos.length 76 | const completedCount = todos.filter(i => i.completed).length 77 | return { loading, todos, count, completedCount, error } 78 | } 79 | ``` 80 | 81 | ### 更优雅地共享数据 82 | 83 | 自定义 hooks 让数据跨组件共享数据更加优雅: 84 | 85 | ```tsx 86 | interface Todo { 87 | id: number 88 | title: string 89 | completed: boolean 90 | } 91 | 92 | const useFetchTodos = () => { 93 | const { loading, data: todos = [], error } = useFetch('/todos') 94 | const count = todos.length 95 | const completedCount = todos.filter(i => i.completed).length 96 | return { loading, todos, count, completedCount, error } 97 | } 98 | 99 | const TodoList = () => { 100 | const { loading, todos, count, completedCount } = useFetchTodos() 101 | if (loading) return
    loading....
    102 | return ( 103 |
    104 |
    TodoList:
    105 |
    todos count: {count}
    106 |
    completed count: {completedCount}
    107 |
    {JSON.stringify(todos, null, 2)}
    108 |
    109 | ) 110 | } 111 | 112 | const ReuseTodoList = () => { 113 | const { loading, todos, count, completedCount } = useFetchTodos() 114 | if (loading) return
    loading....
    115 | return ( 116 |
    117 |
    ReuseTodoList:
    118 |
    todos count: {count}
    119 |
    completed count: {completedCount}
    120 |
    {JSON.stringify(todos, null, 2)}
    121 |
    122 | ) 123 | } 124 | 125 | export default () => ( 126 |
    127 | 128 | 129 |
    130 | ) 131 | ``` 132 | -------------------------------------------------------------------------------- /website/docs/rest/dependent.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: dependent 3 | title: Dependent Fetching 4 | sidebar_label: Dependent Fetching 5 | --- 6 | 7 | 很多时候,一个请求会依赖另外一个请求的数据,这时候请求会有前后顺序,stook-rest 可以非常优雅的处理这种依赖请求: 8 | 9 | ```jsx 10 | import React from 'react' 11 | import { config, useFetch } from 'stook-rest' 12 | 13 | export default () => { 14 | const { data: todos } = useFetch('/todos') 15 | 16 | const { loading, data: todo } = useFetch('/todos/:id', { 17 | params: () => ({ id: todos[9].id }), 18 | }) 19 | 20 | if (loading) return
    loading....
    21 | 22 | return ( 23 |
    24 |
    Todo:
    25 |
    {JSON.stringify(todo, null, 2)}
    26 |
    27 | ) 28 | } 29 | ``` 30 | 31 | 我们知道,`params`、`query`、`body` 三中参数值通常是一个对象,其实他们也可以是一个函数,函数参数可以让我们轻易地使用依赖请求。 32 | 33 | 依赖请求的方式可以大大地减少你的代码量,并让你可以用类似同步的代码书写数据请求代码。 34 | 35 | [![Edit sweet-lake-gu2el](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/sweet-lake-gu2el?fontsize=14&hidenavigation=1&theme=dark) 36 | -------------------------------------------------------------------------------- /website/docs/rest/fetch.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: fetch 3 | title: fetch 4 | sidebar_label: fetch 5 | --- 6 | 7 | 大部分情况下,建议使用 `useFetch` 获取数据。不过有些场景,你不需要在维护异步数据的状态,你只需要发送普通的网络请求。 8 | 9 | 比如,点击一个按钮提交表单,执行一个更新操作: 10 | 11 | **todoService.ts** 12 | 13 | ```jsx 14 | import { createStore } from 'stook-store' 15 | import { fetch } from 'stook-rest' 16 | 17 | export async function fetchTodos() { 18 | return await fetch('/todos') 19 | } 20 | ``` 21 | 22 | stook-rest 的 `fetch` 和原生的 Api Fetch (https://github.github.io/fetch/)非常类似,但又有一些区别。 23 | 24 | 那 stook-rest 的 `fetch` 和原生 `fetch`有什么区别呢? 25 | 26 | ## 直接返回数据 27 | 28 | ```js 29 | const todos = await fetch('/todos') 30 | ``` 31 | 32 | 上面的 todos 就是服务器的数据,你可以指定如何处理远程数据: 33 | 34 | ```js 35 | const todos = await fetch('/todos', { type: 'text' }) 36 | ``` 37 | 38 | type 是可选的,默认为 'json',类型为 `type Type = 'text' | 'json' | 'blob' | 'arrayBuffer' | 'formData'` 39 | 40 | ## Request body 支持 JS 对象 41 | 42 | ```js 43 | const todos = await fetch('/todos', { 44 | method: 'POST', 45 | body: { 46 | title: 'do something', 47 | }, 48 | }) 49 | ``` 50 | 51 | ## 支持 query 参数 52 | 53 | ```js 54 | const todos = await fetch('/todos', { 55 | query: { 56 | pageSize: 10, 57 | pageNum: 1, 58 | }, 59 | }) 60 | ``` 61 | 62 | 将请求 `/todos?pageSize=10&pageNum=1`,甚至你可以使用嵌套的 query 对象。 63 | 64 | ## 支持 params 参数 65 | 66 | ```js 67 | const todos = await fetch('/todos/:id', { 68 | params: { id: 1 }, 69 | }) 70 | ``` 71 | 72 | 处理这些不一样,其他参数跟原生 `fetch`保持一致,详情请看文档:https://github.github.io/fetch 。 73 | -------------------------------------------------------------------------------- /website/docs/rest/interceptor.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: interceptor 3 | title: 拦截器 4 | sidebar_label: 拦截器 5 | --- 6 | 7 | wip... -------------------------------------------------------------------------------- /website/docs/rest/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: intro 3 | title: Introduction 4 | sidebar_label: Introduction 5 | --- 6 | 7 | 异步数据管理一直是一个难点,在 React 的生态圈中,很多人把异步数据使用状态管理维护,比如使用 Redux,用异步 Action 获取远程数据。我个人不喜欢使用 Redux 状态管理维护异步数据,我更倾向于在组件内直接获取异步数据,使用 hooks,简化数据的获取和管理。 8 | 9 | 为什么要使用 Hooks 管理呢?可以看 [React 异步数据管理思考](https://zhuanlan.zhihu.com/p/64648552) 10 | 11 | 下面我们看看和使用 Redux 有什么本质上的区别。 12 | 13 | 假设我们要实现一个功能,获取一个 TodoList 数据,并且用 UI 渲染。 14 | 15 | TodoList 数据源:https://jsonplaceholder.typicode.com/todos 。 16 | 17 | ## 使用 `stook-rest` 18 | 19 | 我们使用 `stook-rest` 的 `useFetch` 获取数据,可以轻松的拿到数据的状态 `{ loading, data, error }`,然后渲染处理: 20 | 21 | ```jsx 22 | import React from 'react' 23 | import { useFetch } from 'stook-rest' 24 | 25 | const Todos = () => { 26 | const { loading, data, error } = useFetch('https://jsonplaceholder.typicode.com/todos') 27 | 28 | if (loading) return loading... 29 | if (error) return error! 30 | 31 | return ( 32 |
      33 | {data.map(item => ( 34 |
    • {item.title}
    • 35 | ))} 36 |
    37 | ) 38 | } 39 | 40 | export default Todos 41 | ``` 42 | 43 | [![Edit bitter-frog-t2tbm](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/bitter-frog-t2tbm?fontsize=14&hidenavigation=1&theme=dark) 44 | 45 | 如果你是 graphql 用户,类似的,可以使用 `stook-graphql` 的 `useQuery`。 46 | 47 | ## 使用 Redux 48 | 49 | 使用 Redux 管理异步数据,假设我们已经使用了 Redux 中间件 `redux-thunk`,我们会有下面类似的代码: 50 | 51 | 首先,我们会把字符串定义定义为常量到一个 `constant.js` 52 | 53 | ```js 54 | export const LOADING_TODOS = 'LOADING_TODOS' 55 | export const LOAD_TODOS_SUCCESS = 'LOAD_TODOS_SUCCESS' 56 | export const LOAD_TODOS_ERROR = 'LOAD_TODOS_ERROR' 57 | ``` 58 | 59 | 然后,编写异步的 action, `actions.js`: 60 | 61 | ```js 62 | import { LOADING_TODOS, LOAD_TODOS_SUCCESS, LOAD_TODOS_ERROR } from '../constant' 63 | 64 | export function fetchTodos() { 65 | return dispatch => { 66 | dispatch({ type: LOADING_TODOS }) 67 | return fetch('https://jsonplaceholder.typicode.com/todo') 68 | .then(response => response.json()) 69 | .then(todos => { 70 | dispatch({ 71 | type: LOAD_TODOS_SUCCESS, 72 | todos, 73 | }) 74 | }) 75 | .catch(error => { 76 | dispatch({ 77 | type: LOAD_TODOS_ERROR, 78 | error, 79 | }) 80 | }) 81 | } 82 | } 83 | ``` 84 | 85 | 接着,在 reducer 中处理数据,`reducer.js` 86 | 87 | ```js 88 | import { LOADING_TODOS, LOAD_TODOS_SUCCESS, LOAD_TODOS_ERROR } from '../constant' 89 | 90 | const initialState = { 91 | todos: { 92 | loading: false, 93 | data: [], 94 | error: null, 95 | }, 96 | } 97 | 98 | export default function(state = initalState, action) { 99 | switch (action.type) { 100 | case LOADING_TODOS: 101 | const { todos } = state 102 | return { ...state, todos: { ...todos, loading: true } } 103 | case LOAD_TODOS_SUCCESS: 104 | const { todos } = state 105 | return { ...state, todos: { ...todos, data: action.todos } } 106 | case LOAD_TODOS_ERROR: 107 | const { todos } = state 108 | return { ...state, todos: { ...todos, error: action.error } } 109 | default: 110 | return state 111 | } 112 | } 113 | ``` 114 | 115 | 还没完,最后,在组件中使用: 116 | 117 | ```jsx 118 | import React, { Component } from 'react' 119 | import { connect } from 'react-redux' 120 | import { fetchTodos } from '../actions' 121 | 122 | class Todos extends Component { 123 | componentDidMount() { 124 | const { dispatch } = this.props 125 | dispatch(fetchTodos) 126 | } 127 | 128 | render() { 129 | const { loading, items, error } = this.props 130 | if (loading) return loading... 131 | if (error) return error! 132 | 133 | return ( 134 |
      135 | {items.map(item => ( 136 |
    • {item.title}
    • 137 | ))} 138 |
    139 | ) 140 | } 141 | } 142 | 143 | const mapStateToProps = state => { 144 | const { todos } = state 145 | return { 146 | loading: todos.loaing, 147 | items: todos.data, 148 | error: todos.error, 149 | } 150 | } 151 | 152 | export default connect(mapStateToProps)(Todos) 153 | ``` 154 | 155 | [![Edit fetch-data-with-redux](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/xjl84rjvno?fontsize=14&hidenavigation=1&theme=dark) 156 | 157 | 我们可以发现,使用 Redux 管理异步数据,代码量激增,是 `stook-rest` 5 倍以上的代码量,不管开发效率还是开发体验,亦或是可以维护性和可读性,个人认为,类似的 redux 这样的解决方案并不优秀。 Hooks 简单直观,Redux 本地冗长,并且链路太长,需维护多个文件,更多的代码量。 158 | -------------------------------------------------------------------------------- /website/docs/rest/middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: middleware 3 | title: Middleware 4 | sidebar_label: Middleware 5 | --- 6 | 7 | ## 中间件用法 8 | 9 | 为了方便的方便的拦截请求,stook-graphql 提供了中间件机制。 10 | 11 | ```js 12 | import { applyMiddleware } from 'stook-graphql' 13 | 14 | // add a middleware 15 | applyMiddleware(async (ctx, next) => { 16 | ctx.headers.Authorization = `bearer token...` 17 | await next() 18 | ctx.body = { data: ctx.body } 19 | }) 20 | 21 | // add another middleware 22 | applyMiddleware(async (ctx, next) => { 23 | try { 24 | await next() 25 | } catch (error) { 26 | ctx.body = { error: ctx.body } 27 | } 28 | }) 29 | ``` 30 | 31 | ## 中间件机制 32 | 33 | stook-graphql 的机制和 [koa](https://github.com/koajs/koa) 类似,都是洋葱模型。 34 | 35 | 每个中间件是一个 async 函数,类型定义如下: 36 | 37 | ```ts 38 | type Middleware = (ctx: Ctx, next: () => Promise) => anyjs 39 | 40 | type NextFn = () => Promise 41 | 42 | interface Ctx { 43 | headers: { 44 | [key: string]: string 45 | } 46 | body: any 47 | } 48 | ``` 49 | 50 | ## 使用场景 51 | 52 | 下面是一些实际应用场景的例子: 53 | 54 | **为所有请求添加 token** 55 | 56 | ```js 57 | applyMiddleware(async (ctx, next) => { 58 | ctx.headers.Authorization = `bearer my_token_qazxsw` 59 | await next() 60 | }) 61 | ``` 62 | 63 | **格式化 response** 64 | 65 | ```js 66 | applyMiddleware(async (ctx, next) => { 67 | await next() 68 | ctx.body = { 69 | code: 0, 70 | data: ctx.body, 71 | msg: 'fetch data success', 72 | } 73 | }) 74 | ``` 75 | 76 | **统一错误处理** 77 | 78 | ```js 79 | applyMiddleware(async (ctx, next) => { 80 | try { 81 | await next() 82 | } catch (error) { 83 | throw { 84 | code: 1, 85 | error: error, 86 | msg: 'fetch data error', 87 | } 88 | } 89 | }) 90 | ``` 91 | -------------------------------------------------------------------------------- /website/docs/rest/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: quick-start 3 | title: Quick start 4 | sidebar_label: Quick start 5 | --- 6 | 7 | stook-rest 一个基于 hooks 实现的 Restful Api 数据请求工具。 8 | 9 | ## 安装 10 | 11 | ```bash 12 | npm i stook-rest 13 | ``` 14 | 15 | ## 获取数据 16 | 17 | 下面展示如何快速获取 Restful Api 数据。你就可以使用 `stook-rest` 提供的一个 hooks `useFetch`来获取远程服务器数据。下面是获取 todos 列表并渲染到组件,可以看到,代码相当简洁: 18 | 19 | ```tsx 20 | import { useFetch } from 'stook-rest' 21 | 22 | interface Todo { 23 | id: number 24 | title: string 25 | completed: boolean 26 | } 27 | 28 | const Todos = () => { 29 | const { loading, data, error } = useFetch('/todos') 30 | 31 | if (loading) return loading... 32 | if (error) return error! 33 | 34 | return ( 35 |
      36 | {data.map(item => ( 37 |
    • {item.title}
    • 38 | ))} 39 |
    40 | ) 41 | } 42 | ``` 43 | 44 | [![Edit bitter-frog-t2tbm](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/bitter-frog-t2tbm?fontsize=14&hidenavigation=1&theme=dark) 45 | 46 | 当然,这只是 `useFetch` 最基本功能,如果你想深入了解它的其他功能,比如 refetch、retry 等高级功能,你看详情阅读 `useFetch` Api。 47 | 48 | ## 下一步 49 | 50 | 上面就是用获取数据最简单的例子,如果你要深入了解如何使用 `stook-rest`,建议细看: 51 | 52 | - [获取数据](/docs/rest/useFetch): 深入了解 `useFetch` 的使用 53 | - [更新数据](/docs/rest/useUpdate): 深入了解 `useUpdate` 的使用 54 | - [网络请求](/docs/rest/fetch): 深入了解 `fetch` 的使用 55 | -------------------------------------------------------------------------------- /website/docs/rest/refetch.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: refetch 3 | title: refetch 4 | sidebar_label: refetch 5 | --- 6 | 7 | 很多场景中,你需要更新异步数据,比如在 CRUD 功能中,新增、删除、修改、分页、筛选等功能都需要更新异步数据。`stook-rest` 提供了三中方式更新数据,三种方式可在不同业务场景中使用,这是`stook-rest`的重要功能之一,你应该仔细阅读并理解它的使用场景,使用这种方式管理异步数据,整个应用的状态将变得更加简单,代码量会成本的减少,相应的可维护性大大增加。 8 | 9 | ## 重新获取数据的三种方式 10 | 11 | 但很多时候,你需要更新异步数据,`stook-rest`提供三种方式更新数据: 12 | 13 | - 内部 Refetch 14 | - 更新依赖 deps 15 | - 使用 fetcher 16 | 17 | ## 内部 Refetch 18 | 19 | 这是最简单的重新获取数据的方式,通常,如果触发更新的动作和`useFetch`在统一组件内,可以使用这种方式。 20 | 21 | ```tsx 22 | const Todos = () => { 23 | const { loading, data, error, refetch } = useFetch('/todos', { 24 | query: { _start: 0, _limit: 5 }, // first page 25 | }) 26 | 27 | if (loading) return loading... 28 | if (error) return error! 29 | 30 | const getSecondPage = () => { 31 | refetch({ 32 | query: { _start: 5, _limit: 5 }, // second page 33 | }) 34 | } 35 | 36 | return ( 37 |
    38 | 39 |
      40 | {data.map(item => ( 41 |
    • {item.title}
    • 42 | ))} 43 |
    44 |
    45 | ) 46 | } 47 | ``` 48 | 49 | [![Edit vigilant-bouman-y0gu7](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vigilant-bouman-y0gu7?fontsize=14&hidenavigation=1&theme=dark) 50 | 51 | ## 更新依赖 deps 52 | 53 | 通过更新依赖来重新获取数据,这也是常用的方式之一,因为在很多业务场景中,触发动作会在其他组件中,下面演示如何通过更新依赖触发数据更新: 54 | 55 | ```tsx 56 | import { useState } from 'react' 57 | import { useFetch } from 'stook-rest' 58 | 59 | export default () => { 60 | const [count, setCount] = useState(1) 61 | const { loading, data, error } = useFetch('/todos', { 62 | deps: [count], 63 | }) 64 | 65 | if (loading) return loading... 66 | if (error) return error! 67 | 68 | const update = () => { 69 | setCount(count + 1) 70 | } 71 | 72 | return ( 73 |
    74 | 75 |
      76 | {data.map(item => ( 77 |
    • {item.title}
    • 78 | ))} 79 |
    80 |
    81 | ) 82 | } 83 | ``` 84 | [![Edit loving-cray-b6xvq](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/loving-cray-b6xvq?fontsize=14&hidenavigation=1&theme=dark) 85 | 86 | 你可以在任意地方,不管组件内还是组件外,你都可以更新依赖,从而实现数据更新。 87 | 88 | 注意:这里的依赖是个对象,你必须更新整个对象的引用,如果你只更新对象的属性是无效的。 89 | 90 | ## 使用 fetcher 91 | 92 | 有时候,你需要在组件外部重新获取数据,但`useFetch` 却没有任何可以被依赖的参数,这时你可以使用 fetcher: 93 | 94 | ```tsx 95 | import { useFetch, fetcher } from 'stook-rest' 96 | 97 | const Todos = () => { 98 | const { loading, data, error } = useFetch('/todos', { key: 'GetTodos' }) 99 | 100 | if (loading) return loading... 101 | if (error) return error! 102 | 103 | return ( 104 |
      105 | {data.map(item => ( 106 |
    • {item.title}
    • 107 | ))} 108 |
    109 | ) 110 | } 111 | 112 | const Refresh = () => 113 | 114 | const TodoApp = () => ( 115 |
    116 | 117 | 118 |
    119 | ) 120 | ``` 121 | 122 | [![Edit stoic-bardeen-y15mg](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/stoic-bardeen-y15mg?fontsize=14&hidenavigation=1&theme=dark) 123 | 124 | 使用 fetcher 是,你需要为`useFetch` 提供 name 参数,用法是:`fetcher['name'].refetch()`,这里的 `refetch` 和内部 `refetch` 是同一个函数,所以它也有 options 参数。 125 | 126 | ## 高级用法 127 | 128 | 使用 fetcher 时,为一个 HTTP 请求命名 (name) 不是必须的,每个 HTTP 请求都有一个默认的名字,默认名字为该请求的 url 参数。 129 | 130 | 为了项目代码的可维护性,推荐把所以 Api 的 url 集中化,比如: 131 | 132 | ```tsx 133 | // apiService.ts 134 | enum Api { 135 | GetTodo = 'GET /todos/:id', 136 | GetTodos = 'GET /todos', 137 | } 138 | 139 | export default Api 140 | ``` 141 | 142 | 在组件中: 143 | 144 | ```tsx 145 | import { useFetch, fetcher } from 'stook-rest' 146 | import Api from '@service/apiService' 147 | 148 | const Todos = () => { 149 | const { loading, data, error } = useFetch(Api.GetTodos) 150 | 151 | if (loading) return loading... 152 | if (error) return error! 153 | 154 | return ( 155 |
    156 | 157 |
      158 | {data.map(item => ( 159 |
    • {item.title}
    • 160 | ))} 161 |
    162 |
    163 | ) 164 | } 165 | ``` 166 | -------------------------------------------------------------------------------- /website/docs/rest/share-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: share-state 3 | title: Share state 4 | sidebar_label: Share state 5 | --- 6 | 7 | ## 使用 8 | 9 | stook-rest 另一个强大的特性是请求数据的共享,由于 stook-rest 底层的数据管理是基于 stook 的,所以跨组件共享数据将变得非常简单: 10 | 11 | ```jsx 12 | const TodoItem = () => { 13 | const { loading, data: todo } = useFetch('/todos/1') 14 | if (loading) return
    loading....
    15 | return ( 16 |
    17 |
    {JSON.stringify(todo, null, 2)}
    18 |
    19 | ) 20 | } 21 | 22 | const ReuseTodoItem = () => { 23 | const { loading, data: todo } = useFetch('/todos/1') 24 | if (loading) return
    loading....
    25 | return ( 26 |
    27 |
    ReuseTodoItem:
    28 |
    {JSON.stringify(todo, null, 2)}
    29 |
    30 | ) 31 | } 32 | 33 | export default () => ( 34 |
    35 | 36 | 37 |
    38 | ) 39 | ``` 40 | 41 | [![Edit wizardly-ellis-nqmqj](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/wizardly-ellis-nqmqj?fontsize=14&hidenavigation=1&theme=dark) 42 | 43 | 上面我们在两个组件中使用了 `useFetch`,它们的唯一 key 是一样的 (都是 `GET /todos/1`),而且只会发送一次请求,两个组件会使用同一份数据。 44 | 45 | ## 优化 46 | 47 | 个人不太建议直接在多个组件使用同一个 `useFetch`,更进一步使用自定义 hooks,增强业务逻辑的复用性: 48 | 49 | ```jsx 50 | const useFetchTodo = () => { 51 | const { loading, data: todo, error } = useFetch('/todos/1') 52 | return { loading, todo, error } 53 | } 54 | 55 | const TodoItem = () => { 56 | const { loading, todo } = useFetchTodo() 57 | if (loading) return
    loading....
    58 | return ( 59 |
    60 |
    TodoItem:
    61 |
    {JSON.stringify(todo, null, 2)}
    62 |
    63 | ) 64 | } 65 | 66 | const ReuseTodoItem = () => { 67 | const { loading, todo } = useFetchTodo() 68 | if (loading) return
    loading....
    69 | return ( 70 |
    71 |
    ReuseTodoItem:
    72 |
    {JSON.stringify(todo, null, 2)}
    73 |
    74 | ) 75 | } 76 | 77 | export default () => ( 78 |
    79 | 80 | 81 |
    82 | ) 83 | ``` 84 | 85 | [![Edit blue-glitter-zysrb](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/blue-glitter-zysrb?fontsize=14&hidenavigation=1&theme=dark) 86 | -------------------------------------------------------------------------------- /website/docs/rest/useFetch.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: useFetch 3 | title: useFetch 4 | sidebar_label: useFetch 5 | --- 6 | 7 | > const result = useFetch(url, options) 8 | 9 | 以简单高效的方式获取和管理异步数据是 stook-rest 的核心功能。接下来,你将学习如何通过 `useFetch` 获取数据并渲染成 UI。 10 | 11 | 下面是一个示例,这里假设你已经配置好 client,如果不了解如何配置,请看 [配置](/docs/rest/config)。 12 | 13 | ## 使用 `useFetch` 14 | 15 | ```jsx 16 | import { useFetch } from 'stook-rest' 17 | 18 | interface Todo { 19 | id: number 20 | title: string 21 | completed: boolean 22 | } 23 | 24 | const Todos = () => { 25 | const { loading, data, error } = useFetch('/todos') 26 | 27 | if (loading) return loading... 28 | if (error) return error! 29 | 30 | return ( 31 |
      32 | {data.map(item => ( 33 |
    • {item.title}
    • 34 | ))} 35 |
    36 | ) 37 | } 38 | ``` 39 | 40 | [![Edit bitter-frog-t2tbm](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/bitter-frog-t2tbm?fontsize=14&hidenavigation=1&theme=dark) 41 | 42 | ## URL(string) 43 | 44 | HTTP 请求的 URL,eg: "/todos"。 45 | 46 | ## options 47 | 48 | **`method?: Method`** 49 | 50 | HTTP 请求的类型,默认为 `GET`, 全部可选值: `type Method = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH' | 'HEAD'` 51 | 52 | ```js 53 | const { loading, data, error } = useFetch('/todos', { method: 'POST' }) 54 | ``` 55 | 56 | **`query?: Query`** 57 | 58 | HTTP 请求的 query 对象,通常 `GET` 类型的请求比较常用。 59 | 60 | ```js 61 | const { loading, data, error } = useFetch('/todos', { 62 | query: { pageNum: 1, pageSize: 20 } 63 | }) 64 | ``` 65 | 66 | 上面会把 url 转换为: `/todos?pageNum=1&pageSize=20`。详细的转换规则请参照 [qs](https://github.com/ljharb/qs) 67 | 68 | **`body?: Body`** 69 | 70 | HTTP 请求的 body 对象,和原生 `fetch` 的 [body](https://github.github.io/fetch/#request-body) 类似,不同的是,`useFetch` 的 body 支持 JS 对象: 71 | 72 | ```js 73 | const { loading, data, error } = useFetch('/todos', { 74 | body: { title: 'todo1' }, 75 | }) 76 | ``` 77 | 78 | **`params?: Params`** 79 | 80 | URL 的参数对象,用法如下: 81 | 82 | ```js 83 | const { loading, data, error } = useFetch('/todos/:id', { 84 | params: { id: 10 }, 85 | }) 86 | ``` 87 | 88 | 请求发送后, `/todos/:id` 会转换为 `/todos/10`。 89 | 90 | **`headers?: HeadersInit;`** 91 | 92 | HTTP 请求头,和原生`fetch`的 [`Headers`](https://github.github.io/fetch/#Headers) 一致,但有默认值: `{ 'content-type': 'application/json; charset=utf-8' }` 93 | 94 | **`deps?: Deps`** 95 | 96 | `useFetch` 是一个自定义的 React hooks,默认情况下,组件多次渲染,`useFetch` 只会执行一次,不过如果你设置了依赖 (deps),并且依赖发生更新,`useFetch`会重新执行,就是会重新获取数据,其机制类似于 `useEffect` 的依赖,不同的是不设置任何依赖值时,当组件发生多次渲染,`useFetch` 只会执行一次,`useFetch` 执行多次。 97 | 98 | 依赖值 deps 是个数组,类型为:`type Deps = ReadonlyArray` 99 | 100 | **`key?: string`** 101 | 102 | 该请求的唯一标识符,因为 stook-rest 是基于 stook,这个 key 就是 stook 的唯一 key,对于 refetch 非常有用。默认是为 `${method} ${url}`,比如请求如下: 103 | 104 | ```js 105 | const { loading, data } = useFetch('/todos', { method: 'POST' }) 106 | ``` 107 | 108 | 那默认的 key 为: `POST /todos` 109 | 110 | ## 结果 (Result) 111 | 112 | **`loading: boolean`** 113 | 114 | 一个布尔值,表示数据是否加载中。 115 | 116 | **`data: T`** 117 | 118 | 服务器返回的数据。 119 | 120 | **`error: RestError`** 121 | 122 | 服务器返回错误。 123 | 124 | **`refetch: (options?: Options) => Promise`** 125 | 126 | 重新发起一个请求获取数据,eg: 127 | 128 | ```tsx 129 | const Todos = () => { 130 | const { loading, data, error, refetch } = useFetch('/todos') 131 | 132 | if (loading) return loading... 133 | if (error) return error! 134 | 135 | return ( 136 |
    137 | 138 |
      139 | {data.map(item => ( 140 |
    • {item.title}
    • 141 | ))} 142 |
    143 |
    144 | ) 145 | } 146 | ``` 147 | -------------------------------------------------------------------------------- /website/docs/rest/useUpdate.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: useUpdate 3 | title: useUpdate 4 | sidebar_label: useUpdate 5 | --- 6 | 7 | `useFetch` 通常用于获取数据,`useUpdate` 通常用于创建/更新/删除数据。 8 | 9 | ## 基本用法 10 | 11 | 下面是一个添加一个 TodoItem 的例子,使用 `useUpdate` 你可以轻易的获取到网络请求的各种状态: 12 | 13 | ```jsx 14 | export default () => { 15 | const [addTodo, { loading, called, data, error }] = useUpdate('/todos') 16 | 17 | return ( 18 |
    19 | 29 | 30 | {error &&
    {JSON.stringify(error, null, 2)}
    } 31 | {data &&
    {JSON.stringify(data, null, 2)}
    } 32 |
    33 | ) 34 | } 35 | ``` 36 | 37 | ## 更新反馈 38 | 39 | 通常更新数据成功后,我们需要提示用户,使用`useUpdate` 可以有两种方式实现交互反馈: 40 | 41 | **第一种,直接判断 data 和 error 的状态:** 42 | 43 | ```jsx 44 | export default () => { 45 | const [addTodo, { loading, called, data, error }] = useUpdate('/todos') 46 | 47 | if (data) { 48 | alert('Add Todo successfully') 49 | } 50 | 51 | if (error) { 52 | alert('Add Todo fali') 53 | } 54 | 55 | return ( 56 |
    57 | 67 | 68 | {error &&
    {JSON.stringify(error, null, 2)}
    } 69 | {data &&
    {JSON.stringify(data, null, 2)}
    } 70 |
    71 | ) 72 | } 73 | ``` 74 | 75 | **第一种,在 update 函数中获取 data 和 error 的状态:** 76 | 77 | ```jsx 78 | export default () => { 79 | const [update, { loading, called, data, error }] = useUpdate('/todos') 80 | 81 | const addTodo = async () => { 82 | const { data } = await update({ 83 | body: { title: 'new TODO' }, 84 | }) 85 | if (data) { 86 | alert('Add Todo successfully') 87 | } 88 | } 89 | 90 | return ( 91 |
    92 | 96 | 97 | {error &&
    {JSON.stringify(error, null, 2)}
    } 98 | {data &&
    {JSON.stringify(data, null, 2)}
    } 99 |
    100 | ) 101 | } 102 | ``` 103 | 104 | ## Options 105 | 106 | `useUpdate` 的 options 和 `useFetch` 基本一样。 -------------------------------------------------------------------------------- /website/docs/stook/custom-hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: custom-hooks 3 | title: Custom hooks 4 | sidebar_label: Custom hooks 5 | --- 6 | 7 | 为了能使组件和状态管理逻辑分离,强烈建议使用自定义 hooks 管理状态,比如说你要管理 Counter 的状态,那就是自定义一个叫 `useCounter` 的 hooks,然后在各组件中使用 `useCounter()`, 而不是直接使用 `useStore('Counter')`。 8 | 9 | 示例: 10 | 11 | ```jsx 12 | import React from 'react' 13 | import { useStore } from 'stook' 14 | 15 | function useCounter() { 16 | const [count, setCount] = useStore('Counter', 0) 17 | const decrease = () => setCount(count - 1) 18 | const increase = () => setCount(count + 1) 19 | return { count, increase, decrease } 20 | } 21 | 22 | function Display() { 23 | const { count } = useCounter() 24 | return
    {count}
    25 | } 26 | 27 | function Increase() { 28 | const { increase } = useCounter() 29 | return + 30 | } 31 | 32 | function Decrease() { 33 | const { decrease } = useCounter() 34 | return - 35 | } 36 | 37 | export default function App() { 38 | return ( 39 |
    40 | 41 | 42 | 43 |
    44 | ) 45 | } 46 | ``` 47 | 48 | [![Edit nameless-shadow-ozke5](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/nameless-shadow-ozke5?fontsize=14&hidenavigation=1&theme=dark) 49 | 50 | 上面三个子组件,都用到了 useCounter,它们实现了状态共享。 51 | -------------------------------------------------------------------------------- /website/docs/stook/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: faq 3 | title: FAQ 4 | sidebar_label: FAQ 5 | --- 6 | 7 | ## 和 Redux、Mobx 有什么区别? 可以代替他们吗? 8 | 9 | 和 Redux 相比,Stook 更简单,学习成本更低,对 TypeScript 支持更好,相同点是两者都是 immutable 的。 10 | 11 | 和 Mobx 相比, Mobx 是 mutable 的,Stook 是类 Redux 的 immutable。 12 | 13 | Stook 和 Redux、Mobx 是解决同一个问题的不同解决方案,他们之间可以互相替代,也可以混合使用,Stook 会更灵活,因为它能即插即用。 14 | 15 | ## 支持 TypeScript 吗? 16 | 17 | 完美支持,详情请看[TypeScript](docs/stook/typescript)。 18 | 19 | ## 性能怎样?会有 "Too many re-renders"问题吗? 20 | 21 | 性能跟原始的 useState 无差别,遵循 useState 的用法,就不会有 "Too many re-renders" 问题。 22 | -------------------------------------------------------------------------------- /website/docs/stook/get-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: get-state 3 | title: getState 4 | sidebar_label: getState 5 | --- 6 | 7 | 在某些场景,你可能需要更灵活的读取 state,这时你可以使用 `getState`,比如下面两种场景: 8 | 9 | - 在组件外读取 state 10 | - 在组件内读取 state,却不订阅其更新 11 | 12 | ## 在组件外读取 state 13 | 14 | 为了业务逻辑能和组件渲染分离,我们把 `handleSubmit` 放在组件外: 15 | 16 | ```jsx 17 | import React from 'react' 18 | import { useStore, getState } from 'stook' 19 | 20 | function submitUser() { 21 | const name = getState('[UserForm]') 22 | alert(name) 23 | } 24 | 25 | export const UserForm = () => { 26 | const [name, setName] = useStore('[UserForm]', 'initial name') 27 | return ( 28 |
    29 | setName(e.target.value)} /> 30 | 31 |
    32 | ) 33 | } 34 | ``` 35 | 36 | [![Edit cool-framework-4mziz](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/cool-framework-4mziz?fontsize=14&hidenavigation=1&theme=dark) 37 | 38 | 对于大型项目,我们可能会为项目的架构分层,我们可能会有 service 层,这时 `getState` 就非常有用: 39 | 40 | `user.service.ts` 41 | 42 | ```jsx 43 | export function submitUser() { 44 | const name = getState('[UserForm]') 45 | alert(name) 46 | } 47 | ``` 48 | 49 | `UserForm.ts` 50 | 51 | ```jsx 52 | import React from 'react' 53 | import { useStore } from 'stook' 54 | import { submitUser } from './user.service.ts' 55 | 56 | export const UserForm = () => { 57 | const [name, setName] = useStore('[UserForm]', 'initial name') 58 | return ( 59 |
    60 | setName(e.target.value)} /> 61 | 62 |
    63 | ) 64 | } 65 | ``` 66 | 67 | ## 在组件内读取 state 68 | 69 | 有时,由于某些特殊原因,我们可能会在组件内单纯地读取 state: 70 | 71 | ```jsx 72 | export const User = () => { 73 | const name = getState('[UserName]') 74 | return ( 75 |
    76 | {name} 77 |
    78 | ) 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /website/docs/stook/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: intro 3 | title: Introduction 4 | sidebar_label: Introduction 5 | --- 6 | 7 | ## What is Stook? 8 | 9 | Stook 是一系列基于 React hooks 的工具,它的核心一个基于 hooks 实现的状态管理工具。 10 | 11 | ## motivation 12 | 13 | 我喜欢 React,一年来,我一直在探索如何更简单的管理 React 应用状态,对于我而言,一个状态管理库的最重要的两点是:1.简单;2.完美支持 TypeScript 。很抱歉,React 生态中的王者 Redux 没有满足这两点中的任何一点。自从 React Hooks 出现,我发现基于 Hooks 的状态管理可以非常简单,并且完美支持 TypeScript,所以会有了 [stook](https://github.com/forsigner/stook)。它的名字由 store 和 hook 两个单词组合而成。 14 | 15 | ## Why use Stook? 16 | 17 | - **Minimalism** 核心 Api 几乎和 `useState`用法一样,极低的学习成本. 18 | 19 | - **Hooks** 基于 hooks 实现,符合 React 的发展趋势. 20 | 21 | - **TypeScript** TypeScipt 支持非常完美. 22 | 23 | - **Extensible**扩展性强,使用起来非常灵活. 24 | 25 | ## More than state Management 26 | 27 | 但它不仅仅是一个状态管理工具,它衍生出了一个基于 stook 的生态体系,让你用 hooks 的思维开发 React 应用。 28 | 29 | 比如下面这些工具: 30 | 31 | - [stook-rest](/docs/rest/intro) 32 | - [stook-graphql](/docs/graphql/intro) 33 | - [entity-form](https://github.com/forsigner/entity-form) 34 | 35 | more... 36 | -------------------------------------------------------------------------------- /website/docs/stook/mutate.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: mutate 3 | title: mutate 4 | sidebar_label: mutate 5 | --- 6 | 7 | `mutate` 用来改变一个 store 的 state,它的用法类似 `[state setState] = useStore(key)` 中的 setState,但需要传入某个 store 的 key,强大地方是它可以在任何地方使用。 8 | 9 | ```js 10 | // 直接 replace 11 | mutate('User', { id: 1, name: 'foo' }) 12 | 13 | //or use function 14 | mutate('User', state => ({ 15 | ...state, 16 | name: 'foo', 17 | })) 18 | 19 | //or use immer 20 | mutate('User', state => { 21 | state.name = 'foo' 22 | }) 23 | ``` 24 | 25 | mutate 你可以初始化一个**不存在的** store,这是一个特殊的使用场景。 26 | 27 | 举个例子,你可以用 mutate 初始化一个用户的 store: 28 | 29 | ```jsx 30 | const user = localStorage.getItem('User') // { id: 1, name: 'foo' } 31 | mutate('User', user) 32 | ``` 33 | 34 | 然后在组件使用 (不需要再初始化): 35 | 36 | ```jsx 37 | const Profile = () => { 38 | const [user, updateUser] = useStore('User') 39 | return ( 40 |
    41 | {user.name} 42 | 43 |
    44 | ) 45 | } 46 | ``` 47 | 48 | [![Edit broken-snow-7k6r8](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/broken-snow-7k6r8?fontsize=14&hidenavigation=1&theme=dark) 49 | -------------------------------------------------------------------------------- /website/docs/stook/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: quick-start 3 | title: Quick start 4 | sidebar_label: Quick start 5 | --- 6 | 7 | ## Installation 8 | 9 | ### Install with npm 10 | 11 | ```bash 12 | npm install stook 13 | ``` 14 | 15 | ### Install with yarn 16 | 17 | ```bash 18 | yarn add stook 19 | ``` 20 | 21 | ## Usage 22 | 23 | 下面是一个经典的 Counter 组件,展示了 `stook` 的最基本用法: 24 | 25 | ```tsx 26 | import React from 'react' 27 | import { useStore } from 'stook' 28 | 29 | function Counter() { 30 | const [count, setCount] = useStore('Counter', 0) 31 | return ( 32 |
    33 |

    You clicked {count} times

    34 | 35 |
    36 | ) 37 | } 38 | ``` 39 | 40 | [![Edit ancient-night-gyres](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ancient-night-gyres?fontsize=14&hidenavigation=1&theme=dark) 41 | 42 | `stook` 最核心的 Api 就是 `useStore`,也许你发现了,它和 `useState` 非常像,实际上,`useStore` 除了多了一个参数以外,其他用法一模一样。 43 | -------------------------------------------------------------------------------- /website/docs/stook/share-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: share-state 3 | title: Share state 4 | sidebar_label: Share state 5 | --- 6 | 7 | 对于状态管理,最核心的功能就是状态的跨组件通信。useState 用于管理单一组件内的状态,useStore 则可以跨组件管理整个应用的状态。 8 | 9 | 下面展示了如何多个组件如何共享状态: 10 | 11 | ```jsx 12 | import React from 'react' 13 | import { useStore } from 'stook' 14 | 15 | function Counter() { 16 | const [count, setCount] = useStore('Counter', 0) 17 | return ( 18 |
    19 |

    You clicked {count} times

    20 | 21 |
    22 | ) 23 | } 24 | 25 | function Display() { 26 | const [count] = useStore('Counter') 27 | return

    {count}

    28 | } 29 | 30 | function App() { 31 | return ( 32 |
    33 | 34 | 35 |
    36 | ) 37 | } 38 | ``` 39 | 40 | [![Edit vibrant-swirles-jw7kw](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vibrant-swirles-jw7kw?fontsize=14&hidenavigation=1&theme=dark) 41 | 42 | 在这个例子中,我们可以看到,要共享状态,只需使用 useStore 订阅同一个 key 即可,非常简单。可以说,只要你学会了 useState,也就学会了 useStore,只要你学会了 useStore,你就学会了 React 的状态管理。 43 | -------------------------------------------------------------------------------- /website/docs/stook/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: test 3 | title: Test 4 | sidebar_label: Test 5 | --- 6 | 7 | 8 | 测试 stook 是一件非常简单的事,因为测试 stook 也就是在测试 react hooks。 9 | 10 | 推荐使用 [react-hooks-testing-library](https://react-hooks-testing-library.com/)工具来测试 stook。 11 | 12 | ## 安装依赖 13 | 14 | ```bash 15 | npm install -D @testing-library/react-hooks react-test-renderer 16 | ``` 17 | 18 | ## 例子 19 | 20 | **`useCounter.ts`** 21 | 22 | ```js 23 | function useCounter() { 24 | const [count, setCount] = useStore('Counter', 0) 25 | const decrease = () => setCount(count - 1) 26 | const increase = () => setCount(count + 1) 27 | return { count, increase, decrease } 28 | } 29 | ``` 30 | 31 | **`useCounter.test.ts`** 32 | 33 | ```js 34 | import { renderHook, act } from '@testing-library/react-hooks' 35 | import useCounter from './useCounter' 36 | 37 | test('should increment counter', () => { 38 | const { result } = renderHook(() => useCounter()) 39 | 40 | act(() => { 41 | result.current.increase() 42 | }) 43 | 44 | expect(result.current.count).toBe(1) 45 | }) 46 | ``` 47 | 48 | 更多的测试技巧请看:[react-hooks-testing-library](https://react-hooks-testing-library.com/)。 49 | -------------------------------------------------------------------------------- /website/docs/stook/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: typescript 3 | title: Typescript 4 | sidebar_label: Typescript 5 | --- 6 | 7 | 我是 TypeScript 的忠实拥泵,创建 Stook 的初衷之一这就是完美地支持 TypeScript。得益于 React Hooks 对 TypeScript 完备的支持,Stook 也完美地支持 TypeScript。 8 | 9 | ## Store key 10 | 11 | ```Stook``` 的核心 Api 只有3个:useStore、getState、mutate,它们的共同点之一就是第一个参数是 store 的唯一key,默认 key 可以是任何字符串,如果我们想在编辑器中获得智能提示,我们可以这样去实现: 12 | 13 | 在项目根目录新建文件 ```index.d.ts```,添加类似下面内容: 14 | ```ts 15 | import stook from 'stook' 16 | 17 | declare module 'stook' { 18 | interface Key { 19 | User: string 20 | Counter: string 21 | } 22 | } 23 | ``` 24 | 这样你使用 useStore、getState、mutate 就可以获得智能提示。原理其实就是用了 TypeScript 的 [declaration-merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html)。 25 | 26 | 27 | ## useStore 28 | 29 | 和 `useState` 一样,`useStore` 能根据 initialState 推导 state 的类型: 30 | 31 | ```jsx 32 | const [user, setUser] = useStore('User', { id: 1, name: 'foo' }) 33 | ``` 34 | 35 | 上面代码会自动推导出 user 的类型为: 36 | 37 | ```ts 38 | interface User { 39 | id: number 40 | name: string 41 | } 42 | ``` 43 | 44 | 当然,你也可以通过泛型显示地声明类型: 45 | 46 | ```ts 47 | interface User { 48 | id: number 49 | name: string 50 | } 51 | 52 | const [user, setUser] = useStore('User', { id: 1, name: 'foo' }) 53 | ``` 54 | 55 | ## mutate 56 | 57 | 使用泛型声明 mutate 的类型: 58 | 59 | ```ts 60 | interface User { 61 | id: number 62 | name: string 63 | } 64 | 65 | mutate('User', user => { 66 | user.name = 'bar' 67 | }) 68 | ``` 69 | 70 | ## getState 71 | 72 | 使用泛型声明 getState 的类型: 73 | 74 | ```ts 75 | interface User { 76 | id: number 77 | name: string 78 | } 79 | 80 | const user = getState('User') 81 | ``` 82 | -------------------------------------------------------------------------------- /website/docs/stook/use-store.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: use-store 3 | title: useStore 4 | sidebar_label: useStore 5 | --- 6 | 7 | `const [state, setState] = useStore(key, initialState)` 8 | 9 | `useStore` 是 stook 的核心 Api,上面的例子展示它的基本用法。实际上,和 `useState` 相比, `useStore` 除了多了一个参数以外,其他用法一模一样,不同的是他们实现的效果:`useState` 的状态是局部的,`useStore` 的状态全局的。 10 | 11 | 还一个不同的是,`useStore` 内置了 immer,所以你可以有以下的用法: 12 | 13 | ```jsx 14 | const [user, setUser] = useStore('User', { id: 1, name: 'foo' }) 15 | 16 | setUser(state => { 17 | state.name = 'bar' 18 | }) 19 | ``` 20 | 21 | 还一个指得注意的是,如果你对同一个 key 进行了 initialState 的初始化,stook 只会使用第一个 initialState。 22 | 23 | 在某个组件初始化了 `User` store: 24 | 25 | ```jsx 26 | // component A 27 | const [user, setUser] = useStore('User', { id: 1, name: 'foo' }) 28 | ``` 29 | 30 | 后续,如果再初始化 `User` store 是无效的,因为这个 `User` store 已经初始化过了,所以这时你不用再传 initialState 参数。 31 | 32 | ```jsx 33 | // component B 34 | const [user, setUser] = useStore('User', { id: 1, name: 'bar' }) // bad 35 | 36 | const [user, setUser] = useStore('User') // good,直接读取 store 37 | ``` 38 | 39 | 如果你提前使用 mutate 初始化过一个 store,后续 `useStore` 一样不用再传 initialState 参数。 40 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@docusaurus/types').DocusaurusConfig} */ 2 | module.exports = { 3 | title: 'Stook', 4 | tagline: 'A minimalist design state management library for React', 5 | url: 'https://your-docusaurus-test-site.com', 6 | baseUrl: '/', 7 | onBrokenLinks: 'throw', 8 | onBrokenMarkdownLinks: 'warn', 9 | favicon: 'img/favicon.ico', 10 | organizationName: 'forsigner', // Usually your GitHub org/user name. 11 | projectName: 'stook', 12 | themeConfig: { 13 | navbar: { 14 | title: 'Stook', 15 | items: [ 16 | { 17 | type: 'doc', 18 | docId: 'stook/intro', 19 | position: 'left', 20 | label: 'Docs', 21 | }, 22 | 23 | { to: '/ecosystem', label: 'Ecosystem', position: 'right' }, 24 | 25 | { 26 | href: 'https://www.github.com/forsigner/stook', 27 | label: 'GitHub', 28 | position: 'right', 29 | }, 30 | ], 31 | }, 32 | footer: { 33 | style: 'dark', 34 | links: [ 35 | { 36 | title: '文档', 37 | items: [ 38 | { 39 | label: 'Quick Start', 40 | to: '/docs/stook/quick-start', 41 | }, 42 | { 43 | label: 'Stook', 44 | to: '/docs/stook/intro', 45 | }, 46 | { 47 | label: 'GraphQL', 48 | to: '/docs/graphql/intro', 49 | }, 50 | ], 51 | }, 52 | { 53 | title: '社区', 54 | items: [ 55 | { 56 | label: 'Stack Overflow', 57 | href: 'https://stackoverflow.com/questions/tagged/stook', 58 | }, 59 | { 60 | label: 'Feedback', 61 | to: 'https://github.com/forsigner/stook/issues', 62 | }, 63 | ], 64 | }, 65 | { 66 | title: '社交', 67 | items: [ 68 | { 69 | label: 'GitHub', 70 | href: 'https://github.com/forsigner/stook', 71 | }, 72 | ], 73 | }, 74 | ], 75 | copyright: `Copyright © ${new Date().getFullYear()} forsigner.`, 76 | }, 77 | }, 78 | presets: [ 79 | [ 80 | '@docusaurus/preset-classic', 81 | { 82 | docs: { 83 | sidebarPath: require.resolve('./sidebars.js'), 84 | // Please change this to your repo. 85 | editUrl: 'https://github.com/facebook/docusaurus/edit/master/website/', 86 | }, 87 | blog: { 88 | showReadingTime: true, 89 | // Please change this to your repo. 90 | editUrl: 'https://github.com/facebook/docusaurus/edit/master/website/blog/', 91 | }, 92 | theme: { 93 | customCss: require.resolve('./src/css/custom.css'), 94 | }, 95 | }, 96 | ], 97 | ], 98 | } 99 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "1.15.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "echo", 9 | "build:prod": "docusaurus build", 10 | "swizzle": "docusaurus swizzle", 11 | "deploy": "docusaurus deploy", 12 | "clear": "docusaurus clear", 13 | "serve": "docusaurus serve", 14 | "write-translations": "docusaurus write-translations", 15 | "write-heading-ids": "docusaurus write-heading-ids" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.0.0-alpha.73", 19 | "@docusaurus/preset-classic": "2.0.0-alpha.73", 20 | "@fower/react": "^1.4.0", 21 | "@mdx-js/react": "^1.6.21", 22 | "clsx": "^1.1.1", 23 | "react": "^17.0.1", 24 | "react-dom": "^17.0.1", 25 | "react-live": "^2.2.3", 26 | "styled-components": "^5.2.3" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.5%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | module.exports = { 13 | someSidebar: { 14 | stook: [ 15 | 'stook/intro', 16 | 'stook/quick-start', 17 | 'stook/share-state', 18 | 'stook/custom-hooks', 19 | 'stook/use-store', 20 | 'stook/mutate', 21 | 'stook/get-state', 22 | 'stook/typescript', 23 | 'stook/test', 24 | 'stook/faq', 25 | ], 26 | 27 | 'stook-devtools': ['devtools/intro'], 28 | 29 | 'stook-rest': [ 30 | 'rest/intro', 31 | 'rest/quick-start', 32 | 'rest/config', 33 | 'rest/useFetch', 34 | 'rest/useUpdate', 35 | 'rest/dependent', 36 | 'rest/share-state', 37 | 'rest/custom-hooks', 38 | 'rest/fetch', 39 | 'rest/refetch', 40 | 'rest/middleware', 41 | ], 42 | 'stook-graphql': [ 43 | 'graphql/intro', 44 | 'graphql/quick-start', 45 | 'graphql/config', 46 | 'graphql/useQuery', 47 | 'graphql/useMutation', 48 | 'graphql/query', 49 | 'graphql/middleware', 50 | ], 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /website/src/components/UserForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () => { 4 | return
    user form
    5 | } 6 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #25c2a0; 11 | --ifm-color-primary-dark: rgb(33, 175, 144); 12 | --ifm-color-primary-darker: rgb(31, 165, 136); 13 | --ifm-color-primary-darkest: rgb(26, 136, 112); 14 | --ifm-color-primary-light: rgb(70, 203, 174); 15 | --ifm-color-primary-lighter: rgb(102, 212, 189); 16 | --ifm-color-primary-lightest: rgb(146, 224, 208); 17 | --ifm-code-font-size: 95%; 18 | } 19 | 20 | .docusaurus-highlight-code-line { 21 | background-color: rgb(72, 77, 91); 22 | display: block; 23 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 24 | padding: 0 var(--ifm-pre-padding); 25 | } 26 | -------------------------------------------------------------------------------- /website/src/pages/ecosystem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import React from 'react' 9 | import classnames from 'classnames' 10 | import Layout from '@theme/Layout' 11 | import Link from '@docusaurus/Link' 12 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext' 13 | import styled from 'styled-components' 14 | 15 | const Main = styled.div` 16 | margin: 30px auto 0; 17 | width: 760px; 18 | ` 19 | 20 | const List = styled.ul` 21 | list-style: none; 22 | ` 23 | 24 | const Item = styled.li` 25 | position: relative; 26 | /* box-shadow: 1px 5px 10px #f0f0f0; */ 27 | border: 1px solid #f0f0f0; 28 | color: #586069; 29 | border-radius: 10px; 30 | padding: 10px; 31 | margin: 10px 0; 32 | ` 33 | 34 | const Btn = styled.a` 35 | position: absolute; 36 | top: 10px; 37 | right: 10px; 38 | display: inline-block; 39 | padding: 0 15px; 40 | box-sizing: border-box; 41 | text-align: center; 42 | height: 30px; 43 | line-height: 30px; 44 | font-size: 12px; 45 | border-radius: 15px; 46 | border: 1px solid #eaeaea; 47 | &:hover { 48 | text-decoration: none; 49 | } 50 | ` 51 | 52 | const Name = styled.div` 53 | font-size: 24px; 54 | margin-top: 5px; 55 | ` 56 | 57 | const Author = styled.div` 58 | color: #aaa; 59 | font-size: 12px; 60 | margin-top: 5px; 61 | ` 62 | 63 | const list = [ 64 | { 65 | name: 'stook-rest', 66 | intro: 'React Hooks to Restful Api', 67 | github: 'https://github.com/forsigner/stook/blob/master/packages/stook-rest/README.md', 68 | author: 'forsigner', 69 | tag: ['graphql', 'stook', 'useQuery'], 70 | }, 71 | { 72 | name: 'stook-graphql', 73 | intro: 'React Hooks to query GraphQL', 74 | github: 'https://github.com/forsigner/stook/blob/master/packages/stook-graphql/README.md', 75 | author: 'stook', 76 | tag: ['graphql', 'stook', 'useQuery'], 77 | }, 78 | { 79 | name: 'entity-form', 80 | intro: 'React form base on Hooks', 81 | github: 'https://github.com/forsigner/entity-form', 82 | author: 'forsigner', 83 | tag: ['form', 'stook'], 84 | }, 85 | ] 86 | 87 | function Home() { 88 | const context = useDocusaurusContext() 89 | const { siteConfig = {} } = context 90 | return ( 91 | 95 |
    96 | 97 | {list.map(item => ( 98 | 99 | 100 | {item.name} 101 | 102 |
    {item.intro}
    103 | built by {item.author} 104 | 105 | Github 106 | 107 |
    108 | ))} 109 |
    110 |
    111 |
    112 | ) 113 | } 114 | 115 | export default Home 116 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import clsx from 'clsx' 3 | import Layout from '@theme/Layout' 4 | import DLink from '@docusaurus/Link' 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext' 6 | import useBaseUrl from '@docusaurus/useBaseUrl' 7 | import styles from './styles.module.css' 8 | import Translate from '@docusaurus/Translate' 9 | import { Box } from '@fower/react' 10 | import theme from 'prism-react-renderer/themes/duotoneDark' 11 | 12 | import { LiveProvider, LiveEditor, LiveError, LivePreview } from 'react-live' 13 | import { styled } from '@fower/styled' 14 | 15 | const Link = styled(DLink) 16 | 17 | const code1 = ` 18 | 19 | 20 | 21 | 22 | ` 23 | 24 | const features = [ 25 | { 26 | title: 'Minimalism', 27 | description: '核心 Api 几乎和 `useState`用法一样,极低的学习成本.', 28 | }, 29 | { 30 | title: 'Hooks', 31 | description: '基于 hooks 实现,符合 React 的发展趋势.', 32 | }, 33 | 34 | { 35 | title: 'TypeScript', 36 | description: 'TypeScipt 支持非常完美.', 37 | }, 38 | ] 39 | 40 | function Feature({ title, description, idx }) { 41 | return ( 42 |
    49 |

    {title}

    50 |

    {description}

    51 |
    52 | ) 53 | } 54 | 55 | function Home() { 56 | const context = useDocusaurusContext() 57 | const { siteConfig = {}, tagline } = context 58 | return ( 59 | 60 |
    61 | 62 | 63 | 64 | A minimalist design state management library for React. 65 | 66 | 67 | 68 | 80 | Getting Start 81 | 82 |