├── .prettierrc
├── .gitattributes
├── website
├── .babelrc
├── next.config.js
├── pages
│ ├── index.js
│ ├── reference
│ │ ├── use-api.js
│ │ ├── use-params.js
│ │ └── use-inf-api.js
│ ├── examples
│ │ ├── basic.js
│ │ ├── pagination.js
│ │ ├── filter.js
│ │ └── inf-scroll.js
│ └── _document.js
├── constants.js
├── components
│ ├── APIComponentWrapper.js
│ ├── status
│ │ ├── InlineLoading.js
│ │ ├── NoResults.js
│ │ ├── Error.js
│ │ └── Loading.js
│ ├── layout
│ │ ├── Grid.js
│ │ ├── Menu.js
│ │ └── BaseLayout.js
│ ├── ExampleComponent.js
│ ├── ReferenceDisplay.js
│ └── GoogleBooksList.js
├── examples
│ ├── FilterExample
│ │ ├── SearchInput.js
│ │ ├── TypeSelect.js
│ │ └── FilterExample.js
│ ├── BasicExample.js
│ ├── PaginationExample
│ │ ├── OffsetPagination.js
│ │ ├── PaginationExample.js
│ │ └── Paginator.js
│ └── InfScrolLExample
│ │ ├── functions.js
│ │ └── InfScrollExample.js
├── package.json
└── reference
│ ├── use-api.md
│ ├── use-params.md
│ └── use-inf-api.md
├── .editorconfig
├── .eslintrc
├── src
├── index.js
├── utils.js
├── useParams.js
├── useAPI.js
└── useInfAPI.js
├── now.json
├── .gitignore
├── generate-reference.js
├── LICENSE
├── package.json
└── README.md
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "singleQuote": true
4 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/website/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [["styled-components", { "ssr": true }]]
4 | }
5 |
--------------------------------------------------------------------------------
/website/next.config.js:
--------------------------------------------------------------------------------
1 | // next.config.js
2 | const withCSS = require('@zeit/next-css');
3 |
4 | module.exports = withCSS();
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = lf
--------------------------------------------------------------------------------
/website/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Basic from './examples/basic';
3 |
4 | const Index = props => {
5 | return (
6 |
7 | );
8 | };
9 |
10 | export default Index;
11 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb",
4 | "prettier"
5 | ],
6 | "plugins": [
7 | "prettier"
8 | ],
9 | "rules": {
10 | "prettier/prettier": [
11 | "error"
12 | ]
13 | }
14 | }
--------------------------------------------------------------------------------
/website/constants.js:
--------------------------------------------------------------------------------
1 | // https://coolors.co/dff8eb-2e4052-092327-0b5351-00a9a5
2 | export const booksURL = 'https://www.googleapis.com/books/v1/volumes';
3 | export const booksInitialParams = { q: 'intitle:react', maxResults: 5 };
4 |
--------------------------------------------------------------------------------
/website/components/APIComponentWrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const APIComponentWrapper = styled.div`
4 | height: 400px;
5 | overflow-y: auto;
6 | border: 1px solid #eee;
7 | `;
8 |
9 | export default APIComponentWrapper;
10 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as useAPI } from './useAPI';
2 | export { default as useParams } from './useParams';
3 | export { default as useInfAPI } from './useInfAPI';
4 | export { getOffsetPaginator } from './utils';
5 | export { default as axios } from 'axios'; // Allow access to lib axios instance in order to override defaults
6 |
--------------------------------------------------------------------------------
/website/components/status/InlineLoading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import 'spectre.css/dist/spectre-exp.css';
3 |
4 | const InlineLoading = () => (
5 |
9 | );
10 |
11 | export default InlineLoading;
12 |
--------------------------------------------------------------------------------
/website/pages/reference/use-api.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReferenceDisplay from '../../components/ReferenceDisplay';
3 | import UseApiReference from '!!raw-loader!../../reference/use-api.md';
4 |
5 | const UseApi = props => {
6 | return (
7 |
8 | );
9 | };
10 |
11 | UseApi.propTypes = {};
12 |
13 | export default UseApi;
14 |
--------------------------------------------------------------------------------
/website/examples/FilterExample/SearchInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const SearchInput = ({ onChange, defaultValue, ...passProps }) => {
4 | return (
5 |
6 |
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "react-api-hooks",
4 | "alias": "react-api-hooks",
5 | "builds": [
6 | {
7 | "src": "website/package.json",
8 | "use": "@now/static-build",
9 | "config": {
10 | "distDir": "out"
11 | }
12 | }
13 | ],
14 | "routes": [
15 | {
16 | "src": "/(.*)",
17 | "dest": "/website/$1"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/website/pages/reference/use-params.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import UseParamsReference from '!!raw-loader!../../reference/use-params.md';
3 | import ReferenceDisplay from '../../components/ReferenceDisplay';
4 |
5 | const UseParams = props => {
6 | return (
7 |
8 | );
9 | };
10 |
11 | UseParams.propTypes = {};
12 |
13 | export default UseParams;
14 |
--------------------------------------------------------------------------------
/website/pages/reference/use-inf-api.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import UseInfAPIReference from '!!raw-loader!../../reference/use-inf-api.md';
4 | import ReferenceDisplay from '../../components/ReferenceDisplay';
5 |
6 | const UseInfApi = () => {
7 | return ;
8 | };
9 |
10 | UseInfApi.propTypes = {};
11 |
12 | export default UseInfApi;
13 |
--------------------------------------------------------------------------------
/.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 |
25 | .next
26 | .idea
27 | dist
28 | out
29 | *.tgz
30 |
--------------------------------------------------------------------------------
/generate-reference.js:
--------------------------------------------------------------------------------
1 | const jsdoc2md = require('jsdoc-to-markdown');
2 | const fs = require('fs');
3 |
4 | function generateReferenceDoc(inputFl, outputFl) {
5 | jsdoc2md
6 | .render({
7 | files: inputFl,
8 | template: `{{>all-docs~}}`
9 | })
10 | .then(markdown => {
11 | fs.writeFileSync(outputFl, markdown);
12 | });
13 | }
14 |
15 | generateReferenceDoc('src/useAPI.js', 'website/reference/use-api.md');
16 | generateReferenceDoc('src/useParams.js', 'website/reference/use-params.md');
17 | generateReferenceDoc('src/useInfAPI.js', 'website/reference/use-inf-api.md');
18 |
--------------------------------------------------------------------------------
/website/examples/BasicExample.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAPI } from 'react-api-hooks';
3 | import GoogleBooksList from '../components/GoogleBooksList';
4 | import { booksInitialParams, booksURL } from '../constants';
5 | import Error from '../components/status/Error';
6 | import Loading from '../components/status/Loading';
7 |
8 | const BasicExample = () => {
9 | const { data = [], error, isLoading } = useAPI(booksURL, { params: booksInitialParams });
10 |
11 | if (error) {
12 | return ;
13 | }
14 |
15 | return isLoading ? : ;
16 | };
17 |
18 | export default BasicExample;
19 |
--------------------------------------------------------------------------------
/website/components/layout/Grid.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import React from 'react';
3 |
4 | const GridWrapper = styled.div`
5 | display: grid;
6 | grid-template-columns: 1fr auto 1fr;
7 | grid-template-areas: "menu content blank";
8 |
9 | > :nth-child(2) {
10 | width: 100vw;
11 | max-width: 900px;
12 | }
13 |
14 | .menu{
15 | grid-area: menu;
16 | }
17 |
18 | .content{
19 | grid-area: content;
20 | }
21 | `;
22 |
23 | const Grid = ({ children }) => {
24 | return (
25 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | Grid.propTypes = {};
32 |
33 | export default Grid;
34 |
--------------------------------------------------------------------------------
/website/components/status/NoResults.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import 'spectre.css/dist/spectre-exp.css';
3 | import APIComponentWrapper from '../APIComponentWrapper';
4 |
5 | const NoResults = () => (
6 |
7 |
15 |
16 |
17 |
No Results
18 |
19 |
20 |
21 |
22 | );
23 |
24 | export default NoResults;
25 |
--------------------------------------------------------------------------------
/website/components/ExampleComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import 'prismjs';
3 | import 'prismjs/components/prism-jsx';
4 | import 'prismjs/themes/prism-coy.css';
5 | import { PrismCode } from 'react-prism';
6 |
7 | function removeImportFromSource(sourceStr){
8 | return sourceStr.replace(
9 | /import.*;[\n\r]*/ig,
10 | ''
11 | )
12 | }
13 |
14 | const ExampleComponent = ({ Component, componentSource }) => {
15 | return (
16 |
17 | {Component &&
}
18 |
19 | {removeImportFromSource(componentSource)}
20 |
21 |
22 | );
23 | };
24 |
25 | export default ExampleComponent;
26 |
--------------------------------------------------------------------------------
/website/components/status/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import APIComponentWrapper from '../APIComponentWrapper';
3 |
4 | const Error = ({ error }) => {
5 | return (
6 |
7 |
15 |
16 |
17 |
Error
18 |
An error occurred.
19 |
{error.message || ''}
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default Error;
28 |
--------------------------------------------------------------------------------
/website/components/status/Loading.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import 'spectre.css/dist/spectre-exp.css';
3 | import APIComponentWrapper from '../APIComponentWrapper';
4 |
5 | const Loading = () => (
6 |
7 |
22 |
23 | );
24 |
25 | export default Loading;
26 |
--------------------------------------------------------------------------------
/website/examples/PaginationExample/OffsetPagination.js:
--------------------------------------------------------------------------------
1 | class OffsetPagination {
2 | constructor(data, params, updateParams, pageSize) {
3 | this.data = data;
4 | this.params = params;
5 | this.updateParams = updateParams;
6 | this.pageSize = pageSize;
7 | }
8 | onNext = () => this.updateParams({ startIndex: this.getCurrentOffset() + this.pageSize });
9 | onPrevious = () => this.updateParams({ startIndex: this.getCurrentOffset() - this.pageSize });
10 | hasPreviousPage = () => this.params.startIndex || 0 > 0;
11 | hasNextPage = () => this.data.length === this.pageSize;
12 | getCurrentOffset = () => this.params.startIndex || 0;
13 | getPageCnt = () => Math.round(this.getCurrentOffset() / this.pageSize) + 1;
14 | }
15 |
16 | export default OffsetPagination;
17 |
--------------------------------------------------------------------------------
/website/examples/FilterExample/TypeSelect.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const TypeSelect = ({ onChange, ...passProps }) => {
5 | return (
6 |
7 |
15 |
16 | );
17 | };
18 |
19 | TypeSelect.propTypes = {
20 | onChange: PropTypes.func.isRequired
21 | };
22 |
23 | export default TypeSelect;
24 |
--------------------------------------------------------------------------------
/website/examples/InfScrolLExample/functions.js:
--------------------------------------------------------------------------------
1 | import { getOffsetPaginator } from 'react-api-hooks';
2 |
3 | const pageSize = 5;
4 |
5 | /**
6 | * Google Books Paginator function.
7 | *
8 | * Alter the axios `config` object to fetch the next page.
9 | *
10 | * Update the `paginationState` object to keep track of page numbers internally.
11 | *
12 | * @param config {Object} - The axios config object passed to the hook.
13 | * @param paginationState {Object} - An object kept in state to keep track of pagination.
14 | */
15 | export const paginator = getOffsetPaginator('startIndex', pageSize);
16 |
17 | /**
18 | * Google Books Item Extractor
19 | *
20 | * Return a list of items to the hook, given the axios response object.
21 | *
22 | * @param response {Object} - The axios response object.
23 | */
24 | export function responseToItems(response) {
25 | const { items } = response.data;
26 | const hasMore = items.length === pageSize;
27 | return [items, hasMore];
28 | }
29 |
--------------------------------------------------------------------------------
/website/pages/examples/basic.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 | import BasicExampleSource from '!!raw-loader!../../examples/BasicExample';
4 | import BasicExample from '../../examples/BasicExample';
5 | import ExampleComponent from '../../components/ExampleComponent';
6 | import BaseLayout from '../../components/layout/BaseLayout';
7 |
8 | const description = `
9 | # Basic Example
10 |
11 | Basic usage of the \`useAPI\` hook to fetch a list of books from the Google Books API.
12 |
13 | The \`data\` property provides the API response if the API request is successful.
14 |
15 | The \`isLoading\` and \`error\` properties can be used to indicate the request status to the user.
16 | `;
17 |
18 | const Basic = props => {
19 | return (
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | Basic.propTypes = {};
28 |
29 | export default Basic;
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Andrew
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.
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-api-hooks-website",
3 | "version": "1.0.0",
4 | "description": "Website for the react-api-hooks library.",
5 | "main": "index.js",
6 | "repository": "https://github.com/ABWalters/react-api-hooks",
7 | "scripts": {
8 | "start": "next",
9 | "build": "next build",
10 | "production": "next start",
11 | "export": "npm run build && next export -o ../docs -f",
12 | "now-build": "next build && next export"
13 | },
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "@zeit/next-css": "^1.0.1",
18 | "next": "^8.0.3",
19 | "prismjs": "^1.15.0",
20 | "query-string": "^6.4.0",
21 | "react-api-hooks": "../react-api-hooks-0.2.2.tgz",
22 | "react-dom": "^16.8.4",
23 | "react-github-corner": "^2.3.0",
24 | "react-markdown": "^4.0.8",
25 | "react-prism": "^4.3.2",
26 | "spectre.css": "^0.5.8",
27 | "styled-components": "^4.1.3"
28 | },
29 | "devDependencies": {
30 | "babel-plugin-styled-components": "^1.10.0",
31 | "raw-loader": "^2.0.0",
32 | "react-infinite-scroller": "^1.2.4",
33 | "webpack": "^4.29.6",
34 | "webpack-dev-server": "^3.2.1"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/website/examples/PaginationExample/PaginationExample.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAPI, useParams } from 'react-api-hooks';
3 | import GoogleBooksList from '../../components/GoogleBooksList';
4 | import { booksInitialParams, booksURL } from '../../constants';
5 | import Error from '../../components/status/Error';
6 | import Loading from '../../components/status/Loading';
7 | import Paginator from './Paginator';
8 | import OffsetPagination from './OffsetPagination';
9 |
10 | const PaginationExample = () => {
11 | const { params, updateParams } = useParams(booksInitialParams);
12 | const { data = [], error, isLoading } = useAPI(booksURL, { params });
13 | const pagination = new OffsetPagination(data.items || [], params, updateParams, 5);
14 |
15 | if (error) {
16 | return ;
17 | }
18 |
19 | return (
20 | <>
21 | {isLoading ? : }
22 |
29 | >
30 | );
31 | };
32 |
33 | export default PaginationExample;
34 |
--------------------------------------------------------------------------------
/website/components/ReferenceDisplay.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import BaseLayout from '../components/layout/BaseLayout';
5 |
6 | const ReactMarkdown = require('react-markdown');
7 |
8 | const MarkDownWrapper = styled.div`
9 | table {
10 | width: 100%;
11 | border-spacing: 0;
12 | }
13 |
14 | thead {
15 | background-color: rgba(0, 0, 0, 0.05);
16 | border-color: rgba(0, 0, 0, 0.05);
17 | }
18 |
19 | th {
20 | border-bottom-width: 0.1rem;
21 | }
22 |
23 | th,
24 | td {
25 | border-bottom: 0.05rem solid #dadee4;
26 | padding: 0.6rem 0.4rem;
27 | border-right: 1px solid rgba(0, 0, 0, 0.07);
28 | }
29 |
30 | th:first-child,
31 | td:first-child {
32 | border-left: 1px solid rgba(0, 0, 0, 0.07);
33 | }
34 |
35 | pre {
36 | white-space: normal;
37 | }
38 | `;
39 |
40 | const ReferenceDisplay = ({ source }) => {
41 | return (
42 |
43 |
44 |
Reference
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | ReferenceDisplay.propTypes = {
54 | source: PropTypes.string.isRequired
55 | };
56 |
57 | export default ReferenceDisplay;
58 |
--------------------------------------------------------------------------------
/website/examples/InfScrolLExample/InfScrollExample.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useInfAPI } from 'react-api-hooks';
3 | import InfiniteScroll from 'react-infinite-scroller';
4 | import { GoogleBooksListInner } from '../../components/GoogleBooksList';
5 | import Error from '../../components/status/Error';
6 | import InlineLoading from '../../components/status/InlineLoading';
7 | import { booksInitialParams, booksURL } from '../../constants';
8 | import { paginator, responseToItems } from './functions';
9 |
10 | const InfScrollExample = () => {
11 | const { items, error, isPaging, hasMore, fetchPage } = useInfAPI(
12 | booksURL,
13 | { params: booksInitialParams },
14 | paginator,
15 | responseToItems
16 | );
17 |
18 | if (error) {
19 | return ;
20 | }
21 |
22 | return (
23 |
24 | {
27 | if (!isPaging) {
28 | fetchPage();
29 | }
30 | }}
31 | hasMore={hasMore}
32 | loader={}
33 | useWindow={false}
34 | >
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | InfScrollExample.propTypes = {};
42 |
43 | export default InfScrollExample;
44 |
--------------------------------------------------------------------------------
/website/pages/examples/pagination.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 | import ExampleComponent from '../../components/ExampleComponent';
4 | import BaseLayout from '../../components/layout/BaseLayout';
5 | import APIWithPagination from '../../examples/PaginationExample/PaginationExample';
6 | import APIWithPaginationSource from '!!raw-loader!../../examples/PaginationExample/PaginationExample';
7 | import OffsetPaginationSource from '!!raw-loader!../../examples/PaginationExample/OffsetPagination';
8 |
9 | const description = `
10 | # Pagination Example
11 |
12 | An example showing how the \`useParams\` and \`useAPI\` hooks can be used together to paginate results from an API.
13 |
14 | The \`useParams\` hook keeps a params object in it's state, that can be used when making calls to the API.
15 |
16 | The API uses offset pagination, by updating the \`startIndex\` param, the component can paginate through the results.
17 | `;
18 |
19 | const Pagination = props => {
20 | return (
21 |
22 | <>
23 |
24 |
25 |
26 | >
27 |
28 | );
29 | };
30 |
31 | Pagination.propTypes = {};
32 |
33 | export default Pagination;
34 |
--------------------------------------------------------------------------------
/website/pages/examples/filter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 | import ExampleComponent from '../../components/ExampleComponent';
4 | import BaseLayout from '../../components/layout/BaseLayout';
5 | import APIWithSearch from '../../examples/FilterExample/FilterExample';
6 | import APIWithSearchSource from '!!raw-loader!../../examples/FilterExample/FilterExample';
7 |
8 | const description = `
9 | # Filter Example
10 |
11 | An example showing how the \`useParams\` and \`useAPI\` hooks can be used together to filter results from an API.
12 |
13 | The \`useParams\` hook keeps a params object in it's state, that can be used when making calls to the API.
14 |
15 | Use the \`updateParams\` method to immediately update the params object, and trigger a refresh.
16 |
17 | Use the \`debouncedUpdateParams\` method to delay the params update until \`wait\` ms have passed between function calls.
18 | ([Using the lodash debounce function](https://lodash.com/docs/4.17.11#debounce)).
19 |
20 | The \`isStale\` property indicates whether the is a debounced params update pending.
21 | `;
22 |
23 | const Pagination = props => {
24 | return (
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | Pagination.propTypes = {};
33 |
34 | export default Pagination;
35 |
--------------------------------------------------------------------------------
/website/pages/examples/inf-scroll.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 | import ExampleComponent from '../../components/ExampleComponent';
4 | import BaseLayout from '../../components/layout/BaseLayout';
5 | import InfScrollExample from '../../examples/InfScrolLExample/InfScrollExample';
6 | import InfScrollExampleSource from '!!raw-loader!../../examples/InfScrolLExample/InfScrollExample';
7 | import InfScrollFunctionSource from '!!raw-loader!../../examples/InfScrolLExample/functions';
8 |
9 | const description = `
10 | # Infinite Scroll Example
11 |
12 | Basic usage of the \`useInfAPI\` hook to scroll through a list of books from the Google Books API.
13 |
14 | Use the \`react-infinite-scroller\` component to track scrolling, and call \`fetchPage\` when required.
15 |
16 | By default the hook expects the API to paginate using a parameter called \`offset\` and that the API returns an array of items.
17 |
18 | If these defaults do not work for your API, then you will need to provide your own \`paginator\` and \`responseToItems\` functions.
19 | `;
20 |
21 | const InfScroll = () => {
22 | return (
23 |
24 | <>
25 |
26 |
27 |
28 | >
29 |
30 | );
31 | };
32 |
33 | InfScroll.propTypes = {};
34 |
35 | export default InfScroll;
36 |
--------------------------------------------------------------------------------
/website/examples/PaginationExample/Paginator.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import 'spectre.css/dist/spectre-icons.css';
4 | import styled from 'styled-components';
5 |
6 | const PaginatorWrapper = styled.div`
7 | display: flex;
8 | margin-top: 5px;
9 |
10 | .previous,
11 | .next {
12 | }
13 |
14 | .center {
15 | flex-grow: 1;
16 | text-align: center;
17 | line-height: 36px;
18 | }
19 |
20 | .btn,
21 | .btn:focus {
22 | background-color: #0b5351;
23 | border-color: #00a9a5;
24 |
25 | :hover,
26 | :active {
27 | background-color: #009793;
28 | border-color: #0b5351;
29 | }
30 | }
31 | `;
32 |
33 | const Paginator = ({ hasNext, hasPrevious, onNext, pageCnt, onPrevious, ...passProps }) => {
34 | return (
35 |
36 |
37 |
41 |
42 | Page #{pageCnt}
43 |
44 |
48 |
49 |
50 | );
51 | };
52 |
53 | Paginator.propTypes = {
54 | hasNext: PropTypes.bool.isRequired,
55 | hasPrevious: PropTypes.bool.isRequired
56 | };
57 |
58 | export default Paginator;
59 |
--------------------------------------------------------------------------------
/website/examples/FilterExample/FilterExample.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAPI, useParams } from 'react-api-hooks';
3 | import GoogleBooksList from '../../components/GoogleBooksList';
4 | import { booksInitialParams, booksURL } from '../../constants';
5 | import TypeSelect from './TypeSelect';
6 | import Error from '../../components/status/Error';
7 | import Loading from '../../components/status/Loading';
8 | import { SearchInput } from './SearchInput';
9 |
10 | const FilterExample = () => {
11 | const { params, updateParams, debouncedUpdateParams, isStale } = useParams(booksInitialParams);
12 | const { data = [], error, isLoading } = useAPI(booksURL, { params });
13 |
14 | if (error) {
15 | return ;
16 | }
17 |
18 | return (
19 | <>
20 |
21 |
24 | debouncedUpdateParams({
25 | q: `intitle:${e.target.value.toLowerCase()}`
26 | })
27 | }
28 | defaultValue="react"
29 | />
30 |
33 | updateParams({
34 | filter: e.target.value ? e.target.value : undefined
35 | })
36 | }
37 | />
38 |
39 | {isLoading ? (
40 |
41 | ) : (
42 |
43 | )}
44 | >
45 | );
46 | };
47 |
48 | export default FilterExample;
49 |
--------------------------------------------------------------------------------
/website/components/layout/Menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import styled from 'styled-components';
4 |
5 | const MenuWrapper = styled.div`
6 | max-width: 300px;
7 | margin: 0 1rem;
8 | `;
9 |
10 | const MenuItem = ({ children, href, subTitle }) => {
11 | return (
12 |
13 |
14 |
15 | {children}
16 |
17 | {subTitle}
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | const Menu = () => {
25 | return (
26 |
27 |
28 |
29 |
32 |
35 |
38 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | Menu.propTypes = {};
51 |
52 | export default Menu;
53 |
--------------------------------------------------------------------------------
/website/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Head, Html, Main, NextScript } from 'next/document';
2 | import { ServerStyleSheet } from 'styled-components';
3 |
4 | const googleAnalyticsInner = `
5 | window.dataLayer = window.dataLayer || [];
6 | function gtag(){dataLayer.push(arguments);}
7 | gtag('js', new Date());
8 |
9 | gtag('config', 'UA-53236741-7');
10 | `;
11 |
12 | export default class MyDocument extends Document {
13 | static async getInitialProps(ctx) {
14 | const sheet = new ServerStyleSheet();
15 | const originalRenderPage = ctx.renderPage;
16 |
17 | try {
18 | ctx.renderPage = () =>
19 | originalRenderPage({
20 | enhanceApp: App => props => sheet.collectStyles()
21 | });
22 |
23 | const initialProps = await Document.getInitialProps(ctx);
24 |
25 | // Used by Google Analytics
26 | const isProduction = process.env.NODE_ENV === 'production';
27 |
28 | return {
29 | ...initialProps,
30 | isProduction,
31 | styles: <>{initialProps.styles}{sheet.getStyleElement()}>
32 | };
33 | } finally {
34 | sheet.seal();
35 | }
36 | }
37 |
38 | render() {
39 | const { isProduction } = this.props;
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {isProduction && (<>
49 |
51 |
52 | >)}
53 |
54 |
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-api-hooks",
3 | "version": "0.2.2",
4 | "description": "A set of React hooks for working with APIs.",
5 | "main": "dist/index.js",
6 | "source": "src/index.js",
7 | "unpkg": "dist/index.umd.js",
8 | "dependencies": {
9 | "axios": "^0.18.1",
10 | "hash-object": "^0.1.7",
11 | "lodash.debounce": "^4.0.8"
12 | },
13 | "peerDependencies": {
14 | "react": "^16.8.0",
15 | "react-dom": "^16.8.0"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.4.5",
19 | "babel-eslint": "^10.0.1",
20 | "eslint": "^5.16.0",
21 | "eslint-config-airbnb": "^17.1.0",
22 | "eslint-config-airbnb-base": "^13.1.0",
23 | "eslint-config-prettier": "^4.3.0",
24 | "eslint-plugin-import": "^2.17.3",
25 | "eslint-plugin-jsx-a11y": "^6.2.1",
26 | "eslint-plugin-prettier": "^3.1.0",
27 | "eslint-plugin-react": "^7.13.0",
28 | "eslint-plugin-react-hooks": "^1.6.0",
29 | "jsdoc-to-markdown": "^4.0.1",
30 | "microbundle": "^0.11.0",
31 | "prettier": "^1.18.2",
32 | "react": "^16.8.6",
33 | "react-dom": "^16.8.6",
34 | "rollup": "^1.14.4"
35 | },
36 | "files": [
37 | "dist"
38 | ],
39 | "scripts": {
40 | "start": "microbundle watch",
41 | "build": "microbundle --no-compress --no-sourcemap",
42 | "test": "npm test",
43 | "docs": "node scripts/generate-reference.js"
44 | },
45 | "repository": {
46 | "type": "git",
47 | "url": "git+https://github.com/ABWalters/react-api-hooks.git"
48 | },
49 | "keywords": [
50 | "reactjs",
51 | "react-hooks",
52 | "axios",
53 | "api",
54 | "stateless-components",
55 | "reactjs",
56 | "hooks"
57 | ],
58 | "author": "ABWalters",
59 | "license": "MIT",
60 | "bugs": {
61 | "url": "https://github.com/ABWalters/react-api-hooks/issues"
62 | },
63 | "homepage": "https://github.com/ABWalters/react-api-hooks#readme"
64 | }
65 |
--------------------------------------------------------------------------------
/website/reference/use-api.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## useAPI(url, config, initialFetch) ⇒ [useAPIOutput](#useAPIOutput)
4 | React hook used to make a an API call using axios.
5 |
6 | ```
7 | const { data, response, error, isLoading, setData, fetch } = useAPI(url, config, initialFetch);
8 | ```
9 |
10 | Allows you to pass an [axios config object](https://github.com/axios/axios#request-config), for complete control of the request being sent.
11 |
12 | **Kind**: global function
13 | **Returns**: [useAPIOutput](#useAPIOutput) - output
14 |
15 | | Param | Type | Default | Description |
16 | | --- | --- | --- | --- |
17 | | url | string | | URL that the API call is made to. |
18 | | config | Object | {} | Axios config object passed to the axios.request method. |
19 | | initialFetch | boolean | true | Should the first api call automatically be made. |
20 |
21 |
22 |
23 | ## useAPIOutput : Object
24 | The object returned by the useAPI hook.
25 |
26 | **Kind**: global typedef
27 | **Properties**
28 |
29 | | Name | Type | Description |
30 | | --- | --- | --- |
31 | | data | Object \| undefined | The data attribute from the axios response. |
32 | | response | Object \| undefined | The axios response. |
33 | | error | Object \| undefined | The axios error object if an error occurs. |
34 | | isLoading | boolean | Indicates if their is a pending API call. |
35 | | fetch | function | Function used to manually call a fetch method. |
36 | | setData | [setDataFunc](#setDataFunc) | Set the response data object. |
37 |
38 |
39 |
40 | ## setDataFunc : function
41 | `setData` property of `useAPIOutput`.
42 |
43 | Function used to overwrite the `data` object held instate.
44 |
45 | **Kind**: global typedef
46 |
47 | | Param | Type | Description |
48 | | --- | --- | --- |
49 | | newData | Array.<Object> | New data array that overwrites current data. |
50 |
51 |
--------------------------------------------------------------------------------
/website/reference/use-params.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## useParams(initialParams, debounceWait) ⇒ [useParamsOutput](#useParamsOutput)
4 | React hook to keep query parameters in state.
Used in conjunction with the other hooks to filter and paginate APIs.
Includes the ability the debounce an update, which is useful for delaying API calls while the user is typing.
5 |
6 | **Kind**: global function
7 | **Returns**: [useParamsOutput](#useParamsOutput) - output
8 |
9 | | Param | Type | Default | Description |
10 | | --- | --- | --- | --- |
11 | | initialParams | Object | | The initial parameters to keep in states |
12 | | debounceWait | number | 500 | The time to debounce the params update when calling debouncedUpdateParams |
13 |
14 |
15 |
16 | ## useParamsOutput : Object
17 | The object returned by the useParams hook.
18 |
19 | **Kind**: global typedef
20 | **Properties**
21 |
22 | | Name | Type | Description |
23 | | --- | --- | --- |
24 | | params | Object | The current params to be used when making an API call. |
25 | | isStale | boolean | Is their a debounced params update waiting to timeout. (Are we waiting for the user to stop typing) |
26 | | setParams | [setParamsFunc](#setParamsFunc) | Function used to set new parameters |
27 | | updateParams | [updateParamsFunc](#updateParamsFunc) | Function used to update current parameters |
28 | | debouncedSetParams | [setParamsFunc](#setParamsFunc) | Debounced call made to `setParams` |
29 | | debouncedUpdateParams | [updateParamsFunc](#updateParamsFunc) | Debounced call made to `updateParams` |
30 |
31 |
32 |
33 | ## setParamsFunc : function
34 | `setParams` property of `useParamsOutput`
35 |
36 | **Kind**: global typedef
37 |
38 | | Param | Type | Description |
39 | | --- | --- | --- |
40 | | newParams | Object | New params object that overwrites the current params. |
41 |
42 |
43 |
44 | ## updateParamsFunc : function
45 | `updateParams` property of `useParamsOutput`
46 |
47 | **Kind**: global typedef
48 |
49 | | Param | Type | Description |
50 | | --- | --- | --- |
51 | | paramsUpdate | Object | Partial update to be merged with current params. |
52 |
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React API Hooks
2 | 
3 | 
4 | 
5 | 
6 |
7 | React hooks to interact with an API from a stateless component using axios.
8 |
9 | ## Features:
10 | - Uses axios and allows for complete request control
11 | - Works with stateless/functional components
12 | - Ability to filter and paginate API results
13 | - Ability to delay API calls while the user is typing
14 | - Works with infinite scroll components
15 | - Request auto-cancellation for concurrent requests or component unmount
16 |
17 | ## Examples:
18 | - [Basic](https://react-api-hooks.abwalters.now.sh/examples/basic/)
19 | - [Pagination](https://react-api-hooks.abwalters.now.sh/examples/pagination/)
20 | - [Filtering](https://react-api-hooks.abwalters.now.sh/examples/filter/)
21 | - [Infinite Scroll](https://react-api-hooks.abwalters.now.sh/examples/inf-scroll/)
22 |
23 | ## Installation:
24 | ```
25 | npm i react-api-hooks -s
26 | ```
27 |
28 | ## Basic Usage:
29 | ```javascript
30 | import { useAPI } from 'react-api-hooks';
31 |
32 | const TestComponent = () => {
33 | const { data=[], error, isLoading } = useAPI(url);
34 |
35 | if (error){
36 | return
37 | }
38 |
39 | if (isLoading){
40 | return
41 | }
42 |
43 | return (
44 |
45 | {data.map(item => {item.text})}
46 |
47 | )
48 | }
49 | ```
50 |
51 | ## Advanced Usage
52 | ```javascript
53 | import { useAPI } from 'react-api-hooks';
54 |
55 | const TestComponent = () => {
56 | const axiosConfig = {
57 | method: 'POST',
58 | data: { foo: 'bar' },
59 | params: { id: '14' }
60 | };
61 | const { response, error, isLoading } = useAPI(url, axiosConfig);
62 |
63 | if (error){
64 | return
65 | }
66 |
67 | if (isLoading){
68 | return
69 | }
70 |
71 | return (
72 |
73 | {response.data.map(item => {item.text})}
74 |
75 | )
76 | }
77 | ```
78 |
79 | ## Reference:
80 |
81 | - [useAPI](https://react-api-hooks.abwalters.now.sh/reference/use-api/)
82 | - [useParams](https://react-api-hooks.abwalters.now.sh/reference/use-params/)
83 | - [useInfAPI](https://react-api-hooks.abwalters.now.sh/reference/use-inf-api/)
84 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Uses offset pagination strategy to page through results in an infite scroller.
3 | *
4 | * Change the 'offsetParamName' to customize the paginator to your API.
5 | *
6 | * E.g. you may need to use `offsetParamName='skip'`, `offsetParamName='start'`
7 | *
8 | * @param {Object} config - Axios config object
9 | * @param {Object} paginationState - Object kept internally to keep track of pagination
10 | * @param {string} offsetParamName - Param name used to send offset value to the API
11 | * @param {number} pageSize - Increment size for offset.
12 | * @return {Object[]} output
13 | * @return {Object} output.updatedConfig - Config object including pagination changes
14 | * @return {Object} output.updatedPaginationState - Updated pagination state
15 | */
16 | export function offsetPaginator(config, paginationState, offsetParamName, pageSize) {
17 | const { params = {} } = config;
18 | const { offset } = paginationState;
19 | const newOffset = offset !== undefined ? offset + pageSize : 0;
20 | const updatedParams = { ...params, [offsetParamName]: newOffset };
21 |
22 | const updatedConfig = { ...config, params: updatedParams };
23 | const updatedPaginationState = { offset: newOffset };
24 |
25 | return [updatedConfig, updatedPaginationState];
26 | }
27 |
28 | /**
29 | * Generate your own offset paginator with custom offset param names and page sizes.
30 | * @param offsetParamName
31 | * @param pageSize
32 | * @return {function} paginatorFunc
33 | */
34 | export function getOffsetPaginator(offsetParamName, pageSize) {
35 | return (config, paginationState) =>
36 | offsetPaginator(config, paginationState, offsetParamName, pageSize);
37 | }
38 |
39 | /**
40 | * Default `responseToItems` function. Assumes that the API response is an array of items.
41 | * @param response {Object} - Axios response object
42 | * @return output {Object}
43 | * @return output.items {Object[]} - Items extracted from the response
44 | * @return output.hasMore {boolean} - Are there more items available?
45 | */
46 | export function responseToData(response, pageSize) {
47 | const items = response.data;
48 | const hasMore = items.length === pageSize;
49 | return [items, hasMore];
50 | }
51 | /**
52 | * Generate a response to data function using a custom page size.
53 | *
54 | * If results.length === pageSize then it is assumed that there are more pages available.
55 | * @param pageSize
56 | * @return {function} responseToDataFunc
57 | */
58 | export function getResponseToData(pageSize) {
59 | return response => {
60 | return responseToData(response, pageSize);
61 | };
62 | }
63 |
--------------------------------------------------------------------------------
/src/useParams.js:
--------------------------------------------------------------------------------
1 | import { useState, useMemo } from 'react';
2 | import debounce from 'lodash.debounce';
3 |
4 | /**
5 | * The object returned by the useParams hook.
6 | * @typedef {Object} useParamsOutput
7 | * @property {Object} params - The current params to be used when making an API call.
8 | * @property {boolean} isStale - Is their a debounced params update waiting to timeout. (Are we waiting for the user to stop typing)
9 | * @property {setParamsFunc} setParams - Function used to set new parameters
10 | * @property {updateParamsFunc} updateParams - Function used to update current parameters
11 | * @property {setParamsFunc} debouncedSetParams - Debounced call made to `setParams`
12 | * @property {updateParamsFunc} debouncedUpdateParams - Debounced call made to `updateParams`
13 | */
14 |
15 | /**
16 | * `setParams` property of `useParamsOutput`
17 | * @typedef {function} setParamsFunc
18 | * @param {Object} newParams - New params object that overwrites the current params.
19 | */
20 |
21 | /**
22 | * `updateParams` property of `useParamsOutput`
23 | * @typedef {function} updateParamsFunc
24 | * @param {Object} paramsUpdate - Partial update to be merged with current params.
25 | */
26 |
27 | /**
28 | * React hook to keep query parameters in state.
29 | *
30 | * Used in conjunction with the other hooks to filter and paginate APIs.
31 | *
32 | * Includes the ability the debounce an update, which is useful for delaying API calls while the user is typing.
33 | *
34 | * @param {Object} initialParams - The initial parameters to keep in states
35 | * @param {number} debounceWait=500 - The time to debounce the params update when calling debouncedUpdateParams
36 | * @returns {useParamsOutput} output
37 | */
38 | function useParams(initialParams = {}, debounceWait = 500) {
39 | const [params, setParams] = useState(initialParams);
40 | const [isStale, setIsStale] = useState(false);
41 |
42 | function updateParams(updatedParams) {
43 | const newParams = { ...params, ...updatedParams };
44 | setParams(newParams);
45 | }
46 |
47 | const debouncedSetParams = useMemo(
48 | () =>
49 | debounce(newParams => {
50 | setIsStale(false);
51 | setParams(newParams);
52 | }, debounceWait),
53 | []
54 | );
55 |
56 | return {
57 | params,
58 | isStale,
59 | setParams,
60 | debouncedSetParams: newParams => {
61 | setIsStale(true);
62 | debouncedSetParams(newParams);
63 | },
64 | updateParams,
65 | debouncedUpdateParams: updatedParams => {
66 | setIsStale(true);
67 | debouncedSetParams({ ...params, ...updatedParams });
68 | }
69 | };
70 | }
71 |
72 | export default useParams;
73 |
--------------------------------------------------------------------------------
/website/components/GoogleBooksList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import APIComponentWrapper from './APIComponentWrapper';
4 | import NoResults from './status/NoResults';
5 |
6 | const GoogleBookListWrapper = styled.ul`
7 | box-shadow: none;
8 | margin: 0;
9 | padding: 0;
10 | list-style: none;
11 | transform: none;
12 | `;
13 |
14 | const GoogleBookLi = styled.li`
15 | cursor: pointer;
16 | background: #fff;
17 | margin: 0;
18 | padding: 2px 2px;
19 | transition: background 300ms linear;
20 | border-bottom: 1px solid #f1f1fc;
21 | opacity: 1;
22 | transition: opacity 300ms linear;
23 |
24 | :last-child {
25 | border-bottom: none;
26 | }
27 |
28 | :hover {
29 | background: #f1f1fc;
30 | color: #5755d9;
31 | }
32 |
33 | .text {
34 | padding: 5px 15px;
35 | }
36 |
37 | h5 {
38 | margin-bottom: 0.1em;
39 | }
40 |
41 | .label {
42 | font-size: 12px;
43 | margin-right: 2px;
44 | }
45 | `;
46 |
47 | export const GoogleBook = ({ item }) => {
48 | const { volumeInfo } = item;
49 | const { imageLinks, title, authors } = volumeInfo;
50 | return (
51 |
52 |
53 |
64 |
65 |
{title}
66 |
67 | {(authors || []).map(a => (
68 |
69 | {a}
70 |
71 | ))}
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export const GoogleBooksListInner = ({ items }) => {
80 | return (
81 |
82 | {items.map(item => (
83 |
84 | ))}
85 |
86 | );
87 | };
88 |
89 | const GoogleBooksList = ({ data, ...passProps }) => {
90 | const items = data.items || [];
91 |
92 | if (items.length === 0) {
93 | return ;
94 | }
95 |
96 | return (
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | export default GoogleBooksList;
104 |
--------------------------------------------------------------------------------
/src/useAPI.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import hash from 'hash-object';
3 | import { useEffect, useState } from 'react';
4 |
5 | const { CancelToken } = axios;
6 |
7 | /**
8 | * The object returned by the useAPI hook.
9 | * @typedef {Object} useAPIOutput
10 | * @property {Object|undefined} data - The data attribute from the axios response.
11 | * @property {Object|undefined} response - The axios response.
12 | * @property {Object|undefined} error - The axios error object if an error occurs.
13 | * @property {boolean} isLoading - Indicates if their is a pending API call.
14 | * @property {function} fetch - Function used to manually call a fetch method.
15 | * @property {setDataFunc} setData - Set the response data object.
16 | */
17 |
18 | /**
19 | * `setData` property of `useAPIOutput`.
20 | *
21 | * Function used to overwrite the `data` object held instate.
22 | *
23 | * @typedef {function} setDataFunc
24 | * @param {Object[]} newData - New data array that overwrites current data.
25 | */
26 |
27 | /**
28 | * React hook used to make a an API call using axios.
29 | *
30 | * ```
31 | * const { data, response, error, isLoading, setData, fetch } = useAPI(url, config, initialFetch);
32 | * ```
33 | *
34 | * Allows you to pass an [axios config object](https://github.com/axios/axios#request-config), for complete control of the request being sent.
35 | *
36 | * @param {string} url - URL that the API call is made to.
37 | * @param {Object} config={} - Axios config object passed to the axios.request method.
38 | * @param {boolean} initialFetch=true - Should the first api call automatically be made.
39 | * @returns {useAPIOutput} output
40 | */
41 | function useAPI(url, config = {}, initialFetch = true) {
42 | const [state, setState] = useState({
43 | response: undefined,
44 | error: undefined,
45 | isLoading: true
46 | });
47 |
48 | const configHash = hash(config);
49 |
50 | const source = CancelToken.source();
51 |
52 | function fetch() {
53 | axios(url, {
54 | ...config,
55 | cancelToken: source.token
56 | })
57 | .then(response => {
58 | setState({ error: undefined, response, isLoading: false });
59 | })
60 | .catch(error => {
61 | if (axios.isCancel(error)) {
62 | console.log('Request canceled by cleanup: ', error.message);
63 | } else {
64 | setState({ error, response: undefined, isLoading: false });
65 | }
66 | });
67 | }
68 |
69 | useEffect(() => {
70 | setState({ ...state, isLoading: true });
71 |
72 | if (initialFetch) {
73 | fetch();
74 | }
75 |
76 | return () => {
77 | source.cancel('useEffect cleanup.');
78 | };
79 | }, [url, configHash]);
80 |
81 | const { response, error, isLoading } = state;
82 |
83 | function setData(newData) {
84 | // Used to update state from component
85 | const newResponse = { ...response, data: newData };
86 | setState({ ...state, response: newResponse });
87 | }
88 |
89 | const data = response ? response.data : undefined;
90 | return { data, response, error, isLoading, setData, fetch };
91 | }
92 |
93 | export default useAPI;
94 |
--------------------------------------------------------------------------------
/website/reference/use-inf-api.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## useInfAPI(url, config, paginator, responseToItems) ⇒ [useInfAPIOutput](#useInfAPIOutput)
4 | React hook used by an infinite scrolling component to make API calls using axios.
5 |
6 | ```
7 | const { items, error, isPaging, isLoading, hasMore, setItems, fetchPage } = useAPI(url, config, initialFetch);
8 | ```
9 |
10 | Allows you to pass an [axios config object](https://github.com/axios/axios#request-config), for complete control of the request being sent.
11 |
12 | By default it will paginate using a query param `offset`, and assumes that the API returns an array of items.
13 |
14 | If this is not appropriate for your API, then you will need to provide your own `paginator` and `responseToItems` functions.
15 |
16 | **Kind**: global function
17 | **Returns**: [useInfAPIOutput](#useInfAPIOutput) - output
18 |
19 | | Param | Type | Default | Description |
20 | | --- | --- | --- | --- |
21 | | url | string | | URL that the API call is made to. |
22 | | config | Object | {} | Axios config object passed to the axios.request method. |
23 | | paginator | [paginatorFunc](#paginatorFunc) | offsetPaginator | Function used to update the config object in order to paginate |
24 | | responseToItems | function | responseToData | Function used to extract an array of items from response object. |
25 |
26 |
27 |
28 | ## paginatorFunc ⇒ Array.<Object>
29 | Paginator function used to alter the axios config object, in order to fetch the next page.
30 |
31 | **Kind**: global typedef
32 | **Returns**: Array.<Object> - output - Return tuple \[updatedConfig: Object, updatedPaginationState: Object\]
33 |
34 | | Param | Type | Description |
35 | | --- | --- | --- |
36 | | config | Object | Axios config object |
37 | | paginationState | Object | Object kept internally to keep track of pagination |
38 |
39 |
40 |
41 | ## responseToItemsFunc ⇒ Object
42 | Function used to extract items from the API response.
43 |
44 | **Kind**: global typedef
45 | **Returns**: Object - output - Return tuple \[items: Object[], hasMore: boolean\]
46 |
47 | | Param | Type | Description |
48 | | --- | --- | --- |
49 | | response | Object | Axios response object |
50 |
51 |
52 |
53 | ## useInfAPIOutput : Object
54 | The object returned by the useInfAPI hook.
55 |
56 | **Kind**: global typedef
57 | **Properties**
58 |
59 | | Name | Type | Description |
60 | | --- | --- | --- |
61 | | items | Array.<Object> | Items provided by the API |
62 | | error | Object \| undefined | The axios error object is an error occurs. |
63 | | isLoading | boolean | Indicates if their is a pending API call for the **first** page of items. |
64 | | isPaging | boolean | Indicates if their is a pending API call for the **any** page of items. |
65 | | setItems | [setItemsFunc](#setItemsFunc) | Set the items being kept in state |
66 | | fetchPage | [responseToItemsFunc](#responseToItemsFunc) | Function called from the component in order to fetch the next page |
67 |
68 |
69 |
70 | ## setItemsFunc : function
71 | `setItems` property of `useInfAPIOutput`
72 |
73 | **Kind**: global typedef
74 |
75 | | Param | Type | Description |
76 | | --- | --- | --- |
77 | | newItems | Array.<Object> | New items array that overwrites current data. |
78 |
79 |
--------------------------------------------------------------------------------
/website/components/layout/BaseLayout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import GithubCorner from 'react-github-corner';
3 | import '../../node_modules/spectre.css/dist/spectre.css';
4 | import styled, { createGlobalStyle } from 'styled-components';
5 | import Menu from './Menu';
6 | import Grid from './Grid';
7 |
8 | const OuterWrapper = styled.div`
9 | display: grid;
10 | grid-template-rows: auto auto 1fr;
11 | height: 100%;
12 | position: relative;
13 | `;
14 |
15 | const GlobalStyle = createGlobalStyle`
16 | body {
17 | background-color: #f8f8f8;
18 | }
19 |
20 | body, html, #__next{
21 | min-height: 100%;
22 | }
23 |
24 | pre[class*="language-"]{
25 | border: 1px solid #eee;
26 | padding: 10px 15px;
27 | font-size: 13px;
28 | }
29 | `;
30 |
31 | const BodyWrapper = styled.div`
32 | margin-bottom: 40px;
33 | border-radius: 4px;
34 | overflow: hidden;
35 | padding: 10px 12px;
36 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
37 | background-color: #fff;
38 | `;
39 |
40 | const bodyInnerWrapper = styled.div``;
41 |
42 | const Container = styled.div`
43 | max-width: 900px;
44 | width: 100%;
45 | margin: 0 auto;
46 | `;
47 |
48 | const NavbarWrapper = styled.div`
49 | background-color: #2e4052;
50 | color: #fff;
51 | padding: 10px 0;
52 |
53 | a {
54 | color: #fff;
55 | }
56 |
57 | .btn.btn-link {
58 | color: rgba(255, 255, 255, 0.6);
59 | }
60 | `;
61 |
62 | const FooterWrapper = styled.div`
63 | background-color: #0b5351;
64 | `;
65 |
66 | const HeroWrapper = styled.div`
67 | // background-color: #0b5351;
68 | // background-color: #0093e9;
69 | // background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
70 | background: #4b6cb7; /* fallback for old browsers */
71 | background: linear-gradient(
72 | to right,
73 | #182848,
74 | #4b6cb7
75 | ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
76 | text-align: center;
77 | color: #dff8eb;
78 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
79 | margin-bottom: 0.6rem;
80 |
81 | .hero {
82 | padding-bottom: 1rem;
83 | padding-top: 1rem;
84 | }
85 | `;
86 |
87 | const Tag = ({ children }) => {
88 | return (
89 |
98 | {children}
99 |
100 | );
101 | };
102 |
103 | const MenuLayoutWrapper = styled.div`
104 | max-width: 10rem;
105 | position: absolute;
106 | `;
107 |
108 | const BaseLayout = ({ children }) => {
109 | return (
110 |
111 |
112 |
113 |
114 |
115 |
React API Hooks
116 |
React hooks to interact with an API from a stateless component using axios.
117 |
npm i react-api-hooks -s
118 |
119 | react
120 | hooks
121 | API
122 | axios
123 | stateless
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 |
139 |
140 |
141 | );
142 | };
143 |
144 | BaseLayout.propTypes = {};
145 |
146 | export default BaseLayout;
147 |
--------------------------------------------------------------------------------
/src/useInfAPI.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import axios from 'axios';
3 | import hash from 'hash-object';
4 | import { getOffsetPaginator, getResponseToData } from './utils';
5 |
6 | const { CancelToken } = axios;
7 |
8 | const defaultPaginator = getOffsetPaginator('offset', 40);
9 | const defaultResponseToData = getResponseToData(40);
10 |
11 | /**
12 | * Paginator function used to alter the axios config object, in order to fetch the next page.
13 | * @typedef {function} paginatorFunc
14 | * @param config {Object} - Axios config object
15 | * @param paginationState {Object} - Object kept internally to keep track of pagination
16 | * @return output {Object[]} - Return tuple \[updatedConfig: Object, updatedPaginationState: Object\]
17 | */
18 |
19 | /**
20 | * Function used to extract items from the API response.
21 | * @typedef {function} responseToItemsFunc
22 | * @param response {Object} - Axios response object
23 | * @return output {Object} - Return tuple \[items: Object[], hasMore: boolean\]
24 | */
25 |
26 | /**
27 | * The object returned by the useInfAPI hook.
28 | * @typedef {Object} useInfAPIOutput
29 | * @property {Object[]} items - Items provided by the API
30 | * @property {Object|undefined} error - The axios error object is an error occurs.
31 | * @property {boolean} isLoading - Indicates if their is a pending API call for the **first** page of items.
32 | * @property {boolean} isPaging - Indicates if their is a pending API call for the **any** page of items.
33 | * @property {setItemsFunc} setItems - Set the items being kept in state
34 | * @property {responseToItemsFunc} fetchPage - Function called from the component in order to fetch the next page
35 | */
36 |
37 | /**
38 | * `setItems` property of `useInfAPIOutput`
39 | * @typedef {function} setItemsFunc
40 | * @param {Object[]} newItems - New items array that overwrites current data.
41 | */
42 |
43 | /**
44 | * React hook used by an infinite scrolling component to make API calls using axios.
45 | *
46 | * ```
47 | * const { items, error, isPaging, isLoading, hasMore, setItems, fetchPage } = useAPI(url, config, initialFetch);
48 | * ```
49 | *
50 | * Allows you to pass an [axios config object](https://github.com/axios/axios#request-config), for complete control of the request being sent.
51 | *
52 | * By default it will paginate using a query param `offset`, and assumes that the API returns an array of items.
53 | *
54 | * If this is not appropriate for your API, then you will need to provide your own `paginator` and `responseToItems` functions.
55 | *
56 | * @param {string} url - URL that the API call is made to.
57 | * @param {Object} config={} - Axios config object passed to the axios.request method.
58 | * @param {paginatorFunc} paginator=offsetPaginator - Function used to update the config object in order to paginate
59 | * @param {function} responseToItems=responseToData - Function used to extract an array of items from response object.
60 | * @returns {useInfAPIOutput} output
61 | */
62 | function useInfAPI(
63 | url,
64 | config = {},
65 | paginator = defaultPaginator,
66 | responseToItems = defaultResponseToData
67 | ) {
68 | const [state, setState] = useState({
69 | items: [],
70 | paginationState: {},
71 | error: undefined,
72 | isLoading: true,
73 | isPaging: true,
74 | hasMore: false
75 | });
76 |
77 | const configHash = hash(config);
78 |
79 | const { items, paginationState } = state;
80 |
81 | const source = CancelToken.source();
82 |
83 | function callAPI(isLoading) {
84 | const activePaginationState = isLoading ? {} : paginationState; // Reset pagination when config object changes.
85 | const [updatedConfig, updatedPaginationState] = paginator(config, activePaginationState);
86 |
87 | setState({
88 | ...state,
89 | isLoading,
90 | isPaging: true,
91 | paginationState: updatedPaginationState,
92 | items: isLoading ? [] : items // Clear items when config changes
93 | });
94 | axios(url, {
95 | ...updatedConfig,
96 | cancelToken: source.token
97 | })
98 | .then(response => {
99 | const [pageItems, hasMore] = responseToItems(response);
100 | if (typeof pageItems === typeof []) {
101 | setState({
102 | ...state,
103 | items: isLoading ? pageItems : items.concat(pageItems), // If the config object changed, the reset items
104 | error: undefined,
105 | isLoading: false,
106 | isPaging: false,
107 | hasMore,
108 | paginationState: updatedPaginationState
109 | });
110 | } else {
111 | console.log("Warning: responseToItems didn't return an array.");
112 | }
113 | })
114 | .catch(error => {
115 | if (axios.isCancel(error)) {
116 | console.log('Request canceled by cleanup: ', error.message);
117 | } else {
118 | setState({ ...state, error, isLoading: false, isPaging: false });
119 | }
120 | });
121 | }
122 |
123 | useEffect(() => {
124 | callAPI(true);
125 | return () => {
126 | source.cancel('useEffect cleanup.');
127 | };
128 | }, [url, configHash]);
129 |
130 | const { error, isPaging, isLoading, hasMore } = state;
131 | return {
132 | items,
133 | error,
134 | isPaging,
135 | isLoading,
136 | hasMore,
137 | setItems: newItems => setState({ ...state, items: newItems }),
138 | fetchPage: () => callAPI(false)
139 | };
140 | }
141 |
142 | export default useInfAPI;
143 |
--------------------------------------------------------------------------------