├── .babelrc
├── .circleci
└── config.yml
├── .eslintignore
├── .flowconfig
├── .github
└── FUNDING.yml
├── .gitignore
├── .npmignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── examples
├── README.md
├── basic
│ ├── README.md
│ ├── basic.gif
│ ├── index.html
│ └── index.js
├── depends
│ ├── README.md
│ ├── depends.gif
│ ├── index.html
│ └── index.js
└── pagination
│ ├── README.md
│ ├── index.html
│ ├── index.js
│ └── pagination.gif
├── index.d.ts
├── index.js
├── index.js.flow
├── jest
└── setupTests.js
├── package.json
├── test
├── __snapshots__
│ └── usePaginatedRequest.test.js.snap
├── flattenInput.test.js
├── useFetch.test.js
└── usePaginatedRequest.test.js
├── tsconfig.json
├── usePaginatedRequest.d.ts
├── usePaginatedRequest.js
├── usePaginatedRequest.js.flow
├── usePromise.d.ts
├── usePromise.js
├── usePromise.js.flow
├── utils
└── flattenInput.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"],
3 | "plugins": [
4 | "babel-plugin-transform-flow-strip-types",
5 | ["@babel/plugin-transform-runtime",
6 | {
7 | "regenerator": true
8 | }
9 | ]
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 |
7 | defaults: &defaults
8 | working_directory: ~/repo
9 | docker:
10 | - image: circleci/node:10.15
11 |
12 | jobs:
13 | install:
14 | <<: *defaults
15 |
16 | steps:
17 | - checkout
18 |
19 | # Download and cache dependencies
20 | - restore_cache:
21 | keys:
22 | - v1-dependencies-{{ checksum "package.json" }}
23 | # fallback to using the latest cache if no exact match is found
24 | - v1-dependencies-
25 |
26 | - run: yarn install
27 |
28 | - save_cache:
29 | paths:
30 | - node_modules
31 | key: v1-dependencies-{{ checksum "package.json" }}
32 |
33 | - persist_to_workspace:
34 | root: ~/repo
35 | paths: .
36 |
37 | flow:
38 | <<: *defaults
39 |
40 | steps:
41 | - attach_workspace:
42 | at: ~/repo
43 | - run:
44 | name: Run Flow
45 | command: npm run flow:check
46 |
47 | typescript:
48 | <<: *defaults
49 |
50 | steps:
51 | - attach_workspace:
52 | at: ~/repo
53 | - run:
54 | name: Run TypeScript
55 | command: npm run typescript
56 |
57 | test:
58 | <<: *defaults
59 |
60 | steps:
61 | - attach_workspace:
62 | at: ~/repo
63 | - run:
64 | name: Run tests
65 | command: npm test
66 |
67 | lint:
68 | <<: *defaults
69 |
70 | steps:
71 | - attach_workspace:
72 | at: ~/repo
73 | - run:
74 | name: Run linting check
75 | command: npm run lint
76 |
77 | size:
78 | <<: *defaults
79 |
80 | steps:
81 | - attach_workspace:
82 | at: ~/repo
83 | - run:
84 | name: Run package size check
85 | command: npm run size
86 |
87 | deploy:
88 | <<: *defaults
89 | steps:
90 | - attach_workspace:
91 | at: ~/repo
92 | - run:
93 | name: Authenticate with registry
94 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
95 | - run:
96 | name: Publish package
97 | command: npm publish
98 |
99 | workflows:
100 | version: 2
101 | test-deploy:
102 | jobs:
103 | - install:
104 | filters:
105 | tags:
106 | only: /^v.*/
107 | - flow:
108 | requires:
109 | - install
110 | filters:
111 | tags:
112 | only: /^v.*/
113 | - typescript:
114 | requires:
115 | - install
116 | filters:
117 | tags:
118 | only: /^v.*/
119 | - test:
120 | requires:
121 | - install
122 | filters:
123 | tags:
124 | only: /^v.*/
125 | - lint:
126 | requires:
127 | - install
128 | filters:
129 | tags:
130 | only: /^v.*/
131 | - size:
132 | requires:
133 | - install
134 | filters:
135 | tags:
136 | only: /^v.*/
137 | - deploy:
138 | requires:
139 | - flow
140 | - typescript
141 | - test
142 | - lint
143 | - size
144 | filters:
145 | tags:
146 | only: /^v.*/
147 | branches:
148 | ignore: /.*/
149 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .cache/
2 | dist/
3 | node_modules/
4 | flow-typed/
5 | test/
6 | types/
7 | examples/
8 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 | ./flow-typed
7 |
8 | [lints]
9 |
10 | [options]
11 | module.file_ext=.js
12 | module.file_ext=.jsx
13 |
14 | [strict]
15 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: lm-a
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 |
4 | .cache/
5 | dist/
6 | node_modules/
7 | .idea/
8 | .vscode/
9 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.log
2 | npm-debug.log*
3 |
4 | # Coverage directory used by tools like istanbul
5 | coverage
6 | .nyc_output
7 |
8 | # Dependency directories
9 | node_modules
10 |
11 | # npm package lock
12 | package-lock.json
13 | yarn.lock
14 |
15 | # project files
16 | src
17 | test
18 | examples
19 | .travis.yml
20 | .babelrc
21 | .gitignore
22 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at ilialesik@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Ilya Lesik
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-fetch-hook
2 |
3 | [](https://circleci.com/gh/ilyalesik/react-fetch-hook)
4 | [](https://www.npmjs.com/package/react-fetch-hook)
5 | [](https://www.npmjs.com/package/react-fetch-hook)
6 |
7 | React hook for conveniently use Fetch API.
8 |
9 | * **Tiny** (556 B). Calculated by [size-limit](https://github.com/ai/size-limit)
10 | * Both **Flow** and **TypeScript** types included
11 |
12 | ```javascript
13 | import React from "react";
14 | import useFetch from "react-fetch-hook";
15 |
16 | const Component = () => {
17 | const { isLoading, data } = useFetch("https://swapi.co/api/people/1");
18 |
19 | return isLoading ? (
20 |
Loading...
21 | ) : (
22 |
23 | );
24 | };
25 |
26 | ```
27 |
28 | *useFetch* accepts the same arguments as *fetch* function.
29 |
30 | ## Installation
31 |
32 | Install it with yarn:
33 |
34 | ```
35 | yarn add react-fetch-hook
36 | ```
37 |
38 | Or with npm:
39 |
40 | ```
41 | npm i react-fetch-hook --save
42 | ```
43 |
44 | ## Usage
45 |
46 | ### Custom formatter
47 |
48 | Default is `response => response.json()` formatter. You can pass custom formatter:
49 |
50 | ```javascript
51 | const { isLoading, data } = useFetch("https://swapi.co/api/people/1", {
52 | formatter: (response) => response.text()
53 | });
54 |
55 | ```
56 |
57 | ### Error handling
58 |
59 | The `useFetch` hook returns an `error` field at any fetch exception.
60 | The `error` field extends [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
61 | and has `status` and `statusText` fields equal to [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
62 |
63 | ```javascript
64 | ...
65 |
66 | const Component = () => {
67 | const { isLoading, data, error } = useFetch("https://swapi.co/api/people/1");
68 |
69 | if (error) {
70 | return
71 |
Code: ${error.status}
72 |
Message: ${error.statusText}
73 |
74 | }
75 |
76 | ...
77 | };
78 |
79 | ```
80 |
81 | ### Multiple requests
82 | Multiple `useFetch` in the same file/component supported:
83 |
84 | ```javascript
85 | const result1 = useFetch("https://swapi.co/api/people/1");
86 | const result2 = useFetch("https://swapi.co/api/people/2");
87 |
88 | if (result1.isLoading && result2.isLoading) {
89 | return Loading...
;
90 | }
91 |
92 | return
93 |
94 |
95 |
96 | ```
97 |
98 | ### Depends
99 | The request will not be called until all elements of `depends` array be truthy. Example:
100 |
101 | ```javascript
102 | const {authToken} = useContext(authTokenContext);
103 | const [someState, setSomeState] = useState(false);
104 | const { isLoading, data } = useFetch("https://swapi.co/api/people/1", {
105 | depends: [!!authToken, someState] // don't call request, if haven't authToken OR someState: false
106 | });
107 |
108 | ```
109 | See [example](examples/depends).
110 |
111 | ### Re-call requests
112 | If any element of `depends` changed, request will be re-call. For example, you can use [react-use-trigger](https://github.com/ilyalesik/react-use-trigger) for re-call the request:
113 | ```javascript
114 | import createTrigger from "react-use-trigger";
115 | import useTrigger from "react-use-trigger/useTrigger";
116 |
117 | const requestTrigger = createTrigger();
118 |
119 | export const Subscriber = () => {
120 | const requestTriggerValue = useTrigger(requestTrigger);
121 |
122 | const { isLoading, data } = useFetch("https://swapi.co/api/people/1", {
123 | depends: [requestTriggerValue]
124 | });
125 |
126 | return ;
127 | }
128 |
129 | export const Sender = () => {
130 | return
133 | }
134 | ```
135 |
136 | ### usePromise
137 | For custom promised function.
138 |
139 | ```javascript
140 | import React from "react";
141 | import usePromise from "react-fetch-hook/usePromise";
142 | import callPromise from "..."
143 |
144 | const Component = () => {
145 | const { isLoading, data } = usePromise(() => callPromise(...params), [...params]);
146 |
147 | return isLoading ? (
148 | Loading...
149 | ) : (
150 |
151 | );
152 | };
153 | ```
154 |
155 | ## [Examples](examples)
156 |
157 | * [Basic](examples/basic) - Just fetch data with `useFetch`.
158 | * [Depends](examples/depends) - Usage `depends` option for refresh query.
159 | * [Pagination](examples/pagination) - Usage `usePaginationRequest` for infinite scroll implementation.
160 |
161 | ## API
162 |
163 | ### `useFetch`
164 | Create a hook wrapper for `fetch` call.
165 | ```javascript
166 | useFetch(
167 | path: RequestInfo,
168 | options?: {
169 | ...RequestOptions,
170 | formatter?: Response => Promise
171 | depends?: Array
172 | },
173 | specialOptions?: {
174 | formatter?: Response => Promise
175 | depends?: Array
176 | }
177 | ): TUseFetchResult
178 | ```
179 | where `TUseFetchResult` is:
180 | ```javascript
181 | {
182 | data: any,
183 | isLoading: boolean,
184 | error: any
185 | }
186 | ```
187 |
188 | `RequestInfo`, `RequestOptions` is [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) args.
189 |
190 |
191 | ### `usePromise`
192 | ```javascript
193 | usePromise>(
194 | callFunction: ?(...args: I) => Promise,
195 | ...inputs: I
196 | ): TUsePromiseResult
197 | ```
198 | where `TUsePromiseResult` is
199 | ```javascript
200 | type TUsePromiseResult = {
201 | data: ?T,
202 | isLoading: boolean,
203 | error: mixed
204 | }
205 | ```
206 |
207 | ### Experimental: `usePaginatedRequest`
208 | ⚠️ Warning: this method is experimental, API can be changed.
209 |
210 | Create a paginated request.
211 | ```javascript
212 | usePaginatedRequest = (
213 | request: (params: { limit: number, offset: number }) => Promise>,
214 | limit: number,
215 | ...depends: Array
216 | ): {
217 | data: Array,
218 | loadMore?: () => mixed,
219 | hasMore: boolean
220 | };
221 | ```
222 |
223 | ## Who Uses react-fetch-hook
224 |
225 | ### Open Source projects
226 |
227 | * [react-figma](https://github.com/react-figma/react-figma)
228 | * [awesome-web-animation](https://github.com/sergey-pimenov/awesome-web-animation)
229 | * [redux-helpers](https://github.com/lecstor/redux-helpers)
230 | * [flowmap.blue](https://github.com/FlowmapBlue/flowmap.blue)
231 |
232 | ### Companies
233 |
234 | * [Redis Agency](https://redis.agency/)
235 | * [Lessmess Agency](https://lessmess.agency)
236 | * [BigDatr](https://bigdatr.com/)
237 | * [Fabrique numérique des Ministères Sociaux](https://incubateur.social.gouv.fr/)
238 |
239 | [See more](https://github.com/ilyalesik/react-fetch-hook/network/dependents)
240 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | * [Basic](basic) - Just fetch data with `useFetch`.
4 | * [Depends](depends) - Usage `depends` option for refresh query.
5 | * [Pagination](pagination) - Usage `usePaginationRequest` for infinite scroll implementation.
6 |
--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
1 | # Basic
2 |
3 | 
4 |
5 | Just fetch data with `useFetch`.
6 |
7 | Try at CodeSandbox:
8 |
9 | [](https://codesandbox.io/s/react-fetch-hook-basic-g1in5)
10 |
--------------------------------------------------------------------------------
/examples/basic/basic.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ilyalesik/react-fetch-hook/52999750d5120f60a90746cba7982c2550150ba9/examples/basic/basic.gif
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/basic/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import useFetch from "../../index";
4 |
5 |
6 | const App = () => {
7 | const {isLoading, data, error} = useFetch(`https://swapi.co/api/people/1`);
8 |
9 | console.log(isLoading, data, error && error.status);
10 |
11 | return
12 |
isLoading: {isLoading && "true" || "false"}
13 |
Name: {data && data.name}
14 |
15 | };
16 |
17 |
18 | if (root) {
19 | ReactDOM.render(
20 | ,
21 | document.getElementById("root")
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/examples/depends/README.md:
--------------------------------------------------------------------------------
1 | # Depends
2 |
3 | 
4 |
5 | Usage `depends` option for refresh query.
6 |
7 | Try at CodeSandbox:
8 |
9 | [](https://codesandbox.io/s/react-fetch-hook-depends-zpm51)
10 |
--------------------------------------------------------------------------------
/examples/depends/depends.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ilyalesik/react-fetch-hook/52999750d5120f60a90746cba7982c2550150ba9/examples/depends/depends.gif
--------------------------------------------------------------------------------
/examples/depends/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/depends/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import useFetch from "../../index";
4 |
5 |
6 | const App = () => {
7 | const [counter, setCounter] = React.useState(false);
8 | const {isLoading, data} = useFetch(`http://worldtimeapi.org/api/timezone/Europe/London`,{
9 | depends: [counter]
10 | });
11 |
12 | React.useEffect(() => {
13 | const timeoutId = setInterval(() => {
14 | console.log("setTimeoutEnded: true");
15 | setCounter((counter) => counter + 1);
16 | }, 500);
17 | return () => {
18 | clearTimeout(timeoutId);
19 | }
20 | }, []);
21 |
22 | console.log(isLoading, data);
23 |
24 | return
25 |
counter: {counter}
26 |
isLoading: {isLoading && "true" || "false"}
27 |
UTC date time: {data && data.utc_datetime}
28 |
29 | };
30 |
31 |
32 | if (root) {
33 | ReactDOM.render(
34 | ,
35 | document.getElementById("root")
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/examples/pagination/README.md:
--------------------------------------------------------------------------------
1 | # Pagination
2 |
3 | 
4 |
5 | Usage `usePaginationRequest` for infinite scroll implementation.
6 |
7 | Try at CodeSandbox:
8 |
9 | [](https://codesandbox.io/s/react-fetch-hook-pagination-nq561)
10 |
--------------------------------------------------------------------------------
/examples/pagination/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/pagination/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import useFetch from "../../index";
4 | import InfiniteScroll from "react-infinite-scroller";
5 | import usePaginatedRequest from "../../usePaginatedRequest";
6 |
7 | const searchRepositories = ({searchString}) => async ({offset, limit}) => {
8 | const response = await fetch(`https://api.github.com/search/repositories?q=${searchString}&sort=stars&order=desc&page=${parseInt(offset / limit)}&per_page=${limit}`);
9 | if (!response.ok) {
10 | throw Error(response.statusText);
11 | }
12 | const result = await response.json();
13 | return result.items;
14 | };
15 |
16 | const App = () => {
17 | const [searchString, setSearchString] = React.useState("");
18 |
19 | const result = usePaginatedRequest(
20 | searchRepositories({searchString}),
21 | 100,
22 | searchString);
23 |
24 | console.log(result.data);
25 |
26 | return
27 |
setSearchString(e.target.value)} />
28 |
29 | }>
34 | {result.data && result.data.map((item) => (
35 | - {item.name} ({item.stargazers_count})
36 | ))}
37 |
38 |
39 |
40 | };
41 |
42 |
43 | if (root) {
44 | ReactDOM.render(
45 | ,
46 | document.getElementById("root")
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/examples/pagination/pagination.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ilyalesik/react-fetch-hook/52999750d5120f60a90746cba7982c2550150ba9/examples/pagination/pagination.gif
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare namespace useFetch {
3 | export interface UseFetchError extends Error {
4 | status: number,
5 | statusText: string
6 | }
7 |
8 | export interface FetchResult {
9 | data?: T,
10 | isLoading: boolean,
11 | error?: UseFetchError
12 | }
13 |
14 | export interface HookOptions extends RequestInit {
15 | depends?: Array
16 | }
17 |
18 | export interface HookOptionsWithFormatter extends HookOptions {
19 | formatter(response: Response): Promise
20 | }
21 | }
22 |
23 |
24 | declare function useFetch(path: RequestInfo,
25 | options?: useFetch.HookOptions | useFetch.HookOptionsWithFormatter,
26 | specialOptions?: useFetch.HookOptions): useFetch.FetchResult;
27 |
28 | export = useFetch;
29 |
30 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var usePromise = require('./usePromise')
2 |
3 | function UseFetchError (status, statusText, message, fileName, lineNumber) {
4 | var instance = new Error(message, fileName, lineNumber)
5 | instance.name = 'UseFetchError'
6 | instance.status = status
7 | instance.statusText = statusText
8 | Object.setPrototypeOf(instance, Object.getPrototypeOf(this))
9 | if (Error.captureStackTrace) {
10 | Error.captureStackTrace(instance, UseFetchError)
11 | }
12 | return instance
13 | }
14 |
15 | UseFetchError.prototype = Object.create(Error.prototype, {
16 | constructor: {
17 | value: Error,
18 | enumerable: false,
19 | writable: true,
20 | configurable: true
21 | }
22 | })
23 |
24 | Object.setPrototypeOf(UseFetchError, Error)
25 |
26 | function useFetch (
27 | path,
28 | options,
29 | specialOptions
30 | ) {
31 | var blocked = ((specialOptions && specialOptions.depends) ||
32 | (options && options.depends) || [])
33 | .reduce(function (acc, dep) { return acc || !dep }, false)
34 | return usePromise(!blocked && function (p, o, s) {
35 | return fetch(p, o)
36 | .then((s && s.formatter) || (o && o.formatter) || function (response) {
37 | if (!response.ok) {
38 | throw new UseFetchError(
39 | response.status,
40 | response.statusText,
41 | 'Fetch error'
42 | )
43 | }
44 | return response.json()
45 | })
46 | },
47 | path, options || {}, specialOptions || {})
48 | }
49 |
50 | module.exports = useFetch
51 |
--------------------------------------------------------------------------------
/index.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | type TUseFetchResult = {
4 | data: ?T,
5 | isLoading: boolean,
6 | error?: Error & {
7 | status: number,
8 | statusText: string
9 | }
10 | };
11 | declare function useFetch(path: RequestInfo, options?: { ...RequestOptions,
12 | formatter?: (Response) => Promise,
13 | depends?: Array,
14 | }, specialOptions?: {
15 | formatter?: (Response) => Promise,
16 | depends?: Array
17 | }): TUseFetchResult;
18 |
19 | declare export default typeof useFetch;
20 |
--------------------------------------------------------------------------------
/jest/setupTests.js:
--------------------------------------------------------------------------------
1 | require('jest-dom/extend-expect')
2 | global.fetch = require('jest-fetch-mock')
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-fetch-hook",
3 | "version": "1.9.5",
4 | "main": "index.js",
5 | "types": "index.d.ts",
6 | "description": "React fetch hook",
7 | "scripts": {
8 | "flow": "flow",
9 | "flow:check": "flow check",
10 | "typescript": "tsc -p tsconfig.json --noEmit",
11 | "test": "jest --silent",
12 | "size": "size-limit",
13 | "lint": "eslint '**/*.{js,jsx}' --quiet",
14 | "run:example:basic": "parcel ./examples/basic/index.html",
15 | "run:example:depends": "parcel ./examples/depends/index.html",
16 | "run:example:pagination": "parcel ./examples/pagination/index.html --no-cache"
17 | },
18 | "eslintConfig": {
19 | "extends": [
20 | "lessmess/lib",
21 | "lessmess/react-hooks"
22 | ],
23 | "rules": {
24 | "func-style": "off"
25 | }
26 | },
27 | "jest": {
28 | "setupTestFrameworkScriptFile": "./jest/setupTests.js"
29 | },
30 | "size-limit": [
31 | {
32 | "ignore": [
33 | "react"
34 | ],
35 | "limit": "556 B",
36 | "path": "index.js",
37 | "running": false
38 | }
39 | ],
40 | "files": [
41 | "utils",
42 | "index.js",
43 | "index.js.flow",
44 | "index.d.ts",
45 | "usePromise.js",
46 | "usePromise.js.flow",
47 | "usePromise.d.ts",
48 | "usePaginatedRequest.js",
49 | "usePaginatedRequest.js.flow",
50 | "usePaginatedRequest.d.ts"
51 | ],
52 | "repository": {
53 | "type": "git",
54 | "url": "git+ssh://git@github.com/ilyalesik/react-fetch-hook.git"
55 | },
56 | "keywords": [
57 | "react",
58 | "hooks",
59 | "fetch"
60 | ],
61 | "author": "Ilya Lesik",
62 | "license": "MIT",
63 | "bugs": {
64 | "url": "https://github.com/ilyalesik/react-fetch-hook/issues"
65 | },
66 | "homepage": "https://github.com/ilyalesik/react-fetch-hook#readme",
67 | "peerDependencies": {
68 | "react": ">=16.8.0 <19.0.0"
69 | },
70 | "devDependencies": {
71 | "@babel/cli": "^7.2.0",
72 | "@babel/core": "^7.2.2",
73 | "@babel/plugin-transform-runtime": "^7.4.4",
74 | "@babel/preset-env": "^7.2.0",
75 | "@babel/preset-flow": "^7.0.0",
76 | "@babel/preset-react": "^7.0.0",
77 | "babel-core": "^7.0.0-bridge.0",
78 | "babel-plugin-react-flow-props-to-prop-types": "^0.15.0",
79 | "babel-plugin-transform-flow-strip-types": "^6.22.0",
80 | "cross-env": "^5.2.0",
81 | "eslint": "^5.16.0",
82 | "eslint-config-lessmess": "0.0.1",
83 | "eslint-config-standard": "^12.0.0",
84 | "eslint-plugin-es5": "^1.3.1",
85 | "eslint-plugin-import": "^2.17.2",
86 | "eslint-plugin-import-helpers": "^0.1.4",
87 | "eslint-plugin-jest": "^22.5.1",
88 | "eslint-plugin-node": "^9.0.1",
89 | "eslint-plugin-prefer-let": "^1.0.1",
90 | "eslint-plugin-promise": "^4.1.1",
91 | "eslint-plugin-react-hooks": "^1.6.0",
92 | "eslint-plugin-security": "^1.4.0",
93 | "eslint-plugin-standard": "^4.0.0",
94 | "flow-bin": "^0.89.0",
95 | "jest": "^23.6.0",
96 | "jest-dom": "^3.0.0",
97 | "jest-fetch-mock": "^2.1.0",
98 | "parcel-bundler": "^1.12.3",
99 | "react": "^16.8.0",
100 | "react-dom": "^16.8.0",
101 | "react-infinite-scroller": "^1.2.4",
102 | "react-test-renderer": "^16.8.0",
103 | "react-testing-library": "^6.0.0",
104 | "rimraf": "^2.6.2",
105 | "size-limit": "^1.3.2",
106 | "typescript": "^3.4.5"
107 | },
108 | "resolutions": {
109 | "babel-core": "^7.0.0-bridge.0"
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/test/__snapshots__/usePaginatedRequest.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`usePaginatedRequest call loadMore (expect hasMore - false) 1`] = `
4 |
5 |
6 |
7 | 1
8 |
9 |
10 | 2
11 |
12 |
13 | 3
14 |
15 |
18 |
19 |
20 | `;
21 |
22 | exports[`usePaginatedRequest call loadMore 1`] = `
23 |
24 |
25 |
26 | 1
27 |
28 |
29 | 2
30 |
31 |
32 | 3
33 |
34 |
35 | 4
36 |
37 |
38 | hasMore
39 |
40 |
43 |
44 |
45 | `;
46 |
47 | exports[`usePaginatedRequest first load (expect hasMore - false) 1`] = `
48 |
49 |
50 |
51 | 1
52 |
53 |
56 |
57 |
58 | `;
59 |
60 | exports[`usePaginatedRequest first load 1`] = `
61 |
62 |
63 |
64 | 1
65 |
66 |
67 | 2
68 |
69 |
70 | hasMore
71 |
72 |
75 |
76 |
77 | `;
78 |
--------------------------------------------------------------------------------
/test/flattenInput.test.js:
--------------------------------------------------------------------------------
1 | import flattenInput from "../utils/flattenInput";
2 |
3 | describe("flattenInput", () => {
4 | it("simple", () => {
5 | expect(flattenInput(1, 2)).toMatchObject([1, 2]);
6 | });
7 |
8 | it("with array", () => {
9 | expect(flattenInput(1, [2, 3])).toMatchObject([1, 2, 3]);
10 | });
11 |
12 | it("with array recursive", () => {
13 | expect(flattenInput(1, [2, [3, 4]])).toMatchObject([1, 2, 3, 4]);
14 | });
15 |
16 | it("with object", () => {
17 | expect(flattenInput(1, { x: 1, y: 2 })).toMatchObject([1, "x", 1, "y", 2]);
18 | });
19 |
20 | it("with object recursive", () => {
21 | expect(flattenInput(1, { x: 1, y: 2, z: { x1: 3 } })).toMatchObject([1, "x", 1, "y", 2, "z", "x1", 3]);
22 | });
23 |
24 | it("with URL instance", () => {
25 | const url = new URL("https://google.com");
26 | url.search = new URLSearchParams({ a: "aaa", b: "bbb" }).toString();
27 | const result = flattenInput(url, { x: 1 });
28 | expect(result).toMatchObject([
29 | "https://google.com/?a=aaa&b=bbb",
30 | "x",
31 | 1
32 | ]);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/test/useFetch.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, wait } from "react-testing-library";
3 | import useFetch from "../index";
4 |
5 | describe("useFetch", () => {
6 | beforeEach(() => {
7 | fetch.resetMocks();
8 | });
9 |
10 | it("call with only url", async () => {
11 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
12 |
13 | const Component = () => {
14 | const result = useFetch("https://google.com");
15 | return {result.data && result.data.data}
;
16 | };
17 |
18 | const { container, rerender } = render();
19 |
20 | await wait(() => {
21 | rerender();
22 |
23 | expect(fetch.mock.calls.length).toEqual(1);
24 | expect(container).toHaveTextContent("12345");
25 | expect(fetch.mock.calls[0][0]).toEqual("https://google.com");
26 | });
27 | });
28 |
29 | it("isLoading by default", async () => {
30 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
31 |
32 | const Component = () => {
33 | const result = useFetch("https://google.com");
34 | return {result.isLoading && "test"}
;
35 | };
36 |
37 | const { container, rerender } = render();
38 | expect(container).toHaveTextContent("test");
39 | });
40 |
41 | it("call with url and options", async () => {
42 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
43 | const options = {
44 | headers: {
45 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
46 | "Content-Type": "application/json; charset=utf-8"
47 | }
48 | };
49 |
50 | const Component = () => {
51 | const result = useFetch("https://google.com", { ...options });
52 | return {result.isLoading}
;
53 | };
54 |
55 | const { rerender } = render();
56 |
57 | await wait(() => {
58 | rerender();
59 |
60 | expect(fetch.mock.calls.length).toEqual(1);
61 | expect(fetch.mock.calls[0][0]).toEqual("https://google.com");
62 | expect(fetch.mock.calls[0][1]).toMatchObject({ ...options });
63 | });
64 | });
65 |
66 | it("call with url, options with formatter", async () => {
67 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
68 | const options = {
69 | headers: {
70 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
71 | "Content-Type": "application/json; charset=utf-8"
72 | }
73 | };
74 | const formatterMock = jest.fn();
75 | formatterMock.mockReturnValueOnce("xxx");
76 |
77 | const Component = () => {
78 | const result = useFetch("https://google.com", { ...options, formatter: formatterMock });
79 | return {result.data}
;
80 | };
81 |
82 | const { container, rerender } = render();
83 |
84 | await wait(() => {
85 | rerender();
86 |
87 | expect(fetch.mock.calls.length).toEqual(1);
88 | expect(formatterMock.mock.calls.length).toEqual(1);
89 | expect(container).toHaveTextContent("xxx");
90 | expect(fetch.mock.calls[0][0]).toEqual("https://google.com");
91 | expect(fetch.mock.calls[0][1]).toMatchObject({ ...options });
92 | });
93 | });
94 |
95 | it("call with url, options, special options with formatter", async () => {
96 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
97 | const options = {
98 | headers: {
99 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
100 | "Content-Type": "application/json; charset=utf-8"
101 | }
102 | };
103 | const formatterMock = jest.fn();
104 | formatterMock.mockReturnValueOnce("xxx");
105 |
106 | const Component = () => {
107 | const result = useFetch("https://google.com", { ...options }, { formatter: formatterMock});
108 | return {result.data}
;
109 | };
110 |
111 | const { container, rerender } = render();
112 |
113 | await wait(() => {
114 | rerender();
115 |
116 | expect(fetch.mock.calls.length).toEqual(1);
117 | expect(formatterMock.mock.calls.length).toEqual(1);
118 | expect(container).toHaveTextContent("xxx");
119 | expect(fetch.mock.calls[0][0]).toEqual("https://google.com");
120 | expect(fetch.mock.calls[0][1]).toMatchObject({ ...options });
121 | });
122 | });
123 |
124 | it("call with url, options with preventCallFetch: false and depends", async () => {
125 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
126 | const options = {
127 | headers: {
128 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
129 | "Content-Type": "application/json; charset=utf-8"
130 | }
131 | };
132 |
133 | const Component = () => {
134 | const result = useFetch("https://google.com", { ...options, depends: ["xxx"], preventCallFetch: false });
135 | return {result.data && result.data.data}
;
136 | };
137 |
138 | const { container, rerender } = render();
139 |
140 | await wait(() => {
141 | rerender();
142 |
143 | expect(fetch.mock.calls.length).toEqual(1);
144 | });
145 | });
146 |
147 | it("call with url, options with depends", async () => {
148 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
149 | const options = {
150 | headers: {
151 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
152 | "Content-Type": "application/json; charset=utf-8"
153 | }
154 | };
155 |
156 | const Component = () => {
157 | const result = useFetch("https://google.com", { ...options, depends: [true, false] });
158 | return {result.data && result.data.data}
;
159 | };
160 |
161 | const { container, rerender } = render();
162 |
163 | await wait(() => {
164 | rerender();
165 |
166 | expect(fetch.mock.calls.length).toEqual(0);
167 | });
168 | });
169 |
170 | it("call with url, options with depends never set isLoading", async () => {
171 | const isLoadingWatcher = jest.fn();
172 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
173 | const options = {
174 | headers: {
175 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
176 | "Content-Type": "application/json; charset=utf-8"
177 | }
178 | };
179 |
180 | const Component = () => {
181 | const result = useFetch("https://google.com", { ...options, depends: [false] });
182 | const isLoading = result.isLoading;
183 | React.useEffect(() => {
184 | if (isLoading) {
185 | isLoadingWatcher();
186 | }
187 | }, [isLoading])
188 | return {result.data && result.data.data}
;
189 | };
190 |
191 | const { container, rerender } = render();
192 |
193 | await wait(() => {
194 | rerender();
195 |
196 | expect(isLoadingWatcher.mock.calls.length).toEqual(0);
197 | });
198 | });
199 |
200 | it("call with url, options with depends at next arg", async () => {
201 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
202 | const options = {
203 | headers: {
204 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
205 | "Content-Type": "application/json; charset=utf-8"
206 | }
207 | };
208 |
209 | const fetchParams = ["https://google.com", options];
210 |
211 | const Component = () => {
212 | const result = useFetch(...fetchParams, { depends: [true, false] });
213 | return {result.data && result.data.data}
;
214 | };
215 |
216 | const { container, rerender } = render();
217 |
218 | await wait(() => {
219 | rerender();
220 |
221 | expect(fetch.mock.calls.length).toEqual(0);
222 | });
223 | });
224 |
225 | it("call with url, options with depends: [true] at next arg", async () => {
226 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
227 | const options = {
228 | headers: {
229 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
230 | "Content-Type": "application/json; charset=utf-8"
231 | }
232 | };
233 |
234 | const fetchParams = ["https://google.com", options];
235 |
236 | const Component = () => {
237 | const result = useFetch(...fetchParams, { depends: [true] });
238 | return {result.data && result.data.data}
;
239 | };
240 |
241 | const { container, rerender } = render();
242 |
243 | await wait(() => {
244 | rerender();
245 |
246 | expect(fetch.mock.calls.length).toEqual(1);
247 | });
248 | });
249 |
250 | it("call with url, options with depends with empty string", async () => {
251 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
252 | const options = {
253 | headers: {
254 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
255 | "Content-Type": "application/json; charset=utf-8"
256 | }
257 | };
258 |
259 | const Component = () => {
260 | const result = useFetch("https://google.com", { ...options, depends: [""] });
261 | return {result.data && result.data.data}
;
262 | };
263 |
264 | const { container, rerender } = render();
265 |
266 | await wait(() => {
267 | rerender();
268 |
269 | expect(fetch.mock.calls.length).toEqual(0);
270 | });
271 | });
272 |
273 | it("call with url, options with empty depends", async () => {
274 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
275 | const options = {
276 | headers: {
277 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
278 | "Content-Type": "application/json; charset=utf-8"
279 | }
280 | };
281 |
282 | const Component = () => {
283 | const result = useFetch("https://google.com", { ...options, depends: [] });
284 | return {result.data && result.data.data}
;
285 | };
286 |
287 | const { container, rerender } = render();
288 |
289 | await wait(() => {
290 | rerender();
291 |
292 | expect(fetch.mock.calls.length).toEqual(1);
293 | });
294 | });
295 |
296 | it("call with url, options with all true depends", async () => {
297 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
298 | const options = {
299 | headers: {
300 | Accept: "application/json, application/xml, text/plain, text/html, *.*",
301 | "Content-Type": "application/json; charset=utf-8"
302 | }
303 | };
304 |
305 | const Component = () => {
306 | const result = useFetch("https://google.com", { ...options, depends: [true, true] });
307 | return {result.data && result.data.data}
;
308 | };
309 |
310 | const { container, rerender } = render();
311 |
312 | await wait(() => {
313 | rerender();
314 |
315 | expect(fetch.mock.calls.length).toEqual(1);
316 | });
317 | });
318 |
319 | it("error on throw error", async () => {
320 | fetch.mockReject(new Error("fake error message"));
321 |
322 | const Component = () => {
323 | const result = useFetch("https://google.com");
324 | return (result.error && result.error.message) || "text";
325 | };
326 |
327 | const { container, rerender } = render();
328 |
329 | await wait(() => {
330 | rerender();
331 |
332 | expect(fetch.mock.calls.length).toEqual(1);
333 | expect(container).toHaveTextContent("fake error message");
334 | expect(fetch.mock.calls[0][0]).toEqual("https://google.com");
335 | });
336 | });
337 |
338 | it("call with URL", async () => {
339 | fetch.mockResponse(JSON.stringify({ data: "12345" }));
340 |
341 | const url = new URL("https://google.com");
342 |
343 | url.search = new URLSearchParams({a: 1, b: 2}).toString();
344 |
345 | const Component = () => {
346 | const result = useFetch(url, {
347 | depends: ["1", 1]
348 | });
349 | return {result.data && result.data.data}
;
350 | };
351 |
352 | const { container, rerender } = render();
353 |
354 | await wait(() => {
355 | rerender();
356 | });
357 |
358 | await wait(() => {
359 | rerender();
360 |
361 | expect(fetch.mock.calls.length).toEqual(1);
362 | expect(container).toHaveTextContent("12345");
363 | expect(fetch.mock.calls[0][0]).toMatchObject(url);
364 | });
365 | });
366 | });
367 |
--------------------------------------------------------------------------------
/test/usePaginatedRequest.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, wait, fireEvent, cleanup } from "react-testing-library";
3 | import usePaginatedRequest from "../usePaginatedRequest";
4 |
5 | const ChildComponent = props => {
6 | return (
7 |
8 | {props.data.map(item => (
9 |
{item}
10 | ))}
11 | {props.hasMore &&
hasMore
}
12 |
13 |
14 | );
15 | };
16 |
17 | describe("usePaginatedRequest", () => {
18 | afterEach(cleanup);
19 |
20 | it("first load", async () => {
21 | const request = jest.fn(() => Promise.resolve([1, 2]));
22 |
23 | const Component = () => {
24 | const { data, hasMore, loadMore } = usePaginatedRequest(request, 2);
25 |
26 | return ;
27 | };
28 |
29 | const { container, rerender } = render();
30 |
31 | await wait(() => {
32 | expect(container).toMatchSnapshot();
33 | });
34 |
35 | await wait(() => {
36 | rerender();
37 | expect(request.mock.calls.length).toBe(1);
38 | });
39 | });
40 |
41 | it("first load (expect hasMore - false)", async () => {
42 | const request = jest.fn(() => Promise.resolve([1]));
43 |
44 | const Component = () => {
45 | const { data, hasMore, loadMore } = usePaginatedRequest(request, 2);
46 |
47 | return ;
48 | };
49 |
50 | const { container, rerender } = render();
51 |
52 | await wait(() => {
53 | expect(container).toMatchSnapshot();
54 | });
55 |
56 | await wait(() => {
57 | rerender();
58 | expect(request.mock.calls.length).toBe(1);
59 | });
60 | });
61 |
62 | it("call loadMore", async () => {
63 | const request = jest.fn();
64 | request.mockReturnValueOnce(Promise.resolve([1, 2])).mockReturnValueOnce(Promise.resolve([3, 4]));
65 |
66 | const Component = () => {
67 | const { data, hasMore, loadMore } = usePaginatedRequest(request, 2);
68 |
69 | return ;
70 | };
71 |
72 | const { container, rerender, getByTestId } = render();
73 |
74 | await wait(() => {
75 | fireEvent.click(getByTestId("trigger"));
76 | });
77 |
78 | await wait(() => {
79 | rerender();
80 | });
81 |
82 | await wait(() => {
83 | expect(container).toMatchSnapshot();
84 | expect(request.mock.calls.length).toBe(2);
85 | });
86 | });
87 |
88 | it("call loadMore (expect hasMore - false)", async () => {
89 | const request = jest.fn();
90 | request.mockReturnValueOnce(Promise.resolve([1, 2])).mockReturnValueOnce(Promise.resolve([3]));
91 |
92 | const Component = () => {
93 | const { data, hasMore, loadMore } = usePaginatedRequest(request, 2);
94 |
95 | return ;
96 | };
97 |
98 | const { container, rerender, getByTestId } = render();
99 |
100 | await wait(() => {
101 | fireEvent.click(getByTestId("trigger"));
102 | });
103 |
104 | await wait(() => {
105 | rerender();
106 | });
107 |
108 | await wait(() => {
109 | expect(container).toMatchSnapshot();
110 | expect(request.mock.calls.length).toBe(2);
111 | });
112 | });
113 | });
114 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "jsx": "react",
5 | "lib": [
6 | "dom",
7 | "es2018"
8 | ],
9 | "module": "esnext",
10 | "moduleResolution": "node",
11 | "noImplicitAny": false,
12 | "noImplicitReturns": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "strict": true,
16 | "strictFunctionTypes": false,
17 | "stripInternal": true
18 | },
19 | "include": [
20 | "**/*.d.ts"
21 | ],
22 | "exclude": [
23 | "node_modules"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/usePaginatedRequest.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace usePaginatedRequest {
2 | export type requestFunction = (params: {
3 | limit: number,
4 | offset: number,
5 | }) => Promise>
6 | }
7 |
8 | declare function usePaginatedRequest(request: usePaginatedRequest.requestFunction, limit: number, ...depends: Array): {
9 | data: Array,
10 | loadMore?: () => any,
11 | hasMore: boolean,
12 | };
13 |
14 | export = usePaginatedRequest;
15 |
--------------------------------------------------------------------------------
/usePaginatedRequest.js:
--------------------------------------------------------------------------------
1 | var React = require('react')
2 |
3 | function usePaginatedRequest (
4 | request,
5 | limit
6 | ) {
7 | var depends = Array.prototype.slice.call(arguments, [2])
8 | var dataState = React.useState([])
9 | var currentUpdate = React.useRef()
10 | var offsetState = React.useState(0)
11 | var hasMoreState = React.useState(true)
12 |
13 | var loadMoreRef = React.useRef(function () {})
14 |
15 | React.useEffect(function () {
16 | dataState[1]([])
17 | offsetState[1](0)
18 | hasMoreState[1](true)
19 | }, depends)
20 |
21 | React.useEffect(
22 | function () {
23 | loadMoreRef.current = async function () {
24 | if (currentUpdate.current) {
25 | await currentUpdate.current
26 | }
27 | if (hasMoreState[0]) {
28 | offsetState[1](offsetState[0] + limit)
29 | }
30 | }
31 |
32 | var update = async function () {
33 | var result = await request({ limit: limit, offset: offsetState[0] })
34 | hasMoreState[1](result.length === limit)
35 | dataState[1](function (prev) { return [].concat(prev, result) })
36 | }
37 | currentUpdate.current = update()
38 | },
39 | [offsetState[0]].concat(depends)
40 | )
41 |
42 | return {
43 | data: dataState[0],
44 | loadMore: loadMoreRef.current || function () {},
45 | hasMore: hasMoreState[0]
46 | }
47 | }
48 |
49 | module.exports = usePaginatedRequest
50 |
--------------------------------------------------------------------------------
/usePaginatedRequest.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 | declare function usePaginatedRequest(request: (params: {
3 | limit: number,
4 | offset: number,
5 | }) => Promise>, limit: number, ...depends: Array): {
6 | data: Array,
7 | loadMore?: () => mixed,
8 | hasMore: boolean,
9 | };
10 |
11 | declare export default typeof usePaginatedRequest;
12 |
--------------------------------------------------------------------------------
/usePromise.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare namespace usePromise {
3 | export type TUsePromiseResult = {
4 | data?: T,
5 | isLoading: boolean,
6 | error: any,
7 | };
8 | }
9 |
10 | declare function usePromise>(callFunction?: (...args: I) => Promise, ...inputs: I): usePromise.TUsePromiseResult;
11 |
12 | export = usePromise;
13 |
--------------------------------------------------------------------------------
/usePromise.js:
--------------------------------------------------------------------------------
1 | var React = require('react')
2 |
3 | var flattenInput = require('./utils/flattenInput')
4 |
5 | function usePromise (
6 | callFunction
7 | ) {
8 | var inputs = Array.prototype.slice.call(arguments, [1])
9 | var state = React.useState({
10 | isLoading: !!callFunction
11 | })
12 |
13 | React.useEffect(function () {
14 | if (!callFunction) {
15 | return
16 | }
17 | !state[0].isLoading && state[1]({ data: state[0].data, isLoading: true })
18 | callFunction.apply(null, inputs)
19 | .then(function (data) {
20 | state[1]({
21 | data: data,
22 | isLoading: false
23 | })
24 | })
25 | .catch(function (error) {
26 | state[1]({
27 | error: error,
28 | isLoading: false
29 | })
30 | })
31 | }, flattenInput(inputs))
32 |
33 | return state[0]
34 | }
35 |
36 | module.exports = usePromise
37 |
--------------------------------------------------------------------------------
/usePromise.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 | type TUsePromiseResult = {
3 | data: ?T,
4 | isLoading: boolean,
5 | error: mixed,
6 | };
7 | declare function usePromise>(callFunction: ?(...args: I) => Promise, ...inputs: I): TUsePromiseResult;
8 | declare export default typeof usePromise;
9 |
--------------------------------------------------------------------------------
/utils/flattenInput.js:
--------------------------------------------------------------------------------
1 | function flattenInput () {
2 | var res = []
3 | for (var i = 0; i < arguments.length; i++) {
4 | var input = arguments[i]
5 | if (input instanceof Array) {
6 | for (var j = 0; j < input.length; j++) {
7 | res = res.concat(flattenInput(input[j]))
8 | }
9 | } else if (typeof URL !== 'undefined' && input instanceof URL) {
10 | res = res.concat(input.toJSON())
11 | } else if (input instanceof Object) {
12 | var keys = Object.keys(input)
13 | for (var k = 0; k < keys.length; k++) {
14 | var key = keys[k]
15 | res = res.concat([key]).concat(flattenInput(input[key]))
16 | }
17 | } else {
18 | res = res.concat(input)
19 | }
20 | }
21 | return res
22 | }
23 |
24 | module.exports = flattenInput
25 |
--------------------------------------------------------------------------------