├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── src └── index.js ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | /dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | .gitignore 3 | package-lock.json 4 | .prettierrc -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 LEE JEONGWOO 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Cache API 2 | 3 | React Cache API is a React Hooks library for data fetching. 4 | 5 | It was inspired by the [swr](https://swr.vercel.app/). 6 | 7 | **[Demo website](https://cache-api.awmaker.com)** 8 | 9 | ## 🚀Quick Overview 10 | 11 | ```javascript 12 | // Please wrap the component to call useCacheApi with this component. 13 | // In the case of nextJS, it is recommended to wrap the component in the _app file. 14 | 15 | 16 | 17 | ``` 18 | 19 | ```javascript 20 | // If the response value is cached, return cached value. 21 | // If not, request api through fetch. 22 | const { data, error, isValidation } = useCacheApi('/', query) 23 | 24 | // Even if you write without a query on another page, it gets the cached value. 25 | const { data, error, isValidation } = useCacheApi('/') 26 | ``` 27 | 28 | ## ✨Feature 29 | 30 | - When using the cache, it is not necessary to write the boiler plate code due to query. 31 | 32 | ## 👏 Contributing 33 | 34 | Pull requests and 🌟 stars are always welcome. 35 | 36 | For major changes, please [open an issue](https://github.com/zao95/react-cache-api/issues/new) first to discuss what you would like to change. 37 | 38 | ## ☕ Donate 39 | 40 | - Please buy me coffee so that I can continue to make convenient packages. 41 | 42 | https://www.buymeacoffee.com/awmaker 43 | 44 | ## 📩 Contact 45 | 46 | awmaker@kakao.com 47 | 48 | ## Others 49 | 50 | 👍 We recommend third-party services with react-cache-api. 51 | 52 | - We recommand [nextJS](https://nextjs.org/) service for more convenient react use. 53 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { RequestInit } from 'node-fetch' 2 | 3 | declare module 'react-cache-api' { 4 | interface ICacheApiConfig { 5 | baseURL?: string | null 6 | children: React.ReactNode 7 | } 8 | /** 9 | * Please enclose the component to call useCacheApi with this component. 10 | * In the case of nextJS, it is recommended to wrap the component in the _app file. 11 | * @param props 12 | * @param props.baseURL The base URL of apis to call to useCacheApi 13 | * @param props.children Components to use useCacheApi 14 | */ 15 | export function CacheApiConfig({ baseURL, children }: ICacheApiConfig): any 16 | 17 | interface query { 18 | [key: string]: any 19 | } 20 | interface IReturn { 21 | data: { 22 | [key: string]: any 23 | } 24 | error: { 25 | [key: string]: any 26 | } 27 | isValidation: boolean 28 | } 29 | interface IOptions extends RequestInit { 30 | immutability?: boolean 31 | } 32 | /** 33 | * If the cache has a response value for the corresponding key and requests it without query, the value is taken from the cache. 34 | * If not, request api through fetch. 35 | * @param key The key of the cache and api's url 36 | * @param query A query to send to api 37 | * @param options Fetch api's options 38 | */ 39 | export function useCacheApi( 40 | key: string | (() => string | null | undefined), 41 | query?: any | null, 42 | options?: IOptions | null 43 | ): IReturn 44 | } 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { 2 | useCacheApi: useCacheApi_, 3 | CacheApiConfig: CacheApiConfig_, 4 | } = require('./src') 5 | 6 | module.exports = { 7 | useCacheApi: useCacheApi_, 8 | CacheApiConfig: CacheApiConfig_, 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-cache-api", 3 | "version": "1.6.11", 4 | "description": "Save network request using cache.", 5 | "keywords": [ 6 | "api", 7 | "cache", 8 | "react", 9 | "next" 10 | ], 11 | "homepage": "https://github.com/zao95/react-cache-api#readme", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/zao95/react-cache-api" 15 | }, 16 | "license": "MIT License", 17 | "author": { 18 | "name": "Lee Jeongwoo", 19 | "email": "awmaker@kakao.com", 20 | "url": "https://portfolio.awmaker.com" 21 | }, 22 | "files": [ 23 | "/dist/*", 24 | "/src/*", 25 | "/index.js", 26 | "/index.d.ts" 27 | ], 28 | "scripts": { 29 | "babel": "node babel.js", 30 | "prePublish": "webpack --mode=production" 31 | }, 32 | "peerDependencies": { 33 | "react": ">=16.8.0", 34 | "react-dom": ">=16.8.0", 35 | "node-fetch": "^3.2.0" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.16.12", 39 | "@babel/preset-env": "^7.16.11", 40 | "@types/react": "^17.0.38", 41 | "babel-loader": "^8.2.3", 42 | "webpack": "^5.67.0", 43 | "webpack-cli": "^4.9.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { createContext, useContext, useEffect, useState } = require('react') 3 | 4 | const _objectToString = (obj) => { 5 | let query = '' 6 | for (const [key, value] of Object.entries(obj)) { 7 | if (query) query += `&` 8 | query += key 9 | if (value !== undefined) query += `=${value}` 10 | } 11 | return query 12 | } 13 | const _objectIsNull = (obj) => { 14 | return JSON.stringify(obj) === '{}' || obj === null || obj === undefined 15 | } 16 | const _objectIsSame = (currentObj, targetObj) => { 17 | return JSON.stringify(currentObj) === JSON.stringify(targetObj) 18 | } 19 | const _objectWithoutProperties = (obj, keys) => { 20 | const target = {} 21 | for (const key in obj) { 22 | if (keys.indexOf(key) >= 0) continue 23 | if (!Object.prototype.hasOwnProperty.call(obj, key)) continue 24 | target[key] = obj[key] 25 | } 26 | return target 27 | } 28 | 29 | const cacheApiConfig = { 30 | baseURL: null, 31 | cache: new Map(), 32 | } 33 | const CacheApiContext = createContext(cacheApiConfig) 34 | const CacheApiConfig = ({ baseURL, children }) => { 35 | return React.createElement( 36 | CacheApiContext.Provider, 37 | { value: { baseURL: baseURL, cache: cacheApiConfig.cache } }, 38 | React.Children.only(children) 39 | ) 40 | } 41 | 42 | const useCacheApi = (key, query = {}, options = null) => { 43 | try { 44 | const { baseURL, cache } = useContext(CacheApiContext) 45 | const [data, setData] = useState(null) 46 | const [isValidation, setIsValidation] = useState(false) 47 | const [doFetch, setDoFetch] = useState(false) 48 | const cacheData = cache.get(key) && cache.get(key).data 49 | const documentObject = () => { 50 | if (typeof window === 'object' && immutability !== true) { 51 | return document.location.pathname 52 | } 53 | } 54 | 55 | // Set immutability 56 | let immutability = false 57 | try { 58 | if (options && options.immutability === true) { 59 | immutability = true 60 | } 61 | } catch (e) {} 62 | options = _objectWithoutProperties(options, ['immutability']) 63 | 64 | // Execute key when key is function 65 | if (typeof key !== 'string') { 66 | key = key() 67 | } 68 | 69 | const getData = async () => { 70 | setDoFetch(false) 71 | setIsValidation(true) 72 | const requestUrl = 73 | baseURL + 74 | key + 75 | String(_objectIsNull(query) ? '' : `?${_objectToString(query)}`) 76 | const data = await (await fetch(requestUrl, options)).json() 77 | setData(data) 78 | setIsValidation(false) 79 | cache.set(key, { data, query }) 80 | } 81 | 82 | useEffect(() => { 83 | if (!key) { 84 | return { data, error: null, isValidation } 85 | } else if ( 86 | cache.has(key) && 87 | (_objectIsNull(query) || 88 | _objectIsSame(cache.get(key).query, query)) 89 | ) { 90 | setData(cache.get(key).data) 91 | setIsValidation(false) 92 | } else { 93 | setDoFetch(true) 94 | } 95 | }, [key, JSON.stringify(query), cacheData]) 96 | 97 | useEffect(() => { 98 | if (documentObject()) { 99 | setDoFetch(true) 100 | } 101 | }, [documentObject()]) 102 | 103 | useEffect(() => { 104 | if (doFetch === true) { 105 | getData() 106 | } 107 | }, [doFetch]) 108 | 109 | return { data, error: null, isValidation } 110 | } catch (error) { 111 | return { data: null, error, isValidation: false } 112 | } 113 | } 114 | 115 | module.exports = { 116 | CacheApiConfig, 117 | useCacheApi, 118 | } 119 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "noImplicitAny": false, 17 | "baseUrl": ".", 18 | "declaration": true 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/index.cjs"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: { 5 | index: './src/index.js', 6 | }, 7 | output: { 8 | path: path.resolve(__dirname, './dist'), 9 | filename: 'index.js', 10 | library: ['useCacheApi', 'CacheApiConfig'], 11 | libraryTarget: 'commonjs2', 12 | globalObject: 'this', 13 | }, 14 | externals: { 15 | react: 'commonjs react', 16 | 'react-dom': 'commonjs react-dom', 17 | }, 18 | } 19 | --------------------------------------------------------------------------------