Skip to content

Commit

Permalink
feat: new hook useSafeState (#31)
Browse files Browse the repository at this point in the history
* feat: new hook `useSafeState`

* fix: some typos
  • Loading branch information
xobotyi authored Apr 30, 2021
1 parent 9f71704 commit 0718afe
Show file tree
Hide file tree
Showing 19 changed files with 149 additions and 29 deletions.
3 changes: 2 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ max_line_length = 100
tab_width = 2
trim_trailing_whitespace = true

[*.md]
[*.{md,mdx}]
trim_trailing_whitespace = false
indent_size = unset
max_line_length = 100
12 changes: 12 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ module.exports = {
jsx: true,
},
},
rules: {
'prettier/prettier': [
'error',
{
PRINT_WIDTH,
singleQuote: true,
jsxBracketSameLine: true,
trailingComma: 'es5',
endOfLine: 'lf',
},
],
},
},
],
};
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ npm i @react-hookz/web
yarn add @react-hookz/web
```

As hooks was introduced to the world in React 16.8, `@react-hookz/web` requires - you gessed
it - `react` and `react-dom` 16.8+.
Also, as React does not support IE, `@react-hookz/web` does not do so either. You'll have to
transpile your `node-modules` in order to run in IE.
As hooks was introduced to the world in React 16.8, `@react-hookz/web` requires - you gessed it

- `react` and `react-dom` 16.8+.
Also, as React does not support IE, `@react-hookz/web` does not do so either. You'll have to
transpile your `node-modules` in order to run in IE.

## Usage

Expand Down Expand Up @@ -71,3 +72,5 @@ import { useMountEffect } from "@react-hookz/web/esnext";
— Like `useState`, but can only become `true` or `false`.
- [`usePrevious`](https://react-hookz.github.io/web/?path=/docs/lifecycle-useprevious)
— Returns the value passed to the hook on previous render.
- [`useSafeState`](https://react-hookz.github.io/web/?path=/docs/lifecycle-usesafestate)
— Like `useState` but its state setter is guarded against sets on unmounted component.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { usePrevious } from './usePrevious';
export { useIsMounted } from './useIsMounted';
export { useConditionalEffect } from './useConditionalEffect';
export { useConditionalUpdateEffect } from './useConditionalUpdateEffect';
export { useSafeState } from './useSafeState';
25 changes: 25 additions & 0 deletions src/useSafeState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { useIsMounted } from './useIsMounted';

/**
* Like `useState` but its state setter is guarded against sets on unmounted component.
*
* @param initialState
*/
export function useSafeState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
export function useSafeState<S = undefined>(): [
S | undefined,
Dispatch<SetStateAction<S | undefined>>
];
export function useSafeState<S>(initialState?: S | (() => S)): [S, Dispatch<SetStateAction<S>>] {
const [state, setState] = useState(initialState);
const isMounted = useIsMounted();

return [
state,
useCallback((value) => {
if (isMounted()) setState(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) as Dispatch<SetStateAction<S>>,
];
}
8 changes: 4 additions & 4 deletions stories/Introduction.story.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Meta } from "@storybook/addon-docs/blocks";
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Home" />

Expand Down Expand Up @@ -44,9 +44,9 @@ So, if you need the `useMountEffect` hook, depending on your needs, you can impo

```ts
// in case you need cjs modules
import { useMountEffect } from "@react-hookz/web";
import { useMountEffect } from '@react-hookz/web';
// in case you need esm modules
import { useMountEffect } from "@react-hookz/web/esm";
import { useMountEffect } from '@react-hookz/web/esm';
// in case you want all the recent ES features
import { useMountEffect } from "@react-hookz/web/esnext";
import { useMountEffect } from '@react-hookz/web/esnext';
```
4 changes: 2 additions & 2 deletions stories/useConditionalEffect.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import { Example } from "./useConditionalEffect.stories";
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './useConditionalEffect.stories';

<Meta title="Lifecycle/useConditionalEffect" component={Example} />

Expand Down
4 changes: 2 additions & 2 deletions stories/useConditionalUpdateEffect.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import { Example } from "./useConditionalUpdateEffect.stories";
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './useConditionalUpdateEffect.stories';

<Meta title="Lifecycle/useConditionalUpdateEffect" component={Example} />

Expand Down
4 changes: 2 additions & 2 deletions stories/useFirstMountState.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story, Typeset } from "@storybook/addon-docs/blocks";
import { Example } from "./useFirstMountState.stories";
import { Canvas, Meta, Story, Typeset } from '@storybook/addon-docs/blocks';
import { Example } from './useFirstMountState.stories';

<Meta title="Lifecycle/useFirstMountState" component={Example} />

Expand Down
4 changes: 2 additions & 2 deletions stories/useIsMounted.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import { Example } from "./useIsMounted.stories";
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './useIsMounted.stories';

<Meta title="Lifecycle/useIsMounted" component={Example} />

Expand Down
4 changes: 2 additions & 2 deletions stories/useMountEffect.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import { Example } from "./useMountEffect.stories";
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './useMountEffect.stories';

<Meta title="Lifecycle/useMountEffect" component={Example} />

Expand Down
4 changes: 2 additions & 2 deletions stories/usePrevious.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import { Example } from "./usePrevious.stories";
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './usePrevious.stories';

<Meta title="State/usePrevious" component={Example} />

Expand Down
4 changes: 2 additions & 2 deletions stories/useRerender.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import { Example } from "./useRerender.stories";
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './useRerender.stories';

<Meta title="Lifecycle/useRerender" component={Example} />

Expand Down
22 changes: 22 additions & 0 deletions stories/useSafeState.story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="State/useSafeState" />

# useSafeState

Have you ever been caught on `Can't perform a React state update on an unmounted component.`? Of
course you do, we're all been there😅
Async callback invoked, it tries to set state, but component is already unmounted, that annoying
warning happens, and you have to track component mount state manually.

