├── .dockerignore
├── .eslintrc.js
├── .github
└── workflows
│ ├── build.yml
│ ├── lint.yml
│ └── test.yml
├── .gitignore
├── .travis.yml
├── .vscode
├── launch.json
└── settings.json
├── CHANGELOG.md
├── Dockerfile
├── LICENSE.md
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── babel.config.js
├── docker-compose.yml
├── docs
├── api
│ └── about-api-docs.md
├── assets
│ ├── component_sketch.jpg
│ ├── section_1_finished.gif
│ ├── section_1_redux_dev_console.png
│ ├── section_2_finished.gif
│ ├── section_3_finished.gif
│ ├── section_4_finished.gif
│ ├── section_5_finished.gif
│ └── section_6_finished.gif
├── examples
│ ├── custom-hooks-react-router.md
│ ├── infinite-scroll.md
│ └── resift-notes.md
├── guides
│ ├── http-proxies.md
│ ├── resift-vs-apollo-relay.md
│ ├── usage-with-classes.md
│ ├── usage-with-redux.md
│ └── usage-with-typescript.md
├── introduction
│ ├── installation.md
│ └── what-is-resift.md
├── main-concepts
│ ├── custom-hooks.md
│ ├── error-handling.md
│ ├── how-to-define-a-fetch.md
│ ├── making-sense-of-statuses.md
│ ├── making-state-consistent.md
│ ├── what-are-data-services.md
│ └── whats-a-fetch.md
└── tutorial
│ └── resift-rentals.md
├── jest.config.js
├── package-lock.json
├── package.json
├── prettier.config.js
├── rollup.config.js
├── scripts
└── build.js
├── src
├── CanceledError
│ ├── CanceledError.d.ts
│ ├── CanceledError.js
│ ├── CanceledError.test.js
│ ├── index.d.ts
│ └── index.js
├── DeferredPromise
│ ├── DeferredPromise.d.ts
│ ├── DeferredPromise.js
│ ├── DeferredPromise.test.js
│ ├── index.d.ts
│ └── index.js
├── ERROR
│ ├── ERROR.d.ts
│ ├── ERROR.js
│ ├── index.d.ts
│ └── index.js
├── Guard
│ ├── Guard.d.ts
│ ├── Guard.js
│ ├── Guard.test.js
│ ├── Guard.types-test.tsx
│ ├── index.d.ts
│ └── index.js
├── LOADING
│ ├── LOADING.d.ts
│ ├── LOADING.js
│ ├── index.d.ts
│ └── index.js
├── NORMAL
│ ├── NORMAL.d.ts
│ ├── NORMAL.js
│ ├── index.d.ts
│ └── index.js
├── ResiftProvider
│ ├── ResiftProvider.d.ts
│ ├── ResiftProvider.js
│ ├── ResiftProvider.test.js
│ ├── index.d.ts
│ └── index.js
├── UNKNOWN
│ ├── UNKNOWN.d.ts
│ ├── UNKNOWN.js
│ ├── index.d.ts
│ └── index.js
├── clearFetch
│ ├── clearFetch.d.ts
│ ├── clearFetch.js
│ ├── clearFetch.test.js
│ ├── index.d.ts
│ └── index.js
├── combineStatuses
│ ├── combineStatuses.d.ts
│ ├── combineStatuses.js
│ ├── combineStatuses.test.js
│ ├── index.d.ts
│ └── index.js
├── createActionType
│ ├── createActionType.d.ts
│ ├── createActionType.js
│ ├── createActionType.test.js
│ ├── index.d.ts
│ └── index.js
├── createContextFetch
│ ├── createContextFetch.d.ts
│ ├── createContextFetch.js
│ ├── createContextFetch.test.js
│ ├── createContextFetch.types-test.tsx
│ ├── index.d.ts
│ └── index.js
├── createDataService
│ ├── createDataService.d.ts
│ ├── createDataService.js
│ ├── createDataService.test.js
│ ├── createDataService.types-test.ts
│ ├── index.d.ts
│ └── index.js
├── createHttpProxy
│ ├── createHttpProxy.d.ts
│ ├── createHttpProxy.js
│ ├── index.d.ts
│ ├── index.js
│ ├── matchPath.d.ts
│ ├── matchPath.js
│ └── matchPath.test.js
├── createHttpService
│ ├── createHttpService.d.ts
│ ├── createHttpService.js
│ ├── createHttpService.test.js
│ ├── index.d.ts
│ └── index.js
├── createStoreKey
│ ├── createStoreKey.d.ts
│ ├── createStoreKey.js
│ ├── createStoreKey.test.js
│ ├── index.d.ts
│ └── index.js
├── dataServiceReducer
│ ├── actionsReducer.d.ts
│ ├── actionsReducer.js
│ ├── actionsReducer.test.js
│ ├── dataServiceReducer.d.ts
│ ├── dataServiceReducer.js
│ ├── dataServiceReducer.test.js
│ ├── index.d.ts
│ ├── index.js
│ ├── sharedReducer.d.ts
│ ├── sharedReducer.js
│ └── sharedReducer.test.js
├── defineFetch
│ ├── defineFetch.d.ts
│ ├── defineFetch.js
│ ├── defineFetch.test.js
│ ├── defineFetch.types-test.ts
│ ├── index.d.ts
│ └── index.js
├── index.d.ts
├── index.js
├── index.test.js
├── isError
│ ├── index.d.ts
│ ├── index.js
│ ├── isError.d.ts
│ ├── isError.js
│ └── isError.test.js
├── isLoading
│ ├── index.d.ts
│ ├── index.js
│ ├── isLoading.d.ts
│ ├── isLoading.js
│ └── isLoading.test.js
├── isNormal
│ ├── index.d.ts
│ ├── index.js
│ ├── isNormal.d.ts
│ ├── isNormal.js
│ └── isNormal.test.js
├── isUnknown
│ ├── index.d.ts
│ ├── index.js
│ ├── isUnknown.d.ts
│ ├── isUnknown.js
│ └── isUnknown.test.js
├── prefixes
│ ├── CLEAR.d.ts
│ ├── CLEAR.js
│ ├── ERROR.d.ts
│ ├── ERROR.js
│ ├── FETCH.d.ts
│ ├── FETCH.js
│ ├── SUCCESS.d.ts
│ ├── SUCCESS.js
│ ├── index.d.ts
│ └── index.js
├── shallowEqual
│ ├── index.d.ts
│ ├── index.js
│ ├── shallowEqual.d.ts
│ ├── shallowEqual.js
│ └── shallowEqual.test.js
├── timestamp
│ ├── index.js
│ ├── timestamp.js
│ └── timestamp.test.js
├── useClearFetch
│ ├── index.d.ts
│ ├── index.js
│ ├── useClearFetch.d.ts
│ ├── useClearFetch.js
│ └── useClearFetch.test.js
├── useData
│ ├── index.d.ts
│ ├── index.js
│ ├── useData.d.ts
│ ├── useData.js
│ ├── useData.test.js
│ └── useData.types-test.tsx
├── useDispatch
│ ├── index.d.ts
│ ├── index.js
│ ├── useDispatch.d.ts
│ ├── useDispatch.js
│ └── useDispatch.test.js
├── useError
│ ├── index.d.ts
│ ├── index.js
│ ├── useError.d.ts
│ ├── useError.js
│ └── useError.test.js
├── useFetch
│ ├── index.d.ts
│ ├── index.js
│ ├── useFetch.d.ts
│ ├── useFetch.js
│ └── useFetch.test.js
└── useStatus
│ ├── index.d.ts
│ ├── index.js
│ ├── useStatus.d.ts
│ ├── useStatus.js
│ └── useStatus.test.js
├── tsconfig.json
└── website
├── README.md
├── core
└── Footer.js
├── generate-api-doc.js
├── generate-api-doc.test.js
├── generate-api-docs.js
├── i18n
└── en.json
├── package-lock.json
├── package.json
├── pages
└── en
│ └── help.js
├── replaceTwitterCardType.js
├── sidebars.json
├── siteConfig.js
├── static
├── css
│ ├── custom.css
│ └── main.css
├── img
│ ├── favicon.ico
│ ├── resift-logo.png
│ ├── resift-og-image.png
│ ├── resift-wordmark.png
│ ├── sift-logo.png
│ └── sift-wordmark.png
└── index.html
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | */node_modules
2 | *.log
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | },
6 | extends: 'react-app',
7 | globals: {
8 | Atomics: 'readonly',
9 | SharedArrayBuffer: 'readonly',
10 | },
11 | parserOptions: {
12 | ecmaFeatures: {
13 | jsx: true,
14 | },
15 | ecmaVersion: 2018,
16 | sourceType: 'module',
17 | },
18 | plugins: [],
19 | rules: {},
20 | };
21 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | - pull_request
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-node@v1
11 | with:
12 | node-version: '12.x'
13 | - run: npm i
14 | - run: npm run build
15 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on:
3 | - pull_request
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-node@v1
11 | with:
12 | node-version: '12.x'
13 | - run: npm i
14 | - run: npm run lint
15 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | - pull_request
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-node@v1
11 | with:
12 | node-version: '12.x'
13 | - run: npm i
14 | - run: npm t
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | coverage
4 | .DS_Store
5 | website/build
6 | docs/api/**/*
7 | !docs/api/about-api-docs.md
8 | stats.json
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - '10'
5 |
6 | cache:
7 | directories:
8 | - node_modules
9 |
10 | branches:
11 | only:
12 | - master
13 | - develop
14 |
15 | install:
16 | - npm install
17 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Jest Current File",
11 | "program": "${workspaceFolder}/node_modules/.bin/jest",
12 | "args": ["${fileBasenameNoExtension}", "--config", "jest.config.js"],
13 | "console": "integratedTerminal",
14 | "internalConsoleOptions": "neverOpen",
15 | "disableOptimisticBPs": true,
16 | "windows": {
17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
18 | }
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.colorCustomizations": {}
3 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog for JustSift/Resift
2 |
3 | All notable changes to this project will be documented in this file, starting on July 10, 2020.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
6 |
7 | ## [0.1.1] - 2020-07-07
8 |
9 | ### Fixed
10 |
11 | - Export useError in index.d.ts so it can be imported as module
12 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8.11.4
2 |
3 | WORKDIR /app/website
4 |
5 | EXPOSE 3000 35729
6 | COPY ./docs /app/docs
7 | COPY ./website /app/website
8 | RUN yarn install
9 |
10 | CMD ["yarn", "start"]
11 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Sift
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 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## What does this PR do?
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReSift · [](https://travis-ci.org/JustSift/ReSift) [](https://coveralls.io/github/JustSift/ReSift?branch=master)
2 |
3 | 
4 |
5 | ## Introduction
6 |
7 | ReSift is a React state management library for fetches with the goal of giving your team a capable standard for fetching, storing, and reacting to data with a great developer experience.
8 |
9 | **Features:**
10 |
11 | - 💾 Global, consistent, injectable-anywhere data cache
12 | - 🔄 Supports ["fetch-then-render"](https://reactjs.org/docs/concurrent-mode-suspense.html#approach-2-fetch-then-render-not-using-suspense) (with ["render-as-you-fetch"](https://reactjs.org/docs/concurrent-mode-suspense.html#approach-3-render-as-you-fetch-using-suspense) coming [soon](https://github.com/JustSift/ReSift/issues/32))
13 | - 📬 Status reporting
14 | - 🔌 Pluggable
15 | - 🔗 REST oriented
16 | - 👽 Backend agnostic
17 | - 🌐 Universal — Share code amongst your apps. **Works with React Native!**
18 | - 🎣 Hooks API
19 | - 🤝 Full TypeScript support
20 |
21 | We like to think of ReSift as the [Relay](https://relay.dev/) of REST. ReSift is in the same class of tools as [Relay](https://relay.dev/) and [the Apollo Client](https://www.apollographql.com/docs/react/). However, **ReSift does not require GraphQL**.
22 |
23 | [See this doc for definitions and comparisons of ReSift vs Relay/Apollo](https://resift.org/docs/guides/resift-vs-apollo-relay.md).
24 |
25 | ## Basic usage
26 |
27 | In order to get the benefits of these frameworks within REST, we first define a ["fetch factory"](https://resift.org/docs/main-concepts/whats-a-fetch#defining-a-fetch).
28 |
29 | `makeGetPerson.js`
30 |
31 | ```js
32 | import { defineFetch } from 'resift';
33 |
34 | // 👇 this is a fetch factory
35 | //
36 | const makeGetPerson = defineFetch({
37 | displayName: 'Get Person',
38 | make: personId => ({
39 | request: () => ({ http }) =>
40 | http({
41 | method: 'GET',
42 | route: `/people/${personId}`,
43 | }),
44 | }),
45 | });
46 |
47 | export default makeGetPerson;
48 | ```
49 |
50 | Then you can use this fetch factory to:
51 |
52 | 1. kick off the initial request
53 | 2. get the status of the fetch
54 | 3. pull data from the fetch
55 |
56 | `Person.js`
57 |
58 | ```js
59 | import React, { useEffect } from 'react';
60 | import { useDispatch, useStatus, useData, isLoading } from 'resift';
61 | import makeGetPerson from './makeGetPerson';
62 |
63 | function Person({ personId }) {
64 | const dispatch = useDispatch();
65 |
66 | // 👇👇👇 this is using the "fetch factory" from above
67 | const getPerson = makeGetPerson(personId);
68 | // 👆👆👆 this is a "fetch" from
69 |
70 | useEffect(() => {
71 | // 1) kick off the initial request
72 | dispatch(getPerson());
73 | }, [dispatch, getPerson]);
74 |
75 | // 2) get the status of the fetch
76 | const status = useStatus(getPerson);
77 |
78 | // 3) pull data from the fetch
79 | const person = useData(getPerson);
80 |
81 | return (
82 |
83 | {isLoading(status) &&
Loading...
}
84 | {person && <>Hello, {person.name}!>}
85 |
86 | );
87 | }
88 |
89 | export default Person;
90 | ```
91 |
92 | > In this basic example, we fetched and pulled data in the same component, but with ReSift, you don't have to!
93 | >
94 | > **With ReSift, you can dispatch a fetch in one component and pull it from another. This makes it much easier to reuse data across components and enables the concept of pre-fetching**
95 |
96 | ## Why ReSift?
97 |
98 | What's wrong with [`window.fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)? Why use ReSift over traditional methods?
99 |
100 | **To put it simply, `window.fetch` (or [similar](https://github.com/axios/axios)), isn't a state management library.**
101 | It's _just_ a function that returns a promise of your data.
102 |
103 | "State management" is up to you. If your application doesn't have a lot of changing state regarding data fetches, then it's easy to manage this state by yourself and you should.
104 |
105 | However, you may not be so lucky. Here are some questions to help you consider if you should use this library:
106 |
107 | - Does your app have to load data from multiple different endpoints?
108 | - Do you want to cache the results of these fetches?
109 | - Are you reusing this data across different components?
110 | - Do you want state to stay consistent and update components when you do PUT, POST, etc?
111 | - Do you have a plan for reporting loading and error statuses?
112 | - Is it easy to onboard new members of your team to your data state management solution?
113 |
114 | ReSift is an opinionated state management solution for data fetches for REST-oriented applications.
115 |
116 | You could manage the state of your data fetches yourself (using Redux or similar) to get the same benefits but the question is: Do you want to?
117 |
118 | Are you confident that your solution is something your team can follow easily? Does it scale well? Is it reusable?
119 |
120 | If you answered "No" to any of those questions then give ReSift a try.
121 |
122 | ---
123 |
124 | Intrigued? This only scratches the surface. [Head on over to the docs!](https://resift.org/)
125 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@babel/preset-typescript',
4 | [
5 | '@babel/preset-env',
6 | {
7 | targets: {
8 | node: 'current',
9 | },
10 | },
11 | ],
12 | '@babel/preset-react',
13 | ],
14 | plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-optional-chaining'],
15 | };
16 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | docusaurus:
5 | build: .
6 | ports:
7 | - 3000:3000
8 | - 35729:35729
9 | volumes:
10 | - ./docs:/app/docs
11 | - ./website/blog:/app/website/blog
12 | - ./website/core:/app/website/core
13 | - ./website/i18n:/app/website/i18n
14 | - ./website/pages:/app/website/pages
15 | - ./website/static:/app/website/static
16 | - ./website/sidebars.json:/app/website/sidebars.json
17 | - ./website/siteConfig.js:/app/website/siteConfig.js
18 | working_dir: /app/website
19 |
--------------------------------------------------------------------------------
/docs/api/about-api-docs.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: about-api-docs
3 | title: About API docs
4 | sidebar_label: About these docs
5 | ---
6 |
7 | These docs are auto-generated by [`generate-api-docs.js`](https://github.com/JustSift/ReSift/blob/master/website/generate-api-doc.js) which generates the docs based on the ambient typings files within the repository (the `*.d.ts` files). [See below for more info.](#how-these-docs-work)
8 |
9 | If you are using an editor that uses the typescript language service (e.g. vs-code, visual studio, webstorm/intelliJ), you should be able to hover over the definitions within your editor to get the same documentation.
10 |
11 | Please open any issues if you find any within these docs. Open pull requests modifying the `*.d.ts` files.
12 |
13 | ## How these docs work
14 |
15 | These docs work by parsing the ambient typings files (`*.d.ts`) files in `/src` and turning them into markdown files (`*.md`).
16 |
17 | This works by using the typescript API (i.e. `const ts = require('typescript');`) to parse the ambient typings for JS doc comments, interfaces, and other code.
18 |
19 | The generator will only pick up code that is commented with a JSDoc comment that contains the `@docs` directive.
20 |
21 | ```ts
22 | // this comment must be a JSDoc comment
23 | // 👇 (which is more than a multi-line comment)
24 | /**
25 | * @docs Title of `module`
26 | *
27 | * Description of module. You can also put _markdown_ in **here**
28 | */
29 | function myModule(x: number): string;
30 | ```
31 |
32 | This will generate the following document:
33 |
34 | ---
35 |
36 | #### Title of `module`
37 |
38 | Description of module. You can also put _markdown_ in **here**
39 |
40 | ```ts
41 | function module(x: number): string;
42 | ```
43 |
44 | ---
45 |
46 | ## Generating tables for `interface`s
47 |
48 | When you put the `@docs` directive above an interface, the generator will go through all the property declarations and generate a table.
49 |
50 | ```ts
51 | /**
52 | * @docs `ExampleModule`
53 | *
54 | * This is a description of `ExampleModule`
55 | */
56 | interface ExampleModule {
57 | /**
58 | * This is the description of foo.
59 | */
60 | foo: string;
61 | /**
62 | * This is the description of bar.
63 | */
64 | bar?: number;
65 | }
66 | ```
67 |
68 | ---
69 |
70 | #### `ExampleModule`
71 |
72 | This is a description of `ExampleModule`
73 |
74 | | Name | Description | Type | Required |
75 | | ---- | ------------------------------- | ------------------- | -------- |
76 | | foo | This is the description of foo. | string
| yes |
77 | | bar | This is the description of bar. | number
| no |
78 |
79 | ---
80 |
81 | ## Other code behavior
82 |
83 | When the generator sees the `@docs` directive in a JSDoc comment before anything else that isn't an `interface`, it will simply take the declaration and wrap it in a code block with one difference—it will remove the generics from the types (e.g. ``).
84 |
85 | Why? Non-TS users are confused by generics. Simply removing the generics declaration (i.e. the stuff between the `<>`s) fixes the readability issue while also keeping the generics there.
86 |
87 | For example, see the input code block vs the output code block.
88 |
89 | Input:
90 |
91 | ```ts
92 | export function myModule(
93 | ...args: Args
94 | ): {
95 | foo: Foo;
96 | bar: (bar: Bar) => Result;
97 | };
98 | ```
99 |
100 | Output (more readable to JS users):
101 |
102 | ```ts
103 | function myModule(
104 | ...args: Args
105 | ): {
106 | foo: Foo;
107 | bar: (bar: Bar) => Result;
108 | };
109 | ```
110 |
--------------------------------------------------------------------------------
/docs/assets/component_sketch.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustSift/ReSift/49c1afa64549d194e6025466ef4c489f0aaec056/docs/assets/component_sketch.jpg
--------------------------------------------------------------------------------
/docs/assets/section_1_finished.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustSift/ReSift/49c1afa64549d194e6025466ef4c489f0aaec056/docs/assets/section_1_finished.gif
--------------------------------------------------------------------------------
/docs/assets/section_1_redux_dev_console.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustSift/ReSift/49c1afa64549d194e6025466ef4c489f0aaec056/docs/assets/section_1_redux_dev_console.png
--------------------------------------------------------------------------------
/docs/assets/section_2_finished.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustSift/ReSift/49c1afa64549d194e6025466ef4c489f0aaec056/docs/assets/section_2_finished.gif
--------------------------------------------------------------------------------
/docs/assets/section_3_finished.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustSift/ReSift/49c1afa64549d194e6025466ef4c489f0aaec056/docs/assets/section_3_finished.gif
--------------------------------------------------------------------------------
/docs/assets/section_4_finished.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustSift/ReSift/49c1afa64549d194e6025466ef4c489f0aaec056/docs/assets/section_4_finished.gif
--------------------------------------------------------------------------------
/docs/assets/section_5_finished.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustSift/ReSift/49c1afa64549d194e6025466ef4c489f0aaec056/docs/assets/section_5_finished.gif
--------------------------------------------------------------------------------
/docs/assets/section_6_finished.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustSift/ReSift/49c1afa64549d194e6025466ef4c489f0aaec056/docs/assets/section_6_finished.gif
--------------------------------------------------------------------------------
/docs/examples/custom-hooks-react-router.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: custom-hooks-react-router
3 | title: Custom hooks and React Router
4 | sidebar_label: Custom hooks and React Router
5 | ---
6 |
7 | This example is the same example from the [Custom hooks doc](../main-concepts/custom-hooks.md).
8 |
9 | This example shows how you can build a custom hook using both ReSift's and [React Router's custom hooks](https://reacttraining.com/react-router/web/api/Hooks).
10 |
11 | [See the code to learn more.](https://codesandbox.io/s/custom-hooks-43pkz?fontsize=14)
12 |
13 |
19 |
--------------------------------------------------------------------------------
/docs/examples/infinite-scroll.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: infinite-scroll
3 | title: Infinite scroll
4 | sidebar_label: Infinite scroll
5 | ---
6 |
7 | The following demo is from the [Making state consistent](../main-concepts/making-state-consistent.md#merges) doc. This example demonstrates how you can use `share.merge` do infinite scrolling.
8 |
9 |
15 |
16 |
17 | Don't hesitate to reach out if you've found an issue.
18 |
--------------------------------------------------------------------------------
/docs/examples/resift-notes.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: resift-notes
3 | title: ReSift Notes
4 | sidebar_label: ReSift Notes (CRUD)
5 | ---
6 |
7 | The following application is a basic CRUD application made to be a quick reference in how to do CRUD and loading spinner correctly.
8 |
9 | Read the notes and explore the code.
10 |
11 |
17 |
18 |
19 | Don't hesitate to reach out if you've found an issue.
20 |
--------------------------------------------------------------------------------
/docs/guides/http-proxies.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: http-proxies
3 | title: HTTP proxies
4 | sidebar_label: HTTP proxies
5 | ---
6 |
7 | **HTTP proxies** is a feature of the HTTP [data service](../main-concepts/what-are-data-services.md).
8 |
9 | HTTP proxies allow you to intercept `http` data service calls within fetches and potentially do something else.
10 |
11 | You can define an HTTP proxy by calling `createHttpProxy` from ReSift.
12 |
13 | A common use case of HTTP proxies is to use them for mocking an HTTP server. We use HTTP proxies to power the demos in these docs.
14 |
15 | `mockApi.js`
16 |
17 | ```js
18 | import { createHttpProxy } from 'resift';
19 |
20 | export const mockPeopleEndpoint = createHttpProxy(
21 | '/people/:personId',
22 | async ({ match, requestParams, http }) => {
23 | // ...
24 | },
25 | );
26 | ```
27 |
28 | After you create your proxy, add it to the `proxies` array when you create the HTTP service:
29 |
30 | ```js
31 | import { createDataService, createHttpService } from 'resift';
32 | import { mockPeopleEndpoint } from './mockApi';
33 |
34 | const http = createHttpService({
35 | proxies: [mockPeopleEndpoint],
36 | });
37 |
38 | const dataService = createDataService({
39 | services: { http },
40 | onError: (e) => {
41 | throw e;
42 | },
43 | });
44 |
45 | export default dataService;
46 | ```
47 |
48 | ## `createHttpProxy` API
49 |
50 | When you create an HTTP proxy, you provide a path (a string) or [path options](https://reacttraining.com/react-router/web/api/matchPath/props). This argument determines whether or not the proxy will run. The HTTP service will iterate through the proxies until it finds a match. The first match found will be the proxy it uses. If an `http` call matches this path, the second argument, the handler, will run.
51 |
52 | The matching algorithm is a blatant copy/paste of `react-router`'s [`matchPath`](https://reacttraining.com/react-router/web/api/matchPath)
53 |
54 | In the handler, you can destructure: `requestParams`, `http`, `match`, and [more](../api/create-http-proxy.md#httpproxyparams)
55 |
56 | - The `requestParams` are the parameters the caller (in the data service) passes to the `http` call
57 | - `http` is the original `http` service. You can call it to make an actual HTTP request.
58 | - `match` is the result of react-router's [`matchPath`](https://reacttraining.com/react-router/web/api/matchPath) function. It contains the match params in `match.params`
59 |
60 | Here are some example mock endpoints from the [ReSift Notes example](../examples/resift-notes.md).
61 |
62 | ```js
63 | import { createHttpProxy } from 'resift';
64 | import shortId from 'shortid';
65 | import delay from 'delay';
66 | import moment from 'moment';
67 | import { stripIndent } from 'common-tags';
68 |
69 | const waitTime = 1000;
70 |
71 | let noteData = [
72 | {
73 | id: 'sJxbrzBcn',
74 | content: stripIndent`
75 | # This is a note
76 | `,
77 | updatedAt: moment().toISOString(),
78 | },
79 | ];
80 |
81 | export const notes = createHttpProxy({ path: '/notes', exact: true }, async ({ requestParams }) => {
82 | await delay(waitTime);
83 |
84 | const { method, data } = requestParams;
85 |
86 | if (method === 'GET') {
87 | // send a shallow copy just in case.
88 | // with a real API, the object returned would always be fresh references
89 | return [...noteData];
90 | }
91 |
92 | if (method === 'POST') {
93 | const newNote = {
94 | ...data,
95 | id: shortId(),
96 | };
97 | noteData.push(newNote);
98 |
99 | return newNote;
100 | }
101 | });
102 |
103 | export const note = createHttpProxy('/notes/:id', async ({ requestParams, match }) => {
104 | await delay(waitTime);
105 |
106 | const { method, data } = requestParams;
107 | const { id } = match.params;
108 |
109 | if (method === 'GET') {
110 | const note = noteData.find((note) => note.id === id);
111 | if (!note) throw new Error('Not Found');
112 |
113 | return note;
114 | }
115 |
116 | if (method === 'PUT') {
117 | const index = noteData.findIndex((note) => note.id === id);
118 | if (index === -1) throw new Error('Not Found');
119 |
120 | noteData[index] = data;
121 | return data;
122 | }
123 |
124 | if (method === 'DELETE') {
125 | const note = noteData.find((note) => note.id === id);
126 | if (!note) throw new Error('Not Found');
127 |
128 | noteData = noteData.filter((note) => note.id !== id);
129 | return undefined;
130 | }
131 | });
132 | ```
133 |
--------------------------------------------------------------------------------
/docs/guides/resift-vs-apollo-relay.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: resift-vs-apollo-relay
3 | title: ReSift vs Apollo and Relay
4 | sidebar_label: ReSift vs Apollo and Relay
5 | ---
6 |
7 | ## What are Apollo and Relay?
8 |
9 | Similar to ReSift, [the Apollo client](https://www.apollographql.com/docs/react/) and [Relay](https://relay.dev/) are state management libraries for fetches. They have the same goals as ReSift. However, the biggest distinction between Apollo and Relay and ReSift is that Apollo and Relay are [GraphQL](https://graphql.org/) clients. This means that you must to use [GraphQL](https://graphql.org/) if you want to use Apollo or Relay.
10 |
11 | ## ReSift vs Apollo/Relay
12 |
13 | ReSift has the same responsibilities as the Apollo client and Relay. The Apollo client, Relay, and ReSift function as libraries that handle fetching, caching, and reporting the status of inflight requests. They're are all global state containers that will hold the state of your data fetches outside of your component tree.
14 |
15 | > ReSift is has one major advantage: **ReSift does _not_ require GraphQL**.
16 |
17 | ReSift is agnostic on how to get data. You can use traditional RESTful services, local or async storage, and [even GraphQL too](../main-concepts/what-are-data-services.md#writing-a-data-service).
18 |
19 | However, since Apollo and Relay are GraphQL only, they can leverage data schemas from GraphQL to normalize incoming data inside their caching solutions automatically. This means that if you update a piece of information using Apollo or Relay, that piece of information will update anywhere it's used.
20 |
21 | > This is the major trade off of ReSift: because ReSift doesn't have schema information, **ReSift _can't_ automatically normalize your data**.
22 |
23 | Instead of requiring schemas, **ReSift allows you specify how a piece of information should be updated when another piece of information changes**. See the [Making state consistent](../main-concepts/making-state-consistent.md#merges-across-namespaces) doc for more info.
24 |
25 | We believe this is sufficient for creating data-driven applications and a great alternative to Apollo and Relay.
26 |
27 | (However, if you are using GraphQL, we recommend you just use Apollo or Relay.)
28 |
29 | ## Comparison chart
30 |
31 | | Feature 👇 | ReSift | Relay | Apollo |
32 | | ----------------------------- | ------ | ----- | ------ |
33 | | Global cache/global injection | ✅ | ✅ | ✅ |
34 | | Automatic pending status | ✅ | ✅ | ✅ |
35 | | Automatic normalization | 🔴 | ✅ | ✅ |
36 | | Compatible with REST | ✅ | 🔴 | 🔴 |
37 |
--------------------------------------------------------------------------------
/docs/guides/usage-with-classes.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: usage-with-classes
3 | title: Usage with classes
4 | sidebar_label: Usage with classes
5 | ---
6 |
7 | > Disclaimer: ReSift is meant to be used with function components because we use hooks to drive our APIs.
8 | >
9 | > However, we acknowledge that you can't rewrite you application. This guide is meant to show you how to use ReSift with you existing components for compatibility's sake. **If you're creating new experiences, we strongly advise you to use function components.**
10 |
11 | In order to use ReSift's hooks with your existing class components, we recommend using the library [`hocify`](https://github.com/ricokahler/hocify) aka higher-order-component-ify.
12 |
13 | This library lets you use hooks within class components by wrapping your hooks with a function component internally.
14 |
15 | See this example:
16 |
17 | ```js
18 | import { Component } from 'react';
19 | import { useStatus, Guard } from 'resift';
20 | import hocify from 'hocify';
21 | import makeGetPerson from './makeGetPerson';
22 |
23 | // 👇 these are incoming props
24 | const withFetchAndStatus = hocify(({ personId }) => {
25 | const getPerson = makeGetPerson(personId);
26 | const status = useStatus(getPerson);
27 |
28 | // these will be injected as props
29 | // 👇
30 | return { getPerson, status };
31 | });
32 |
33 | class MyExistingComponent extends Component {
34 | render() {
35 | const { getPerson, status } = this.props;
36 |
37 | return (
38 |
39 | {isLoading(status) && (
40 |
41 |
42 |
43 | )}
44 |
{(person) => {person.name}
}
45 |
46 | );
47 | }
48 | }
49 |
50 | export default withFetchAndStatus(MyExistingComponent);
51 | ```
52 |
--------------------------------------------------------------------------------
/docs/guides/usage-with-redux.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: usage-with-redux
3 | title: Usage with Redux
4 | sidebar_label: Usage with Redux
5 | ---
6 |
7 | > The installation is a bit different if you're already using Redux. **Please follow this guide _only_ if you're already using Redux.** If you're not please follow the [normal installation](../introduction/installation.md).
8 | >
9 | > We'll most likely be moving away from a Redux implementation. [See here.](https://github.com/JustSift/ReSift/issues/32)
10 |
11 | Create the data service as you normally…
12 |
13 | ```js
14 | import { createDataService } from 'resift';
15 |
16 | const dataService = createDataService(/* ... */);
17 |
18 | export default dataService;
19 | ```
20 |
21 | …but instead of adding in the ReSift provider, add the data service as a middleware.
22 |
23 | ```js
24 | import { createStore, applyMiddleware } from 'redux';
25 | import dataService from './dataService';
26 | import rootReducer from './rootReducer';
27 |
28 | const store = createStore(rootReducer, {}, applyMiddleware(dataService));
29 | ```
30 |
31 | Lastly, add the reducer `dataService` to the root reducer from `dataServiceReducer`:
32 |
33 | ```js
34 | import { dataServiceReducer } from 'resift';
35 | import { combineReducers } from 'redux';
36 |
37 | const rootReducer = combineReducers({
38 | // ...
39 | dataService: dataServiceReducer,
40 | // ...
41 | });
42 |
43 | export default rootReducer;
44 | ```
45 |
46 | That's it!
47 |
--------------------------------------------------------------------------------
/docs/guides/usage-with-typescript.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: usage-with-typescript
3 | title: Usage with TypeScript
4 | sidebar_label: Usage with TypeScript
5 | ---
6 |
7 | We love TypeScript at Sift. We use it in our production apps and we care deeply about getting the developer experience of TypeScript right within ReSift.
8 |
9 | Proper usage with TypeScript requires that you type your [fetch factories](../main-concepts/whats-a-fetch.md#defining-a-fetch) and [data services](../main-concepts/what-are-data-services.md).
10 |
11 | ## Getting the types of the data services
12 |
13 | When creating your data services, use the typing helper `ServicesFrom`, to get the type of the services from your services object.
14 |
15 | ```ts
16 | import { createDataService, createHttpService, ServicesFrom } from 'resift';
17 |
18 | const http = createHttpService(/* ... */);
19 | const services = { http };
20 |
21 | export type Services = ServicesFrom;
22 |
23 | const dataService = createDataService({
24 | services,
25 | onError: (e) => {
26 | throw e;
27 | },
28 | });
29 |
30 | export default dataService;
31 | ```
32 |
33 | You can then import this where you define your fetch factories.
34 |
35 | ## Setting types for fetch factories
36 |
37 | In order to add the correct type to the fetch factories, you need to use the helper `typedFetchFactory`. This helper allows you to set a type for your fetch. This type will be used by the `useData` and the `Guard` components.
38 |
39 | `makeUpdatePerson.ts`
40 |
41 | ```ts
42 | import { defineFetch, typedFetchFactory } from 'resift';
43 | import { Services } from './dataService'; // this was created from the above example
44 |
45 | interface Person {
46 | id: string;
47 | name: string;
48 | }
49 |
50 | const makeUpdatePerson = defineFetch({
51 | displayName: 'Update Person',
52 | // add types like so 👇
53 | make: (personId: string) => ({
54 | // add types like so 👇
55 | request: (updatedPerson: Person) => ({ http }: Services) =>
56 | http({
57 | method: 'GET',
58 | route: `/people/${personId}`,
59 | }),
60 | }),
61 | });
62 |
63 | // then export with `typedFetchFactory` with the type of the data
64 | // 👇
65 | export default typedFetchFactory()(makeUpdatePerson);
66 | ```
67 |
68 | This fetch factory asserts that the shape of the data is the shape of the `Person`.
69 |
70 | When you go to use this fetch factory, it should just work.
71 |
72 | ```tsx
73 | import React from 'react';
74 |
75 | import makeGetPerson from './makeGetPerson';
76 | import makeUpdatePerson from './makeUpdatePerson';
77 | import getPersonFromEvent from './getPersonFromEvent';
78 |
79 | interface Props {
80 | id: string;
81 | }
82 |
83 | function Component({ id }: Props) {
84 | // typescript will enforce that this arg is a string
85 | // 👇
86 | const updatePerson = makeUpdatePerson(id);
87 | const getPerson = makeUpdatePerson(id);
88 |
89 | const handleUpdate = (e: React.FormEvent) => {
90 | const person = getPersonFromEvent(e);
91 |
92 | // typescript will enforce that this is the correct type
93 | // 👇
94 | dispatch(updatePerson(person));
95 | };
96 |
97 | // typescript will type this as `Person | null`
98 | const person = useData(getPerson);
99 |
100 | return (
101 |
102 | {(person) => {
103 | // 👆 typescript knows the shape of this
104 | return person.name;
105 | }}
106 |
107 | );
108 | }
109 |
110 | export default Component;
111 | ```
112 |
--------------------------------------------------------------------------------
/docs/introduction/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: installation
3 | title: Installation
4 | sidebar_label: Installation
5 | ---
6 |
7 | ## Installation
8 |
9 | ```
10 | npm i resift redux react-redux
11 | ```
12 |
13 | or if you're using yarn
14 |
15 | ```
16 | yarn add resift redux react-redux
17 | ```
18 |
19 | > Redux 4 and React-Redux >= 7.1 are required peer dependencies to ReSift, [However, we may remove these dependencies in the future.](https://github.com/JustSift/ReSift/issues/32#issuecomment-547537720)
20 |
21 | > ⚠️ If you're already using Redux in your project, [follow this guide here](../guides/usage-with-redux.md).
22 |
23 | ## Creating the data service and HTTP service
24 |
25 | Create a file called `dataService`. Create a data service instance and then export it.
26 |
27 | `dataService.js`
28 |
29 | ```js
30 | import { createHttpService, createDataService } from 'resift';
31 |
32 | const http = createHttpService({
33 | // if all your endpoints share the same prefix, you can prefix them all here
34 | prefix: '/api',
35 | // if you need to add headers (for auth etc), you can do so using `getHeaders`
36 | getHeaders: () => {
37 | const token = localStorage.getItem('auth_token');
38 |
39 | return {
40 | Authorization: `Bearer ${token}`,
41 | };
42 | },
43 | });
44 |
45 | const dataService = createDataService({
46 | services: { http },
47 | onError: (e) => {
48 | // see https://resift.org/docs/main-concepts/error-handling for more info
49 | // on how to handle errors in resift.
50 | throw e;
51 | },
52 | });
53 |
54 | export default dataService;
55 | ```
56 |
57 | ## Adding the ``
58 |
59 | Lastly, wrap your application in the `ResiftProvider`. This will enable all the hooks APIs.
60 |
61 | `App.js`
62 |
63 | ```js
64 | import React from 'react';
65 | import { ResiftProvider } from 'resift';
66 | import RestOfYourApplication from '...';
67 |
68 | // import the data service we just created
69 | import dataService from './dataService';
70 |
71 | function App() {
72 | return (
73 |
74 |
75 |
76 | );
77 | }
78 | ```
79 |
--------------------------------------------------------------------------------
/docs/main-concepts/custom-hooks.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: custom-hooks
3 | title: Custom hooks
4 | sidebar_label: Custom hooks
5 | ---
6 |
7 | The last concept we want to highlight is [custom hooks](https://reactjs.org/docs/hooks-custom.html).
8 |
9 | [React hooks](https://reactjs.org/docs/hooks-intro.html) are great because they allow you to build on top of other existing hooks. They allow you to generalize many of the common tasks you'll do in your application.
10 |
11 | This is why ReSift's APIs are hooks based — **so you can build on top of them** and combine them with other custom hooks.
12 |
13 | ---
14 |
15 | Below is a simple demo of a list. When you select an item from the list, the details show up on the right.
16 |
17 | The data in this list is powered by the custom hook `useCurrentPerson`.
18 |
19 | `useCurrentPerson` combines ReSift's `useData` hook with React Router's [`useRouteMatch` hook](https://reacttraining.com/react-router/web/api/Hooks/useroutematch) to grab the current item using an ID from URL.
20 |
21 | `useCurrentPerson.js`
22 |
23 | ```js
24 | import { useEffect } from 'react';
25 | import { useData, useDispatch, useStatus } from 'resift';
26 | import { useRouteMatch } from 'react-router-dom';
27 | import makeGetPerson from './makeGetPerson';
28 |
29 | // this 👇 is the custom hook 🎉
30 | function useCurrentPerson() {
31 | const dispatch = useDispatch();
32 |
33 | // it uses `useRouteMatch` from `react-router`...
34 | const match = useRouteMatch('/people/:id');
35 | const id = match ? match.params.id : null;
36 | const getPerson = id ? makeGetPerson(id) : null;
37 |
38 | // ...along with `useData` from resift to join values!
39 | const data = useData(getPerson);
40 | const status = useStatus(getPerson);
41 |
42 | // it does dispatching as well
43 | useEffect(() => {
44 | if (getPerson) {
45 | dispatch(getPerson());
46 | }
47 | }, [dispatch, getPerson]);
48 |
49 | return { data, status };
50 | }
51 |
52 | export default useCurrentPerson;
53 | ```
54 |
55 | ## Demo
56 |
57 |
63 |
64 |
65 | > ⚠️ Custom hooks are great… but, as with any abstraction, it's easy to get carried away.
66 | >
67 | > **[Be deliberate, when you create any abstractions](https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction) aka [avoid hasty abstractions](https://kentcdodds.com/blog/aha-programming)!**
68 |
--------------------------------------------------------------------------------
/docs/main-concepts/making-sense-of-statuses.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: making-sense-of-statuses
3 | title: Making sense of statuses
4 | sidebar_label: Making sense of statuses
5 | ---
6 |
7 | In the previous section, we went over how to use fetches and get data out of them. You saw how `useStatus` returned _something_ and how you can pass that something into helper functions like `isLoading`:
8 |
9 | ```js
10 | function MyComponent() {
11 | const status = useStatus(getPerson);
12 |
13 | if (!isNormal(status)) return ;
14 |
15 | return; // ...
16 | }
17 | ```
18 |
19 | So what is that `status` thing?
20 |
21 | If you `console.log` it, you'll get a strange and seemingly random number.
22 |
23 | Why? Because it's [bitmask](https://dev.to/somedood/bitmasks-a-very-esoteric-and-impractical-way-of-managing-booleans-1hlf).
24 |
25 | ---
26 |
27 | A bitmask is a number that encodes multiple different boolean values in a single number.
28 |
29 | Though you might think this is over engineered, it's not!
30 |
31 | When statuses are just plain ole numbers, it saves you the pain of having to memoize them. This means that you can pass statuses into components, hooks, and more without having to worry about reference issues.
32 |
33 | ## The different kinds of statuses
34 |
35 | There are 4 different statuses in Resift:
36 |
37 | 1. `ERROR` - true when the action resulted in an error
38 | 2. `LOADING` - true when there is an inflight request for data
39 | 3. `NORMAL` - true when there is available data from the data service
40 | 4. `UNKNOWN` - true when none of the above are true
41 |
42 | > **Note:** these statuses (besides `UNKNOWN`) are **not mutually exclusive**. e.g. a status can be `NORMAL` _and_ `LOADING` denoting that there is an inflight request and there is data available from a previous request
43 |
44 | If you ever need to (though you probably won't), you can import these statuses values as numbers from resift like so:
45 |
46 | ```js
47 | import { ERROR, LOADING, NORMAL, UNKNOWN } from 'resift';
48 | ```
49 |
50 | ## Checking the status of a status
51 |
52 | To check if a status is `NORMAL` or is `ERROR` import the `isNormal` or `isError` from resift.
53 |
54 | ```js
55 | import { isError, isLoading, isNormal, isUnknown, useStatus, useData } from 'resift';
56 |
57 | import Spinner from '...';
58 | import ErrorView from '...';
59 | import Person from './Person';
60 |
61 | function Component() {
62 | const status = useStatus(getPerson);
63 | const person = useData(getPerson);
64 |
65 | if (isLoading(status)) return ;
66 | if (isError(status)) return ;
67 | if (isNormal(status)) return ;
68 | return null;
69 | }
70 |
71 | export default Component;
72 | ```
73 |
74 | ## Checking the status of multiple statuses
75 |
76 | You can also supply more than one status to any of the functions above to get an overall status of each status passed in.
77 |
78 | Here is the logic for each type of check:
79 |
80 | - **`isLoading`** - at least one must have `LOADING` for this to be true
81 | - **`isNormal`** - all statuses must have `NORMAL` for this to be true
82 | - **`isError`** - at least one must have `ERROR` for this to be true
83 | - **`isUnknown`** - all statuses must be `UNKNOWN` for this to be true
84 |
85 | ```js
86 | import { useStatus, isLoading } from 'resift';
87 |
88 | import { fetchPerson } from 'store/people';
89 |
90 | function Container() {
91 | const status123 = useStatus(getPerson123);
92 | const status456 = useStatus(getPerson456);
93 |
94 | const atLeastOneLoading = isLoading(status123, status456);
95 | const allAreNormal = isNormal(status123, status456);
96 | const atLeastOneError = isError(status123, status456);
97 | const allAreUnknown = isUnknown(status123, status456);
98 |
99 | // ...
100 | }
101 | ```
102 |
103 | ## Combining statuses
104 |
105 | You can combine statuses using the same logic as above with the function `combineStatuses` from `'resift'`.
106 |
107 | ```js
108 | import { combineStatuses, useStatus } from 'resift';
109 |
110 | function MyComponent() {
111 | const getPerson123 = makeGetPerson('123');
112 | const getPerson456 = makeGetPerson('456');
113 |
114 | const status123 = useStatus(getPerson123);
115 | const status456 = useStatus(getPerson456);
116 |
117 | const status = combineStatuses(status123, status456);
118 |
119 | // ...
120 | }
121 | ```
122 |
123 | ## The behavior of shared fetches
124 |
125 | You might have noticed that when you have a shared fetch, its status is affected by any fetch instances that are loading.
126 |
127 | So for example, if you had two fetches that were shared e.g. `getPerson` and `updatePerson`, then when the `updatePerson` fetch has a status of `LOADING` so does the `getPerson` fetch. This is intensional. However, if you would only like the status of your fetch to be the fetch in question (vs a combined status of all the related shared fetches), then you pass the option `isolatedStatus: true` to the options fo `useStatus`.
128 |
129 | ```js
130 | function MyComponent() {
131 | const updateStatus = useStatus(updatePerson, {
132 | isolatedStatus: true, // 👈 this tells resift to only consider the status of `updatePerson`
133 | });
134 |
135 | if (isLoading(updateStatus)) {
136 | // ...
137 | }
138 |
139 | return /* ... */;
140 | }
141 | ```
142 |
--------------------------------------------------------------------------------
/docs/main-concepts/what-are-data-services.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: what-are-data-services
3 | title: What are data services?
4 | sidebar_label: What are data services?
5 | ---
6 |
7 | ## What do data services do?
8 |
9 | Data services allow you to write custom ways to get data and put it into ReSift's cache.
10 |
11 | The `http` service is an example of a data service. You create this service by calling `createHttpService` from ReSift and then put the resulting service into the `services` property of the `createDataService` call.
12 |
13 | ```js
14 | import { createHttpService, createDataService } from 'resift';
15 |
16 | // 👇 this is a data service
17 | const http = createHttpService();
18 |
19 | const dataService = createDataService({
20 | // 👇 what you name this matters (see below)
21 | services: { http },
22 | onError: (e) => {
23 | throw e;
24 | },
25 | });
26 |
27 | export default dataService;
28 | ```
29 |
30 | Because, the `http` service was added to the `services` object with the name `http`, it's available to use within fetch factories.
31 |
32 | ```js
33 | import { defineFetch } from 'resift';
34 |
35 | const makeGetPerson = defineFetch({
36 | displayName: 'Get Person',
37 | make: (personId) => ({
38 | // 👇 we can "import" this with
39 | // this name because we passed
40 | // in `http` into the `services` above
41 | request: () => ({ http }) =>
42 | http({
43 | method: 'GET',
44 | route: `/people/${personId}`,
45 | }),
46 | }),
47 | });
48 | ```
49 |
50 | If we changed the name of the service to something else besides `http`, then that's the name we'd have to use when "importing" (well destructuring really) that service.
51 |
52 | ## Writing a data service
53 |
54 | The following example defines a function `createGraphQlService` that will return a service that makes GraphQL requests.
55 |
56 | In order to write a data service, you must define a curried function. The first set of parameters (`onCancel` and `getCanceled`) are injected by ReSift. These methods are for the cancellation mechanism. The second set of parameters are injected by the callers of your services.
57 |
58 | It takes in a `rootEndpoint` for configuration and returns the curried function defining what the services does.
59 |
60 | ```js
61 | import { CanceledError } from 'resift';
62 |
63 | function createGraphQlService({ rootEndpoint }) {
64 | return ({ onCancel, getCanceled }) => async ({ query, operationName, variables }) => {
65 | if (getCanceled()) {
66 | throw new CanceledError();
67 | }
68 |
69 | onCancel(() => {
70 | // this function will be called when this request is canceled.
71 | // since we're using `window.fetch`, it can't be canceled so this does nothing for now
72 | });
73 |
74 | const response = await fetch(rootEndpoint, {
75 | method: 'POST',
76 | headers: {
77 | 'Content-Type': 'application/json',
78 | },
79 | body: JSON.stringify({
80 | query,
81 | operationName,
82 | variables: JSON.stringify(variables),
83 | }),
84 | });
85 |
86 | const json = await response.json();
87 | const { errors, data } = json;
88 |
89 | // add this manual check for errors and throw if there are any
90 | if (errors) {
91 | const error = new Error('GraphQL call returned with errors');
92 | error.errors = errors;
93 | throw error;
94 | }
95 |
96 | return data;
97 | };
98 | }
99 |
100 | export default createGraphQlService;
101 | ```
102 |
103 | When writing a data service, you should always:
104 |
105 | - return a function that takes in the cancellation mechanism parameters (`onCancel`, `getCanceled`)
106 | - check to see if the request was cancelled before continuing flow
107 | - throw the `CancelledError` if the request has been cancelled
108 | - check to see if there is an error and `throw` if there is one
109 | - return the results of the request. These results will be saved to ReSift's cache.
110 |
111 | ---
112 |
113 | Check out this example of writing a custom GraphQL service. It uses a third-party QraphQLHub API to get data.
114 |
115 |
121 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "resift",
3 | "version": "0.2.0",
4 | "private": true,
5 | "description": "A state management for data fetches in React",
6 | "keywords": [
7 | "react",
8 | "relay",
9 | "hooks",
10 | "fetch"
11 | ],
12 | "homepage": "https://resift.org/",
13 | "bugs": {
14 | "url": "https://github.com/JustSift/ReSift/issues"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/JustSift/ReSift.git"
19 | },
20 | "license": "MIT",
21 | "author": "team@justsift.com",
22 | "sideEffects": false,
23 | "main": "index.js",
24 | "scripts": {
25 | "analyze-bundle": "npx webpack --profile --json > stats.json && npx webpack-bundle-analyzer stats.json",
26 | "build": "node ./scripts/build.js",
27 | "check-types": "npx tsc",
28 | "clean": "rm -rf node_modules && npm i",
29 | "lint": "eslint src",
30 | "publish": "npx jest && npx eslint src && npm run check-types && npm run build && cd build && npm publish && cd ..",
31 | "test": "jest"
32 | },
33 | "browserslist": [
34 | ">0.2%",
35 | "not dead",
36 | "not ie < 11",
37 | "not op_mini all"
38 | ],
39 | "dependencies": {
40 | "@babel/cli": "^7.11.5",
41 | "path-to-regexp": "^1.8.0",
42 | "@types/superagent": "^4.1.10",
43 | "delay": "^4.4.0",
44 | "shortid": "^2.2.15",
45 | "superagent": "^6.0.0"
46 | },
47 | "devDependencies": {
48 | "@babel/plugin-transform-runtime": "^7.11.5",
49 | "@rollup/plugin-babel": "^5.2.0",
50 | "@rollup/plugin-node-resolve": "^9.0.0",
51 | "@babel/core": "^7.11.5",
52 | "@babel/plugin-proposal-class-properties": "^7.10.4",
53 | "@babel/plugin-proposal-optional-chaining": "^7.11.0",
54 | "@babel/preset-env": "^7.11.0",
55 | "@babel/preset-react": "^7.10.4",
56 | "@babel/preset-typescript": "^7.10.4",
57 | "@types/react": "^16.9.49",
58 | "@typescript-eslint/eslint-plugin": "^1.13.0",
59 | "@typescript-eslint/parser": "^1.13.0",
60 | "babel-eslint": "^10.1.0",
61 | "common-tags": "^1.8.0",
62 | "eslint": "^7.8.0",
63 | "eslint-config-react-app": "^5.0.1",
64 | "eslint-plugin-flowtype": "^3.13.0",
65 | "eslint-plugin-import": "^2.22.0",
66 | "eslint-plugin-jest": "^23.20.0",
67 | "eslint-plugin-jsx-a11y": "^6.3.1",
68 | "eslint-plugin-react": "^7.20.6",
69 | "eslint-plugin-react-hooks": "^1.7.0",
70 | "express": "^4.17.1",
71 | "jest": "^26.4.2",
72 | "lodash": "^4.17.20",
73 | "markdown-table": "^2.0.0",
74 | "node-fetch": "^2.6.1",
75 | "prettier": "^2.1.1",
76 | "react": "^16.13.1",
77 | "react-dom": "^16.13.1",
78 | "react-redux": "^7.2.1",
79 | "react-test-renderer": "^16.13.1",
80 | "redux": "^4.0.5",
81 | "rollup": "^2.23.1",
82 | "typescript": "^3.9.7"
83 | },
84 | "peerDependencies": {
85 | "@types/react": "^16.9.49",
86 | "react": "^16.8.0",
87 | "redux": "^4.0.5",
88 | "react-redux": "^7.2.1"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | printWidth: 100,
4 | trailingComma: 'all',
5 | };
6 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | // rollup.config.js
2 | import babel from '@rollup/plugin-babel';
3 | import resolve from '@rollup/plugin-node-resolve';
4 | import packageJson from './package.json';
5 |
6 | const extensions = ['.js', '.ts', '.tsx'];
7 |
8 | // gets the dependencies from the package json so that they can be marked as
9 | // external in rollup
10 | const external = [
11 | /^@babel\/runtime/,
12 | ...Object.keys(packageJson.dependencies),
13 | ...Object.keys(packageJson.peerDependencies),
14 | ];
15 |
16 | export default [
17 | {
18 | input: './src/index.js',
19 | output: {
20 | file: './build/index.js',
21 | format: 'umd',
22 | sourcemap: true,
23 | name: 'Resift',
24 | globals: {
25 | react: 'React',
26 | redux: 'Redux',
27 | 'react-redux': 'ReactRedux',
28 | shortid: 'shortId',
29 | superagent: 'superagent',
30 | 'path-to-regexp': 'pathToRegexp',
31 | },
32 | },
33 | plugins: [
34 | resolve({
35 | extensions,
36 | }),
37 | babel({
38 | babelrc: false,
39 | presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'],
40 | babelHelpers: 'bundled',
41 | extensions,
42 | }),
43 | ],
44 | external,
45 | },
46 | {
47 | input: './src/index.js',
48 | output: {
49 | file: './build/index.esm.js',
50 | format: 'esm',
51 | sourcemap: true,
52 | },
53 | plugins: [
54 | resolve({
55 | extensions,
56 | modulesOnly: true,
57 | }),
58 | babel({
59 | babelrc: false,
60 | presets: ['@babel/preset-typescript', '@babel/preset-react'],
61 | plugins: ['@babel/plugin-transform-runtime'],
62 | babelHelpers: 'runtime',
63 | extensions,
64 | }),
65 | ],
66 | external,
67 | },
68 | ];
69 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const fs = require('fs');
3 | const path = require('path');
4 | const { promisify } = require('util');
5 | const { exec } = require('child_process');
6 |
7 | const writeFile = promisify(fs.writeFile);
8 | const readFile = promisify(fs.readFile);
9 |
10 | const root = path.resolve(__dirname, '../');
11 |
12 | const args = process.argv.slice(2);
13 |
14 | function execute(command) {
15 | return new Promise((resolve, reject) => {
16 | exec(command, (err, stdout, stderr) => {
17 | console.log(stdout);
18 | console.error(stderr);
19 |
20 | if (err) {
21 | reject(err);
22 | } else {
23 | resolve();
24 | }
25 | });
26 | });
27 | }
28 |
29 | async function createPackageJson() {
30 | const packageBuffer = await readFile(path.resolve(root, './package.json'));
31 | const packageJson = JSON.parse(packageBuffer.toString());
32 |
33 | const minimalPackage = {
34 | author: packageJson.author,
35 | version: packageJson.version,
36 | description: packageJson.description,
37 | keywords: packageJson.keywords,
38 | repository: packageJson.repository,
39 | license: packageJson.license,
40 | bugs: packageJson.bugs,
41 | homepage: packageJson.homepage,
42 | peerDependencies: packageJson.peerDependencies,
43 | dependencies: packageJson.dependencies,
44 | sideEffects: false,
45 | name: 'resift',
46 | main: 'index.js',
47 | module: 'index.esm.js',
48 | };
49 |
50 | await writeFile(
51 | path.resolve(root, './build/package.json'),
52 | JSON.stringify(minimalPackage, null, 2),
53 | );
54 | }
55 |
56 | async function build() {
57 | if (!args.includes('--no-clean')) {
58 | console.log('Cleaning…');
59 | await execute('rm -rf node_modules build && npm i');
60 | }
61 |
62 | console.log('Copying typings (rsync)…');
63 | await execute('rsync -r --include "*.d.ts" --include "*/" --exclude="*" --quiet ./src/* ./build');
64 |
65 | console.log('Checking Types (tsc)…');
66 | await execute('npx tsc');
67 |
68 | console.log('Compiling (rollup)…');
69 | await execute('npx rollup -c');
70 |
71 | console.log('Writing package.json…');
72 | await createPackageJson();
73 |
74 | console.log('Done building!');
75 | }
76 |
77 | build()
78 | .then(() => process.exit(0))
79 | .catch((err) => {
80 | console.error(err);
81 | process.exit(1);
82 | });
83 |
--------------------------------------------------------------------------------
/src/CanceledError/CanceledError.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `CanceledError`
3 | * Throw this error inside custom data services to early-stop the execution of the request body of a
4 | * fetch factory. See the [data service docs](../main-concepts/what-are-data-services.md) for more info.
5 | */
6 | export default class CanceledError extends Error {
7 | constructor(message?: string);
8 | isCanceledError: true;
9 | }
10 |
--------------------------------------------------------------------------------
/src/CanceledError/CanceledError.js:
--------------------------------------------------------------------------------
1 | function CanceledError(message) {
2 | this.message = message;
3 | this.isCanceledError = true;
4 | }
5 | CanceledError.prototype = Object.create(Error.prototype);
6 | CanceledError.prototype.name = 'CanceledError';
7 | CanceledError.prototype = new Error();
8 |
9 | export default CanceledError;
10 |
--------------------------------------------------------------------------------
/src/CanceledError/CanceledError.test.js:
--------------------------------------------------------------------------------
1 | import CanceledError from './CanceledError';
2 |
3 | test('it is an instanceof Error', () => {
4 | const canceledError = new CanceledError('test message');
5 | expect(canceledError).toBeInstanceOf(Error);
6 | });
7 |
8 | test('has the property isCanceledError', () => {
9 | const canceledError = new CanceledError('test message');
10 | expect(canceledError.isCanceledError).toBe(true);
11 | });
12 |
--------------------------------------------------------------------------------
/src/CanceledError/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './CanceledError';
2 |
--------------------------------------------------------------------------------
/src/CanceledError/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './CanceledError';
2 |
--------------------------------------------------------------------------------
/src/DeferredPromise/DeferredPromise.d.ts:
--------------------------------------------------------------------------------
1 | export default class DeferredPromise extends Promise {
2 | private _promise: Promise;
3 | resolve: (t?: T) => void;
4 | reject: (error?: any) => void;
5 | state: 'pending' | 'fulfilled' | 'rejected';
6 | }
7 |
--------------------------------------------------------------------------------
/src/DeferredPromise/DeferredPromise.js:
--------------------------------------------------------------------------------
1 | export default class DeferredPromise {
2 | constructor() {
3 | this.state = 'pending';
4 | this._promise = new Promise((resolve, reject) => {
5 | this.resolve = (value) => {
6 | this.state = 'fulfilled';
7 | resolve(value);
8 | };
9 | this.reject = (reason) => {
10 | this.state = 'rejected';
11 | reject(reason);
12 | };
13 | });
14 |
15 | this.then = this._promise.then.bind(this._promise);
16 | this.catch = this._promise.catch.bind(this._promise);
17 | this.finally = this._promise.finally.bind(this._promise);
18 | }
19 |
20 | [Symbol.toStringTag] = 'Promise';
21 | }
22 |
--------------------------------------------------------------------------------
/src/DeferredPromise/DeferredPromise.test.js:
--------------------------------------------------------------------------------
1 | import DeferredPromise from './DeferredPromise';
2 | import delay from 'delay';
3 |
4 | test('it allows resolve to be called outside of the scope of itself', async () => {
5 | const deferredPromise = new DeferredPromise();
6 |
7 | delay(0).then(() => deferredPromise.resolve('done'));
8 |
9 | const value = await deferredPromise;
10 | expect(value).toBe('done');
11 | });
12 |
13 | test('it allows reject to be called outside of the scope of itself', async () => {
14 | const deferredPromise = new DeferredPromise();
15 | const testError = new Error('test');
16 |
17 | delay(0).then(() => deferredPromise.reject(testError));
18 |
19 | let gotHere = false;
20 | try {
21 | await deferredPromise;
22 | gotHere = true;
23 | } catch (e) {
24 | expect(e).toBe(testError);
25 | } finally {
26 | expect(gotHere).toBe(false);
27 | }
28 | });
29 |
--------------------------------------------------------------------------------
/src/DeferredPromise/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './DeferredPromise';
2 |
--------------------------------------------------------------------------------
/src/DeferredPromise/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './DeferredPromise';
2 |
--------------------------------------------------------------------------------
/src/ERROR/ERROR.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `ERROR`
3 | *
4 | * The bitmask representing the `ERROR` status.
5 | *
6 | * [See Making sense of statuses for more info.](../main-concepts/making-sense-of-statuses.md)
7 | */
8 | declare const ERROR: 4;
9 | export default ERROR;
10 |
--------------------------------------------------------------------------------
/src/ERROR/ERROR.js:
--------------------------------------------------------------------------------
1 | /** "error" loading state */
2 | export default 4;
3 |
--------------------------------------------------------------------------------
/src/ERROR/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ERROR';
2 |
--------------------------------------------------------------------------------
/src/ERROR/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ERROR';
2 |
--------------------------------------------------------------------------------
/src/Guard/Guard.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FetchInstance } from '../defineFetch';
3 |
4 | /**
5 | * @docs `Guard`
6 | *
7 | * The Guard component takes in a [fetch instance](./define-fetch.md) and a
8 | * [render prop](https://reactjs.org/docs/render-props.html). This component calls your render prop
9 | * with data when the data becomes available effectively guarding you against nullish data.
10 | */
11 | declare function Guard(
12 | props: Props,
13 | ): JSX.Element;
14 |
15 | /**
16 | * @docs Guard `Props`
17 | */
18 | interface Props {
19 | /**
20 | * the [fetch](../main-concepts/whats-a-fetch.md)
21 | */
22 | fetch: FetchInstance;
23 | /**
24 | * a [render prop](https://reactjs.org/docs/render-props.html) that is invoked when there is data
25 | * available.
26 | */
27 | children: (data: Data) => JSX.Element;
28 | }
29 |
30 | export default Guard;
31 |
--------------------------------------------------------------------------------
/src/Guard/Guard.js:
--------------------------------------------------------------------------------
1 | import isNormal from '../isNormal';
2 | import useData from '../useData';
3 | import useStatus from '../useStatus';
4 |
5 | function Guard({ fetch, children }) {
6 | const data = useData(fetch);
7 | const status = useStatus(fetch);
8 |
9 | if (!isNormal(status)) return null;
10 | if (data === null) return null;
11 | if (data === undefined) return null;
12 | return children(data);
13 | }
14 |
15 | export default Guard;
16 |
--------------------------------------------------------------------------------
/src/Guard/Guard.test.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { act, create } from 'react-test-renderer';
3 | import Guard from './Guard';
4 | import defineFetch from '../defineFetch';
5 | import { createStore, applyMiddleware, combineReducers } from 'redux';
6 | import dataServiceReducer from '../dataServiceReducer';
7 | import createDataService from '../createDataService';
8 | import { Provider } from 'react-redux';
9 | import DeferredPromise from '../DeferredPromise';
10 | import { makeStatusSelector } from '../useStatus/useStatus';
11 | import useStatus from '../useStatus';
12 | import isUnknown from '../isUnknown';
13 | import delay from 'delay';
14 | import isNormal from '../isNormal';
15 | import isLoading from '../isLoading';
16 |
17 | test('it does not render if the status does not contain NORMAL', async () => {
18 | const makeGetMovie = defineFetch({
19 | displayName: 'Get Movie',
20 | make: (movieId) => ({
21 | request: () => async () => {
22 | await delay(100);
23 | return {
24 | id: movieId,
25 | name: 'Hello World',
26 | };
27 | },
28 | }),
29 | });
30 |
31 | const getMovie = makeGetMovie('movie123');
32 | const dataService = createDataService({
33 | services: {},
34 | onError: (e) => {
35 | throw e;
36 | },
37 | });
38 |
39 | const rootReducer = combineReducers({ dataService: dataServiceReducer });
40 | const store = createStore(rootReducer, {}, applyMiddleware(dataService));
41 |
42 | // first render with UNKNOWN state, expect not to render
43 | expect(isUnknown(makeStatusSelector(getMovie)(store.getState()))).toBe(true);
44 | const guardHandler = jest.fn(() => null);
45 |
46 | const first = new DeferredPromise();
47 | const second = new DeferredPromise();
48 | const third = new DeferredPromise();
49 |
50 | let calls = 0;
51 |
52 | function ExampleComponent() {
53 | const status = useStatus(getMovie);
54 |
55 | useEffect(() => {
56 | if (calls === 0) {
57 | first.resolve(status);
58 | }
59 |
60 | if (calls === 1) {
61 | second.resolve(status);
62 | }
63 |
64 | if (calls === 2) {
65 | third.resolve(status);
66 | }
67 |
68 | calls += 1;
69 | }, [status]);
70 |
71 | return ;
72 | }
73 |
74 | await act(async () => {
75 | create(
76 |
77 |
78 | ,
79 | );
80 |
81 | // first render with UNKNOWN state
82 | const firstStatus = await first;
83 | expect(isUnknown(firstStatus)).toBe(true);
84 | expect(guardHandler).not.toHaveBeenCalled();
85 |
86 | // second render with LOADING state
87 | store.dispatch(getMovie());
88 | const secondStatus = await second;
89 | expect(isLoading(secondStatus)).toBe(true);
90 | expect(guardHandler).not.toHaveBeenCalled();
91 |
92 | // third render with NORMAL state
93 | const thirdStatus = await third;
94 | expect(isNormal(thirdStatus)).toBe(true);
95 | expect(guardHandler).toHaveBeenCalled();
96 |
97 | await delay(500);
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/src/Guard/Guard.types-test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import defineFetch, { typedFetchFactory } from '../defineFetch';
3 | import Guard from './Guard';
4 |
5 | const _makeGetMovie = defineFetch({
6 | displayName: 'Get Movie',
7 | make: (movieId) => ({
8 | request: () => () => ({
9 | id: 'movie123',
10 | name: 'test',
11 | }),
12 | }),
13 | });
14 |
15 | interface Movie {
16 | id: string;
17 | name: string;
18 | }
19 |
20 | const makeGetMovie = typedFetchFactory()(_makeGetMovie);
21 |
22 | function Example() {
23 | const getMovie = makeGetMovie('movie123');
24 |
25 | return {(movie) => {movie.name}};
26 | }
27 |
--------------------------------------------------------------------------------
/src/Guard/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Guard';
2 |
--------------------------------------------------------------------------------
/src/Guard/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Guard';
2 |
--------------------------------------------------------------------------------
/src/LOADING/LOADING.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `LOADING`
3 | *
4 | * The bitmask representing the `LOADING` status.
5 | *
6 | * [See Making sense of statuses for more info.](../main-concepts/making-sense-of-statuses.md)
7 | */
8 | declare const LOADING: 2;
9 | export default LOADING;
10 |
--------------------------------------------------------------------------------
/src/LOADING/LOADING.js:
--------------------------------------------------------------------------------
1 | /** "loading" loading state */
2 | export default 2;
3 |
--------------------------------------------------------------------------------
/src/LOADING/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './LOADING';
2 |
--------------------------------------------------------------------------------
/src/LOADING/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './LOADING';
2 |
--------------------------------------------------------------------------------
/src/NORMAL/NORMAL.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `NORMAL`
3 | *
4 | * The bitmask representing the `NORMAL` status.
5 | *
6 | * [See Making sense of statuses for more info.](../main-concepts/making-sense-of-statuses.md)
7 | */
8 | declare const NORMAL: 1;
9 | export default NORMAL;
10 |
--------------------------------------------------------------------------------
/src/NORMAL/NORMAL.js:
--------------------------------------------------------------------------------
1 | /** "normal" loading state */
2 | export default 1;
3 |
--------------------------------------------------------------------------------
/src/NORMAL/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './NORMAL';
2 |
--------------------------------------------------------------------------------
/src/NORMAL/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './NORMAL';
2 |
--------------------------------------------------------------------------------
/src/ResiftProvider/ResiftProvider.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /**
4 | * @docs `ResiftProvider`
5 | *
6 | * The provider ReSift gives you that configures Redux for you.
7 | *
8 | * See [Usage with Redux](../guides/usage-with-redux) for more info.
9 | */
10 | declare const ResiftProvider: React.ComponentType;
11 |
12 | /**
13 | * @docs `Props`
14 | */
15 | interface Props {
16 | /**
17 | * Provide the data service created from [`createDataService`](./create-data-service.md)
18 | */
19 | dataService: any;
20 | /**
21 | * Use this option to suppress any warnings about external Redux contexts.
22 | */
23 | suppressOutsideReduxWarning?: boolean;
24 | children?: React.ReactNode;
25 | }
26 |
27 | export default ResiftProvider;
28 |
--------------------------------------------------------------------------------
/src/ResiftProvider/ResiftProvider.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useMemo } from 'react';
2 | import { ReactReduxContext, Provider } from 'react-redux';
3 |
4 | import { createStore as createReduxStore, combineReducers, applyMiddleware, compose } from 'redux';
5 | import dataServiceReducer from '../dataServiceReducer';
6 |
7 | /**
8 | * used to create the store if the store isn't passed down from redux context
9 | */
10 | function createStore(dataService) {
11 | const reducer = combineReducers({ dataService: dataServiceReducer });
12 |
13 | const composeEnhancers =
14 | typeof __REDUX_DEVTOOLS_EXTENSION_COMPOSE__ !== 'undefined'
15 | ? // eslint-disable-next-line no-undef
16 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__
17 | : compose;
18 |
19 | const enhancers = composeEnhancers(applyMiddleware(dataService));
20 | const store = createReduxStore(reducer, enhancers);
21 |
22 | return store;
23 | }
24 |
25 | function ResiftProvider({ children, dataService, suppressOutsideReduxWarning }) {
26 | const sawReduxContext = !!useContext(ReactReduxContext);
27 |
28 | if (process.env.NODE_ENV !== 'production') {
29 | if (sawReduxContext && !suppressOutsideReduxWarning) {
30 | console.warn(
31 | "[ResiftProvider] Saw an outside Redux context in this tree. If you're using Redux in your application, you don't need to wrap your app in the ResiftProvider. See https://resift.org/docs/guides/usage-with-redux",
32 | );
33 | }
34 | }
35 |
36 | if (!dataService) {
37 | throw new Error('[ResiftProvider] `dataService` missing from props.');
38 | }
39 |
40 | const store = useMemo(() => {
41 | return createStore(dataService);
42 | }, [dataService]);
43 |
44 | return {children};
45 | }
46 |
47 | export default ResiftProvider;
48 |
--------------------------------------------------------------------------------
/src/ResiftProvider/ResiftProvider.test.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { act, create } from 'react-test-renderer';
3 | import ResiftProvider from './ResiftProvider';
4 | import useDispatch from '../useDispatch';
5 | import DeferredPromise from '../DeferredPromise';
6 | import createDataService from '../createDataService';
7 |
8 | // see here...
9 | // https://github.com/facebook/react/issues/11098#issuecomment-370614347
10 | // ...for why these exist. not an ideal solution imo but it works
11 | beforeEach(() => {
12 | jest.spyOn(console, 'error');
13 | global.console.error.mockImplementation(() => {});
14 | });
15 |
16 | afterEach(() => {
17 | global.console.error.mockRestore();
18 | });
19 |
20 | test('creates a redux store adding the data service middleware', async () => {
21 | const gotDispatch = new DeferredPromise();
22 |
23 | function ExampleComponent() {
24 | // using dispatch because it requires getting the store from context
25 | const dispatch = useDispatch();
26 |
27 | useEffect(() => {
28 | gotDispatch.resolve(dispatch);
29 | }, [dispatch]);
30 |
31 | return null;
32 | }
33 |
34 | const dataService = createDataService({
35 | services: {},
36 | onError: () => {},
37 | });
38 |
39 | await act(async () => {
40 | create(
41 |
42 |
43 | ,
44 | );
45 |
46 | await gotDispatch;
47 | });
48 |
49 | const dispatch = await gotDispatch;
50 | expect(typeof dispatch).toBe('function');
51 | });
52 |
53 | test('throws if there is no dataService key', async () => {
54 | const gotError = new DeferredPromise();
55 | class ErrorBoundary extends React.Component {
56 | componentDidCatch(e) {
57 | gotError.resolve(e);
58 | }
59 |
60 | render() {
61 | return this.props.children;
62 | }
63 | }
64 |
65 | await act(async () => {
66 | create(
67 |
68 |
69 | ,
70 | );
71 | await gotError;
72 | });
73 |
74 | const error = await gotError;
75 | expect(error.message).toMatchInlineSnapshot(
76 | `"[ResiftProvider] \`dataService\` missing from props."`,
77 | );
78 | });
79 |
--------------------------------------------------------------------------------
/src/ResiftProvider/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ResiftProvider';
2 |
--------------------------------------------------------------------------------
/src/ResiftProvider/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ResiftProvider';
2 |
--------------------------------------------------------------------------------
/src/UNKNOWN/UNKNOWN.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `UNKNOWN`
3 | *
4 | * The `UNKNOWN` status is `0`.
5 | *
6 | * [See Making sense of statuses for more info.](../main-concepts/making-sense-of-statuses.md)
7 | */
8 | declare const UNKNOWN: 0;
9 | export default UNKNOWN;
10 |
--------------------------------------------------------------------------------
/src/UNKNOWN/UNKNOWN.js:
--------------------------------------------------------------------------------
1 | /** "unknown" loading state */
2 | export default 0;
3 |
--------------------------------------------------------------------------------
/src/UNKNOWN/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './UNKNOWN';
2 |
--------------------------------------------------------------------------------
/src/UNKNOWN/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './UNKNOWN';
2 |
--------------------------------------------------------------------------------
/src/clearFetch/clearFetch.d.ts:
--------------------------------------------------------------------------------
1 | import { FetchInstance, FetchActionMeta } from '../defineFetch';
2 |
3 | /**
4 | * Given a fetch instance, it returns an object that can be dispatched using `useDispatch` or Redux's
5 | * `store.dispatch`.
6 | *
7 | * > It's recommended to use [`useClearFetch`](./use-clear-fetch.md) instead of this function.
8 | */
9 | export default function clearFetch(fetch: FetchInstance): ClearFetchAction;
10 | export interface ClearFetchAction {
11 | type: string;
12 | meta: FetchActionMeta;
13 | }
14 | export function isClearAction(action: any): action is ClearFetchAction;
15 |
--------------------------------------------------------------------------------
/src/clearFetch/clearFetch.js:
--------------------------------------------------------------------------------
1 | // TODO: this file should be removed in favor of putting the clearFetch
2 | // action creator inline in useClearFetch
3 | import createActionType from '../createActionType';
4 | import CLEAR from '../prefixes/CLEAR';
5 |
6 | export function isClearAction(action) {
7 | if (typeof action !== 'object') return false;
8 | if (typeof action.type !== 'string') return false;
9 | return action.type.startsWith(CLEAR);
10 | }
11 |
12 | // TODO: `defineFetch` creates a memoized "fetch factory" against the key given
13 | // to it so the fetches can be compared via value equal (e.g. `===`).
14 | //
15 | // Investigate: this may be a good place to try to clear memoized fetches
16 | export default function clearFetch(fetch) {
17 | const isActionCreatorFactory = fetch?.meta?.type === 'FETCH_INSTANCE_FACTORY';
18 | if (isActionCreatorFactory) {
19 | throw new Error(
20 | '[clearFetch] you tried to pass an action creatorFactory to clearFetch. Ask rico until he write docs.',
21 | );
22 | }
23 |
24 | return {
25 | type: createActionType(CLEAR, fetch.meta),
26 | meta: fetch.meta,
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/src/clearFetch/clearFetch.test.js:
--------------------------------------------------------------------------------
1 | import defineFetch from '../defineFetch';
2 | import clearFetch, { isClearAction } from './clearFetch';
3 | import CLEAR from '../prefixes/CLEAR';
4 |
5 | jest.mock('shortid', () => () => 'test-shortid');
6 |
7 | test('it creates a clear fetch action from a fetch with a dynamic key', () => {
8 | const makePersonFetch = defineFetch({
9 | displayName: 'person fetch',
10 | make: (personId) => ({
11 | request: () => ({ exampleService }) => exampleService(personId),
12 | }),
13 | });
14 |
15 | const personFetch = makePersonFetch('person123');
16 |
17 | const clearFetchAction = clearFetch(personFetch);
18 |
19 | expect(clearFetchAction).toMatchInlineSnapshot(`
20 | Object {
21 | "meta": Object {
22 | "conflict": "cancel",
23 | "displayName": "person fetch",
24 | "fetchFactoryId": "test-shortid",
25 | "key": "key:person123",
26 | "share": undefined,
27 | "type": "FETCH_INSTANCE",
28 | },
29 | "type": "@@RESIFT/CLEAR | person fetch | test-shortid",
30 | }
31 | `);
32 | });
33 |
34 | test('it creates a clear fetch action from a fetch with a static key', () => {
35 | const makeTestFetch = defineFetch({
36 | displayName: 'test fetch',
37 | make: () => ({
38 | request: () => ({ exampleService }) => exampleService(),
39 | }),
40 | });
41 |
42 | const testFetch = makeTestFetch();
43 |
44 | const clearFetchAction = clearFetch(testFetch);
45 |
46 | expect(clearFetchAction).toMatchInlineSnapshot(`
47 | Object {
48 | "meta": Object {
49 | "conflict": "cancel",
50 | "displayName": "test fetch",
51 | "fetchFactoryId": "test-shortid",
52 | "key": "key:",
53 | "share": undefined,
54 | "type": "FETCH_INSTANCE",
55 | },
56 | "type": "@@RESIFT/CLEAR | test fetch | test-shortid",
57 | }
58 | `);
59 | });
60 |
61 | test('it throws if you try to use a dynamic key like a static key', () => {
62 | const personFetch = defineFetch({
63 | displayName: 'person fetch',
64 | make: (personId) => ({
65 | request: () => ({ exampleService }) => exampleService(personId),
66 | }),
67 | });
68 |
69 | expect(() => clearFetch(personFetch)).toThrowErrorMatchingInlineSnapshot(
70 | `"[clearFetch] you tried to pass an action creatorFactory to clearFetch. Ask rico until he write docs."`,
71 | );
72 | });
73 |
74 | describe('isClearAction', () => {
75 | test('positive path', () => {
76 | expect(isClearAction({ type: CLEAR })).toBe(true);
77 | });
78 | test('negative path', () => {
79 | expect(isClearAction(0)).toBe(false);
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/src/clearFetch/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './clearFetch';
2 | export * from './clearFetch';
3 |
--------------------------------------------------------------------------------
/src/clearFetch/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './clearFetch';
2 | export * from './clearFetch';
3 |
--------------------------------------------------------------------------------
/src/combineStatuses/combineStatuses.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `combineStatuses`
3 | *
4 | * Given the many statues, returns a single status that uses the following logic:
5 | *
6 | * - If _all_ the statuses are `UNKNOWN` then the resulting status will include `UNKNOWN`
7 | * - If _one_ of the statuses includes `LOADING` then the resulting status will include `LOADING`
8 | * - If _all_ the statuses include `NORMAL` then the resulting status will include `NORMAL`
9 | * - If _one_ of the statuses includes `ERROR` then the resulting status will include `ERROR`
10 | */
11 | export default function combineStatuses(...statues: number[]): number;
12 |
--------------------------------------------------------------------------------
/src/combineStatuses/combineStatuses.js:
--------------------------------------------------------------------------------
1 | import isNormal from '../isNormal';
2 | import isLoading from '../isLoading';
3 | import isError from '../isError';
4 | import isUnknown from '../isUnknown';
5 | import UNKNOWN from '../UNKNOWN';
6 | import NORMAL from '../NORMAL';
7 | import LOADING from '../LOADING';
8 | import ERROR from '../ERROR';
9 |
10 | export default function combineStatuses(...statues) {
11 | if (isUnknown(...statues)) return UNKNOWN;
12 |
13 | const loading = isLoading(...statues) ? LOADING : UNKNOWN;
14 | const normal = isNormal(...statues) ? NORMAL : UNKNOWN;
15 | const error = isError(...statues) ? ERROR : UNKNOWN;
16 |
17 | return loading | normal | error;
18 | }
19 |
--------------------------------------------------------------------------------
/src/combineStatuses/combineStatuses.test.js:
--------------------------------------------------------------------------------
1 | import UNKNOWN from '../UNKNOWN';
2 | import NORMAL from '../NORMAL';
3 | import ERROR from '../ERROR';
4 | import LOADING from '../LOADING';
5 |
6 | import isUnknown from '../isUnknown';
7 | import isNormal from '../isNormal';
8 | import isLoading from '../isLoading';
9 | import isError from '../isError';
10 |
11 | import combineStatuses from './combineStatuses';
12 |
13 | describe('unknown', () => {
14 | test('all must be unknown', () => {
15 | const combined = combineStatuses(UNKNOWN, UNKNOWN);
16 | expect(isUnknown(combined)).toBe(true);
17 | });
18 | test('otherwise, it is not unknown', () => {
19 | const combined = combineStatuses(UNKNOWN, LOADING, ERROR | NORMAL);
20 | expect(isUnknown(combined)).toBe(false);
21 | });
22 | });
23 |
24 | describe('normal', () => {
25 | test('all must include normal', () => {
26 | const combined = combineStatuses(NORMAL, NORMAL | ERROR, NORMAL | LOADING);
27 | expect(isNormal(combined)).toBe(true);
28 | });
29 | test('otherwise, it is not normal', () => {
30 | const combined = combineStatuses(LOADING, NORMAL | LOADING, NORMAL | ERROR);
31 | expect(isNormal(combined)).toBe(false);
32 | });
33 | });
34 |
35 | describe('error', () => {
36 | test('at least one must be error', () => {
37 | const combined = combineStatuses(NORMAL, NORMAL | ERROR, LOADING, LOADING);
38 | expect(isError(combined)).toBe(true);
39 | });
40 | test("isn't an error if none are error", () => {
41 | const combined = combineStatuses(NORMAL | LOADING, LOADING, UNKNOWN);
42 | expect(isError(combined)).toBe(false);
43 | });
44 | });
45 |
46 | describe('loading', () => {
47 | test('at least one must be loading', () => {
48 | const combined = combineStatuses(NORMAL, NORMAL, ERROR, NORMAL | LOADING);
49 | expect(isLoading(combined)).toBe(true);
50 | });
51 | test("isn't a 'loading' if none are loading", () => {
52 | const combined = combineStatuses(NORMAL | ERROR, UNKNOWN, ERROR);
53 | expect(isLoading(combined)).toBe(false);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/combineStatuses/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './combineStatuses';
2 |
--------------------------------------------------------------------------------
/src/combineStatuses/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './combineStatuses';
2 |
--------------------------------------------------------------------------------
/src/createActionType/createActionType.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `createActionType`
3 | * Given a prefix and a fetch meta, returns a `string` action type.
4 | *
5 | * To get a prefix:
6 | * ```js
7 | * import { PREFIXES } from 'resift';
8 | * // then use like `PREFIXES.SUCCESS`
9 | * ```
10 | *
11 | * > **Note**: This is an advanced API that you'll probably never use. Search in the tests for usage.
12 | *
13 | * Function signature:
14 | */
15 | export default function createActionType(
16 | prefix: string,
17 | meta: { displayName: string; fetchFactoryId: string },
18 | ): string;
19 |
--------------------------------------------------------------------------------
/src/createActionType/createActionType.js:
--------------------------------------------------------------------------------
1 | export default function createActionType(prefix, meta) {
2 | return `${prefix} | ${meta.displayName} | ${meta.fetchFactoryId}`;
3 | }
4 |
--------------------------------------------------------------------------------
/src/createActionType/createActionType.test.js:
--------------------------------------------------------------------------------
1 | import FETCH from '../prefixes/FETCH';
2 | import defineFetch from '../defineFetch';
3 |
4 | import createActionType from './createActionType';
5 |
6 | jest.mock('shortid', () => () => 'test-short-id');
7 |
8 | test('it takes in a fetch action and returns an action type string', () => {
9 | // given
10 | const makeActionCreator = defineFetch({
11 | displayName: 'example fetch',
12 | make: () => ({
13 | request: (exampleArg) => ({ exampleService }) => exampleService(exampleArg),
14 | }),
15 | });
16 | const actionCreator = makeActionCreator();
17 | const action = actionCreator('test');
18 |
19 | // when
20 | const actionType = createActionType(FETCH, action.meta);
21 |
22 | // then
23 | expect(actionType).toMatchInlineSnapshot(`"@@RESIFT/FETCH | example fetch | test-short-id"`);
24 | });
25 |
--------------------------------------------------------------------------------
/src/createActionType/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './createActionType';
2 |
--------------------------------------------------------------------------------
/src/createActionType/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './createActionType';
2 |
--------------------------------------------------------------------------------
/src/createContextFetch/createContextFetch.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FetchInstance } from '../defineFetch';
3 |
4 | /**
5 | * Context fetches are deprecated given that we'll probably switch to a complete
6 | * context implementation to support concurrent mode.
7 | */
8 | export default function createContextFetch(
9 | fetch: FetchInstance,
10 | ): {
11 | ContextFetchProvider: GlobalFetchProvider;
12 | useContextFetch: UseValueHook;
13 | ContextFetchConsumer: ContextFetchConsumer;
14 | };
15 |
16 | type GlobalFetchProvider = React.ComponentType<{ children: React.ReactNode }>;
17 |
18 | type UseValueHook = () => [Data | null, number];
19 |
20 | type ContextFetchConsumer = React.ComponentType<{
21 | children: (params: [Data | null, number]) => React.ReactNode;
22 | }>;
23 |
--------------------------------------------------------------------------------
/src/createContextFetch/createContextFetch.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, createContext, useMemo } from 'react';
2 | import useStatus from '../useStatus';
3 | import useData from '../useData';
4 |
5 | function toSnakeCase(displayName) {
6 | const split = displayName.split(' ');
7 |
8 | return split
9 | .map((s) => s.replace(/\W/g, ''))
10 | .map((s) => s.substring(0, 1).toUpperCase() + s.substring(1))
11 | .join('');
12 | }
13 |
14 | export default function createContextFetch(fetch) {
15 | const Context = createContext(null);
16 |
17 | Context.displayName = `FetchProvider${toSnakeCase(fetch.meta.displayName)}`;
18 |
19 | function ContextFetchProvider({ children }) {
20 | const value = useData(fetch);
21 | const status = useStatus(fetch);
22 |
23 | const contextValue = useMemo(() => [value, status], [value, status]);
24 |
25 | return {children};
26 | }
27 |
28 | function useContextFetch() {
29 | const contextValue = useContext(Context);
30 |
31 | if (!contextValue) {
32 | throw new Error(
33 | '[createContextFetch] could not find global fetch context. Did you forget to wrap this tree with the provider?',
34 | );
35 | }
36 |
37 | return contextValue;
38 | }
39 |
40 | function ContextFetchConsumer({ children }) {
41 | return {children};
42 | }
43 |
44 | return { ContextFetchProvider, useContextFetch, ContextFetchConsumer };
45 | }
46 |
--------------------------------------------------------------------------------
/src/createContextFetch/createContextFetch.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { act, create } from 'react-test-renderer';
3 | import defineFetch from '../defineFetch';
4 | import { Provider as ReduxProvider } from 'react-redux';
5 | import { createStore, combineReducers, applyMiddleware } from 'redux';
6 | import createDataService from '../createDataService';
7 | import createContextFetch from './createContextFetch';
8 | import dataService from '../dataServiceReducer';
9 | import DeferredPromise from '../DeferredPromise';
10 |
11 | // see here...
12 | // https://github.com/facebook/react/issues/11098#issuecomment-370614347
13 | // ...for why these exist. not an ideal solution imo but it works
14 | beforeEach(() => {
15 | jest.spyOn(console, 'error');
16 | global.console.error.mockImplementation(() => {});
17 | });
18 |
19 | afterEach(() => {
20 | global.console.error.mockRestore();
21 | });
22 |
23 | test('createContextFetch hooks', async () => {
24 | const rootReducer = combineReducers({ dataService });
25 | const handleError = jest.fn();
26 | const dataServiceMiddleware = createDataService({ onError: handleError, services: {} });
27 | const store = createStore(rootReducer, {}, applyMiddleware(dataServiceMiddleware));
28 | const gotExampleValue = new DeferredPromise();
29 |
30 | const makeFetch = defineFetch({
31 | displayName: 'Get Example Fetch',
32 | make: (id) => ({
33 | request: (x) => () => ({ exampleValue: x }),
34 | }),
35 | });
36 |
37 | const fetch = makeFetch('123');
38 |
39 | const { ContextFetchProvider, useContextFetch } = createContextFetch(fetch);
40 |
41 | function App() {
42 | return (
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
51 | function Component() {
52 | const [data] = useContextFetch();
53 |
54 | const exampleValue = data?.exampleValue;
55 |
56 | if (exampleValue) {
57 | gotExampleValue.resolve(exampleValue);
58 | }
59 |
60 | if (!data) {
61 | return null;
62 | }
63 | return {data.exampleValue}
;
64 | }
65 |
66 | act(() => {
67 | create();
68 | });
69 |
70 | await act(async () => {
71 | store.dispatch(fetch(5));
72 | const result = await gotExampleValue;
73 | expect(result).toMatchInlineSnapshot(`5`);
74 | });
75 | });
76 |
77 | test('createContextFetch hooks throws when there is no context value', async () => {
78 | const gotError = new DeferredPromise();
79 |
80 | class ErrorBoundary extends React.Component {
81 | componentDidCatch(e) {
82 | gotError.resolve(e);
83 | }
84 |
85 | render() {
86 | return this.props.children;
87 | }
88 | }
89 |
90 | const makeFetch = defineFetch({
91 | displayName: 'Example',
92 | make: () => ({
93 | request: () => () => {},
94 | }),
95 | });
96 | const fetch = makeFetch();
97 |
98 | const { useContextFetch } = createContextFetch(fetch);
99 |
100 | function ExampleComponent() {
101 | useContextFetch();
102 | return null;
103 | }
104 |
105 | await act(async () => {
106 | create(
107 |
108 |
109 | ,
110 | );
111 | await gotError;
112 | });
113 |
114 | const error = await gotError;
115 | expect(error.message).toMatchInlineSnapshot(
116 | `"[createContextFetch] could not find global fetch context. Did you forget to wrap this tree with the provider?"`,
117 | );
118 | });
119 |
120 | test('render props/no hooks API', async () => {
121 | const rootReducer = combineReducers({ dataService });
122 | const handleError = jest.fn();
123 | const dataServiceMiddleware = createDataService({ onError: handleError, services: {} });
124 | const store = createStore(rootReducer, {}, applyMiddleware(dataServiceMiddleware));
125 | const gotExampleValue = new DeferredPromise();
126 |
127 | const makeFetch = defineFetch({
128 | displayName: 'Get Example Fetch',
129 | make: (id) => ({
130 | request: (x) => () => ({ exampleValue: x }),
131 | }),
132 | });
133 |
134 | const fetch = makeFetch('123');
135 |
136 | const { ContextFetchProvider, ContextFetchConsumer } = createContextFetch(fetch);
137 |
138 | function App() {
139 | return (
140 |
141 |
142 |
143 |
144 |
145 | );
146 | }
147 |
148 | function Component() {
149 | return (
150 |
151 | {([data]) => {
152 | if (!data) {
153 | return null;
154 | }
155 |
156 | const exampleValue = data?.exampleValue;
157 |
158 | if (exampleValue) {
159 | gotExampleValue.resolve(exampleValue);
160 | }
161 |
162 | return {data.exampleValue}
;
163 | }}
164 |
165 | );
166 | }
167 |
168 | act(() => {
169 | create();
170 | });
171 |
172 | await act(async () => {
173 | store.dispatch(fetch(5));
174 | const result = await gotExampleValue;
175 | expect(result).toMatchInlineSnapshot(`5`);
176 | });
177 | });
178 |
--------------------------------------------------------------------------------
/src/createContextFetch/createContextFetch.types-test.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions */
2 | import React from 'react';
3 | import defineFetch, { typedFetchFactory } from '../defineFetch';
4 | import createContextFetch from './createContextFetch';
5 |
6 | // test with no merge result
7 | () => {
8 | interface ExampleObject {
9 | foo: string;
10 | }
11 |
12 | const _makeFetch = defineFetch({
13 | displayName: 'Get Example',
14 | make: (id: string) => ({
15 | request: (x: number) => ({ http }) =>
16 | http({
17 | method: 'GET',
18 | route: '/test',
19 | }) as Promise,
20 | }),
21 | });
22 |
23 | const makeFetch = typedFetchFactory()(_makeFetch);
24 |
25 | const fetch = makeFetch('123');
26 |
27 | const { ContextFetchProvider, useContextFetch, ContextFetchConsumer } = createContextFetch(fetch);
28 |
29 | function Component() {
30 | const [value, status] = useContextFetch();
31 |
32 | // expected error: possible null
33 | value.foo;
34 |
35 | if (!value) {
36 | throw new Error();
37 | }
38 |
39 | const shouldBeAString = value.foo;
40 | shouldBeAString as string;
41 |
42 | // status should be a number
43 | status as number;
44 |
45 |
46 | {([value, status]) => {
47 | value.foo;
48 | status as number;
49 |
50 | return ;
51 | }}
52 | ;
53 |
54 | return (
55 | <>
56 | {/* No child = expected typing error */}
57 | {/*
58 | // @ts-ignore */}
59 |
60 | {/* with child = it's all good */}
61 |
62 |
63 |
64 | >
65 | );
66 | }
67 |
68 | console.log(Component);
69 | };
70 |
--------------------------------------------------------------------------------
/src/createContextFetch/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './createContextFetch';
2 |
--------------------------------------------------------------------------------
/src/createContextFetch/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './createContextFetch';
2 |
--------------------------------------------------------------------------------
/src/createDataService/createDataService.d.ts:
--------------------------------------------------------------------------------
1 | import { FetchActionMeta } from '../defineFetch';
2 |
3 | /**
4 | * @docs `createDataService`
5 | *
6 | * Creates the data service to be feed into the `` or in
7 | * [Redux middleware](../guides/usage-with-redux.md).
8 | */
9 | export default function createDataService(params: DataServiceParams): any;
10 |
11 | /**
12 | * @docs `DataServiceParams`
13 | *
14 | * You must provide an object with the following shape into `createDataService`.
15 | * See [What are data services](../main-concepts/what-are-data-services.md) for more info.
16 | */
17 | export interface DataServiceParams {
18 | /**
19 | * Defines the shape of the data services object you can destructure in the request body of a
20 | * fetch factory.
21 | * [See here for more info.](../main-concepts/how-to-define-a-fetch#what-are-data-service-parameters)
22 | */
23 | services: { [key: string]: FetchService };
24 | /**
25 | * This callback fires when any promise `throw`s or `catch`es with an error from a fetch service.
26 | * If you're unsure how to handle this error, re-throw:
27 | * `onError: e => { throw e; }`
28 | */
29 | onError: (error: Error) => void;
30 | }
31 |
32 | /**
33 | * @docs `FetchService`
34 | *
35 | * A fetch service is a function that returns data asynchronously. This data is given to ReSift for
36 | * storage and retrieval.
37 | *
38 | * See [What are data services?](../main-concepts/what-are-data-services.md#writing-a-data-service) for more info.
39 | *
40 | * See [`createHttpService`](https://github.com/JustSift/ReSift/blob/master/src/createHttpService/createHttpService.js)
41 | * for a reference implementation of a fetch service.
42 | */
43 | export type FetchService = {
44 | (params: FetchServiceParams): T | Promise;
45 | };
46 |
47 | /**
48 | * @docs `FetchServiceParams`
49 | *
50 | * The data service provides fetches services with this object. Fetch services are expected to use
51 | * these params to implement cancellation correctly.
52 | */
53 | export interface FetchServiceParams {
54 | /**
55 | * Adds a callback listener to the cancellation mechanism. If the request is cancelled, the
56 | * callback given will be invoked.
57 | *
58 | * See [`createHttpService`](https://github.com/JustSift/ReSift/blob/master/src/createHttpService/createHttpService.js)
59 | * for a reference implementation
60 | */
61 | onCancel: (callback: () => void) => void;
62 | /**
63 | * Returns whether or not the request has been cancelled. Use this in conjunction with
64 | * [CanceledError](./canceled-error.md) to early exit when a cancellation occurs.
65 | *
66 | * See [`createHttpService`](https://github.com/JustSift/ReSift/blob/master/src/createHttpService/createHttpService.js)
67 | * for a reference implementation
68 | */
69 | getCanceled: () => boolean;
70 | }
71 |
72 | export function isSuccessAction(action: any): action is SuccessAction;
73 |
74 | export function isErrorAction(action: any): action is SuccessAction;
75 |
76 | export interface SuccessAction {
77 | /**
78 | * `@@RESIFT/SUCCESS`
79 | */
80 | type: string;
81 | /**
82 | * An object containing some information about this action. This comes from `defineFetch.js`
83 | */
84 | meta: FetchActionMeta;
85 | payload: any;
86 | }
87 |
88 | export interface ErrorAction {
89 | /**
90 | * `@@RESIFT/ERROR`
91 | */
92 | type: string;
93 | /**
94 | * An object containing some information about this action. This comes from `defineFetch.js`
95 | */
96 | meta: FetchActionMeta;
97 | /**
98 | * The error
99 | */
100 | payload: Error;
101 | error: true;
102 | }
103 |
104 | /**
105 | * @docs `ServicesFrom`
106 | * @keepGenerics
107 | *
108 | * This is a typescript only module. It's used to get the type of the services object.
109 | * [See here for more info.](../guides/usage-with-typescript.md)
110 | */
111 | export type ServicesFrom any }> = {
112 | [P in keyof ServicesObject]: ReturnType;
113 | };
114 |
--------------------------------------------------------------------------------
/src/createDataService/createDataService.js:
--------------------------------------------------------------------------------
1 | import SUCCESS from '../prefixes/SUCCESS';
2 | import ERROR from '../prefixes/ERROR';
3 | import createActionType from '../createActionType';
4 | import createStoreKey from '../createStoreKey';
5 | import { isFetchAction } from '../defineFetch';
6 | import CanceledError from '../CanceledError';
7 |
8 | export function isSuccessAction(action) {
9 | if (typeof action !== 'object') return false;
10 | if (typeof action.type !== 'string') return false;
11 | return action.type.startsWith(SUCCESS);
12 | }
13 |
14 | export function isErrorAction(action) {
15 | if (typeof action !== 'object') return false;
16 | if (typeof action.type !== 'string') return false;
17 | return action.type.startsWith(ERROR);
18 | }
19 |
20 | const requestsToCancel = new WeakSet();
21 |
22 | // function exists simply to encapsulate async/awaits
23 | export async function handleAction({ state, services, dispatch, action, getState }) {
24 | const { payload, meta } = action;
25 | const { displayName, fetchFactoryId, key, conflict } = meta;
26 |
27 | try {
28 | // `inflight` is the `payload` function of the fetch. it will be defined if there is an action
29 | // that is currently inflight.
30 | const storeKey = createStoreKey(displayName, fetchFactoryId);
31 | const inflight = state?.actions?.[storeKey]?.[key]?.inflight;
32 |
33 | if (inflight) {
34 | // if the `conflict` key part of `defineFetch` was set to `ignore`, and there was an
35 | // existing `inflight` request, that means we should simply early return and "ignore" the
36 | // incoming request
37 | if (conflict === 'ignore') return;
38 |
39 | // otherwise we should cancel the current request and continue the current request
40 | inflight.cancel();
41 | requestsToCancel.add(inflight);
42 | }
43 |
44 | const dispatchService = (action) => {
45 | if (payload.getCanceled()) {
46 | throw new CanceledError();
47 | }
48 | return dispatch(action);
49 | };
50 |
51 | const getStateService = () => {
52 | if (payload.getCanceled()) {
53 | throw new CanceledError();
54 | }
55 | return getState();
56 | };
57 |
58 | // goes through all the services and applies the cancellation mechanism
59 | const servicesWithCancel = Object.entries(services).reduce(
60 | (services, [serviceKey, service]) => {
61 | services[serviceKey] = service({
62 | getCanceled: payload.getCanceled,
63 | onCancel: payload.onCancel,
64 | });
65 | return services;
66 | },
67 | // start with the `dispatch` service and `getState` service (resift provides these by default)
68 | {
69 | dispatch: dispatchService,
70 | getState: getStateService,
71 | },
72 | );
73 |
74 | // this try-catch set is only to ignore canceled errors
75 | // it re-throws any other error
76 | try {
77 | const resolved = await payload(servicesWithCancel);
78 | // since we can't cancel promises, we'll just check if the function was canceled and then
79 | // not dispatch success effectively "canceling" it
80 | if (requestsToCancel.has(payload)) return;
81 |
82 | const successAction = {
83 | type: createActionType(SUCCESS, meta),
84 | meta,
85 | payload: resolved,
86 | };
87 |
88 | dispatch(successAction);
89 | } catch (error) {
90 | // it's possible that the `payload` function would reject because of a canceled request.
91 | // in this case, we'll ignore the request because we were gonna ignore it anyway
92 | if (error.isCanceledError) return;
93 | if (requestsToCancel.has(payload)) return;
94 |
95 | throw error;
96 | }
97 | } catch (error) {
98 | const errorAction = {
99 | type: createActionType(ERROR, meta),
100 | meta,
101 | payload: error,
102 | error: true,
103 | };
104 |
105 | dispatch(errorAction);
106 |
107 | throw error; // this allows the `onError` callback to fire below
108 | }
109 | }
110 |
111 | export default function createDataService({ services, onError }) {
112 | if (!services) throw new Error('`services` key required');
113 | if (!onError) throw new Error('`onError` callback required');
114 |
115 | return (store) => (next) => (action) => {
116 | if (!isFetchAction(action)) {
117 | return next(action);
118 | }
119 |
120 | const actionPromise = handleAction({
121 | state: store.getState().dataService,
122 | services,
123 | dispatch: store.dispatch,
124 | action,
125 | getState: store.getState,
126 | }).catch((e) => {
127 | onError(e);
128 | // when awaiting dispatch...
129 | // 👇 this is the error that will propagate to the awaiter
130 | throw e;
131 | });
132 |
133 | next(action);
134 |
135 | return actionPromise;
136 | };
137 | }
138 |
--------------------------------------------------------------------------------
/src/createDataService/createDataService.types-test.ts:
--------------------------------------------------------------------------------
1 | import createHttpService from '../createHttpService';
2 | import { ServicesFrom } from './createDataService';
3 | import defineFetch from '../defineFetch';
4 |
5 | const http = createHttpService({});
6 |
7 | const services = { http };
8 |
9 | type Services = ServicesFrom;
10 |
11 | const makeGetThing = defineFetch({
12 | displayName: 'Get Thing',
13 | make: (id: string) => ({
14 | request: (stuff: number) => ({ http }: Services) =>
15 | http({
16 | method: 'GET',
17 | route: `/stuff/${id}`,
18 | query: { stuff: stuff.toString() },
19 | }),
20 | }),
21 | });
22 |
--------------------------------------------------------------------------------
/src/createDataService/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './createDataService';
2 | export * from './createDataService';
3 |
--------------------------------------------------------------------------------
/src/createDataService/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './createDataService';
2 | export * from './createDataService';
3 |
--------------------------------------------------------------------------------
/src/createHttpProxy/createHttpProxy.d.ts:
--------------------------------------------------------------------------------
1 | import { MatchParams, Match } from './matchPath';
2 | import { HttpParams } from '../createHttpService';
3 |
4 | /**
5 | * @docs `createHttpProxy`
6 | * Creates an HTTP Proxy that can be used to intercept requests made with the HTTP service.
7 | *
8 | * [See this doc for more info.](../guides/http-proxies.md)
9 | */
10 | export default function createHttpProxy(
11 | // This takes in the same parameters as `matchPath` or `` from `react-router`
12 | // [See `react-router`'s docs for more info.](https://reacttraining.com/react-router/web/api/matchPath)
13 | matchParams: string | string[] | MatchParams,
14 |
15 | // ReSift will provide `params` below. Destructure it to get the desired props.
16 | handler: (params: HttpProxyParams) => any,
17 | ): HttpProxy;
18 |
19 | /**
20 | * @docs `HttpProxyParams`
21 | * @omitRequired
22 | *
23 | * The params to the handler of an HTTP proxy.
24 | */
25 | interface HttpProxyParams {
26 | /**
27 | * The match object from `matchPath` from `react-router`.
28 | *
29 | * [See `react-router`'s docs for more info.](https://reacttraining.com/react-router/web/api/match)
30 | */
31 | match: Match;
32 | /**
33 | * The `http` function that `createHttpService` creates. This is the same `http` you destructure
34 | * to use the HTTP service.
35 | */
36 | http: (requestParams: HttpParams) => Promise;
37 | /**
38 | * The parameters passed into the `http` call from the original request.
39 | */
40 | requestParams: HttpParams;
41 | /**
42 | * Any headers that were passed through from `getHeaders` in `createHttpService`
43 | */
44 | headers: any;
45 | /**
46 | * The `prefix` passed into `createHttpService`.
47 | */
48 | prefix: string;
49 | /**
50 | * The cancellation mechanism from the HTTP service.
51 | */
52 | onCancel: (subscriber: () => void) => void;
53 | /**
54 | * The cancellation mechanism from the HTTP service.
55 | */
56 | getCanceled: () => boolean;
57 | }
58 |
59 | export interface HttpProxy {
60 | matchParams: string | string[] | MatchParams;
61 | handler: (params: HttpProxyParams) => any;
62 | }
63 |
--------------------------------------------------------------------------------
/src/createHttpProxy/createHttpProxy.js:
--------------------------------------------------------------------------------
1 | /*
2 | * this function doesn't do anything more than wrap the params in an object
3 | * the reason this function exists is for a better library experience when the user is using an
4 | * editor that supports the typescript language service (e.g. vs-code, webstorm, visual studio)
5 | */
6 | export default function createHttpProxy(matchParams, handler) {
7 | return { matchParams, handler };
8 | }
9 |
--------------------------------------------------------------------------------
/src/createHttpProxy/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './createHttpProxy';
2 | export * from './createHttpProxy';
3 |
--------------------------------------------------------------------------------
/src/createHttpProxy/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './createHttpProxy';
2 |
--------------------------------------------------------------------------------
/src/createHttpProxy/matchPath.d.ts:
--------------------------------------------------------------------------------
1 | // typings adapted from the `react-router` typings
2 | export interface MatchParams {
3 | path?: string | string[];
4 | exact?: boolean;
5 | sensitive?: boolean;
6 | strict?: boolean;
7 | }
8 |
9 | export interface Match {
10 | params: { [key: string]: string };
11 | isExact: boolean;
12 | path: string;
13 | url: string;
14 | }
15 |
16 | export default function matchPath(
17 | pathname: string,
18 | matchParams: string | string[] | MatchParams,
19 | ): Match | null;
20 |
--------------------------------------------------------------------------------
/src/createHttpProxy/matchPath.js:
--------------------------------------------------------------------------------
1 | /*
2 | * taken directly from `react-router`:
3 | * https://github.com/ReactTraining/react-router/blob/f6eae5e11ed8b26cc936b20b6ee81bafbc43edd2/packages/react-router/modules/matchPath.js
4 | */
5 | import pathToRegexp from 'path-to-regexp';
6 |
7 | const cache = {};
8 | const cacheLimit = 10000;
9 | let cacheCount = 0;
10 |
11 | function compilePath(path, options) {
12 | const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
13 | const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
14 |
15 | if (pathCache[path]) return pathCache[path];
16 |
17 | const keys = [];
18 | const regexp = pathToRegexp(path, keys, options);
19 | const result = { regexp, keys };
20 |
21 | if (cacheCount < cacheLimit) {
22 | pathCache[path] = result;
23 | cacheCount++;
24 | }
25 |
26 | return result;
27 | }
28 |
29 | /**
30 | * Public API for matching a URL pathname to a path.
31 | */
32 | function matchPath(pathname, options = {}) {
33 | if (typeof options === 'string' || Array.isArray(options)) {
34 | options = { path: options };
35 | }
36 |
37 | const { path, exact = false, strict = false, sensitive = false } = options;
38 |
39 | const paths = [].concat(path);
40 |
41 | return paths.reduce((matched, path) => {
42 | if (!path) return null;
43 | if (matched) return matched;
44 |
45 | const { regexp, keys } = compilePath(path, {
46 | end: exact,
47 | strict,
48 | sensitive,
49 | });
50 | const match = regexp.exec(pathname);
51 |
52 | if (!match) return null;
53 |
54 | const [url, ...values] = match;
55 | const isExact = pathname === url;
56 |
57 | if (exact && !isExact) return null;
58 |
59 | return {
60 | path, // the path used to match
61 | url: path === '/' && url === '' ? '/' : url, // the matched portion of the URL
62 | isExact, // whether or not we matched exactly
63 | params: keys.reduce((memo, key, index) => {
64 | memo[key.name] = values[index];
65 | return memo;
66 | }, {}),
67 | };
68 | }, null);
69 | }
70 |
71 | export default matchPath;
72 |
--------------------------------------------------------------------------------
/src/createHttpProxy/matchPath.test.js:
--------------------------------------------------------------------------------
1 | import matchPath from './matchPath';
2 |
3 | describe('matchPath', () => {
4 | describe('without path property on params', () => {
5 | it("doesn't throw an exception", () => {
6 | expect(() => {
7 | matchPath('/milkyway/eridani', { hash: 'foo' });
8 | }).not.toThrow();
9 | });
10 | });
11 |
12 | describe('with path="/"', () => {
13 | it('returns correct url at "/"', () => {
14 | const path = '/';
15 | const pathname = '/';
16 | const match = matchPath(pathname, path);
17 | expect(match.url).toBe('/');
18 | });
19 |
20 | it('returns correct url at "/somewhere/else"', () => {
21 | const path = '/';
22 | const pathname = '/somewhere/else';
23 | const match = matchPath(pathname, path);
24 | expect(match.url).toBe('/');
25 | });
26 | });
27 |
28 | describe('with path="/somewhere"', () => {
29 | it('returns correct url at "/somewhere"', () => {
30 | const path = '/somewhere';
31 | const pathname = '/somewhere';
32 | const match = matchPath(pathname, path);
33 | expect(match.url).toBe('/somewhere');
34 | });
35 |
36 | it('returns correct url at "/somewhere/else"', () => {
37 | const path = '/somewhere';
38 | const pathname = '/somewhere/else';
39 | const match = matchPath(pathname, path);
40 | expect(match.url).toBe('/somewhere');
41 | });
42 | });
43 |
44 | describe('with an array of paths', () => {
45 | it('return the correct url at "/elsewhere"', () => {
46 | const path = ['/somewhere', '/elsewhere'];
47 | const pathname = '/elsewhere';
48 | const match = matchPath(pathname, { path });
49 | expect(match.url).toBe('/elsewhere');
50 | });
51 |
52 | it('returns correct url at "/elsewhere/else"', () => {
53 | const path = ['/somewhere', '/elsewhere'];
54 | const pathname = '/elsewhere/else';
55 | const match = matchPath(pathname, { path });
56 | expect(match.url).toBe('/elsewhere');
57 | });
58 |
59 | it('returns correct url at "/elsewhere/else" with path "/" in array', () => {
60 | const path = ['/somewhere', '/'];
61 | const pathname = '/elsewhere/else';
62 | const match = matchPath(pathname, { path });
63 | expect(match.url).toBe('/');
64 | });
65 |
66 | it('returns correct url at "/somewhere" with path "/" in array', () => {
67 | const path = ['/somewhere', '/'];
68 | const pathname = '/somewhere';
69 | const match = matchPath(pathname, { path });
70 | expect(match.url).toBe('/somewhere');
71 | });
72 | });
73 |
74 | describe('with sensitive path', () => {
75 | it('returns non-sensitive url', () => {
76 | const options = {
77 | path: '/SomeWhere',
78 | };
79 | const pathname = '/somewhere';
80 | const match = matchPath(pathname, options);
81 | expect(match.url).toBe('/somewhere');
82 | });
83 |
84 | it('returns sensitive url', () => {
85 | const options = {
86 | path: '/SomeWhere',
87 | sensitive: true,
88 | };
89 | const pathname = '/somewhere';
90 | const match = matchPath(pathname, options);
91 | expect(match).toBe(null);
92 | });
93 | });
94 |
95 | describe('cache', () => {
96 | it('creates a cache entry for each exact/strict pair', () => {
97 | // true/false and false/true will collide when adding booleans
98 | const trueFalse = matchPath('/one/two', {
99 | path: '/one/two/',
100 | exact: true,
101 | strict: false,
102 | });
103 | const falseTrue = matchPath('/one/two', {
104 | path: '/one/two/',
105 | exact: false,
106 | strict: true,
107 | });
108 | expect(!!trueFalse).toBe(true);
109 | expect(!!falseTrue).toBe(false);
110 | });
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/src/createHttpService/createHttpService.d.ts:
--------------------------------------------------------------------------------
1 | import request from 'superagent';
2 | import { FetchServiceParams } from '../createDataService';
3 | import { HttpProxy } from '../createHttpProxy';
4 |
5 | /**
6 | * @docs `HttpParams`
7 | *
8 | * If you're looking for the params for the `http` calls inside a fetch factory's request, you've
9 | * found 'em!
10 | *
11 | * e.g.
12 | * ```js
13 | * const makePersonFetch = defineFetch({
14 | * displayName: 'Get Person',
15 | * make: personId => ({
16 | * request: () => ({ http }) => http({
17 | * // see below for what can go here
18 | * // 👇👇👇👇👇
19 | * }),
20 | * })
21 | * });
22 | * ```
23 | *
24 | * The HTTP Service uses [superagent](https://visionmedia.github.io/superagent/) behind the scenes.
25 | */
26 | export interface HttpParams {
27 | /**
28 | * The HTTP method to use.
29 | */
30 | method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
31 | /**
32 | * The route to use.
33 | */
34 | route: string;
35 | /**
36 | * An object representing the query parameters to serialize to the URL.
37 | *
38 | * `{foo: ['bar', 'baz'], hello: 'world'}` `==>` `?foo=bar&foo=baz&hello=world`
39 | */
40 | query?: { [key: string]: string };
41 | /**
42 | * The data to be sent to the URL. If this is an object, it will be serialized to JSON. Sending
43 | * a `FormData` object is also supported.
44 | */
45 | data?: any;
46 | /**
47 | * This is directly upstream's `.ok` method:
48 | *
49 | * http://visionmedia.github.io/superagent/#error-handling
50 | *
51 | * "Alternatively, you can use the `.ok(callback)` method to decide whether a response is an
52 | * error or not. The callback to the `ok` function gets a response and returns `true` if the
53 | * response should be interpreted as success."
54 | */
55 | ok?: (response: request.Response) => boolean;
56 | /**
57 | * You can add custom behavior to the superagent `req` using this callback.
58 | * it is added before the `req.send` method is called
59 | */
60 | req?: (request: request.SuperAgentRequest) => void;
61 | }
62 |
63 | /**
64 | * @docs `createHttpService`
65 | *
66 | * Give the required `HttpServiceParams`, this function returns an HTTP service ready to be put into
67 | * the `services` object in [`createDataService`](./create-data-service.md).
68 | */
69 | export default function createHttpService(params: HttpServiceParams): HttpService;
70 |
71 | type HttpService = (dsParams: FetchServiceParams) => (requestParams: HttpParams) => Promise;
72 |
73 | /**
74 | * @docs `HttpServiceParams`
75 | *
76 | * The params that can be passed into the `createHttpService` function.
77 | */
78 | export interface HttpServiceParams {
79 | /**
80 | * Provide a function to the HTTP service to return headers for the request. Use this to inject
81 | * any authentication tokens.
82 | */
83 | getHeaders?: (() => any) | (() => Promise);
84 | /**
85 | * Use this to prefix all requests with a certain path. e.g. `/api`
86 | */
87 | prefix?: string;
88 | /**
89 | * Use this to dynamically determine the prefix.
90 | */
91 | getPrefix?: (() => string) | (() => Promise);
92 | /**
93 | * Pass any proxies into the HTTP service here. Create the proxy with
94 | * [`createHttpProxy`](./create-http-proxy.md) first.
95 | */
96 | proxies?: HttpProxy[];
97 | }
98 |
--------------------------------------------------------------------------------
/src/createHttpService/createHttpService.js:
--------------------------------------------------------------------------------
1 | import request from 'superagent';
2 | import CanceledError from '../CanceledError';
3 | import matchPath from '../createHttpProxy/matchPath';
4 |
5 | async function getPrefix(_prefix, _getPrefix) {
6 | if (_prefix) {
7 | return _prefix;
8 | }
9 |
10 | if (typeof _getPrefix === 'function') {
11 | const prefix = await _getPrefix();
12 | return prefix;
13 | }
14 |
15 | return '';
16 | }
17 |
18 | async function http(
19 | { method, query, route, ok, req: reqHandler, data },
20 | { headers, onCancel, prefix },
21 | ) {
22 | /** @type {'get' | 'post' | 'put' | 'delete' | 'patch'} */
23 | const normalizedMethod = method.toLowerCase();
24 | const path = `${prefix}${route}`;
25 |
26 | const req = request[normalizedMethod](path).accept('application/json');
27 |
28 | const headerEntries = Object.entries(headers);
29 |
30 | // don't know there's this false positive
31 | // https://github.com/eslint/eslint/issues/12117
32 | // eslint-disable-next-line no-unused-vars
33 | for (const [headerKey, headerValue] of headerEntries) {
34 | req.set(headerKey, headerValue);
35 | }
36 |
37 | onCancel(() => {
38 | req.abort();
39 | });
40 |
41 | if (query) {
42 | req.query(query);
43 | }
44 |
45 | if (ok) {
46 | req.ok(ok);
47 | }
48 |
49 | if (reqHandler) {
50 | reqHandler(req);
51 | }
52 |
53 | if (data) {
54 | req.send(data);
55 | }
56 |
57 | const payload = await req;
58 |
59 | return payload.body;
60 | }
61 |
62 | export default function createHttpService({
63 | prefix: _prefix,
64 | getHeaders = () => ({}),
65 | getPrefix: _getPrefix,
66 | proxies = [],
67 | }) {
68 | return ({ onCancel, getCanceled }) => async (requestParams) => {
69 | const { route } = requestParams;
70 | if (process.env.NODE_ENV !== 'production') {
71 | if (route.includes('?')) {
72 | console.warn(
73 | `[createHttpService] You included a \`?\` in your route "${route}". We recommend using \`query\` instead. https://resift.org/docs/api/create-http-service#httpparams`,
74 | );
75 | }
76 | }
77 |
78 | try {
79 | if (getCanceled()) {
80 | throw new CanceledError();
81 | }
82 |
83 | const headers = await getHeaders();
84 | const prefix = await getPrefix(_prefix, _getPrefix);
85 |
86 | const httpOptions = { headers, prefix, onCancel };
87 |
88 | const proxy = proxies.find((proxy) => matchPath(route, proxy.matchParams));
89 |
90 | if (proxy) {
91 | const match = matchPath(route, proxy.matchParams);
92 | return proxy.handler({
93 | match,
94 | requestParams,
95 | headers,
96 | onCancel,
97 | getCanceled,
98 | http: (requestParams) => http(requestParams, httpOptions),
99 | });
100 | }
101 |
102 | // await is needed to bubble promise rejection so the catch block below this works correctly
103 | const data = await http(requestParams, httpOptions);
104 | return data;
105 | } catch (e) {
106 | /*
107 | * replace the error if the request was aborted
108 | * https://github.com/visionmedia/superagent/blob/f3ac20cc7c6497c002a94f5930cf2603ec7c9c6c/lib/request-base.js#L248
109 | */
110 | if (e.code === 'ABORTED') throw new CanceledError();
111 | throw e;
112 | }
113 | };
114 | }
115 |
--------------------------------------------------------------------------------
/src/createHttpService/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './createHttpService';
2 | export * from './createHttpService';
3 |
--------------------------------------------------------------------------------
/src/createHttpService/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './createHttpService';
2 |
--------------------------------------------------------------------------------
/src/createStoreKey/createStoreKey.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `createStoreKey`
3 | *
4 | * Creates a key to determine where to save an lookup fetches that aren't shared.
5 | *
6 | * > **Note**: This is an advanced API that you'll probably never use. Search in the tests for usage.
7 | *
8 | * Function signature:
9 | */
10 | export default function createStoreKey(displayName: string, fetchFactoryId: string): string;
11 |
--------------------------------------------------------------------------------
/src/createStoreKey/createStoreKey.js:
--------------------------------------------------------------------------------
1 | export default function createStoreKey(displayName, fetchFactoryId) {
2 | return `${displayName} | ${fetchFactoryId}`;
3 | }
4 |
--------------------------------------------------------------------------------
/src/createStoreKey/createStoreKey.test.js:
--------------------------------------------------------------------------------
1 | import createStoreKey from './createStoreKey';
2 |
3 | test('createStoreKey', () => {
4 | expect(createStoreKey('display name', 'action-creator-id')).toMatchInlineSnapshot(
5 | `"display name | action-creator-id"`,
6 | );
7 | });
8 |
--------------------------------------------------------------------------------
/src/createStoreKey/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './createStoreKey';
2 |
--------------------------------------------------------------------------------
/src/createStoreKey/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './createStoreKey';
2 |
--------------------------------------------------------------------------------
/src/dataServiceReducer/actionsReducer.d.ts:
--------------------------------------------------------------------------------
1 | import { FetchAction, FetchActionMeta } from '../defineFetch';
2 | import { SuccessAction, ErrorAction } from '../createDataService';
3 | import { ClearFetchAction } from '../clearFetch';
4 |
5 | type ResiftAction = SuccessAction | ErrorAction | FetchAction | ClearFetchAction;
6 |
7 | export interface ActionState {
8 | shared: boolean;
9 | inflight?: Function;
10 | payload: unknown;
11 | hadSuccess?: boolean;
12 | error?: boolean;
13 | meta: FetchActionMeta;
14 | updatedAt: string;
15 | }
16 |
17 | export interface ActionsState {
18 | [storeKey: string]: {
19 | [key: string]: ActionState | null;
20 | };
21 | }
22 |
23 | export default function actionsReducer(state: ActionsState, action: ResiftAction): ActionsState;
24 |
--------------------------------------------------------------------------------
/src/dataServiceReducer/actionsReducer.js:
--------------------------------------------------------------------------------
1 | import createStoreKey from '../createStoreKey';
2 | import timestamp from '../timestamp';
3 | import { isFetchAction } from '../defineFetch';
4 | import { isSuccessAction, isErrorAction } from '../createDataService';
5 | import { isClearAction } from '../clearFetch';
6 |
7 | export default function actionsReducer(state = {}, action) {
8 | if (
9 | !isFetchAction(action) &&
10 | !isSuccessAction(action) &&
11 | !isErrorAction(action) &&
12 | !isClearAction(action)
13 | ) {
14 | return state;
15 | }
16 |
17 | const { meta } = action;
18 | const { displayName, fetchFactoryId, key, share } = meta;
19 |
20 | const storeKey = createStoreKey(displayName, fetchFactoryId);
21 |
22 | if (isFetchAction(action)) {
23 | return {
24 | ...state,
25 | [storeKey]: {
26 | ...state[storeKey],
27 | [key]: {
28 | ...state?.[storeKey]?.[key],
29 | shared: !!share,
30 | inflight: action.payload,
31 | meta,
32 | updatedAt: timestamp(),
33 | },
34 | },
35 | };
36 | }
37 |
38 | if (isSuccessAction(action)) {
39 | return {
40 | ...state,
41 | [storeKey]: {
42 | ...state[storeKey],
43 | [key]: {
44 | ...state?.[storeKey]?.[key],
45 | inflight: undefined,
46 | shared: !!share,
47 | hadSuccess: true,
48 | data: action.payload,
49 | errorData: null,
50 | error: false,
51 | meta,
52 | updatedAt: timestamp(),
53 | },
54 | },
55 | };
56 | }
57 |
58 | if (isErrorAction(action)) {
59 | return {
60 | ...state,
61 | [storeKey]: {
62 | ...state[storeKey],
63 | [key]: {
64 | ...state?.[storeKey]?.[key],
65 | inflight: undefined,
66 | shared: !!share,
67 | errorData: action.payload,
68 | error: true,
69 | meta,
70 | updatedAt: timestamp(),
71 | },
72 | },
73 | };
74 | }
75 |
76 | // otherwise must be a clear action because of the first if statement
77 | return {
78 | ...state,
79 | [storeKey]: Object.entries(state?.[storeKey] || {})
80 | .filter(([k]) => k !== key)
81 | .reduce((acc, [key, value]) => {
82 | acc[key] = value;
83 | return acc;
84 | }, {}),
85 | };
86 | }
87 |
--------------------------------------------------------------------------------
/src/dataServiceReducer/dataServiceReducer.d.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers, Reducer } from 'redux';
2 | import actions, { ActionsState } from './actionsReducer';
3 | import shared, { SharedState } from './sharedReducer';
4 |
5 | export type DataServiceState = { shared: SharedState; actions: ActionsState };
6 |
7 | /**
8 | * @docs `dataServiceReducer`
9 | *
10 | * > **Note:** Note: This API is a lower level API for usage with Redux. You should only need to
11 | * import this reducer if you're configuring Redux yourself.
12 | *
13 | * [See Usage with Redux for more info.](../guides/usage-with-redux.md)
14 | */
15 | declare const dataServiceReducer: Reducer;
16 |
17 | export default dataServiceReducer;
18 |
--------------------------------------------------------------------------------
/src/dataServiceReducer/dataServiceReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import actions from './actionsReducer';
3 | import shared from './sharedReducer';
4 |
5 | const dataServiceReducer = combineReducers({ shared, actions });
6 |
7 | export default dataServiceReducer;
8 |
--------------------------------------------------------------------------------
/src/dataServiceReducer/dataServiceReducer.test.js:
--------------------------------------------------------------------------------
1 | import dataServiceReducer from './dataServiceReducer';
2 |
3 | test('it combines the reducers', () => {
4 | const newState = dataServiceReducer({}, {});
5 |
6 | expect(newState).toMatchInlineSnapshot(`
7 | Object {
8 | "actions": Object {},
9 | "shared": Object {
10 | "data": Object {},
11 | "merges": Object {},
12 | "parents": Object {},
13 | },
14 | }
15 | `);
16 | });
17 |
--------------------------------------------------------------------------------
/src/dataServiceReducer/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './dataServiceReducer';
2 | export * from './dataServiceReducer';
3 | export * from './actionsReducer';
4 | export * from './sharedReducer';
5 |
--------------------------------------------------------------------------------
/src/dataServiceReducer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './dataServiceReducer';
2 | export * from './dataServiceReducer';
3 | export * from './actionsReducer';
4 | export * from './sharedReducer';
5 |
--------------------------------------------------------------------------------
/src/dataServiceReducer/sharedReducer.d.ts:
--------------------------------------------------------------------------------
1 | export interface SharedState {
2 | [cacheKey: string]: {
3 | data: any;
4 | parentActions: {
5 | [storePathHash: string]: { storeKey: string; key: string };
6 | };
7 | };
8 | }
9 |
10 | export default function sharedReducer(state: SharedState, action: any): SharedState;
11 |
--------------------------------------------------------------------------------
/src/dataServiceReducer/sharedReducer.js:
--------------------------------------------------------------------------------
1 | import { isFetchAction } from '../defineFetch';
2 | import { isSuccessAction } from '../createDataService';
3 | import { isClearAction } from '../clearFetch';
4 |
5 | const initialState = {
6 | data: {},
7 | parents: {},
8 | merges: {},
9 | };
10 |
11 | // for each fetch factory, save their normalized merge object into an easy-to-look-up place
12 | // on success, find the merge functions relevant to the current merge
13 |
14 | export default function sharedReducer(state = initialState, action) {
15 | if (isFetchAction(action)) {
16 | const { meta } = action;
17 | const { displayName, fetchFactoryId, key, share } = meta;
18 | // only run this reducer if this action is `share`d
19 | if (!share) return state;
20 |
21 | const { namespace, mergeObj } = share;
22 |
23 | const mergeState = { ...state?.merges };
24 | // (eslint bug)
25 | // eslint-disable-next-line no-unused-vars
26 | for (const [targetNamespace, mergeFn] of Object.entries(mergeObj)) {
27 | const merges = { ...mergeState?.[targetNamespace] };
28 | merges[namespace] = mergeFn;
29 |
30 | mergeState[targetNamespace] = merges;
31 | }
32 |
33 | const parentsState = { ...state?.parents };
34 |
35 | // (eslint bug)
36 | // eslint-disable-next-line no-unused-vars
37 | const parents = { ...parentsState?.[namespace] };
38 | parents[`${displayName} | ${key} | ${fetchFactoryId}`] = {
39 | fetchFactoryId,
40 | key,
41 | displayName,
42 | };
43 |
44 | parentsState[namespace] = parents;
45 |
46 | return {
47 | ...state,
48 | merges: mergeState,
49 | parents: parentsState,
50 | };
51 | }
52 |
53 | if (isSuccessAction(action)) {
54 | const { meta, payload } = action;
55 | const { key, share } = meta;
56 |
57 | // only run this reducer if this action is `share`d
58 | if (!share) return state;
59 | const { namespace } = share;
60 |
61 | const merges = state?.merges?.[namespace] || {};
62 |
63 | const nextData = { ...state?.data };
64 |
65 | // (eslint bug)
66 | // eslint-disable-next-line no-unused-vars
67 | for (const [targetNamespace, mergeFn] of Object.entries(merges)) {
68 | // if the target namespace is the same from the action's namespace
69 | // we should only apply the merge function over the current key
70 | if (targetNamespace === namespace) {
71 | nextData[targetNamespace] = {
72 | ...state.data?.[targetNamespace],
73 | [key]: mergeFn(state.data?.[targetNamespace]?.[key], payload),
74 | };
75 | continue;
76 | }
77 |
78 | const mergedData = Object.entries(state?.data?.[targetNamespace] || {}).reduce(
79 | (acc, [key, value]) => {
80 | acc[key] = mergeFn(value, payload);
81 | return acc;
82 | },
83 | {},
84 | );
85 |
86 | // otherwise we should apply the merge function over all the keys
87 | nextData[targetNamespace] = mergedData;
88 | }
89 |
90 | return {
91 | ...state,
92 | data: nextData,
93 | };
94 | }
95 |
96 | if (isClearAction(action)) {
97 | const { meta } = action;
98 | const { displayName, fetchFactoryId, key, share } = meta;
99 | // only run this reducer if this action is `share`d
100 | if (!share) return state;
101 |
102 | const { namespace, mergeObj } = share;
103 |
104 | const mergeState = { ...state?.merges };
105 | // (eslint bug)
106 | // eslint-disable-next-line no-unused-vars
107 | for (const targetNamespace of Object.keys(mergeObj)) {
108 | const merges = { ...mergeState?.[targetNamespace] };
109 | delete merges[namespace];
110 |
111 | mergeState[targetNamespace] = merges;
112 | }
113 |
114 | const parentsState = { ...state?.parents };
115 |
116 | const parentSet = { ...parentsState?.[namespace] };
117 | const parentKey = `${displayName} | ${key} | ${fetchFactoryId}`;
118 | delete parentSet[parentKey];
119 | if (Object.keys(parentSet || {}).length <= 0) {
120 | delete parentsState[namespace];
121 | } else {
122 | parentsState[namespace] = parentSet;
123 | }
124 |
125 | const dataState = { ...state?.data };
126 | delete dataState[namespace];
127 |
128 | return {
129 | ...state,
130 | data: dataState,
131 | merges: mergeState,
132 | parents: parentsState,
133 | };
134 | }
135 |
136 | return state;
137 | }
138 |
--------------------------------------------------------------------------------
/src/defineFetch/defineFetch.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `defineFetch`
3 | * This function creates a fetch factory. See [How to define a fetch](../main-concepts/how-to-define-a-fetch.md) to learn more
4 | */
5 | export default function defineFetch(
6 | params: DefineFetchParams,
7 | ): FetchFactory;
8 |
9 | /**
10 | * @docs `DefineFetchParams`
11 | * the shape of the parameter object that goes into `defineFetch`
12 | */
13 | export interface DefineFetchParams {
14 | /**
15 | * The display name should be a human readable string to help you debug.
16 | */
17 | displayName: string;
18 |
19 | /**
20 | * The make function defines two things:
21 | *
22 | * - how your fetch factory will make fetch instances and
23 | * - how your fetch instances will get their data.
24 | *
25 | * See [How to define a fetch](../main-concepts/how-to-define-a-fetch.md) for more info.
26 | */
27 | make: (...keyArgs: KeyArgs) => MakeObject;
28 |
29 | /**
30 | * If `share` is present, this fetch factory can have its state shared.
31 | */
32 | share?: ShareParams;
33 |
34 | /**
35 | * This determines the conflict resolution of ReSift. When two of the same
36 | * fetches are inflight, one of the fetches needs to be discard. If the
37 | * conflict resolution is set to `cancel`, the older request will be
38 | * canceled. If the conflict resolution is set to `ignore`, the newer request
39 | * will be ignored in favor of the older request.
40 | *
41 | * The default is `cancel`
42 | */
43 | conflict?: 'cancel' | 'ignore';
44 |
45 | /**
46 | * On creation, fetch factories keep an internal random ID. If you're trying
47 | * to re-hydrate this state and would like your fetch factories to resolve to
48 | * the same ID instead of a random ID, you can set this property.
49 | */
50 | staticFetchFactoryId?: string;
51 | }
52 |
53 | /**
54 | * @docs `MakeObject`
55 | * When defining the `make` function in `defineFetch`, you must return this object.
56 | */
57 | interface MakeObject {
58 | request: (...fetchArgs: FetchArgs) => (services: any) => any;
59 | }
60 |
61 | /**
62 | * @docs `ShareParams`
63 | */
64 | export interface ShareParams {
65 | /**
66 | * This namespace represents the group you want this fetch factory to be in.
67 | * If you are doing CRUD operations to the same resource on the back-end, then
68 | * you probably want to use the same namespace.
69 | *
70 | * See [Making state consistent](../main-concepts/making-state-consistent.md)
71 | * for more info.
72 | */
73 | namespace: string;
74 |
75 | /**
76 | * [See here for more info.](../main-concepts/making-state-consistent.md#merges-across-namespaces)
77 | */
78 | merge?:
79 | | ((previous: any, next: any) => any)
80 | | { [key: string]: (previous: any, next: any) => any };
81 | }
82 |
83 | /**
84 | * @docs `FetchActionFactory`
85 | * the result of calling `defineFetch` is a factory that returns an action creator with meta data
86 | */
87 | export type FetchFactory = (
88 | ...args: KeyArgs
89 | ) => FetchInstance;
90 |
91 | export interface FetchInstance {
92 | (...args: FetchArgs): FetchAction;
93 |
94 | meta: FetchActionMeta;
95 | }
96 |
97 | export interface FetchAction {
98 | type: string;
99 | meta: FetchActionMeta;
100 | payload: FetchActionPayload;
101 | }
102 |
103 | export interface FetchActionPayload {
104 | (services: any): T | Promise;
105 |
106 | cancel: () => void;
107 | getCanceled: () => boolean;
108 | onCancel: (callback: () => void) => void;
109 | }
110 |
111 | export interface FetchActionMeta {
112 | fetchFactoryId: string;
113 | key: string;
114 | displayName: string;
115 | share?: ShareParams;
116 | conflict: 'cancel' | 'ignore';
117 | }
118 |
119 | export function isFetchAction(action: any): action is FetchAction;
120 |
121 | /**
122 | * @docs `typedFetchFactory`
123 | *
124 | * A helper used to allow you to set the type of data your fetch factory will
125 | * return.
126 | *
127 | * See [Usage with typescript](../guides/usage-with-typescript.md) for more info.
128 | */
129 | export function typedFetchFactory(): (
130 | fetchFactory: FetchFactory,
131 | ) => FetchFactory;
132 |
--------------------------------------------------------------------------------
/src/defineFetch/defineFetch.js:
--------------------------------------------------------------------------------
1 | import shortId from 'shortid';
2 | import createActionType from '../createActionType';
3 | import FETCH from '../prefixes/FETCH';
4 |
5 | export function replace(_prev, next) {
6 | return next;
7 | }
8 |
9 | export function normalizeMerge(merge, namespace) {
10 | if (!merge) {
11 | return {
12 | [namespace]: replace,
13 | };
14 | }
15 |
16 | if (typeof merge === 'function') {
17 | return {
18 | [namespace]: merge,
19 | };
20 | }
21 |
22 | if (typeof merge === 'object') {
23 | if (!merge[namespace]) {
24 | return {
25 | ...merge,
26 | [namespace]: replace,
27 | };
28 | }
29 |
30 | return merge;
31 | }
32 |
33 | throw new Error(
34 | '[sharedReducer] Could not match typeof merge. See here for how to define merges: https://resift.org/docs/main-concepts/making-state-consistent#merges',
35 | );
36 | }
37 |
38 | export function isFetchAction(action) {
39 | if (!action) return false;
40 | if (!action.type) return false;
41 | return action.type.startsWith(FETCH);
42 | }
43 |
44 | function memoize(displayName, actionCreatorFactory, make, conflict) {
45 | // TODO: may need a way to clear this memo
46 | const memo = {};
47 |
48 | function memoized(...keyArgs) {
49 | const makeResult = make(...keyArgs);
50 |
51 | if (typeof makeResult !== 'object') {
52 | throw new Error('[defineFetch]: `make` must return an object');
53 | }
54 |
55 | const { request } = makeResult;
56 |
57 | if (!keyArgs.every((key) => typeof key === 'string' || typeof key === 'number')) {
58 | throw new Error(
59 | `[defineFetch] make arguments must be either a string or a number. Check calls to the fetch factory "${displayName}" See here https://resift.org/docs/main-concepts/whats-a-fetch#making-a-fetch-and-pulling-data-from-it`,
60 | );
61 | }
62 | if (typeof request !== 'function') {
63 | throw new Error(
64 | '[defineFetch] `request` must be a function in the object that `make` returns`',
65 | );
66 | }
67 |
68 | const hash = `key:${[...keyArgs, conflict].join(' | ')}`;
69 |
70 | if (memo[hash]) {
71 | return memo[hash];
72 | }
73 |
74 | memo[hash] = actionCreatorFactory(...keyArgs);
75 | return memo[hash];
76 | }
77 |
78 | return memoized;
79 | }
80 |
81 | export default function defineFetch({
82 | displayName,
83 | share,
84 | conflict = 'cancel',
85 | make,
86 | staticFetchFactoryId,
87 | }) {
88 | const fetchFactoryId = staticFetchFactoryId || shortId();
89 |
90 | if (!displayName) throw new Error('`displayName` is required in `defineFetch`');
91 | if (!make) throw new Error('`make` is required in `defineFetch`');
92 |
93 | if (share) {
94 | if (!share.namespace) {
95 | throw new Error('`namespace` is required in `share');
96 | }
97 | }
98 |
99 | function fetchFactory(...keyArgs) {
100 | const makeResult = make(...keyArgs);
101 |
102 | const meta = {
103 | fetchFactoryId,
104 | key: `key:${keyArgs.join(' | ')}`,
105 | displayName,
106 | share: share && {
107 | ...share,
108 | mergeObj: normalizeMerge(share.merge, share.namespace),
109 | },
110 | conflict,
111 | };
112 |
113 | function fetch(...requestArgs) {
114 | // the `request` is a curried function.
115 | // this partially applies the user request arguments. the resulting function is a
116 | // function that takes in the services object and returns a promise of data
117 | const resolvablePayload = makeResult.request(...requestArgs);
118 |
119 | if (typeof resolvablePayload !== 'function') {
120 | throw new Error(
121 | '[defineFetch] Expected `fetch` to return a curried function. https://resift.org/docs/main-concepts/how-to-define-a-fetch#the-request-function',
122 | );
123 | }
124 |
125 | // cancellation mechanism
126 | const canceledRef = { canceled: false }; // pointer used to avoid incorrect value in closures
127 | const subscribers = [];
128 |
129 | resolvablePayload.cancel = () => {
130 | canceledRef.canceled = true;
131 |
132 | // don't know why there's this false positive
133 | // https://github.com/eslint/eslint/issues/12117
134 | // eslint-disable-next-line no-unused-vars
135 | for (const subscriber of subscribers) {
136 | subscriber();
137 | }
138 | };
139 |
140 | resolvablePayload.getCanceled = () => canceledRef.canceled;
141 |
142 | resolvablePayload.onCancel = (callback) => {
143 | subscribers.push(callback);
144 | };
145 |
146 | const request = {
147 | type: createActionType(FETCH, meta),
148 | meta,
149 | payload: resolvablePayload,
150 | };
151 |
152 | return request;
153 | }
154 |
155 | fetch.meta = {
156 | ...meta,
157 | type: 'FETCH_INSTANCE',
158 | };
159 |
160 | return fetch;
161 | }
162 |
163 | const memoizedFetchFactory = memoize(displayName, fetchFactory, make, conflict);
164 |
165 | memoizedFetchFactory.meta = {
166 | fetchFactoryId,
167 | displayName,
168 | type: 'FETCH_INSTANCE_FACTORY',
169 | };
170 |
171 | return memoizedFetchFactory;
172 | }
173 |
174 | // this is for typescript
175 | export const typedFetchFactory = () => (fetchFactory) => fetchFactory;
176 |
--------------------------------------------------------------------------------
/src/defineFetch/defineFetch.types-test.ts:
--------------------------------------------------------------------------------
1 | import defineFetch, { typedFetchFactory } from './defineFetch';
2 |
3 | const exampleResult = {
4 | thisIsAnExampleResult: 'blah',
5 | };
6 |
7 | interface Bar {
8 | bar: number;
9 | }
10 |
11 | const fetchFactory = defineFetch({
12 | displayName: 'example fetch',
13 | make: (foo: string, bar: number) => ({
14 | // eslint-disable-next-line no-empty-pattern
15 | request: (thing: Bar) => ({}: /* services go here */ any) => exampleResult,
16 | }),
17 | });
18 |
19 | interface MyType {
20 | foo: string;
21 | }
22 |
23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
24 | const blah = typedFetchFactory()(fetchFactory);
25 |
26 | console.log(fetchFactory);
27 |
--------------------------------------------------------------------------------
/src/defineFetch/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './defineFetch';
2 | export * from './defineFetch';
3 |
--------------------------------------------------------------------------------
/src/defineFetch/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './defineFetch';
2 | export * from './defineFetch';
3 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as PREFIXES from './prefixes';
2 |
3 | export { PREFIXES };
4 | export { default as CanceledError } from './CanceledError';
5 | export { default as ERROR } from './ERROR';
6 | export { default as LOADING } from './LOADING';
7 | export { default as NORMAL } from './NORMAL';
8 | export { default as ResiftProvider } from './ResiftProvider';
9 | export { default as UNKNOWN } from './UNKNOWN';
10 | export { default as combineStatuses } from './combineStatuses';
11 | export { default as createActionType } from './createActionType';
12 | export { default as createContextFetch } from './createContextFetch';
13 | export { default as createDataService, ServicesFrom } from './createDataService';
14 | export { default as createHttpProxy } from './createHttpProxy';
15 | export { default as createStoreKey } from './createStoreKey';
16 | export { default as createHttpService, HttpParams } from './createHttpService';
17 | export { default as dataServiceReducer } from './dataServiceReducer';
18 | export {
19 | default as defineFetch,
20 | typedFetchFactory,
21 | FetchFactory,
22 | FetchInstance,
23 | } from './defineFetch';
24 | export { default as isError } from './isError';
25 | export { default as isLoading } from './isLoading';
26 | export { default as isNormal } from './isNormal';
27 | export { default as isUnknown } from './isUnknown';
28 | export { default as useClearFetch } from './useClearFetch';
29 | export { default as useDispatch } from './useDispatch';
30 | export { default as useData } from './useData';
31 | export { default as useError } from './useError';
32 | export { default as useStatus } from './useStatus';
33 | export { default as Guard } from './Guard';
34 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import * as PREFIXES from './prefixes';
2 |
3 | export { PREFIXES };
4 | export { default as CanceledError } from './CanceledError';
5 | export { default as ERROR } from './ERROR';
6 | export { default as LOADING } from './LOADING';
7 | export { default as NORMAL } from './NORMAL';
8 | export { default as ResiftProvider } from './ResiftProvider';
9 | export { default as UNKNOWN } from './UNKNOWN';
10 | export { default as combineStatuses } from './combineStatuses';
11 | export { default as createActionType } from './createActionType';
12 | export { default as createContextFetch } from './createContextFetch';
13 | export { default as createDataService } from './createDataService';
14 | export { default as createHttpProxy } from './createHttpProxy';
15 | export { default as createStoreKey } from './createStoreKey';
16 | export { default as createHttpService } from './createHttpService';
17 | export { default as dataServiceReducer } from './dataServiceReducer';
18 | export { default as defineFetch } from './defineFetch';
19 | export { typedFetchFactory } from './defineFetch';
20 | export { default as isError } from './isError';
21 | export { default as isLoading } from './isLoading';
22 | export { default as isNormal } from './isNormal';
23 | export { default as isUnknown } from './isUnknown';
24 | export { default as useClearFetch } from './useClearFetch';
25 | export { default as useDispatch } from './useDispatch';
26 | export { default as useData } from './useData';
27 | export { default as useStatus } from './useStatus';
28 | export { default as Guard } from './Guard';
29 | export { default as useFetch } from './useFetch';
30 | export { default as useError } from './useError';
31 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { create, act } from 'react-test-renderer';
3 | import DeferredPromise from './DeferredPromise';
4 | import {
5 | ResiftProvider,
6 | defineFetch,
7 | useData,
8 | useStatus,
9 | createDataService,
10 | useDispatch,
11 | isLoading,
12 | isNormal,
13 | } from './index';
14 |
15 | test('basic lifecycle', async () => {
16 | const finishedRendering = new DeferredPromise();
17 | const dataHandler = jest.fn();
18 | const statusHandler = jest.fn();
19 |
20 | const makePersonFetch = defineFetch({
21 | displayName: 'Get Person',
22 | make: (personId) => ({
23 | request: () => () => ({
24 | personId,
25 | name: 'It worked!',
26 | }),
27 | }),
28 | });
29 |
30 | function Person({ personId }) {
31 | const dispatch = useDispatch();
32 | const personFetch = makePersonFetch(personId);
33 | const person = useData(personFetch);
34 | const status = useStatus(personFetch);
35 |
36 | useEffect(() => {
37 | dispatch(personFetch());
38 | }, [personFetch, dispatch]);
39 |
40 | useEffect(() => statusHandler(status), [status]);
41 | useEffect(() => dataHandler(person), [person]);
42 |
43 | useEffect(() => {
44 | if (!isNormal(status)) {
45 | return;
46 | }
47 |
48 | finishedRendering.resolve();
49 | }, [status]);
50 |
51 | return (
52 |
53 | {person && person.name}
54 | {isLoading(status) &&
Loading…
}
55 |
56 | );
57 | }
58 |
59 | const handleError = jest.fn();
60 |
61 | const services = {};
62 |
63 | const dataService = createDataService({
64 | services,
65 | onError: handleError,
66 | });
67 |
68 | await act(async () => {
69 | create(
70 |
71 |
72 | ,
73 | );
74 |
75 | await finishedRendering;
76 | });
77 |
78 | expect(statusHandler).toHaveBeenCalledTimes(3);
79 | // 1) first render
80 | // 2) loading
81 | // 3) resolved/normal
82 | expect(statusHandler.mock.calls.map((x) => x[0])).toMatchInlineSnapshot(`
83 | Array [
84 | 0,
85 | 2,
86 | 1,
87 | ]
88 | `);
89 |
90 | expect(dataHandler).toHaveBeenCalledTimes(2);
91 | // 1) no data
92 | // 2) data
93 | expect(dataHandler.mock.calls.map((x) => x[0])).toMatchInlineSnapshot(`
94 | Array [
95 | null,
96 | Object {
97 | "name": "It worked!",
98 | "personId": "person123",
99 | },
100 | ]
101 | `);
102 | });
103 |
--------------------------------------------------------------------------------
/src/isError/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './isError';
2 |
--------------------------------------------------------------------------------
/src/isError/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './isError';
2 |
--------------------------------------------------------------------------------
/src/isError/isError.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `isError`
3 | *
4 | * Given one or more statuses, this function will return whether the combined statues includes `ERROR`.
5 | *
6 | * If at least one status includes `ERROR` this will return `true.`
7 | */
8 | export default function isError(...statuses: number[]): boolean;
9 |
--------------------------------------------------------------------------------
/src/isError/isError.js:
--------------------------------------------------------------------------------
1 | import ERROR from '../ERROR';
2 |
3 | function getError(status) {
4 | return (status & ERROR) !== 0;
5 | }
6 |
7 | export default function isError(...statuses) {
8 | return statuses.some(getError);
9 | }
10 |
--------------------------------------------------------------------------------
/src/isError/isError.test.js:
--------------------------------------------------------------------------------
1 | import ERROR from '../ERROR';
2 | import LOADING from '../LOADING';
3 | import isError from './isError';
4 |
5 | test('identity', () => {
6 | expect(isError(ERROR)).toBe(true);
7 | });
8 |
9 | describe('returns true if one loading state is loading', () => {
10 | test('happy path', () => {
11 | expect(isError(ERROR, LOADING | LOADING, LOADING)).toBe(true);
12 | });
13 |
14 | test('negative path', () => {
15 | expect(isError(LOADING, LOADING, LOADING)).toBe(false);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/isLoading/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './isLoading';
2 |
--------------------------------------------------------------------------------
/src/isLoading/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './isLoading';
2 |
--------------------------------------------------------------------------------
/src/isLoading/isLoading.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `isLoading`
3 | *
4 | * Given one or more statuses, this function will return whether the combined statues includes `LOADING`.
5 | *
6 | * If at least one status includes `LOADING` this will return `true.`
7 | */
8 | export default function isLoading(...statuses: number[]): boolean;
9 |
--------------------------------------------------------------------------------
/src/isLoading/isLoading.js:
--------------------------------------------------------------------------------
1 | import LOADING from '../LOADING';
2 |
3 | function getLoading(status) {
4 | return (status & LOADING) !== 0;
5 | }
6 |
7 | export default function isLoading(...statuses) {
8 | return statuses.some(getLoading);
9 | }
10 |
--------------------------------------------------------------------------------
/src/isLoading/isLoading.test.js:
--------------------------------------------------------------------------------
1 | import NORMAL from '../NORMAL';
2 | import LOADING from '../LOADING';
3 | import isLoading from './isLoading';
4 |
5 | test('identity', () => {
6 | expect(isLoading(LOADING)).toBe(true);
7 | });
8 |
9 | describe('returns true if one loading state is loading', () => {
10 | test('happy path', () => {
11 | expect(isLoading(NORMAL, NORMAL | LOADING, NORMAL)).toBe(true);
12 | });
13 |
14 | test('negative path', () => {
15 | expect(isLoading(NORMAL, NORMAL, NORMAL)).toBe(false);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/isNormal/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './isNormal';
2 |
--------------------------------------------------------------------------------
/src/isNormal/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './isNormal';
2 |
--------------------------------------------------------------------------------
/src/isNormal/isNormal.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `isNormal`
3 | *
4 | * Given one or more statuses, this function will return whether the combined statues includes `NORMAL`.
5 | *
6 | * This will only return `true` if all the status include `NORMAL`.
7 | */
8 | export default function isNormal(...statuses: number[]): boolean;
9 |
--------------------------------------------------------------------------------
/src/isNormal/isNormal.js:
--------------------------------------------------------------------------------
1 | import NORMAL from '../NORMAL';
2 |
3 | function getNormal(status) {
4 | return (status & NORMAL) !== 0;
5 | }
6 |
7 | export default function isNormal(...statuses) {
8 | return statuses.every(getNormal);
9 | }
10 |
--------------------------------------------------------------------------------
/src/isNormal/isNormal.test.js:
--------------------------------------------------------------------------------
1 | import NORMAL from '../NORMAL';
2 | import LOADING from '../LOADING';
3 | import isNormal from './isNormal';
4 |
5 | test('identity', () => {
6 | expect(isNormal(NORMAL)).toBe(true);
7 | });
8 |
9 | describe('only returns true if all loading states are normal', () => {
10 | test('happy path', () => {
11 | expect(isNormal(NORMAL, NORMAL | LOADING, NORMAL)).toBe(true);
12 | });
13 |
14 | test('negative path', () => {
15 | expect(isNormal(NORMAL, LOADING, NORMAL)).toBe(false);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/isUnknown/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './isUnknown';
2 |
--------------------------------------------------------------------------------
/src/isUnknown/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './isUnknown';
2 |
--------------------------------------------------------------------------------
/src/isUnknown/isUnknown.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs `isUnknown`
3 | *
4 | * Given one or more statuses, this function will return whether that status includes `UNKNOWN`.
5 | *
6 | * If one is `UNKNOWN` the result will also be `UNKNOWN`
7 | */
8 | export default function isUnknown(...statuses: number[]): boolean;
9 |
--------------------------------------------------------------------------------
/src/isUnknown/isUnknown.js:
--------------------------------------------------------------------------------
1 | import UNKNOWN from '../UNKNOWN';
2 |
3 | function getUnknown(status) {
4 | return status === UNKNOWN;
5 | }
6 |
7 | export default function isUnknown(...statuses) {
8 | return statuses.every(getUnknown);
9 | }
10 |
--------------------------------------------------------------------------------
/src/isUnknown/isUnknown.test.js:
--------------------------------------------------------------------------------
1 | import UNKNOWN from '../UNKNOWN';
2 | import LOADING from '../LOADING';
3 | import isUnknown from './isUnknown';
4 |
5 | test('identity', () => {
6 | expect(isUnknown(UNKNOWN)).toBe(true);
7 | });
8 |
9 | describe('only returns true if all loading states are normal', () => {
10 | test('happy path', () => {
11 | expect(isUnknown(UNKNOWN, UNKNOWN, UNKNOWN)).toBe(true);
12 | });
13 |
14 | test('negative path', () => {
15 | expect(isUnknown(UNKNOWN, UNKNOWN, LOADING | UNKNOWN)).toBe(false);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/prefixes/CLEAR.d.ts:
--------------------------------------------------------------------------------
1 | declare const CLEAR: '@@RESIFT/CLEAR';
2 | export default CLEAR;
3 |
--------------------------------------------------------------------------------
/src/prefixes/CLEAR.js:
--------------------------------------------------------------------------------
1 | export default '@@RESIFT/CLEAR';
2 |
--------------------------------------------------------------------------------
/src/prefixes/ERROR.d.ts:
--------------------------------------------------------------------------------
1 | declare const ERROR: '@@RESIFT/ERROR';
2 | export default ERROR;
3 |
--------------------------------------------------------------------------------
/src/prefixes/ERROR.js:
--------------------------------------------------------------------------------
1 | export default '@@RESIFT/ERROR';
2 |
--------------------------------------------------------------------------------
/src/prefixes/FETCH.d.ts:
--------------------------------------------------------------------------------
1 | declare const FETCH: '@@RESIFT/FETCH';
2 | export default FETCH;
3 |
--------------------------------------------------------------------------------
/src/prefixes/FETCH.js:
--------------------------------------------------------------------------------
1 | export default '@@RESIFT/FETCH';
2 |
--------------------------------------------------------------------------------
/src/prefixes/SUCCESS.d.ts:
--------------------------------------------------------------------------------
1 | declare const SUCCESS: '@@RESIFT/SUCCESS';
2 | export default SUCCESS;
3 |
--------------------------------------------------------------------------------
/src/prefixes/SUCCESS.js:
--------------------------------------------------------------------------------
1 | export default '@@RESIFT/SUCCESS';
2 |
--------------------------------------------------------------------------------
/src/prefixes/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default as CLEAR } from './CLEAR';
2 | export { default as FETCH } from './FETCH';
3 | export { default as ERROR } from './ERROR';
4 | export { default as SUCCESS } from './SUCCESS';
5 |
--------------------------------------------------------------------------------
/src/prefixes/index.js:
--------------------------------------------------------------------------------
1 | export { default as CLEAR } from './CLEAR';
2 | export { default as FETCH } from './FETCH';
3 | export { default as ERROR } from './ERROR';
4 | export { default as SUCCESS } from './SUCCESS';
5 |
--------------------------------------------------------------------------------
/src/shallowEqual/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './shallowEqual';
2 | export * from './shallowEqual';
3 |
--------------------------------------------------------------------------------
/src/shallowEqual/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './shallowEqual';
2 | export * from './shallowEqual';
3 |
--------------------------------------------------------------------------------
/src/shallowEqual/shallowEqual.d.ts:
--------------------------------------------------------------------------------
1 | export default function shallowEqual(a: any, b: any): boolean;
2 |
--------------------------------------------------------------------------------
/src/shallowEqual/shallowEqual.js:
--------------------------------------------------------------------------------
1 | // copied from https://github.com/acdlite/recompose/blob/master/src/packages/recompose/shallowEqual.js
2 | /**
3 | * Copyright (c) 2013-present, Facebook, Inc.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @providesModule shallowEqual
9 | * @typechecks
10 | */
11 |
12 | /* eslint-disable no-self-compare */
13 |
14 | const hasOwnProperty = Object.prototype.hasOwnProperty;
15 |
16 | /**
17 | * inlined Object.is polyfill to avoid requiring consumers ship their own
18 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
19 | */
20 | function is(x, y) {
21 | // SameValue algorithm
22 | if (x === y) {
23 | // Steps 1-5, 7-10
24 | // Steps 6.b-6.e: +0 != -0
25 | // Added the nonzero y check to make Flow happy, but it is redundant
26 | return x !== 0 || y !== 0 || 1 / x === 1 / y;
27 | }
28 | // Step 6.a: NaN == NaN
29 | return x !== x && y !== y;
30 | }
31 |
32 | /**
33 | * Performs equality by iterating through keys on an object and returning false
34 | * when any key has values which are not strictly equal between the arguments.
35 | * Returns true when the values of all keys are strictly equal.
36 | */
37 | function shallowEqual(objA, objB) {
38 | if (is(objA, objB)) {
39 | return true;
40 | }
41 |
42 | if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
43 | return false;
44 | }
45 |
46 | const keysA = Object.keys(objA);
47 | const keysB = Object.keys(objB);
48 |
49 | if (keysA.length !== keysB.length) {
50 | return false;
51 | }
52 |
53 | // Test for A's keys different from B.
54 | for (let i = 0; i < keysA.length; i++) {
55 | if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
56 | return false;
57 | }
58 | }
59 |
60 | return true;
61 | }
62 |
63 | export default shallowEqual;
64 |
--------------------------------------------------------------------------------
/src/shallowEqual/shallowEqual.test.js:
--------------------------------------------------------------------------------
1 | import shallowEqual from './shallowEqual';
2 |
3 | // Adapted from https://github.com/rackt/react-redux/blob/master/test/utils/shallowEqual.spec.js
4 | test('shallowEqual returns true if arguments are equal, without comparing properties', () => {
5 | const throwOnAccess = {
6 | get foo() {
7 | throw new Error('Property was accessed');
8 | },
9 | };
10 | expect(shallowEqual(throwOnAccess, throwOnAccess)).toBe(true);
11 | });
12 |
13 | test('shallowEqual returns true if arguments fields are equal', () => {
14 | expect(shallowEqual({ a: 1, b: 2, c: undefined }, { a: 1, b: 2, c: undefined })).toBe(true);
15 |
16 | expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(true);
17 |
18 | const o = {};
19 | expect(shallowEqual({ a: 1, b: 2, c: o }, { a: 1, b: 2, c: o })).toBe(true);
20 | });
21 |
22 | test('shallowEqual returns false if either argument is null or undefined', () => {
23 | expect(shallowEqual(null, { a: 1, b: 2 })).toBe(false);
24 | expect(shallowEqual({ a: 1, b: 2 }, null)).toBe(false);
25 | });
26 |
27 | test('shallowEqual returns false if first argument has too many keys', () => {
28 | expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(false);
29 | });
30 |
31 | test('shallowEqual returns false if second argument has too many keys', () => {
32 | expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(false);
33 | });
34 |
35 | test('shallowEqual returns false if arguments have different keys', () => {
36 | expect(shallowEqual({ a: 1, b: 2, c: undefined }, { a: 1, bb: 2, c: undefined })).toBe(false);
37 | });
38 |
--------------------------------------------------------------------------------
/src/timestamp/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './timestamp';
2 |
--------------------------------------------------------------------------------
/src/timestamp/timestamp.js:
--------------------------------------------------------------------------------
1 | /**
2 | * a simple mockable timestamp function
3 | */
4 | export default function timestamp() {
5 | return new Date().toISOString();
6 | }
7 |
--------------------------------------------------------------------------------
/src/timestamp/timestamp.test.js:
--------------------------------------------------------------------------------
1 | import timestamp from './timestamp';
2 |
3 | test('it returns an ISO string', () => {
4 | expect(typeof timestamp()).toBe('string');
5 | });
6 |
--------------------------------------------------------------------------------
/src/useClearFetch/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './useClearFetch';
2 |
--------------------------------------------------------------------------------
/src/useClearFetch/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './useClearFetch';
2 |
--------------------------------------------------------------------------------
/src/useClearFetch/useClearFetch.d.ts:
--------------------------------------------------------------------------------
1 | import { FetchInstance } from '../defineFetch';
2 |
3 | /**
4 | * @docs `useClearFetch`
5 | *
6 | * Returns `clearFetch` — a function that you can call to remove the cached values in ReSift.
7 | *
8 | * Example usage:
9 | *
10 | * ```js
11 | * function ExampleComponent({ personId }) {
12 | * const personFetch = makePersonFetch(personId);
13 | * const clearFetch = useClearFetch();
14 | * const dispatch = useDispatch();
15 | *
16 | * useEffect(() => {
17 | * dispatch(personFetch());
18 | *
19 | * // `clearFetch` fits in well with React's `useEffect`'s clean-up phase
20 | * // (note: you only need to clean up if you wish to remove the cached value)
21 | * return () => clearFetch(personFetch);
22 | * }, [personFetch]);
23 | *
24 | * return // ...
25 | * }
26 | * ```
27 | *
28 | * `useClearFetch` signature:
29 | */
30 | export default function useClearFetch(): (fetch: FetchInstance) => any;
31 |
--------------------------------------------------------------------------------
/src/useClearFetch/useClearFetch.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import useDispatch from '../useDispatch';
3 | import clearFetch from '../clearFetch';
4 |
5 | export default function useClearFetch() {
6 | const dispatch = useDispatch();
7 |
8 | return useCallback(
9 | (fetch) => {
10 | dispatch(clearFetch(fetch));
11 | },
12 | [dispatch],
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/useClearFetch/useClearFetch.test.js:
--------------------------------------------------------------------------------
1 | import useClearFetch from './useClearFetch';
2 | import useDispatch from '../useDispatch';
3 | import defineFetch from '../defineFetch';
4 |
5 | jest.mock('shortid', () => () => 'test-short-id');
6 |
7 | const mockDispatch = jest.fn();
8 | function mockGet() {
9 | return mockDispatch;
10 | }
11 | jest.mock('../useDispatch', () => () => mockGet());
12 | jest.mock('react', () => ({ useCallback: (cb) => cb }));
13 |
14 | test('it returns a function that calls dispatch', () => {
15 | const makePersonFetch = defineFetch({
16 | displayName: 'fetch person',
17 | make: (personId) => ({
18 | request: () => ({ exampleService }) => exampleService(),
19 | }),
20 | });
21 |
22 | const personFetch = makePersonFetch('person123');
23 |
24 | const clear = useClearFetch();
25 |
26 | clear(personFetch);
27 |
28 | const dispatch = useDispatch();
29 | expect(dispatch).toHaveBeenCalled();
30 | expect(dispatch.mock.calls[0][0]).toMatchInlineSnapshot(`
31 | Object {
32 | "meta": Object {
33 | "conflict": "cancel",
34 | "displayName": "fetch person",
35 | "fetchFactoryId": "test-short-id",
36 | "key": "key:person123",
37 | "share": undefined,
38 | "type": "FETCH_INSTANCE",
39 | },
40 | "type": "@@RESIFT/CLEAR | fetch person | test-short-id",
41 | }
42 | `);
43 | });
44 |
--------------------------------------------------------------------------------
/src/useData/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './useData';
2 |
--------------------------------------------------------------------------------
/src/useData/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './useData';
2 |
--------------------------------------------------------------------------------
/src/useData/useData.d.ts:
--------------------------------------------------------------------------------
1 | import { FetchInstance } from '../defineFetch';
2 |
3 | /**
4 | * @docs `useData`
5 | *
6 | * Grabs the data out of the `FetchInstance`. This function may return `null`
7 | * if there is no data available yet.
8 | *
9 | * If you pass in `null` (or any other falsy value), you'll get `null` back out
10 | */
11 | declare function useData(
12 | fetch: FetchInstance | null,
13 | ): Data | null;
14 |
15 | export default useData;
16 |
--------------------------------------------------------------------------------
/src/useData/useData.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import createStoreKey from '../createStoreKey';
4 |
5 | const makeDataSelector = (fetch) => (state) => {
6 | if (!fetch) {
7 | return null;
8 | }
9 |
10 | const isFetchInstance = fetch?.meta?.type === 'FETCH_INSTANCE';
11 | if (!isFetchInstance) {
12 | throw new Error('[useData] expected to see a fetch instance.');
13 | }
14 |
15 | if (!state.dataService) {
16 | throw new Error(
17 | '[useData] "dataService" is a required key. Double check with the installation guide here: https://resift.org/docs/introduction/installation',
18 | );
19 | }
20 |
21 | const { fetchFactoryId, displayName, key, share } = fetch.meta;
22 |
23 | const storeKey = createStoreKey(displayName, fetchFactoryId);
24 |
25 | const value = state?.dataService?.actions?.[storeKey]?.[key];
26 |
27 | // if the fetch is _not_ shared, continue down this code path.
28 | // in this path, all we do is return the "non-shared" value and the "non-shared" state from the
29 | // `actions` sub-store (vs the `shared` sub-store)
30 | if (!share) {
31 | if (!value) return null;
32 | return value.data === undefined ? null : value.data;
33 | }
34 |
35 | // otherwise if the fetch _is_ shared, then continue down this code path
36 | const { namespace } = share;
37 |
38 | // the value comes from the `shared` sub-store instead of the `actions` sub-store
39 | const sharedData = state?.dataService?.shared?.data?.[namespace]?.[key] || null;
40 | return sharedData;
41 | };
42 |
43 | function useData(fetch) {
44 | const dataSelector = useMemo(() => makeDataSelector(fetch), [fetch]);
45 | return useSelector(dataSelector);
46 | }
47 |
48 | export default useData;
49 |
--------------------------------------------------------------------------------
/src/useData/useData.types-test.tsx:
--------------------------------------------------------------------------------
1 | import defineFetch, { typedFetchFactory } from '../defineFetch';
2 | import useData from './useData';
3 |
4 | const _makeGetMovie = defineFetch({
5 | displayName: 'Get Movie',
6 | make: (movieId: string) => ({
7 | key: [movieId],
8 | request: () => () => ({ id: movieId, name: 'blah' }),
9 | }),
10 | });
11 |
12 | interface Movie {
13 | id: string;
14 | name: string;
15 | }
16 |
17 | const makeGetMovie = typedFetchFactory()(_makeGetMovie);
18 |
19 | const getMovie = makeGetMovie('movie123');
20 | const data = useData(getMovie);
21 |
--------------------------------------------------------------------------------
/src/useDispatch/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './useDispatch';
2 | export * from './useDispatch';
3 |
--------------------------------------------------------------------------------
/src/useDispatch/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './useDispatch';
2 | export * from './useDispatch';
3 |
--------------------------------------------------------------------------------
/src/useDispatch/useDispatch.d.ts:
--------------------------------------------------------------------------------
1 | import { FetchAction } from '../defineFetch';
2 |
3 | /**
4 | * @docs `useDispatch`
5 | *
6 | * A simple hook that returns `dispatch`. Use `dispatch` to dispatch requests from fetch instances.
7 | *
8 | * [See here for more info.](../main-concepts/whats-a-fetch.md#making-a-request-then-dispatching-it)
9 | */
10 | export default function useDispatch(): (fetch: FetchAction) => Promise;
11 |
--------------------------------------------------------------------------------
/src/useDispatch/useDispatch.js:
--------------------------------------------------------------------------------
1 | import { useContext, useCallback } from 'react';
2 | import { ReactReduxContext } from 'react-redux';
3 | import CLEAR from '../prefixes/CLEAR';
4 |
5 | export default function useDispatch() {
6 | const contextValue = useContext(ReactReduxContext);
7 | if (!contextValue) {
8 | throw new Error(
9 | '[useDispatch] Could not find the respective context. In order to `useDispatch` you must add the respective provider. https://resift.org/docs/introduction/installation#adding-the-resiftprovider',
10 | );
11 | }
12 | const { store } = contextValue;
13 |
14 | if (process.env.NODE_ENV === 'production') return store.dispatch;
15 |
16 | // eslint-disable-next-line react-hooks/rules-of-hooks
17 | return useCallback(
18 | (action) => {
19 | const isFetchInstance = action?.meta?.type === 'FETCH_INSTANCE';
20 | const isFetchFactory = action?.meta?.type === 'FETCH_INSTANCE_FACTORY';
21 | const isClearAction = (action?.type || '').startsWith(CLEAR);
22 |
23 | if (isFetchInstance && !isClearAction) {
24 | // TODO: add docs for this
25 | throw new Error(
26 | "[useDispatch] You dispatched a fetch instance without calling it when you should've dispatched a request.",
27 | );
28 | }
29 | if (isFetchFactory) {
30 | throw new Error(
31 | // TODO: add docs for this
32 | "[useDispatch] You dispatched a fetch factory when you should've dispatched a request.",
33 | );
34 | }
35 |
36 | return store.dispatch(action);
37 | },
38 | [store],
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/useDispatch/useDispatch.test.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { createStore } from 'redux';
3 | import { Provider as ReduxProvider } from 'react-redux';
4 | import { act, create } from 'react-test-renderer';
5 | import DeferredPromise from '../DeferredPromise';
6 | import defineFetch from '../defineFetch';
7 |
8 | import useDispatch from './useDispatch';
9 |
10 | // see here...
11 | // https://github.com/facebook/react/issues/11098#issuecomment-370614347
12 | // ...for why these exist. not an ideal solution imo but it works
13 | beforeEach(() => {
14 | jest.spyOn(console, 'error');
15 | global.console.error.mockImplementation(() => {});
16 | });
17 |
18 | afterEach(() => {
19 | global.console.error.mockRestore();
20 | });
21 |
22 | test('it throws if there is no provider', async () => {
23 | const gotError = new DeferredPromise();
24 |
25 | class ErrorBoundary extends React.Component {
26 | componentDidCatch(e) {
27 | gotError.resolve(e);
28 | }
29 |
30 | render() {
31 | return this.props.children;
32 | }
33 | }
34 |
35 | function ExampleComponent() {
36 | useDispatch();
37 |
38 | return null;
39 | }
40 |
41 | await act(async () => {
42 | create(
43 |
44 |
45 | ,
46 | );
47 |
48 | await gotError;
49 | });
50 |
51 | const error = await gotError;
52 |
53 | expect(error.message).toMatchInlineSnapshot(
54 | `"[useDispatch] Could not find the respective context. In order to \`useDispatch\` you must add the respective provider. https://resift.org/docs/introduction/installation#adding-the-resiftprovider"`,
55 | );
56 | });
57 |
58 | test('it dispatches actions to the redux store', (done) => {
59 | const store = createStore((state, action) => {
60 | if (action.type === 'TEST_ACTION') {
61 | return {
62 | gotAction: true,
63 | };
64 | }
65 |
66 | return state;
67 | });
68 |
69 | store.subscribe(() => {
70 | if (store.getState().gotAction) {
71 | done();
72 | }
73 | });
74 |
75 | const TestComponent = jest.fn(() => {
76 | const dispatch = useDispatch();
77 | useEffect(() => {
78 | dispatch({ type: 'TEST_ACTION' });
79 | }, [dispatch]);
80 | return null;
81 | });
82 |
83 | act(() => {
84 | create(
85 |
86 |
87 | ,
88 | );
89 | });
90 | });
91 |
92 | test('it returns unwrapped store.dispatch in production', async () => {
93 | const nodeEnv = process.env.NODE_ENV;
94 | process.env.NODE_ENV = 'production';
95 | const done = new DeferredPromise();
96 | expect(process.env.NODE_ENV).toBe('production');
97 |
98 | const dispatch = () => {};
99 |
100 | const store = {
101 | dispatch,
102 | getState: () => ({}),
103 | subscribe: () => {},
104 | };
105 |
106 | function ExampleComponent() {
107 | const dispatch = useDispatch();
108 |
109 | useEffect(() => {
110 | done.resolve(dispatch);
111 | }, [dispatch]);
112 |
113 | return null;
114 | }
115 |
116 | await act(async () => {
117 | create(
118 |
119 |
120 | ,
121 | );
122 |
123 | await done;
124 | });
125 |
126 | const result = await done;
127 | expect(result).toBe(dispatch);
128 |
129 | process.env.NODE_ENV = nodeEnv;
130 | });
131 |
132 | test('throws when you dispatch a fetch instance instead of calling it', async () => {
133 | const makeFetch = defineFetch({
134 | displayName: 'Example',
135 | make: () => ({
136 | request: () => () => {},
137 | }),
138 | });
139 |
140 | const gotError = new DeferredPromise();
141 | class ErrorBoundary extends React.Component {
142 | componentDidCatch(e) {
143 | gotError.resolve(e);
144 | }
145 |
146 | render() {
147 | return this.props.children;
148 | }
149 | }
150 |
151 | function ExampleComponent() {
152 | const dispatch = useDispatch();
153 |
154 | const fetch = makeFetch();
155 |
156 | useEffect(() => {
157 | dispatch(fetch); // don't do this lol
158 | }, [dispatch, fetch]);
159 |
160 | return null;
161 | }
162 |
163 | const store = createStore(() => ({}));
164 |
165 | await act(async () => {
166 | create(
167 |
168 |
169 |
170 |
171 | ,
172 | );
173 |
174 | await gotError;
175 | });
176 |
177 | const error = await gotError;
178 |
179 | expect(error.message).toMatchInlineSnapshot(
180 | `"[useDispatch] You dispatched a fetch instance without calling it when you should've dispatched a request."`,
181 | );
182 | });
183 |
184 | test('throws when you dispatch a fetch factory', async () => {
185 | const makeFetch = defineFetch({
186 | displayName: 'Example',
187 | make: () => ({
188 | request: () => () => {},
189 | }),
190 | });
191 |
192 | const gotError = new DeferredPromise();
193 | class ErrorBoundary extends React.Component {
194 | componentDidCatch(e) {
195 | gotError.resolve(e);
196 | }
197 |
198 | render() {
199 | return this.props.children;
200 | }
201 | }
202 |
203 | function ExampleComponent() {
204 | const dispatch = useDispatch();
205 |
206 | useEffect(() => {
207 | dispatch(makeFetch); // don't do this lol
208 | }, [dispatch]);
209 |
210 | return null;
211 | }
212 |
213 | const store = createStore(() => ({}));
214 |
215 | await act(async () => {
216 | create(
217 |
218 |
219 |
220 |
221 | ,
222 | );
223 |
224 | await gotError;
225 | });
226 |
227 | const error = await gotError;
228 |
229 | expect(error.message).toMatchInlineSnapshot(
230 | `"[useDispatch] You dispatched a fetch factory when you should've dispatched a request."`,
231 | );
232 | });
233 |
--------------------------------------------------------------------------------
/src/useError/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './useError';
2 |
--------------------------------------------------------------------------------
/src/useError/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './useError';
2 |
--------------------------------------------------------------------------------
/src/useError/useError.d.ts:
--------------------------------------------------------------------------------
1 | import { FetchInstance } from '../defineFetch';
2 |
3 | /**
4 | * @docs `useError`
5 | *
6 | * Returns the associated error or `null`.
7 | * See the [error handling doc](../main-concepts/error-handling.md#the-useerror-hook)
8 | * for more info.
9 | */
10 | declare function useError(fetch: FetchInstance): any;
11 |
12 | export default useError;
13 |
--------------------------------------------------------------------------------
/src/useError/useError.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import createStoreKey from '../createStoreKey';
4 |
5 | const makeErrorSelector = (fetch) => (state) => {
6 | if (!fetch) {
7 | return null;
8 | }
9 |
10 | const isFetchInstance = fetch?.meta?.type === 'FETCH_INSTANCE';
11 | if (!isFetchInstance) {
12 | throw new Error('[useError] expected to see a fetch instance.');
13 | }
14 |
15 | if (!state.dataService) {
16 | throw new Error(
17 | '[useError] "dataService" is a required key. Double check with the installation guide here: https://resift.org/docs/introduction/installation',
18 | );
19 | }
20 | const { fetchFactoryId, displayName, key } = fetch.meta;
21 |
22 | const storeKey = createStoreKey(displayName, fetchFactoryId);
23 |
24 | const value = state?.dataService?.actions?.[storeKey]?.[key];
25 |
26 | if (!value) {
27 | return null;
28 | }
29 |
30 | if (value.errorData === undefined) {
31 | return null;
32 | }
33 |
34 | return value.errorData;
35 | };
36 |
37 | function useError(fetch) {
38 | const errorSelector = useMemo(() => makeErrorSelector(fetch), [fetch]);
39 | return useSelector(errorSelector);
40 | }
41 |
42 | export default useError;
43 |
--------------------------------------------------------------------------------
/src/useFetch/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './useFetch';
2 |
--------------------------------------------------------------------------------
/src/useFetch/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './useFetch';
2 |
--------------------------------------------------------------------------------
/src/useFetch/useFetch.d.ts:
--------------------------------------------------------------------------------
1 | import { FetchInstance } from '../defineFetch';
2 |
3 | declare function useFetch(
4 | fetch: FetchInstance,
5 | options?: any,
6 | ): [Data | null, number];
7 |
8 | export default useFetch;
9 |
--------------------------------------------------------------------------------
/src/useFetch/useFetch.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import useData from '../useData';
3 | import useStatus from '../useStatus';
4 |
5 | function useFetch(fetch, options) {
6 | const data = useData(fetch);
7 | const status = useStatus(fetch, options);
8 |
9 | return useMemo(() => [data, status], [data, status]);
10 | }
11 |
12 | export default useFetch;
13 |
--------------------------------------------------------------------------------
/src/useFetch/useFetch.test.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { act, create } from 'react-test-renderer';
3 | import useFetch from './useFetch';
4 | import ResiftProvider from '../ResiftProvider';
5 | import useDispatch from '../useDispatch';
6 | import createDataService from '../createDataService';
7 | import defineFetch from '../defineFetch';
8 | import isNormal from '../isNormal';
9 | import DeferredPromise from '../DeferredPromise';
10 |
11 | test('it groups together useStatus and useData', async () => {
12 | const done = new DeferredPromise();
13 |
14 | const dataService = createDataService({
15 | services: {},
16 | onError: (e) => {
17 | throw e;
18 | },
19 | });
20 |
21 | const makeGetThing = defineFetch({
22 | displayName: 'Get Thing',
23 | make: () => ({
24 | request: () => () => ({ hello: 'world' }),
25 | }),
26 | });
27 | const getThing = makeGetThing();
28 |
29 | const statusHandler = jest.fn();
30 | const dataHandler = jest.fn();
31 |
32 | function ExampleComponent() {
33 | const dispatch = useDispatch();
34 |
35 | const [data, status] = useFetch(getThing);
36 |
37 | useEffect(() => {
38 | statusHandler(status);
39 | if (isNormal(status)) {
40 | done.resolve();
41 | }
42 | }, [status]);
43 |
44 | useEffect(() => {
45 | dataHandler(data);
46 | }, [data]);
47 |
48 | useEffect(() => {
49 | dispatch(getThing());
50 | }, [dispatch]);
51 |
52 | return null;
53 | }
54 |
55 | await act(async () => {
56 | create(
57 |
58 |
59 | ,
60 | );
61 | await done;
62 | });
63 |
64 | expect(dataHandler.mock.calls.map((args) => args[0])).toMatchInlineSnapshot(`
65 | Array [
66 | null,
67 | Object {
68 | "hello": "world",
69 | },
70 | ]
71 | `);
72 | expect(statusHandler.mock.calls.map((args) => args[0])).toMatchInlineSnapshot(`
73 | Array [
74 | 0,
75 | 2,
76 | 1,
77 | ]
78 | `);
79 | });
80 |
--------------------------------------------------------------------------------
/src/useStatus/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default } from './useStatus';
2 |
--------------------------------------------------------------------------------
/src/useStatus/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './useStatus';
2 |
--------------------------------------------------------------------------------
/src/useStatus/useStatus.d.ts:
--------------------------------------------------------------------------------
1 | import { FetchInstance } from '../defineFetch';
2 |
3 | /**
4 | * @docs `useStatus`
5 | *
6 | * Returns the status of a fetch given a fetch instance
7 | *
8 | * If you pass in `null` (or any other falsy value), you'll get `UNKNOWN` back out
9 | */
10 | declare function useStatus(fetch: FetchInstance | null, options?: UseStatusOptions): number;
11 |
12 | /**
13 | * @docs `UseStatusOptions`
14 | */
15 | interface UseStatusOptions {
16 | /**
17 | * If this is present, it will only report the status for the current fetch factory (if the fetch factory is shared).
18 | */
19 | isolatedStatus?: boolean;
20 | }
21 |
22 | export default useStatus;
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNEXT",
4 | "module": "commonjs",
5 | "jsx": "react",
6 | "declaration": true,
7 | "strict": true,
8 | "baseUrl": ".",
9 | "noEmit": true,
10 | "paths": {
11 | "@sift/resift": ["src/index.ts"]
12 | },
13 | "allowSyntheticDefaultImports": true,
14 | "esModuleInterop": true
15 | },
16 | "include": ["./src/**/*"],
17 | "exclude": [
18 | "node_modules",
19 | "**/*.spec.ts",
20 | "**/*.spec.js",
21 | "**/*.test.ts",
22 | "**/*.types-test.ts",
23 | "**/*.types-test.tsx",
24 | "**/*.test.js"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | This website was created with [Docusaurus](https://docusaurus.io/).
2 |
3 | # What's In This Document
4 |
5 | - [Get Started in 5 Minutes](#get-started-in-5-minutes)
6 | - [Directory Structure](#directory-structure)
7 | - [Editing Content](#editing-content)
8 | - [Adding Content](#adding-content)
9 | - [Full Documentation](#full-documentation)
10 |
11 | # Get Started in 5 Minutes
12 |
13 | 1. Make sure all the dependencies for the website are installed:
14 |
15 | ```sh
16 | # Install dependencies
17 | $ yarn
18 | ```
19 |
20 | 2. Run your dev server:
21 |
22 | ```sh
23 | # Start the site
24 | $ yarn start
25 | ```
26 |
27 | ## Directory Structure
28 |
29 | Your project file structure should look something like this
30 |
31 | ```
32 | my-docusaurus/
33 | docs/
34 | doc-1.md
35 | doc-2.md
36 | doc-3.md
37 | website/
38 | blog/
39 | 2016-3-11-oldest-post.md
40 | 2017-10-24-newest-post.md
41 | core/
42 | node_modules/
43 | pages/
44 | static/
45 | css/
46 | img/
47 | package.json
48 | sidebar.json
49 | siteConfig.js
50 | ```
51 |
52 | # Editing Content
53 |
54 | ## Editing an existing docs page
55 |
56 | Edit docs by navigating to `docs/` and editing the corresponding document:
57 |
58 | `docs/doc-to-be-edited.md`
59 |
60 | ```markdown
61 | ---
62 | id: page-needs-edit
63 | title: This Doc Needs To Be Edited
64 | ---
65 |
66 | Edit me...
67 | ```
68 |
69 | For more information about docs, click [here](https://docusaurus.io/docs/en/navigation)
70 |
71 | ## Editing an existing blog post
72 |
73 | Edit blog posts by navigating to `website/blog` and editing the corresponding post:
74 |
75 | `website/blog/post-to-be-edited.md`
76 |
77 | ```markdown
78 | ---
79 | id: post-needs-edit
80 | title: This Blog Post Needs To Be Edited
81 | ---
82 |
83 | Edit me...
84 | ```
85 |
86 | For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog)
87 |
88 | # Adding Content
89 |
90 | ## Adding a new docs page to an existing sidebar
91 |
92 | 1. Create the doc as a new markdown file in `/docs`, example `docs/newly-created-doc.md`:
93 |
94 | ```md
95 | ---
96 | id: newly-created-doc
97 | title: This Doc Needs To Be Edited
98 | ---
99 |
100 | My new content here..
101 | ```
102 |
103 | 1. Refer to that doc's ID in an existing sidebar in `website/sidebar.json`:
104 |
105 | ```javascript
106 | // Add newly-created-doc to the Getting Started category of docs
107 | {
108 | "docs": {
109 | "Getting Started": [
110 | "quick-start",
111 | "newly-created-doc" // new doc here
112 | ],
113 | ...
114 | },
115 | ...
116 | }
117 | ```
118 |
119 | For more information about adding new docs, click [here](https://docusaurus.io/docs/en/navigation)
120 |
121 | ## Adding a new blog post
122 |
123 | 1. Make sure there is a header link to your blog in `website/siteConfig.js`:
124 |
125 | `website/siteConfig.js`
126 |
127 | ```javascript
128 | headerLinks: [
129 | ...
130 | { blog: true, label: 'Blog' },
131 | ...
132 | ]
133 | ```
134 |
135 | 2. Create the blog post with the format `YYYY-MM-DD-My-Blog-Post-Title.md` in `website/blog`:
136 |
137 | `website/blog/2018-05-21-New-Blog-Post.md`
138 |
139 | ```markdown
140 | ---
141 | author: Frank Li
142 | authorURL: https://twitter.com/foobarbaz
143 | authorFBID: 503283835
144 | title: New Blog Post
145 | ---
146 |
147 | Lorem Ipsum...
148 | ```
149 |
150 | For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog)
151 |
152 | ## Adding items to your site's top navigation bar
153 |
154 | 1. Add links to docs, custom pages or external links by editing the headerLinks field of `website/siteConfig.js`:
155 |
156 | `website/siteConfig.js`
157 |
158 | ```javascript
159 | {
160 | headerLinks: [
161 | ...
162 | /* you can add docs */
163 | { doc: 'my-examples', label: 'Examples' },
164 | /* you can add custom pages */
165 | { page: 'help', label: 'Help' },
166 | /* you can add external links */
167 | { href: 'https://github.com/facebook/Docusaurus', label: 'GitHub' },
168 | ...
169 | ],
170 | ...
171 | }
172 | ```
173 |
174 | For more information about the navigation bar, click [here](https://docusaurus.io/docs/en/navigation)
175 |
176 | ## Adding custom pages
177 |
178 | 1. Docusaurus uses React components to build pages. The components are saved as .js files in `website/pages/en`:
179 | 1. If you want your page to show up in your navigation header, you will need to update `website/siteConfig.js` to add to the `headerLinks` element:
180 |
181 | `website/siteConfig.js`
182 |
183 | ```javascript
184 | {
185 | headerLinks: [
186 | ...
187 | { page: 'my-new-custom-page', label: 'My New Custom Page' },
188 | ...
189 | ],
190 | ...
191 | }
192 | ```
193 |
194 | For more information about custom pages, click [here](https://docusaurus.io/docs/en/custom-pages).
195 |
196 | # Full Documentation
197 |
198 | Full documentation can be found on the [website](https://docusaurus.io/).
199 |
--------------------------------------------------------------------------------
/website/core/Footer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | const React = require('react');
9 |
10 | class Footer extends React.Component {
11 | docUrl(doc) {
12 | const baseUrl = this.props.config.baseUrl;
13 | const docsUrl = this.props.config.docsUrl;
14 | const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`;
15 | return `${baseUrl}${docsPart}${doc}`;
16 | }
17 |
18 | pageUrl(doc, language) {
19 | const baseUrl = this.props.config.baseUrl;
20 | return baseUrl + (language ? `${language}/` : '') + doc;
21 | }
22 |
23 | render() {
24 | return (
25 |
101 | );
102 | }
103 | }
104 |
105 | module.exports = Footer;
106 |
--------------------------------------------------------------------------------
/website/generate-api-doc.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const generateApiDoc = require('./generate-api-doc');
4 |
5 | const exampleDoc = `
6 | // this should turn everything that has the @docs directive into markdown
7 |
8 | /**
9 | * @docs This is a title
10 | *
11 | * This is a description. The output should have no generics.
12 | */
13 | function test(t: T): number
14 |
15 | /**
16 | * @docs Test Interface
17 | *
18 | * This interface should turn into a table
19 | */
20 | interface Test {
21 | /**
22 | * this is a description of foo
23 | */
24 | foo: string;
25 | bar?: number;
26 | complex: {
27 | a: Date;
28 | b: Animal;
29 | };
30 | }
31 |
32 | /**
33 | * @docs \`Animal\`
34 | * makes a sound
35 | */
36 | interface Animal {
37 | makeSound(): string;
38 | }
39 |
40 | /**
41 | * this won't be included because it doesn't have the directive
42 | */
43 | function boo(): number;
44 |
45 | /**
46 | * @docs Other code test
47 | * this will have its generics removed
48 | */
49 | type OtherCode = Cool & Generic & Thing;
50 | `;
51 |
52 | it('works', () => {
53 | const apiDoc = generateApiDoc('exampleDoc', exampleDoc);
54 |
55 | expect(apiDoc).toMatchInlineSnapshot(`
56 | "---
57 | id: example-doc
58 | title: exampleDoc API
59 | sidebar_label: exampleDoc
60 | ---
61 |
62 | > These docs are auto-generated from typings files (\`*.d.ts\`).
63 |
64 | ## This is a title
65 |
66 | This is a description. The output should have no generics.
67 |
68 | \`\`\`ts
69 | // this should turn everything that has the @docs directive into markdown
70 |
71 | function test(t: T): number;
72 | \`\`\`
73 |
74 | ## Test Interface
75 |
76 | This interface should turn into a table
77 |
78 | | Name | Description | Type | Required |
79 | | -------------------- | ---------------------------- | --------------------------------------------- | -------- |
80 | | foo
| this is a description of foo | string
| yes |
81 | | bar
| | number
| no |
82 | | complex
| | {
a: Date
b: Animal
}
| yes |
83 |
84 | ## \`Animal\`
85 |
86 | makes a sound
87 |
88 | | Name | Description | Type | Required |
89 | | ---------------------- | ----------- | ------------------- | -------- |
90 | | makeSound
| | string
| yes |
91 |
92 | ## Other code test
93 |
94 | this will have its generics removed
95 |
96 | \`\`\`ts
97 | type OtherCode = Cool & Generic & Thing;
98 | \`\`\`
99 | "
100 | `);
101 | });
102 |
--------------------------------------------------------------------------------
/website/generate-api-docs.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { promisify } = require('util');
4 | const { flatten } = require('lodash');
5 | const readdir = promisify(fs.readdir);
6 | const stat = promisify(fs.stat);
7 | const readFile = promisify(fs.readFile);
8 | const writeFile = promisify(fs.writeFile);
9 | const mkdir = promisify(fs.mkdir);
10 | const generateApiDoc = require('./generate-api-doc');
11 |
12 | async function asyncFilter(array, predicate) {
13 | const result = await Promise.all(
14 | array.map(async (value) => ({
15 | value,
16 | keep: await predicate(value),
17 | })),
18 | );
19 |
20 | return result.filter((x) => x.keep).map((x) => x.value);
21 | }
22 |
23 | function startsWithUppercase(str) {
24 | const first = str[0];
25 | if (!first) return false;
26 | return first === first.toUpperCase();
27 | }
28 |
29 | function isAllCaps(str) {
30 | if (!str) return false;
31 | return str === str.toUpperCase();
32 | }
33 |
34 | function camelToDashed(camel) {
35 | return camel
36 | .split('')
37 | .map((letter, index) =>
38 | letter.toUpperCase() === letter && index !== 0 ? `-${letter}` : letter,
39 | )
40 | .join('')
41 | .toLowerCase();
42 | }
43 |
44 | async function main() {
45 | /**
46 | * @param {string} dir
47 | * @return {string[]}
48 | */
49 | async function findAllTypingsFiles(dir) {
50 | const currentFiles = await readdir(dir);
51 |
52 | const fileNames = flatten(
53 | await Promise.all(
54 | currentFiles.map(async (file) => {
55 | const path = `${dir}/${file}`;
56 | const result = await stat(path);
57 |
58 | if (result.isDirectory()) {
59 | return findAllTypingsFiles(path);
60 | }
61 |
62 | return path;
63 | }),
64 | ),
65 | )
66 | .filter((file) => file.endsWith('.d.ts'))
67 | .filter((file) => !file.endsWith('index.d.ts'));
68 |
69 | return fileNames;
70 | }
71 |
72 | const fileNames = await findAllTypingsFiles(path.resolve(__dirname, '../src'));
73 |
74 | const docFileNames = await asyncFilter(fileNames, async (fileName) => {
75 | const contents = await readFile(fileName);
76 | const text = contents.toString();
77 | return text.includes('@docs');
78 | });
79 |
80 | console.log({ docFileNames });
81 |
82 | for (const docPath of docFileNames) {
83 | const pathSplit = docPath.split('/');
84 | const fileName = pathSplit[pathSplit.length - 1];
85 | const name = fileName.substring(0, fileName.length - '.d.ts'.length);
86 | const id = camelToDashed(name);
87 |
88 | const contents = (await readFile(docPath)).toString();
89 |
90 | const result = generateApiDoc(name, contents);
91 |
92 | try {
93 | await mkdir(path.resolve(__dirname, '../docs/api'));
94 | } catch {}
95 | await writeFile(path.resolve(__dirname, `../docs/api/${id}.md`), result);
96 | }
97 |
98 | const sidebars = JSON.parse(
99 | (await readFile(path.resolve(__dirname, './sidebars.json'))).toString(),
100 | );
101 |
102 | const docIds = docFileNames
103 | .map((docPath) => {
104 | const pathSplit = docPath.split('/');
105 | const fileName = pathSplit[pathSplit.length - 1];
106 | const name = fileName.substring(0, fileName.length - '.d.ts'.length);
107 | const id = camelToDashed(name);
108 |
109 | return { path: `api/${id}`, name };
110 | })
111 | .sort((a, b) => {
112 | if (isAllCaps(a.name) && !isAllCaps(b.name)) return -1;
113 | if (isAllCaps(b.name) && !isAllCaps(a.name)) return 1;
114 | if (startsWithUppercase(a.name) && !startsWithUppercase(b.name)) return -1;
115 | if (startsWithUppercase(b.name) && !startsWithUppercase(a.name)) return 1;
116 | return a.name.localeCompare(b.name);
117 | })
118 | .map((x) => x.path)
119 | .reverse();
120 |
121 | const newSidebars = {
122 | ...sidebars,
123 | docs: {
124 | ...sidebars.docs,
125 | API: ['api/about-api-docs', ...docIds],
126 | },
127 | };
128 |
129 | await writeFile(path.resolve(__dirname, './sidebars.json'), JSON.stringify(newSidebars, null, 2));
130 | }
131 |
132 | main()
133 | .then(() => {
134 | process.exit(0);
135 | })
136 | .catch((e) => {
137 | console.error(e);
138 | process.exit(1);
139 | });
140 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "examples": "docusaurus-examples",
4 | "start": "docusaurus-start",
5 | "build": "node ./generate-api-docs && node ./replaceTwitterCardType && docusaurus-build && echo \"/ /docs/introduction/what-is-resift\" >> ./build/resift/_redirects",
6 | "publish-gh-pages": "docusaurus-publish",
7 | "write-translations": "docusaurus-write-translations",
8 | "version": "docusaurus-version",
9 | "rename-version": "docusaurus-rename-version"
10 | },
11 | "dependencies": {
12 | "common-tags": "^1.8.0",
13 | "docusaurus": "^1.14.0",
14 | "lodash": "^4.17.15",
15 | "markdown-table": "^1.1.3",
16 | "prettier": "^1.18.2",
17 | "typescript": "^3.5.3"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/website/pages/en/help.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | const React = require('react');
9 |
10 | const CompLibrary = require('../../core/CompLibrary.js');
11 |
12 | const Container = CompLibrary.Container;
13 | const GridBlock = CompLibrary.GridBlock;
14 |
15 | function Help() {
16 | const supportLinks = [
17 | {
18 | title: 'Found an issue? Have an idea?',
19 | content: `Don't hesitate to [open an issue](https://github.com/JustSift/ReSift/issues/new)`,
20 | },
21 | {
22 | title: 'Have a question?',
23 | content:
24 | 'Ask it on [StackOverflow](https://stackoverflow.com/questions/tagged/resift). Use the tag `resift`',
25 | },
26 | ];
27 |
28 | return (
29 |
30 |
31 |
32 |
35 |
We're here for you!
36 |
37 |
38 |
39 |
40 | );
41 | }
42 | module.exports = Help;
43 |
--------------------------------------------------------------------------------
/website/replaceTwitterCardType.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const headJs = path.resolve(__dirname, './node_modules/docusaurus/lib/core/Head.js');
4 | const contents = fs.readFileSync(headJs).toString();
5 | fs.writeFileSync(
6 | headJs,
7 | contents.replace(
8 | /name="twitter:card" content="summary"/g,
9 | 'name="twitter:card" content="summary_large_image"',
10 | ),
11 | );
12 |
--------------------------------------------------------------------------------
/website/sidebars.json:
--------------------------------------------------------------------------------
1 | {
2 | "docs": {
3 | "Introduction": ["introduction/what-is-resift", "introduction/installation"],
4 | "Tutorial": ["tutorial/resift-rentals"],
5 | "Main Concepts": [
6 | "main-concepts/whats-a-fetch",
7 | "main-concepts/how-to-define-a-fetch",
8 | "main-concepts/making-state-consistent",
9 | "main-concepts/making-sense-of-statuses",
10 | "main-concepts/what-are-data-services",
11 | "main-concepts/error-handling",
12 | "main-concepts/custom-hooks"
13 | ],
14 | "Examples": [
15 | "examples/resift-notes",
16 | "examples/infinite-scroll",
17 | "examples/custom-hooks-react-router"
18 | ],
19 | "Guides": [
20 | "guides/resift-vs-apollo-relay",
21 | "guides/http-proxies",
22 | "guides/usage-with-typescript",
23 | "guides/usage-with-redux",
24 | "guides/usage-with-classes"
25 | ],
26 | "API": [
27 | "api/about-api-docs",
28 | "api/use-status",
29 | "api/use-error",
30 | "api/use-dispatch",
31 | "api/use-data",
32 | "api/use-clear-fetch",
33 | "api/is-unknown",
34 | "api/is-normal",
35 | "api/is-loading",
36 | "api/is-error",
37 | "api/define-fetch",
38 | "api/data-service-reducer",
39 | "api/create-store-key",
40 | "api/create-http-service",
41 | "api/create-http-proxy",
42 | "api/create-data-service",
43 | "api/create-action-type",
44 | "api/combine-statuses",
45 | "api/resift-provider",
46 | "api/guard",
47 | "api/canceled-error",
48 | "api/u-n-k-n-o-w-n",
49 | "api/n-o-r-m-a-l",
50 | "api/l-o-a-d-i-n-g",
51 | "api/e-r-r-o-r"
52 | ]
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/website/siteConfig.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | // See https://docusaurus.io/docs/site-config for all the possible
9 | // site configuration options.
10 |
11 | const siteConfig = {
12 | title: 'ReSift', // Title for your website.
13 | tagline: 'A React state management library for fetches',
14 | url: 'https://resift.org', // Your website URL
15 | baseUrl: '/', // Base URL for your project */
16 | // For github.io type URLs, you would set the url and baseUrl like:
17 | // url: 'https://facebook.github.io',
18 | // baseUrl: '/test-site/',
19 |
20 | // Used for publishing and more
21 | projectName: 'resift',
22 | // For top-level user or org sites, the organization is still the same.
23 | // e.g., for the https://JoelMarcey.github.io site, it would be set like...
24 | organizationName: 'JustSift',
25 |
26 | // For no header links in the top nav bar -> headerLinks: [],
27 | headerLinks: [
28 | { doc: 'introduction/what-is-resift', label: 'Docs' },
29 | { doc: 'api/about-api-docs', label: 'API' },
30 | { page: 'help', label: 'Help' },
31 | { href: 'https://github.com/JustSift/ReSift', label: 'GitHub' },
32 | ],
33 |
34 | /* path to images for header/footer */
35 | headerIcon: 'img/sift-wordmark.png',
36 | footerIcon: 'img/resift-logo.png',
37 | favicon: 'img/favicon.ico',
38 |
39 | /* Colors for website */
40 | colors: {
41 | primaryColor: '#7512FF',
42 | secondaryColor: '#2962FF',
43 | },
44 |
45 | /* Custom fonts for website */
46 | fonts: {
47 | myFont: ['Source Sans Pro', 'sans-serif'],
48 | myOtherFont: ['Source Sans Pro', 'sans-serif'],
49 | },
50 |
51 | // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds.
52 | copyright: `Copyright © ${new Date().getFullYear()} Sift`,
53 |
54 | highlight: {
55 | // Highlight.js theme to use for syntax highlighting in code blocks.
56 | theme: 'default',
57 | },
58 |
59 | // Add custom scripts here that would be placed in
9 | ReSift · A React state management library for fetches
10 |
11 |
12 | If you are not redirected automatically, follow this
13 | link.
14 |
15 |