├── .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 · [![Build Status](https://travis-ci.org/JustSift/ReSift.svg?branch=master)](https://travis-ci.org/JustSift/ReSift) [![Coverage Status](https://coveralls.io/repos/github/JustSift/ReSift/badge.svg?branch=master)](https://coveralls.io/github/JustSift/ReSift?branch=master) 2 | 3 | ![ReSift Logo](./website/static/img/resift-wordmark.png) 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 |
33 |

Need help?

34 |
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 | 16 | --------------------------------------------------------------------------------