Skip to content

Commit

Permalink
feat: add initializeWithValue option to useCookie hook (#120)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `useCookie` renamed to `useCookieValue`

BREAKING CHANGE: `useCookieValue` default behaviour for browsers
changed to fetch cookie value on state initialisation.

SSR remains untouched, but requires implicit setting of
`initializeWithValue` option to false, to avoid hydration mismatch.
  • Loading branch information
xobotyi authored Jun 14, 2021
1 parent 8077e6e commit 17c9543
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 93 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ import { useMountEffect } from "@react-hookz/web/esnext";

- [**`useAsync`**](https://react-hookz.github.io/web/?path=/docs/side-effect-useasync)
— Executes provided async function and tracks its result and error.
- [**`useCookie`**](https://react-hookz.github.io/web/?path=/docs/side-effect-usecookie)
- [**`useCookieValue`**](https://react-hookz.github.io/web/?path=/docs/side-effect-usecookievalue)
— Manages a single cookie.
- [**`useLocalStorageValue`**](https://react-hookz.github.io/web/?path=/docs/side-effect-uselocalstoragevalue)
— Manages a single LocalStorage key.
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export { useSyncedRef } from './useSyncedRef/useSyncedRef';
// SideEffect
export { useLocalStorageValue } from './useLocalStorageValue/useLocalStorageValue';
export { useSessionStorageValue } from './useSessionStorageValue/useSessionStorageValue';
export { useCookie, IUseCookieReturn } from './useCookie/useCookie';
export { useCookieValue, IUseCookieValueReturn } from './useCookieValue/useCookieValue';
export {
useAsync,
IAsyncState,
Expand Down
50 changes: 0 additions & 50 deletions src/useCookie/__docs__/story.mdx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import { useCookie } from '../..';
import { useCookieValue } from '../..';

export const Example: React.FC = () => {
const [cookie, set, remove] = useCookie('react-hookz', { expires: 3600 });
const [cookie, set, remove] = useCookieValue('react-hookz', { expires: 3600 });

return (
<div>
Expand Down
66 changes: 66 additions & 0 deletions src/useCookieValue/__docs__/story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './example.stories';

<Meta title="Side-effect/useCookieValue" component={Example} />

# useCookieValue

Manages a single cookie.

> Requires installation of `js-cookie` package.
- Uses `js-cookie` package underneath.
- SSR-friendly.
- Hooks that managing same key on same page - are synchronised. This synchronisation does not
involve cross-tabs sync or triggering on changes that performed by third-party code.

> **_This hook provides stable API, meaning returned methods does not change between renders_**
> Uses `null` values as indicator of cookie absence, `undefined` value means that cookie value
> hasn't been fetched yet.
> While using SSR, to avoid hydration mismatch, consider setting `initializeWithValue` option
> to `false`, this will yield `undefined` state on first render and defer value fetch till effects
> execution stage.
#### Example

<Canvas columns={3}>
<Story story={Example} inline />
<Story story={Example} inline />
<Story story={Example} inline />
</Canvas>

## Reference

```ts
export type IUseCookieOptions = Cookies.CookieAttributes & {
initializeWithValue?: boolean;
};

export type IUseCookieReturn = [
value: undefined | null | string,
set: (value: string) => void,
remove: () => void,
fetch: () => void
];

export function useCookieValue(key: string, options: IUseCookieOptions = {}): IUseCookieReturn;
```

#### Arguments

- **key** _`string`_ - Cookie name to manage.
- **options** _`IUseCookieOptions`_ _(default: {})_ - Cookie options that will be
used during cookie set and delete. Has only one extra option, that relates to the hook itself:
- **initializeWithValue** _`boolean`_ _(default: true)_ - Whether to initialize state with
cookie value or initialize with `undefined` state.
_Default to `false` during SSR._

#### Return

0. **state** - cookie value, `undefined` means it is not fetched yet, `null` means absence of
cookie.
1. **set** - Method to set new cookie value.
2. **remove** - Method to remove cookie.
3. **fetch** - Method to re-fetch cookie value.
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { act, renderHook } from '@testing-library/react-hooks/dom';
import * as Cookies from 'js-cookie';
import { IUseCookieReturn, useCookie } from '../..';
import { IUseCookieValueReturn, useCookieValue } from '../..';
import SpyInstance = jest.SpyInstance;

describe('useCookie', () => {
describe('useCookieValue', () => {
let getSpy: SpyInstance<{ [p: string]: string }, []>;
let setSpy: SpyInstance<
string | undefined,
Expand Down Expand Up @@ -35,39 +35,50 @@ describe('useCookie', () => {
});

it('should be defined', () => {
expect(useCookie).toBeDefined();
expect(useCookieValue).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => useCookie('react-hookz'));
const { result } = renderHook(() => useCookieValue('react-hookz'));
expect(result.error).toBeUndefined();
});

it('should return undefined on first render', () => {
const { result } = renderHook(() => useCookie('react-hookz'));
expect((result.all[0] as IUseCookieReturn)[0]).toBeUndefined();
it('should return cookie value on first render', () => {
Cookies.set('react-hookz', 'awesome');

const { result } = renderHook(() => useCookieValue('react-hookz'));
expect((result.all[0] as IUseCookieValueReturn)[0]).toBe('awesome');

Cookies.remove('react-hookz');
});

it('should return undefined on first render if `initializeWithValue` set to false', () => {
const { result } = renderHook(() =>
useCookieValue('react-hookz', { initializeWithValue: false })
);
expect((result.all[0] as IUseCookieValueReturn)[0]).toBeUndefined();
});

it('should return null if cookie not exists', () => {
const { result } = renderHook(() => useCookie('react-hookz'));
const { result } = renderHook(() => useCookieValue('react-hookz'));
expect(result.current[0]).toBe(null);
expect(getSpy).toBeCalledWith('react-hookz');
});

it('should set the cookie value on call to `set`', () => {
const { result } = renderHook(() => useCookie('react-hookz'));
const { result } = renderHook(() => useCookieValue('react-hookz'));

expect(result.current[0]).toBe(null);
act(() => {
result.current[1]('awesome');
});
expect(result.current[0]).toBe('awesome');
expect(setSpy).toBeCalledWith('react-hookz', 'awesome', undefined);
expect(setSpy).toBeCalledWith('react-hookz', 'awesome', {});
Cookies.remove('react-hookz');
});

it('should remove cookie value on call to `remove`', () => {
const { result } = renderHook(() => useCookie('react-hookz'));
const { result } = renderHook(() => useCookieValue('react-hookz'));

expect(result.current[0]).toBe(null);
act(() => {
Expand All @@ -79,12 +90,12 @@ describe('useCookie', () => {
result.current[2]();
});
expect(result.current[0]).toBe(null);
expect(removeSpy).toBeCalledWith('react-hookz', undefined);
expect(removeSpy).toBeCalledWith('react-hookz', {});
Cookies.remove('react-hookz');
});

it('should re-fetch cookie value on call to `fetch`', () => {
const { result } = renderHook(() => useCookie('react-hookz'));
const { result } = renderHook(() => useCookieValue('react-hookz'));

Cookies.set('react-hookz', 'rulez');
expect(result.current[0]).toBe(null);
Expand All @@ -97,8 +108,8 @@ describe('useCookie', () => {
});

it('should be synchronized between several hooks managing same key', () => {
const { result: res1 } = renderHook(() => useCookie('react-hookz'));
const { result: res2 } = renderHook(() => useCookie('react-hookz'));
const { result: res1 } = renderHook(() => useCookieValue('react-hookz'));
const { result: res2 } = renderHook(() => useCookieValue('react-hookz'));

expect(res1.current[0]).toBe(null);
expect(res2.current[0]).toBe(null);
Expand All @@ -119,7 +130,7 @@ describe('useCookie', () => {
});

it('should return stable methods', () => {
const { result, rerender } = renderHook(() => useCookie('react-hookz'));
const { result, rerender } = renderHook(() => useCookieValue('react-hookz'));

const res1 = result.current;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { renderHook } from '@testing-library/react-hooks/server';
import { useCookie } from '../..';
import { useCookieValue } from '../..';

describe('useCookie', () => {
describe('useCookieValue', () => {
it('should be defined', () => {
expect(useCookie).toBeDefined();
expect(useCookieValue).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => useCookie('react-hookz'));
const { result } = renderHook(() => useCookieValue('react-hookz'));
expect(result.error).toBeUndefined();
});

it('should return undefined ', () => {
const { result } = renderHook(() => useCookie('react-hookz'));
const { result } = renderHook(() => useCookieValue('react-hookz'));
expect(result.current[0]).toBeUndefined();
});
});
Loading

0 comments on commit 17c9543

Please sign in to comment.