├── .travis.yml ├── .prettierrc ├── .size-snapshot.json ├── tsconfig.json ├── .gitignore ├── rollup.config.js ├── package.json ├── img ├── useAsset.svg ├── createAsset.svg ├── recipes.svg ├── hooks-global-cache.svg ├── async-assets.svg └── cover.svg ├── .eslintrc.json ├── src └── index.ts └── readme.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "index.js": { 3 | "bundled": 2827, 4 | "minified": 1300, 5 | "gzipped": 575, 6 | "treeshaked": { 7 | "rollup": { 8 | "code": 14, 9 | "import_statements": 14 10 | }, 11 | "webpack": { 12 | "code": 1843 13 | } 14 | } 15 | }, 16 | "index.cjs.js": { 17 | "bundled": 6129, 18 | "minified": 3361, 19 | "gzipped": 1278 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es2018", 5 | "allowSyntheticDefaultImports": true, 6 | "jsx": "react", 7 | "strict": true, 8 | "preserveSymlinks": true, 9 | "moduleResolution": "Node", 10 | "esModuleInterop": true, 11 | "declaration": true, 12 | "declarationDir": "dist", 13 | "skipLibCheck": true, 14 | "removeComments": false, 15 | "baseUrl": ".", 16 | "paths": { 17 | "react-three-flex": ["./src/index.ts"] 18 | } 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # misc 2 | .DS_Store 3 | .env.local 4 | .env.development.local 5 | .env.test.local 6 | .env.production.local 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | *.lcov 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Compiled binary addons (https://nodejs.org/api/addons.html) 26 | build/ 27 | 28 | # Dependency directories 29 | node_modules/ 30 | jspm_packages/ 31 | 32 | # TypeScript cache 33 | *.tsbuildinfo 34 | 35 | # Optional eslint cache 36 | .eslintcache 37 | 38 | # Output of 'npm pack' 39 | *.tgz 40 | 41 | # Yarn Integrity file 42 | .yarn-integrity 43 | 44 | # pnpm lock 45 | 46 | pnpm-lock.yaml 47 | 48 | dist 49 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import babel from '@rollup/plugin-babel' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import { terser } from 'rollup-plugin-terser' 5 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot' 6 | 7 | const root = process.platform === 'win32' ? path.resolve('/') : '/' 8 | const external = (id) => !id.startsWith('.') && !id.startsWith(root) 9 | const extensions = ['.js', '.jsx', '.ts', '.tsx', '.json'] 10 | 11 | const getBabelOptions = ({ useESModules }, targets) => ({ 12 | babelrc: false, 13 | extensions, 14 | exclude: '**/node_modules/**', 15 | babelHelpers: 'runtime', 16 | presets: [ 17 | ['@babel/preset-env', { loose: true, modules: false, targets }], 18 | '@babel/preset-react', 19 | '@babel/preset-typescript', 20 | ], 21 | plugins: [['@babel/transform-runtime', { regenerator: false, useESModules }]], 22 | }) 23 | 24 | export default [ 25 | { 26 | input: `./src/index.ts`, 27 | output: { file: `dist/index.js`, format: 'esm' }, 28 | external, 29 | plugins: [ 30 | babel(getBabelOptions({ useESModules: true }, '>1%, not dead, not ie 11, not op_mini all')), 31 | sizeSnapshot(), 32 | resolve({ extensions }), 33 | ], 34 | }, 35 | { 36 | input: `./src/index.ts`, 37 | output: { file: `dist/index.cjs.js`, format: 'cjs' }, 38 | external, 39 | plugins: [babel(getBabelOptions({ useESModules: false })), sizeSnapshot(), resolve({ extensions })], 40 | }, 41 | ] 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-asset", 3 | "version": "1.0.4", 4 | "description": "A data fetching strategy for React Suspense", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "sideEffects": false, 9 | "keywords": [ 10 | "react", 11 | "suspense", 12 | "resource", 13 | "asset" 14 | ], 15 | "author": "Paul Henschel", 16 | "license": "MIT", 17 | "repository": "pmndrs/use-asset", 18 | "homepage": "https://github.com/pmndrs/use-asset#readme", 19 | "husky": { 20 | "hooks": { 21 | "pre-commit": "lint-staged" 22 | } 23 | }, 24 | "lint-staged": { 25 | "*.{js,jsx,ts,tsx}": [ 26 | "eslint --fix" 27 | ] 28 | }, 29 | "scripts": { 30 | "build": "rollup -c", 31 | "postbuild": "tsc --emitDeclarationOnly", 32 | "prepublishOnly": "npm run build", 33 | "test": "echo no tests yet" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "7.12.10", 37 | "@babel/plugin-proposal-class-properties": "^7.12.1", 38 | "@babel/plugin-transform-modules-commonjs": "7.12.1", 39 | "@babel/plugin-transform-parameters": "7.12.1", 40 | "@babel/plugin-transform-runtime": "7.12.10", 41 | "@babel/plugin-transform-template-literals": "7.12.1", 42 | "@babel/preset-env": "7.12.11", 43 | "@babel/preset-react": "7.12.10", 44 | "@babel/preset-typescript": "^7.12.7", 45 | "@rollup/plugin-babel": "^5.2.2", 46 | "@rollup/plugin-node-resolve": "^11.1.0", 47 | "@types/jest": "^26.0.20", 48 | "@types/node": "^14.14.21", 49 | "@types/react": "^17.0.0", 50 | "@types/react-dom": "^17.0.0", 51 | "@types/react-test-renderer": "^17.0.0", 52 | "@typescript-eslint/eslint-plugin": "^4.13.0", 53 | "@typescript-eslint/parser": "^4.13.0", 54 | "eslint": "^7.18.0", 55 | "eslint-config-prettier": "^6.13.0", 56 | "eslint-import-resolver-alias": "^1.1.2", 57 | "eslint-plugin-import": "^2.22.1", 58 | "eslint-plugin-jest": "^24.1.0", 59 | "eslint-plugin-prettier": "^3.1.4", 60 | "eslint-plugin-react": "^7.21.5", 61 | "eslint-plugin-react-hooks": "^4.2.0", 62 | "husky": "^4.3.8", 63 | "lint-staged": "^10.5.3", 64 | "prettier": "^2.2.1", 65 | "react": "^17.0.1", 66 | "rollup": "^2.36.2", 67 | "rollup-plugin-size-snapshot": "^0.12.0", 68 | "rollup-plugin-terser": "^7.0.2", 69 | "typescript": "^4.1.3" 70 | }, 71 | "peerDependencies": { 72 | "react": ">=17.0" 73 | }, 74 | "dependencies": { 75 | "fast-deep-equal": "^3.1.3" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /img/useAsset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | promiseFn:args:useAsset.lifespanuseAsset.preloadpromiseFn:...args:useAsset.clear...args:useAsset.peek...args: 4 | useAsset 5 | ( , ): = 0 = ( , ) => = ( ) => = ( ) => 6 | functionPromiseFnany[]any PromiseFnany[]void any[]void any[]any 7 | 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "prettier", 9 | "prettier/react", 10 | "prettier/@typescript-eslint", 11 | "plugin:prettier/recommended", 12 | "plugin:react-hooks/recommended", 13 | "plugin:import/errors", 14 | "plugin:import/warnings", 15 | "prettier", 16 | "prettier/react", 17 | "prettier/@typescript-eslint" 18 | ], 19 | "plugins": ["@typescript-eslint", "react", "react-hooks", "import", "jest", "prettier"], 20 | "parser": "@typescript-eslint/parser", 21 | "parserOptions": { 22 | "ecmaFeatures": { 23 | "jsx": true 24 | }, 25 | "ecmaVersion": 2018, 26 | "sourceType": "module", 27 | "rules": { 28 | "curly": ["warn", "multi-line", "consistent"], 29 | "no-console": "off", 30 | "no-empty-pattern": "warn", 31 | "no-duplicate-imports": "error", 32 | "import/no-unresolved": "off", 33 | "import/export": "error", 34 | // https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/FAQ.md#eslint-plugin-import 35 | // We recommend you do not use the following import/* rules, as TypeScript provides the same checks as part of standard type checking: 36 | "import/named": "off", 37 | "import/namespace": "off", 38 | "import/default": "off", 39 | "no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], 40 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], 41 | "@typescript-eslint/no-use-before-define": "off", 42 | "@typescript-eslint/no-empty-function": "off", 43 | "@typescript-eslint/no-empty-interface": "off", 44 | "@typescript-eslint/no-explicit-any": "off", 45 | "jest/consistent-test-it": ["error", { "fn": "it", "withinDescribe": "it" }] 46 | } 47 | }, 48 | "settings": { 49 | "react": { 50 | "version": "detect" 51 | }, 52 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"], 53 | "import/parsers": { 54 | "@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"] 55 | }, 56 | "import/resolver": { 57 | "node": { 58 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], 59 | "paths": ["src"] 60 | }, 61 | "alias": { 62 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], 63 | "map": [["react-three-fiber", "./src/targets/web.tsx"]] 64 | } 65 | } 66 | }, 67 | "overrides": [ 68 | { 69 | "files": ["src"], 70 | "parserOptions": { 71 | "project": "./tsconfig.json" 72 | } 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /img/createAsset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | promiseFnlifespanread...argspreload...argsclear...argspeek...args 4 | createAsset 5 | (: , ): { : (: ) => ; : (: ) => ; : (: ) => ; : (: ) => ; } 6 | functionPromiseFn= 0any[]anyany[]voidany[]voidany[]any 7 | 8 | -------------------------------------------------------------------------------- /img/recipes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import deepEqual from 'fast-deep-equal' 2 | 3 | type PromiseCache = { 4 | promise: Promise 5 | args: Args 6 | error?: any 7 | response?: Response 8 | } 9 | 10 | type PromiseFn = (...args: Args) => Promise 11 | 12 | const globalCache: PromiseCache[] = [] 13 | 14 | function handleAsset( 15 | fn: PromiseFn, 16 | cache: PromiseCache[], 17 | args: Args, 18 | lifespan = 0, 19 | preload = false 20 | ) { 21 | for (const entry of cache) { 22 | // Find a match 23 | if (deepEqual(args, entry.args)) { 24 | // If we're pre-loading and the element is present, just return 25 | if (preload) return 26 | // If an error occurred, throw 27 | if (entry.error) throw entry.error 28 | // If a response was successful, return 29 | if (entry.response) return entry.response 30 | // If the promise is still unresolved, throw 31 | throw entry.promise 32 | } 33 | } 34 | 35 | // The request is new or has changed. 36 | const entry: PromiseCache = { 37 | args, 38 | promise: 39 | // Make the promise request. 40 | fn(...args) 41 | // Response can't be undefined or else the loop above wouldn't be able to return it 42 | // This is for promises that do not return results (delays for instance) 43 | .then((response) => (entry.response = (response ?? true) as Response)) 44 | .catch((e) => (entry.error = e ?? 'unknown error')) 45 | .then(() => { 46 | if (lifespan > 0) { 47 | setTimeout(() => { 48 | const index = cache.indexOf(entry) 49 | if (index !== -1) cache.splice(index, 1) 50 | }, lifespan) 51 | } 52 | }), 53 | } 54 | cache.push(entry) 55 | if (!preload) throw entry.promise 56 | } 57 | 58 | function clear(cache: PromiseCache[], ...args: Args) { 59 | if (args === undefined || args.length === 0) cache.splice(0, cache.length) 60 | else { 61 | const entry = cache.find((entry) => deepEqual(args, entry.args)) 62 | if (entry) { 63 | const index = cache.indexOf(entry) 64 | if (index !== -1) cache.splice(index, 1) 65 | } 66 | } 67 | } 68 | 69 | function createAsset(fn: PromiseFn, lifespan = 0) { 70 | const cache: PromiseCache[] = [] 71 | return { 72 | /** 73 | * @throws Suspense Promise if asset is not yet ready 74 | * @throws Error if the promise rejected for some reason 75 | */ 76 | read: (...args: Args): Response => handleAsset(fn, cache, args, lifespan) as Response, 77 | preload: (...args: Args): void => void handleAsset(fn, cache, args, lifespan, true), 78 | clear: (...args: Args) => clear(cache, ...args), 79 | peek: (...args: Args): void | Response => cache.find((entry) => deepEqual(args, entry.args))?.response, 80 | } 81 | } 82 | 83 | function useAsset(fn: PromiseFn, ...args: Args): Response { 84 | return handleAsset(fn, globalCache as PromiseCache[], args, useAsset.lifespan) as Response 85 | } 86 | 87 | useAsset.lifespan = 0 88 | useAsset.clear = (...args: Args) => clear(globalCache, ...args) 89 | useAsset.preload = (fn: PromiseFn, ...args: Args) => 90 | void handleAsset(fn, globalCache as PromiseCache[], args, useAsset.lifespan, true) 91 | useAsset.peek = (...args: Args) => 92 | globalCache.find((entry) => deepEqual(args, entry.args))?.response as Response 93 | 94 | export { createAsset, useAsset } 95 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | This project is deprecated, please use https://github.com/pmndrs/suspend-react instead! 2 | 3 |

4 | This library allows you to create cached assets, which can be promises, async functions or even dynamic imports. These assets then have the ability to suspend the component in which they are read. This makes it easier to orchestrate async tasks and gives you the ability to set up fallbacks and error-handling declaratively. 5 |

6 | 7 | [![Build Size](https://img.shields.io/bundlephobia/min/use-asset?label=bunlde%20size&style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/result?p=use-asset) 8 | [![Build Status](https://img.shields.io/travis/pmndrs/use-asset/master?style=flat&colorA=000000&colorB=000000)](https://travis-ci.org/pmndrs/use-asset) 9 | [![Version](https://img.shields.io/npm/v/use-asset?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/use-asset) 10 | [![Downloads](https://img.shields.io/npm/dt/use-asset.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/use-asset) 11 | 12 | # Dealing with async assets 13 | 14 | Each asset you create comes with its own cache. When you request something from it, the arguments that you pass will act as cache-keys. If you request later on using the same keys, it won't have to re-fetch but serves the result that it already knows. 15 | 16 | ```jsx 17 | import React, { Suspense } from "react" 18 | import { createAsset } from "use-asset" 19 | 20 | // Create a cached source 21 | const asset = createAsset(async (id, version) => { 22 | // Any async task can run in here, fetch requests, parsing, workers, promises, ... 23 | const res = await fetch(`https://hacker-news.firebaseio.com/${version}/item/${id}.json`) 24 | return await res.json() 25 | }) 26 | 27 | function Post({ id }) { 28 | // Then read from it ... 29 | const { by, title } = asset.read(id, "v0") // As many cache keys as you need 30 | // By the time we're here the async data has resolved 31 | return
{title} by {by}
32 | } 33 | 34 | function App() { 35 | loading...}> 36 | 37 | 38 | } 39 | ``` 40 | 41 | #### Preloading assets 42 | 43 | ```jsx 44 | // You can preload assets, these will be executed and cached immediately 45 | asset.preload("/image.png") 46 | ``` 47 | 48 | #### Cache busting strategies 49 | 50 | ```jsx 51 | // This asset will be removed from the cache in 15 seconds 52 | const asset = createAsset(promiseFn, 15000) 53 | // Clear all cached entries 54 | asset.clear() 55 | // Clear a specific entry 56 | asset.clear("/image.png") 57 | ``` 58 | 59 | #### Peeking into entries outside of suspense 60 | 61 | ```jsx 62 | // This will either return the value (without suspense!) or undefined 63 | asset.peek("/image.png") 64 | ``` 65 | 66 | # Hooks and global cache 67 | 68 | You can also use the `useAsset` hook, which is modelled after [react-promise-suspense](https://github.com/vigzmv/react-promise-suspense). This makes it possible to define assets on the spot instead of having to define them externally. They use a global cache, anything you request at any time is written into it. 69 | 70 | ```jsx 71 | import { useAsset } from "use-asset" 72 | 73 | function Post({ id }) { 74 | const { by, title } = useAsset(async (id, version) => { 75 | // Any async task can run in here, fetch requests, parsing, workers, promises, ... 76 | const res = await fetch(`https://hacker-news.firebaseio.com/${version}/item/${id}.json`) 77 | return await res.json() 78 | }, id, "v0") // As many cache keys as you need 79 | // By the time we're here the async data has resolved 80 | return
{title} by {by}
81 | } 82 | 83 | function App() { 84 | loading...}> 85 | 86 | ``` 87 | 88 | #### Cache busting, preload and peeking 89 | 90 | The hook has the same API as any asset: 91 | 92 | ```jsx 93 | // Bust cache in 15 seconds 94 | useAsset.lifespan = 15000 95 | useAsset(promiseFn, "/image.png") 96 | // Clear all cached entries 97 | useAsset.clear() 98 | // Clear a specific entry 99 | useAsset.clear("/image.png") 100 | // Preload entries 101 | useAsset.preload(promiseFn, "/image.png") 102 | // This will either return the value (without suspense!) or undefined 103 | useAsset.peek("/image.png") 104 | ``` 105 | 106 | # Recipes 107 | 108 | #### Simple data fetching 109 | 110 | Fetching posts from hacker-news: [codesandbox](https://codesandbox.io/s/use-asset-demo-forked-ji8ky) 111 | 112 | #### Infinite load on scroll 113 | 114 | Fetching HN posts infinitely: [codesandbox](https://codesandbox.io/s/use-asset-forked-ouzkc) 115 | 116 | #### Async dependencies 117 | 118 | Component A waits for the result of component B: [codesandbox](https://codesandbox.io/s/use-asset-dependency-70908) 119 | -------------------------------------------------------------------------------- /img/hooks-global-cache.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/async-assets.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/cover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------