`useSafeState` covers your back - it tracks component mount state and does not perform `setState`
action if component is unmounted, otherwise it is the same hook as common `useState`.

#### Example

Sadly we can't provide an example since this documentation built in `production` mode and warning
are only shown in `development` mode.

## Reference

Use it exactly the same as `useState`.
4 changes: 2 additions & 2 deletions stories/useToggle.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import { Example } from "./useToggle.stories";
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './useToggle.stories';

<Meta title="State/useToggle" component={Example} />

Expand Down
4 changes: 2 additions & 2 deletions stories/useUnmountEffect.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import { Example } from "./useUnmountEffect.stories";
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './useUnmountEffect.stories';

<Meta title="Lifecycle/useUnmountEffect" component={Example} />

Expand Down
4 changes: 2 additions & 2 deletions stories/useUpdateEffect.story.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import { Example } from "./useUpdateEffect.stories";
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './useUpdateEffect.stories';

<Meta title="Lifecycle/useUpdateEffect" component={Example} />

Expand Down
38 changes: 38 additions & 0 deletions tests/dom/useSafeState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { act, renderHook } from '@testing-library/react-hooks/dom';
import { useSafeState } from '../../src';

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

it('should render', () => {
renderHook(() => useSafeState());
});

it('should not call ', () => {
const consoleSpy = jest.spyOn(console, 'error');
consoleSpy.mockImplementationOnce(() => {});

const { result, unmount } = renderHook(() => useSafeState(1));
expect(result.current[1]).toBeInstanceOf(Function);
expect(result.current[0]).toBe(1);

act(() => {
result.current[1](321);
});

expect(result.current[0]).toBe(321);

unmount();

act(() => {
result.current[1](123);
});

expect(consoleSpy).toHaveBeenCalledTimes(0);
expect(result.current[0]).toBe(321);

consoleSpy.mockRestore();
});
});
18 changes: 18 additions & 0 deletions tests/ssr/useSafeState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { renderHook } from '@testing-library/react-hooks/server';
import { useSafeState } from '../../src';

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

it('should render', () => {
renderHook(() => useSafeState());
});

it('should not call ', () => {
const { result } = renderHook(() => useSafeState(1));
expect(result.current[1]).toBeInstanceOf(Function);
expect(result.current[0]).toBe(1);
});
});

0 comments on commit 0718afe

Please sign in to comment.