Skip to content

Commit e37e56d

Browse files
authored
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
1 parent 93ec506 commit e37e56d

File tree

5 files changed

+310
-0
lines changed

5 files changed

+310
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,5 @@ dist-ghpages/
234234
coverage.lcov
235235

236236
## as it is intended for projects not libraries
237+
yarn.lock
237238
package-lock.json

docs/useCookie.md

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# useCookie
2+
3+
A hook for storing, updating and deleting values into [CookieStore](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore).
4+
5+
### 💡 Why?
6+
7+
- A quick way to use the `CookieStore` in your React components.
8+
9+
### Basic Usage:
10+
11+
```jsx harmony
12+
import { useCallback } from 'react';
13+
import { Pill, Paragraph, Icon } from 'beautiful-react-ui';
14+
import useCookie from 'beautiful-react-hooks/useCookie';
15+
16+
const UseCookieExample = () => {
17+
const {
18+
onError,
19+
cookieValue,
20+
deleteCookie,
21+
updateCookie
22+
} = useCookie('cookie-key', { secure: false, path: '/', defaultValue: 'default-value' });
23+
24+
onError((error) => {
25+
console.log(error)
26+
27+
alert(error.message)
28+
})
29+
30+
const updateButtonClick = useCallback(() => {
31+
updateCookie('new-cookie-value')
32+
}, [])
33+
34+
const deleteButtonClick = useCallback(() => {
35+
deleteCookie()
36+
}, [])
37+
38+
return (
39+
<DisplayDemo>
40+
<Paragraph>Click on the button to update or clear from the cookieStore</Paragraph>
41+
<Paragraph>{cookieValue || ''}</Paragraph>
42+
<Pill color='primary' onClick={updateButtonClick}>
43+
<Icon name="envelope" />
44+
update the cookieStore
45+
</Pill>
46+
<Pill color='primary' onClick={deleteButtonClick}>
47+
<Icon name="envelope" />
48+
Clear the cookieStore
49+
</Pill>
50+
</DisplayDemo>
51+
)
52+
};
53+
54+
<UseCookieExample />
55+
```
56+
57+
### Mastering the hooks
58+
59+
#### ✅ When to use
60+
61+
- When you need to get/set values from the `cookieStore`
62+
63+
#### 🛑 When not to use
64+
65+
- This hook(cookieStore) can't be used in server-side and http website.

src/useCookie.ts

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useState, useEffect, useCallback } from 'react'
2+
3+
import noop from './shared/noop'
4+
import isClient from './shared/isClient'
5+
import isDevelopment from './shared/isDevelopment'
6+
import isAPISupported from './shared/isAPISupported'
7+
import createHandlerSetter from './factory/createHandlerSetter'
8+
9+
export enum ECookieSameSite {
10+
STRICT = 'strict',
11+
LAX = 'lax',
12+
NONE = 'none',
13+
}
14+
15+
interface ICookieStoreDeleteOptions {
16+
name?: string;
17+
domain?: string;
18+
path?: string;
19+
}
20+
21+
interface ICookieInit extends ICookieStoreDeleteOptions {
22+
sameSite?: ECookieSameSite;
23+
}
24+
25+
interface ICookieInitWithNameAndValue extends ICookieInit {
26+
name?: string;
27+
value?: string;
28+
}
29+
30+
export interface IOptions extends ICookieInit {
31+
defaultValue?: string;
32+
}
33+
34+
interface ICookieStore {
35+
get: (key: string) => Promise<ICookieInitWithNameAndValue>;
36+
set: (options: ICookieInitWithNameAndValue) => Promise<void>;
37+
delete: (options: ICookieStoreDeleteOptions) => Promise<void>;
38+
}
39+
40+
const useCookie = (key: string, options?: IOptions) => {
41+
const hookNotSupportedResponse = Object.freeze({
42+
onError: noop,
43+
updateCookie: noop,
44+
deleteCookie: noop,
45+
cookieValue: options?.defaultValue,
46+
})
47+
48+
if (!isClient) {
49+
if (!isDevelopment) {
50+
// eslint-disable-next-line no-console
51+
console.warn(
52+
'Please be aware that cookieStore could not be available during SSR',
53+
)
54+
}
55+
56+
return hookNotSupportedResponse
57+
}
58+
59+
if (!isAPISupported('cookieStore')) {
60+
// eslint-disable-next-line no-console
61+
console.warn(
62+
"The current device does not support the 'cookieStore' API, you should avoid using useCookie",
63+
)
64+
65+
return hookNotSupportedResponse
66+
}
67+
68+
const [cookieValue, setCookieValue] = useState<string>()
69+
const [onErrorRef, setOnErrorRef] = createHandlerSetter<Error>()
70+
71+
const cookieStoreObject = (window as any).cookieStore as ICookieStore
72+
73+
const onError = (err: Error) => {
74+
if (onErrorRef.current) {
75+
onErrorRef.current(err)
76+
}
77+
}
78+
79+
useEffect(() => {
80+
const getInitialValue = async () => {
81+
try {
82+
const getFunctionResult = await cookieStoreObject.get(key)
83+
84+
if (getFunctionResult?.value) {
85+
return setCookieValue(getFunctionResult.value)
86+
}
87+
88+
await cookieStoreObject.set({
89+
name: key,
90+
value: options?.defaultValue,
91+
...options,
92+
})
93+
return setCookieValue(options?.defaultValue)
94+
} catch (err) {
95+
return onError(err)
96+
}
97+
}
98+
99+
getInitialValue()
100+
}, [])
101+
102+
const updateCookie = useCallback(
103+
(newValue: string) => cookieStoreObject
104+
.set({ name: key, value: newValue, ...options })
105+
.then(() => setCookieValue(newValue))
106+
.catch(onError),
107+
[],
108+
)
109+
110+
const deleteCookie = useCallback(
111+
() => cookieStoreObject
112+
.delete({ name: key, ...options })
113+
.then(() => setCookieValue(undefined))
114+
.catch(onError),
115+
[],
116+
)
117+
118+
return Object.freeze({
119+
cookieValue,
120+
updateCookie,
121+
deleteCookie,
122+
onError: setOnErrorRef,
123+
})
124+
}
125+
126+
export default useCookie

