Skip to content

Commit

Permalink
Add createMemoryStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
webNeat committed Sep 10, 2019
1 parent 9b30bdf commit 2455a5f
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 62 deletions.
101 changes: 94 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,101 @@ A collection of handy, flexible and well-tested React custom hooks.
npm i react-tidy
```

## List of Custom Hooks
- [useStorageItem](#usestorageitem)
## Contents
### Custom Hooks
- [useStorage](#usestorage)
### Functions
- [createMemoryStorage](#creatememorystorage)
### Interfaces
- [Storage](#storage)

## useStorageItem
## Custom Hooks

### useStorage
Get and set an item of a storage like `localStorage` and `sessionStorage`.

```ts
function useStorage(
key: string,
defaultValue: any = null,
storage: Storage = defaultStorage
)
```

#### Parameters
- **key**: The key of the item on the storage.
- **defaultValue**(optional): The value to return and set into the storage when no item with given key is found. (Default: `null`).
- **storage**(optional): The storage object to use. You can pass `window.localStorage`, `window.sessionStorage`, or any object implementing the [`Storage`](#storage) interface. By default, a unique memory storage will be created by [`createMemoryStorage`](#creatememorystorage).

#### Usage
```tsx
import {useStorage} from 'react-tidy'
```
Then inside a React functional component:
```ts
const [token, setToken] = useStorage('auth-token', 'default', window.localStorage)
```
`token` will contain the value stored in localStorage for the key `auth-token`.
if no item with that key is found, then `default` will be stored in localStorage.
```ts
setToken('foo') // stores the value on localStorage and sets token to 'foo'
setToken(null) // removes the item from localStorage and sets token to `null`
```

The value can be anything, not just a string.
```ts
const [state, setState] = useStorage('state', {isLoading: true})
//...
setState({isLoading: false, data: {...}})
```
`JSON.stringify` and `JSON.parse` are used to serialize/unserialize the value.

You can also give a function as value similar to `React.useState`.
```ts
const [lazyState, setLazyState] = useStorage('lazy-state', () => {
// do some computation ...
return data
})
//...
setLazyState(currentState => {
// ...
return newState
})
```

## Functions

### createMemoryStorage
Creates a storage object that stores data in memory using the [Storage](#storage) interface. Similar to `window.sessionStorage` but the data is lost once the page is closed or refreshed.

#### Usage
```ts
import {createMemoryStorage} from 'react-tidy'
const memoryStorage = createMemoryStorage()
// set a new item
memoryStorage.setItem('key', 'value')
// get an item
const value = memoryStorage.getItem('key') // returns `null` if missing
// remove an item
memoryStorage.removeItem('key')
// remove all items
memoryStorage.clear()
```

## Interfaces

### Storage
A simple version of the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Storage) interface. You can implement it to create your own storage objects.
```ts
function useStorageItem(name: string, storage: Storage = localStorage): {
value: string | null // The value of the localStorage item
set: (value: string) => void // Sets a new value and stores it on localStorage
remove: () => void // Removes the item from localStorage and sets the value to `null`
interface Storage {
getItem: (key: string) => string | null
setItem: (key: string, value: string) => void
removeItem: (key: string) => void
clear: () => void
}
```
56 changes: 56 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"trailingComma": "es5"
},
"devDependencies": {
"@testing-library/react": "^9.1.4",
"@testing-library/react-hooks": "^2.0.1",
"@types/jest": "^24.0.18",
"@types/react": "^16.9.2",
Expand Down
18 changes: 18 additions & 0 deletions src/createMemoryStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Storage } from './types'

export const createMemoryStorage = (): Storage => {
let memory: { [key: string]: string } = {}
const getItem = (key: string) => {
return memory[key] || null
}
const setItem = (key: string, value: string) => {
memory[key] = value
}
const removeItem = (key: string) => {
delete memory[key]
}
const clear = () => {
memory = {}
}
return { getItem, setItem, removeItem, clear }
}
4 changes: 3 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './useStorageItem'
export * from './createMemoryStorage'
export * from './useAsync'
export * from './useStorage'
8 changes: 8 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Storage {
getItem: (key: string) => string | null
setItem: (key: string, value: string) => void
removeItem: (key: string) => void
clear: () => void
}

export type Fn<T> = () => T
16 changes: 16 additions & 0 deletions src/useAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Storage, Fn } from 'types'
import { useStorage } from './useStorage'
import { createMemoryStorage } from './createMemoryStorage'

export function useAsync<T>(
key: string,
fn: () => Promise<T>,
storage: Storage = useAsync.__storage
) {
const [value, setValue] = useStorage<T>(key, null, storage)
const reload = () => setValue(null)
if (value === null) throw fn().then(setValue)
return [value, reload] as [T, Fn<void>]
}

useAsync.__storage = createMemoryStorage()
40 changes: 40 additions & 0 deletions src/useStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react'
import { Storage, Fn } from './types'
import { createMemoryStorage } from './createMemoryStorage'
import { isFunction } from './utils'

type Value<T> = T | Fn<T> | null

export function useStorage<T>(
key: string,
initialValue: Value<T> = null,
storage: Storage = useStorage.__storage
): [T | null, (x: Value<T>) => void] {
let storedString = storage.getItem(key)
const [value, setValue] = React.useState<T | null>(
storedString ? (JSON.parse(storedString) as T) : initialValue
)
React.useEffect(() => {
if (!storedString && initialValue) {
storage.setItem(name, JSON.stringify(initialValue))
}
}, [])
const set = React.useCallback(
(newValue: Value<T>) => {
if (newValue === null) {
storage.removeItem(key)
setValue(null)
} else {
if (isFunction(newValue)) {
newValue = (newValue as Fn<T>)()
}
storage.setItem(key, JSON.stringify(newValue))
setValue(newValue)
}
},
[storage, key]
)
return [value, set]
}

useStorage.__storage = createMemoryStorage()
26 changes: 0 additions & 26 deletions src/useStorageItem.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isFunction(x: any) {
return x && {}.toString.call(x) === '[object Function]'
}
63 changes: 63 additions & 0 deletions test/useAsync.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { Suspense } from 'react'
import { act } from 'react-dom/test-utils'
import { render } from '@testing-library/react'
import { useAsync, createMemoryStorage } from '../src'

const storage = createMemoryStorage()

const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

const Resolve = ({ data, ms }: any) => {
const [result] = useAsync(
'key',
async () => {
await delay(ms)
return data
},
storage
)
return <p>{JSON.stringify(result)}</p>
}

describe('useAsync', () => {
afterEach(storage.clear)
it('renders fallback while waiting for the promise', async () => {
const { container } = render(
<Suspense fallback={<p>Loading...</p>}>
<Resolve data="some data" ms={100} />
</Suspense>
)
expect(container.innerHTML).toBe('<p>Loading...</p>')
})

it('renders the result when the promise resolves', async () => {
let container: any = null
await act(async () => {
container = render(
<Suspense fallback={<p>Loading...</p>}>
<Resolve data="some data" ms={100} />
</Suspense>
).container
await delay(100)
})
expect(container.innerHTML).toBe('<p>"some data"</p>')
})

it('caches the result for next renders', async () => {
let result: any = null
await act(async () => {
result = render(
<Suspense fallback={<p>Loading...</p>}>
<Resolve data="some data" ms={100} />
</Suspense>
)
await delay(100)
})
result.rerender(
<Suspense fallback={<p>Loading...</p>}>
<Resolve data="some data" ms={100} />
</Suspense>
)
expect(result.container.innerHTML).toBe('<p>"some data"</p>')
})
})

0 comments on commit 2455a5f

Please sign in to comment.