Skip to content

Commit

Permalink
feat(hook): create useCookie hook (#370)
Browse files Browse the repository at this point in the history
* 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
playerony authored Jun 27, 2022
1 parent 93ec506 commit e37e56d
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,5 @@ dist-ghpages/
coverage.lcov

## as it is intended for projects not libraries
yarn.lock
package-lock.json
65 changes: 65 additions & 0 deletions docs/useCookie.md
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.
126 changes: 126 additions & 0 deletions src/useCookie.ts
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
28 changes: 28 additions & 0 deletions test/mocks/CookieStoreApi.mock.js
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();

90 changes: 90 additions & 0 deletions test/useCookie.spec.js
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
})
})

0 comments on commit e37e56d

Please sign in to comment.