-
Notifications
You must be signed in to change notification settings - Fork 585
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(hook): create useCookie hook (#370)
* feat(hook): create useCookieStore hook * fix: rename + improve typings * fix: useState * fix: tests * chore: final cleanup * fix: cr * chore: remove return * chore: add useCallback
- Loading branch information
Showing
5 changed files
with
310 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -234,4 +234,5 @@ dist-ghpages/ | |
coverage.lcov | ||
|
||
## as it is intended for projects not libraries | ||
yarn.lock | ||
package-lock.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# useCookie | ||
|
||
A hook for storing, updating and deleting values into [CookieStore](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore). | ||
|
||
### 💡 Why? | ||
|
||
- A quick way to use the `CookieStore` in your React components. | ||
|
||
### Basic Usage: | ||
|
||
```jsx harmony | ||
import { useCallback } from 'react'; | ||
import { Pill, Paragraph, Icon } from 'beautiful-react-ui'; | ||
import useCookie from 'beautiful-react-hooks/useCookie'; | ||
|
||
const UseCookieExample = () => { | ||
const { | ||
onError, | ||
cookieValue, | ||
deleteCookie, | ||
updateCookie | ||
} = useCookie('cookie-key', { secure: false, path: '/', defaultValue: 'default-value' }); | ||
|
||
onError((error) => { | ||
console.log(error) | ||
|
||
alert(error.message) | ||
}) | ||
|
||
const updateButtonClick = useCallback(() => { | ||
updateCookie('new-cookie-value') | ||
}, []) | ||
|
||
const deleteButtonClick = useCallback(() => { | ||
deleteCookie() | ||
}, []) | ||
|
||
return ( | ||
<DisplayDemo> | ||
<Paragraph>Click on the button to update or clear from the cookieStore</Paragraph> | ||
<Paragraph>{cookieValue || ''}</Paragraph> | ||
<Pill color='primary' onClick={updateButtonClick}> | ||
<Icon name="envelope" /> | ||
update the cookieStore | ||
</Pill> | ||
<Pill color='primary' onClick={deleteButtonClick}> | ||
<Icon name="envelope" /> | ||
Clear the cookieStore | ||
</Pill> | ||
</DisplayDemo> | ||
) | ||
}; | ||
|
||
<UseCookieExample /> | ||
``` | ||
|
||
### Mastering the hooks | ||
|
||
#### ✅ When to use | ||
|
||
- When you need to get/set values from the `cookieStore` | ||
|
||
#### 🛑 When not to use | ||
|
||
- This hook(cookieStore) can't be used in server-side and http website. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { useState, useEffect, useCallback } from 'react' | ||
|
||
import noop from './shared/noop' | ||
import isClient from './shared/isClient' | ||
import isDevelopment from './shared/isDevelopment' | ||
import isAPISupported from './shared/isAPISupported' | ||
import createHandlerSetter from './factory/createHandlerSetter' | ||
|
||
export enum ECookieSameSite { | ||
STRICT = 'strict', | ||
LAX = 'lax', | ||
NONE = 'none', | ||
} | ||
|
||
interface ICookieStoreDeleteOptions { | ||
name?: string; | ||
domain?: string; | ||
path?: string; | ||
} | ||
|
||
interface ICookieInit extends ICookieStoreDeleteOptions { | ||
sameSite?: ECookieSameSite; | ||
} | ||
|
||
interface ICookieInitWithNameAndValue extends ICookieInit { | ||
name?: string; | ||
value?: string; | ||
} | ||
|
||
export interface IOptions extends ICookieInit { | ||
defaultValue?: string; | ||
} | ||
|
||
interface ICookieStore { | ||
get: (key: string) => Promise<ICookieInitWithNameAndValue>; | ||
set: (options: ICookieInitWithNameAndValue) => Promise<void>; | ||
delete: (options: ICookieStoreDeleteOptions) => Promise<void>; | ||
} | ||
|
||
const useCookie = (key: string, options?: IOptions) => { | ||
const hookNotSupportedResponse = Object.freeze({ | ||
onError: noop, | ||
updateCookie: noop, | ||
deleteCookie: noop, | ||
cookieValue: options?.defaultValue, | ||
}) | ||
|
||
if (!isClient) { | ||
if (!isDevelopment) { | ||
// eslint-disable-next-line no-console | ||
console.warn( | ||
'Please be aware that cookieStore could not be available during SSR', | ||
) | ||
} | ||
|
||
return hookNotSupportedResponse | ||
} | ||
|
||
if (!isAPISupported('cookieStore')) { | ||
// eslint-disable-next-line no-console | ||
console.warn( | ||
"The current device does not support the 'cookieStore' API, you should avoid using useCookie", | ||
) | ||
|
||
return hookNotSupportedResponse | ||
} | ||
|
||
const [cookieValue, setCookieValue] = useState<string>() | ||
const [onErrorRef, setOnErrorRef] = createHandlerSetter<Error>() | ||
|
||
const cookieStoreObject = (window as any).cookieStore as ICookieStore | ||
|
||
const onError = (err: Error) => { | ||
if (onErrorRef.current) { | ||
onErrorRef.current(err) | ||
} | ||
} | ||
|
||
useEffect(() => { | ||
const getInitialValue = async () => { | ||
try { | ||
const getFunctionResult = await cookieStoreObject.get(key) | ||
|
||
if (getFunctionResult?.value) { | ||
return setCookieValue(getFunctionResult.value) | ||
} | ||
|
||
await cookieStoreObject.set({ | ||
name: key, | ||
value: options?.defaultValue, | ||
...options, | ||
}) | ||
return setCookieValue(options?.defaultValue) | ||
} catch (err) { | ||
return onError(err) | ||
} | ||
} | ||
|
||
getInitialValue() | ||
}, []) | ||
|
||
const updateCookie = useCallback( | ||
(newValue: string) => cookieStoreObject | ||
.set({ name: key, value: newValue, ...options }) | ||
.then(() => setCookieValue(newValue)) | ||
.catch(onError), | ||
[], | ||
) | ||
|
||
const deleteCookie = useCallback( | ||
() => cookieStoreObject | ||
.delete({ name: key, ...options }) | ||
.then(() => setCookieValue(undefined)) | ||
.catch(onError), | ||
[], | ||
) | ||
|
||
return Object.freeze({ | ||
cookieValue, | ||
updateCookie, | ||
deleteCookie, | ||
onError: setOnErrorRef, | ||
}) | ||
} | ||
|
||
export default useCookie |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
const createCookieStoreApiMock = () => { | ||
const store = {}; | ||
|
||
const getItem = (key) => { | ||
return Promise.resolve({ name: key, value: store[key] }); | ||
} | ||
|
||
const deleteItem = (key) => { | ||
delete store[key]; | ||
|
||
return Promise.resolve(); | ||
} | ||
|
||
const setItem = ({ name, value }) => { | ||
store[name] = value; | ||
|
||
return Promise.resolve(); | ||
} | ||
|
||
return { | ||
get: getItem, | ||
set: setItem, | ||
delete: deleteItem, | ||
} | ||
} | ||
|
||
export default createCookieStoreApiMock(); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import React from 'react' | ||
import { cleanup as cleanupReact, render } from '@testing-library/react' | ||
import { cleanup as cleanupHook, renderHook } from '@testing-library/react-hooks' | ||
|
||
import useCookie from '../dist/useCookie' | ||
import assertHook from './utils/assertHook' | ||
import CookieStoreApiMock from './mocks/CookieStoreApi.mock' | ||
|
||
const onErrorSpy = sinon.spy() | ||
const consoleWarnSpy = sinon.spy() | ||
const realConsoleWarning = console.warn | ||
|
||
describe('useCookie', () => { | ||
before(() => { | ||
console.warn = consoleWarnSpy | ||
window.cookieStore = CookieStoreApiMock | ||
}) | ||
|
||
after(() => { | ||
delete window.cookieStore | ||
console.warn = realConsoleWarning | ||
}) | ||
|
||
beforeEach(() => { | ||
cleanupHook() | ||
cleanupReact() | ||
sinon.reset() | ||
}) | ||
|
||
assertHook(useCookie) | ||
|
||
it('should return mocked object when browser does not support cookieStore API', () => { | ||
delete window.cookieStore | ||
|
||
const { result } = renderHook(() => useCookie()) | ||
|
||
expect(consoleWarnSpy.called).to.be.true | ||
expect(result.current).to.be.an('object').that.has.all.deep.keys('onError', 'cookieValue', 'updateCookie', 'deleteCookie') | ||
|
||
window.cookieStore = CookieStoreApiMock | ||
}) | ||
|
||
it('should save default value when no cookie is set', async () => { | ||
const { result, waitFor } = renderHook(() => useCookie('test', { defaultValue: 'default' })) | ||
|
||
await waitFor(() => result.current.cookieValue === 'default'); | ||
|
||
expect(result.current.cookieValue).to.equal('default') | ||
}) | ||
|
||
it('should intial, update and then delete cookie', async () => { | ||
const { result, waitForNextUpdate, waitFor } = renderHook(() => useCookie('test', { defaultValue: 'default' })) | ||
|
||
await waitFor(() => result.current.cookieValue === 'default'); | ||
|
||
expect(result.current.cookieValue).to.equal('default') | ||
result.current.updateCookie('newValue') | ||
|
||
await waitForNextUpdate() | ||
expect(result.current.cookieValue).to.equal('newValue') | ||
|
||
result.current.deleteCookie() | ||
|
||
await waitForNextUpdate() | ||
expect(result.current.cookieValue).to.be.undefined | ||
}) | ||
|
||
it('should call onError callback when an arror occurs', async () => { | ||
Object.defineProperty(window, "cookieStore", { | ||
value: { | ||
...window.cookieStore, | ||
get: () => { | ||
throw new Error('error') | ||
} | ||
} | ||
}) | ||
|
||
const TestComponent = () => { | ||
const { onError } = useCookie('test', { defaultValue: 'default' }) | ||
|
||
onError(onErrorSpy) | ||
|
||
return <div /> | ||
} | ||
|
||
render(<TestComponent />) | ||
|
||
expect(onErrorSpy.called).to.be.true | ||
}) | ||
}) |