Skip to content

Commit

Permalink
✨ Add simple cache (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
exah committed May 9, 2020
1 parent 2af3e34 commit 1563f22
Show file tree
Hide file tree
Showing 17 changed files with 1,896 additions and 1,569 deletions.
13 changes: 0 additions & 13 deletions .flowconfig

This file was deleted.

7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
language: node_js
node_js:
- 'lts/*'

before_install:
- npm install -g yarn

jobs:
include:
- stage: Produce Coverage
node_js: lts/*
script: yarn coverage
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).

## [4.1.0-3](https://github.com/exah/react-universal-data/compare/4.1.0-2...4.1.0-3) - 2020-05-08

## [4.1.0-2](https://github.com/exah/react-universal-data/compare/4.1.0-1...4.1.0-2) - 2020-05-08

### Improved

- ♻️ Do not delete cached value after initial render [`b278eb2`](https://github.com/exah/react-universal-data/commit/b278eb2069c30184a2d7f9d3a79dff4d0c332f53)
- ♻️ Use cached value on server if possible [`bfe1118`](https://github.com/exah/react-universal-data/commit/bfe1118f841169c1ff9b1de05a9cb2bbc3dc1d7e)

### Dependencies

- ⬆️ Bump deps [`7ea5b47`](https://github.com/exah/react-universal-data/commit/7ea5b47583d4b9a434b9c2891ea84074eeea5f53)

## [4.1.0-1](https://github.com/exah/react-universal-data/compare/4.1.0-0...4.1.0-1) - 2020-05-08

### Improved

- ♻️ Move "browser" logic to separate file [`ede7b6a`](https://github.com/exah/react-universal-data/commit/ede7b6a80bb4ff02a51d0472bbf399b907c01237)

## [4.1.0-0](https://github.com/exah/react-universal-data/compare/4.0.2...4.1.0-0) - 2020-05-07
### Added

- ✨ Add `ttl` [`e1ce0c3`](https://github.com/exah/react-universal-data/commit/e1ce0c338ed04dd41e674223379f2ad38354a68d)

## [4.0.2](https://github.com/exah/react-universal-data/compare/4.0.1...4.0.2) - 2020-04-26

### Fixed
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# 🗂 react-universal-data

[![](https://flat.badgen.net/npm/v/react-universal-data?cache=600)](https://www.npmjs.com/package/react-universal-data) [![](https://flat.badgen.net/bundlephobia/minzip/react-universal-data?cache=600)](https://bundlephobia.com/result?p=react-universal-data) ![](https://flat.badgen.net/travis/exah/react-universal-data?cache=600)
[![](https://flat.badgen.net/npm/v/react-universal-data?cache=600)](https://www.npmjs.com/package/react-universal-data) [![](https://flat.badgen.net/bundlephobia/minzip/react-universal-data?cache=600)](https://bundlephobia.com/result?p=react-universal-data) ![](https://flat.badgen.net/travis/exah/react-universal-data?cache=600) ![](https://flat.badgen.net/coveralls/c/github/exah/react-universal-data)

#### Easy to use hook for getting data on client and server side with effortless hydration of state

- [x] Only 600B minified and gziped
- [x] Simple hooks API
- [x] TypeScript
- [x] Can handle updates
- [x] Simple cache
- [x] [Suspense](http://reactjs.org/docs/concurrent-mode-suspense.html) on server side via [`react-ssr-prepass`](https://github.com/FormidableLabs/react-ssr-prepass) 💕

> _This is a NO BULLSHIT hook: just PLUG IT in your components, get ALL THE DATA you need (and some more) both CLIENT- and SERVER-side, HYDRATE that ~~bastard~~ app while SSRing like it's NO BIG DEAL, effortlessly PASS IT to the client and render THE SHIT out of it_
Expand All @@ -33,13 +34,15 @@ Requests data and preserves the result to the state.
```ts
type useFetchData<T> = (
// async function that can return any type of data
fetcher: (id: string, context: { isServer: boolean }) => Promise<T>,
// unique id that will be used for storing & hydrating data while SSR
id: string
fetcher: (key: string, context: { isServer: boolean }) => Promise<T>,
// unique key that will be used for storing & hydrating data while SSR
key: string,
// use cached value for specified duration of time, by default it will be requested each time
ttl?: number
) => AsyncState<T>
```
> ⚠️ The `id` must be unique for the whole application.
> ⚠️ The `key` must be unique for the whole application.
Returned object can be in 4 different forms – depending on the promise's state.
Expand Down
38 changes: 20 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-universal-data",
"version": "4.0.2",
"version": "4.1.0-3",
"description": "Easy to use hook for getting data on client and server side with effortless hydration of state",
"author": "John Grishin <hi@johngrish.in>",
"license": "MIT",
Expand Down Expand Up @@ -38,6 +38,7 @@
"build:types": "tsc -p tsconfig.types.json",
"prebuild": "rimraf esm cjs types",
"test": "jest",
"coverage": "jest --coverage && coveralls < coverage/lcov.info",
"lint": "eslint --ext ts --ext js src/",
"size": "size-limit",
"release": "np --no-cleanup",
Expand Down Expand Up @@ -101,43 +102,44 @@
"react-is": "^16.13.0"
},
"devDependencies": {
"@size-limit/preset-small-lib": "^4.4.1",
"@testing-library/jest-dom": "^5.2.0",
"@testing-library/react": "^10.0.1",
"@size-limit/preset-small-lib": "^4.5.0",
"@testing-library/jest-dom": "^5.7.0",
"@testing-library/react": "^10.0.4",
"@testing-library/react-hooks": "^3.2.1",
"@types/eslint": "^6.1.8",
"@types/eslint": "^6.8.0",
"@types/eslint-plugin-prettier": "^2.2.0",
"@types/jest": "^25.1.2",
"@types/prettier": "^1.19.1",
"@types/react": "^16.9.26",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"@types/jest": "^25.2.1",
"@types/prettier": "^2.0.0",
"@types/react": "^16.9.34",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"auto-changelog": "^1.16.3",
"coveralls": "^3.1.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-config-standard": "^14.1.1",
"eslint-config-standard-react": "^9.2.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jest": "^23.7.0",
"eslint-plugin-jest": "^23.9.0",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-standard": "^4.0.1",
"jest": "^25.2.0",
"np": "^6.0.0",
"jest": "^26.0.1",
"np": "^6.2.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.2",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-is": "^16.13.1",
"react-test-renderer": "^16.13.1",
"rimraf": "^3.0.2",
"size-limit": "^4.4.1",
"ts-jest": "^25.2.0",
"size-limit": "^4.5.0",
"ts-jest": "^25.5.0",
"typescript": "^3.7.5"
},
"dependencies": {
"react-ssr-prepass": "^1.1.2"
"react-ssr-prepass": "^1.2.0"
}
}
22 changes: 0 additions & 22 deletions src/constants.ts

This file was deleted.

63 changes: 56 additions & 7 deletions src/get-initial-data.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import { render } from '@testing-library/react'
import { getInitialData } from './get-initial-data'
import { useFetchData } from './use-fetch-data'
import { DataProvider } from './context'
import { defaultStore } from './store'

jest.mock('./constants', () => ({
...jest.requireActual('./constants'),
IS_SERVER: true,
}))
import { createStore, defaultStore } from './store'

beforeEach(() => defaultStore.clear())

Expand Down Expand Up @@ -83,7 +78,7 @@ test('should able to provide custom store', async () => {
return <div data-testid="response">{state.result}</div>
}

const store = new Map()
const store = createStore()
const element = (
<DataProvider value={store}>
<A />
Expand All @@ -97,3 +92,57 @@ test('should able to provide custom store', async () => {
expect(store.get('A')).toBe('Foo')
expect(getByTestId('response')).toHaveTextContent('Foo')
})

test('should not request data if it still fresh', async () => {
const ttl = 1000
const resourceA = jest.fn(() => 'Foo')
const resourceB = jest.fn(() => 'Bar')
const timers = jest.useFakeTimers()

function A() {
useFetchData(resourceA, 'A', ttl)
return null
}

function B() {
useFetchData(resourceB, 'B')
return <A />
}

const p1 = getInitialData(<B />)

expect(defaultStore.get('A')).toBe(undefined)
expect(defaultStore.get('B')).toBe(undefined)

await p1

expect(resourceA).toHaveBeenCalledTimes(1)
expect(resourceB).toHaveBeenCalledTimes(1)

expect(defaultStore.get('A')).toBe('Foo')
expect(defaultStore.get('B')).toBe('Bar')

timers.advanceTimersByTime(ttl / 2)

const p2 = getInitialData(<B />)

expect(defaultStore.get('A')).toBe('Foo')
expect(defaultStore.get('B')).toBe(undefined)

await p2

expect(defaultStore.get('A')).toBe('Foo')
expect(defaultStore.get('B')).toBe('Bar')
expect(resourceA).toHaveBeenCalledTimes(1)

timers.advanceTimersByTime(ttl / 2)

const p3 = getInitialData(<B />)

expect(defaultStore.get('A')).toBe(undefined)
expect(defaultStore.get('B')).toBe(undefined)

await p3

expect(resourceA).toHaveBeenCalledTimes(2)
})
6 changes: 5 additions & 1 deletion src/get-initial-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import { defaultStore } from './store'
*/

function getInitialData<T = any>(element: JSX.Element, store = defaultStore) {
store.clear()
store.forEach((_, key) => {
if (!store.hasTTL(key)) {
store.delete(key)
}
})

return prepass(element).then<[Key, T][]>(() => Array.from(store))
}
Expand Down
2 changes: 1 addition & 1 deletion src/index.browser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { DataProvider } from './context'
export { createStore, hydrateInitialData } from './store'
export { useFetchData } from './use-fetch-data'
export { useFetchData } from './use-fetch-data.browser'
55 changes: 45 additions & 10 deletions src/store.test.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,52 @@
import { createStore, defaultStore, hydrateInitialData } from './store'

test('`creatStore` should return `Map`', () => {
expect(createStore()).toBeInstanceOf(Map)
beforeEach(() => {
defaultStore.clear()
})

test('`defaultStore` should be empty `Map`', () => {
expect(defaultStore).toBeInstanceOf(Map)
expect(defaultStore.size).toBe(0)
describe('createStore', () => {
test('should return `Map`', () => {
expect(createStore()).toBeInstanceOf(Map)
})

test('should be able to set TTL for `key` in store', () => {
const ttl = 1000
const timers = jest.useFakeTimers()
const store = createStore()

store.set('foo', 'bar')

expect(store.has('foo')).toBe(true)
expect(store.hasTTL('foo')).toBe(false)
expect(store.get('foo')).toBe('bar')

store.setTTL('foo', ttl)
timers.advanceTimersByTime(ttl / 2)

expect(store.has('foo')).toBe(true)
expect(store.hasTTL('foo')).toBe(true)
expect(store.get('foo')).toBe('bar')

timers.advanceTimersByTime(ttl / 2)

expect(store.has('foo')).toBe(false)
expect(store.hasTTL('foo')).toBe(false)
expect(store.get('foo')).toBe(undefined)
})
})

describe('defaultStore', () => {
test('should be empty `Map`', () => {
expect(defaultStore).toBeInstanceOf(Map)
expect(defaultStore.size).toBe(0)
})
})

test('`hydrateData` should fill data in `defaultStore`', () => {
expect(defaultStore.size).toBe(0)
hydrateInitialData([['foo', 'bar']])
expect(defaultStore.size).toBe(1)
expect(defaultStore.get('foo')).toBe('bar')
describe('hydrateData', () => {
test('should fill data in `defaultStore`', () => {
expect(defaultStore.size).toBe(0)
hydrateInitialData([['foo', 'bar']])
expect(defaultStore.size).toBe(1)
expect(defaultStore.get('foo')).toBe('bar')
})
})
30 changes: 29 additions & 1 deletion src/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
import { Key } from './types'

export const createStore = <T = any>(input?: [Key, T][]) => new Map(input)
export const createStore = <T = any>(init?: [Key, T][]) => {
const store = new Map(init)
const timer = new Map()

function hasTTL(key: Key) {
return timer.has(key)
}

function deleteTTL(key: Key) {
clearTimeout(timer.get(key))
timer.delete(key)
}

function setTTL(key: Key, ttl?: number) {
deleteTTL(key)

if (ttl) {
timer.set(
key,
setTimeout(() => {
store.delete(key)
timer.delete(key)
}, ttl)
)
}
}

return Object.assign(store, { hasTTL, setTTL })
}

export const defaultStore = createStore()

Expand Down
Loading

0 comments on commit 1563f22

Please sign in to comment.