test/mocks/CookieStoreApi.mock.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const createCookieStoreApiMock = () => {
2+
const store = {};
3+
4+
const getItem = (key) => {
5+
return Promise.resolve({ name: key, value: store[key] });
6+
}
7+
8+
const deleteItem = (key) => {
9+
delete store[key];
10+
11+
return Promise.resolve();
12+
}
13+
14+
const setItem = ({ name, value }) => {
15+
store[name] = value;
16+
17+
return Promise.resolve();
18+
}
19+
20+
return {
21+
get: getItem,
22+
set: setItem,
23+
delete: deleteItem,
24+
}
25+
}
26+
27+
export default createCookieStoreApiMock();
28+

test/useCookie.spec.js

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from 'react'
2+
import { cleanup as cleanupReact, render } from '@testing-library/react'
3+
import { cleanup as cleanupHook, renderHook } from '@testing-library/react-hooks'
4+
5+
import useCookie from '../dist/useCookie'
6+
import assertHook from './utils/assertHook'
7+
import CookieStoreApiMock from './mocks/CookieStoreApi.mock'
8+
9+
const onErrorSpy = sinon.spy()
10+
const consoleWarnSpy = sinon.spy()
11+
const realConsoleWarning = console.warn
12+
13+
describe('useCookie', () => {
14+
before(() => {
15+
console.warn = consoleWarnSpy
16+
window.cookieStore = CookieStoreApiMock
17+
})
18+
19+
after(() => {
20+
delete window.cookieStore
21+
console.warn = realConsoleWarning
22+
})
23+
24+
beforeEach(() => {
25+
cleanupHook()
26+
cleanupReact()
27+
sinon.reset()
28+
})
29+
30+
assertHook(useCookie)
31+
32+
it('should return mocked object when browser does not support cookieStore API', () => {
33+
delete window.cookieStore
34+
35+
const { result } = renderHook(() => useCookie())
36+
37+
expect(consoleWarnSpy.called).to.be.true
38+
expect(result.current).to.be.an('object').that.has.all.deep.keys('onError', 'cookieValue', 'updateCookie', 'deleteCookie')
39+
40+
window.cookieStore = CookieStoreApiMock
41+
})
42+
43+
it('should save default value when no cookie is set', async () => {
44+
const { result, waitFor } = renderHook(() => useCookie('test', { defaultValue: 'default' }))
45+
46+
await waitFor(() => result.current.cookieValue === 'default');
47+
48+
expect(result.current.cookieValue).to.equal('default')
49+
})
50+
51+
it('should intial, update and then delete cookie', async () => {
52+
const { result, waitForNextUpdate, waitFor } = renderHook(() => useCookie('test', { defaultValue: 'default' }))
53+
54+
await waitFor(() => result.current.cookieValue === 'default');
55+
56+
expect(result.current.cookieValue).to.equal('default')
57+
result.current.updateCookie('newValue')
58+
59+
await waitForNextUpdate()
60+
expect(result.current.cookieValue).to.equal('newValue')
61+
62+
result.current.deleteCookie()
63+
64+
await waitForNextUpdate()
65+
expect(result.current.cookieValue).to.be.undefined
66+
})
67+
68+
it('should call onError callback when an arror occurs', async () => {
69+
Object.defineProperty(window, "cookieStore", {
70+
value: {
71+
...window.cookieStore,
72+
get: () => {
73+
throw new Error('error')
74+
}
75+
}
76+
})
77+
78+
const TestComponent = () => {
79+
const { onError } = useCookie('test', { defaultValue: 'default' })
80+
81+
onError(onErrorSpy)
82+
83+
return <div />
84+
}
85+
86+
render(<TestComponent />)
87+
88+
expect(onErrorSpy.called).to.be.true
89+
})
90+
})

0 commit comments

Comments
 (0)