Skip to content

Commit

Permalink
feat(useToggle): ignore react events passed to state setter (#867)
Browse files Browse the repository at this point in the history
* feat(useToggle): ignore react events passed to state setter

fix #861

BREAKING CHANGE: `useToggle` now ignores react events passed to its
state setter, so it can be used as event handler directly.
  • Loading branch information
xobotyi authored Jul 4, 2022
1 parent 75db2b5 commit 085f711
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 21 deletions.
7 changes: 1 addition & 6 deletions src/useToggle/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ export const Example: React.FC = () => {
return (
<div>
<div>{isToggled ? 'The toggle is on' : 'The toggle is off'}</div>
<button
onClick={() => {
toggle();
}}>
Toggle
</button>
<button onClick={toggle}>Toggle</button>
</div>
);
};
17 changes: 14 additions & 3 deletions src/useToggle/__docs__/story.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { ImportPath } from '../../storybookUtil/ImportPath';

Like `useState`, but can only become `true` or `false`.

State setter, in case called without arguments, will change the state to opposite. React synthetic
events are ignored by default so state setter can be used as event handler directly, such behaviour
can be changed by setting 2nd parameter to `false`.

> **_This hook provides stable API, meaning returned functions do not change between renders_**
#### Example
Expand All @@ -19,9 +23,15 @@ Like `useState`, but can only become `true` or `false`.
## Reference

```ts
function useToggle(
initialState: IInitialState<boolean> = false
): [boolean, (nextState?: INewState<boolean>) => void];
export function useToggle(
initialState: IInitialState<boolean>,
ignoreReactEvents?: true
): [boolean, (nextState?: INextState<boolean> | BaseSyntheticEvent) => void];

export function useToggle(
initialState: IInitialState<boolean>,
ignoreReactEvents: false
): [boolean, (nextState?: INextState<boolean>) => void];
```

#### Importing
Expand All @@ -32,6 +42,7 @@ function useToggle(

- _**initialState**_ _`IInitialState<boolean>`_ - initial state or initial state setter as for
`useState`
- _**ignoreReactEvents**_ _`boolean`_ _(default: true)_ - ignore received react synthetic events, so state setter can be used as event handler.

#### Return

Expand Down
19 changes: 17 additions & 2 deletions src/useToggle/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { act, renderHook } from '@testing-library/react-hooks/dom';
import { useRef } from 'react';
import { BaseSyntheticEvent, useRef } from 'react';
import { useToggle } from '../..';

describe('useToggle', () => {
Expand Down Expand Up @@ -58,7 +58,7 @@ describe('useToggle', () => {
});

it('should change state to one that passed to toggler', () => {
const { result } = renderHook(() => useToggle());
const { result } = renderHook(() => useToggle(false, false));
act(() => {
result.current[1](false);
});
Expand All @@ -79,4 +79,19 @@ describe('useToggle', () => {
});
expect(result.current[0]).toBe(true);
});

it('should not account react events', () => {
const { result } = renderHook(() => useToggle());

act(() => {
result.current[1]({ _reactName: 'abcdef' } as unknown as BaseSyntheticEvent);
result.current[1]({ _reactName: 'abcdef' } as unknown as BaseSyntheticEvent);
});
expect(result.current[0]).toBe(false);

act(() => {
result.current[1](new (class SyntheticBaseEvent {})() as unknown as BaseSyntheticEvent);
});
expect(result.current[0]).toBe(true);
});
});
38 changes: 28 additions & 10 deletions src/useToggle/useToggle.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
import { useCallback } from 'react';
import { BaseSyntheticEvent, useCallback } from 'react';
import { useSafeState, useSyncedRef } from '..';
import { IInitialState, INextState, resolveHookState } from '../util/resolveHookState';
import { useSafeState } from '..';

export function useToggle(
initialState: IInitialState<boolean>,
ignoreReactEvents: false
): [boolean, (nextState?: INextState<boolean>) => void];
export function useToggle(
initialState?: IInitialState<boolean>,
ignoreReactEvents?: true
): [boolean, (nextState?: INextState<boolean> | BaseSyntheticEvent) => void];

/**
* Like `useSafeState`, but can only become `true` or `false`.
*
* State setter, in case called without arguments, will change the state to opposite.
*
* @param initialState Initial toggle state, defaults to false.
* State setter, in case called without arguments, will change the state to opposite. React
* synthetic events are ignored by default so state setter can be used as event handler directly,
* such behaviour can be changed by setting 2nd parameter to `false`.
*/
export function useToggle(
initialState: IInitialState<boolean> = false
): [boolean, (nextState?: INextState<boolean>) => void] {
// We dont use useReducer (which would end up with less code), because exposed
initialState: IInitialState<boolean> = false,
ignoreReactEvents = true
): [boolean, (nextState?: INextState<boolean> | BaseSyntheticEvent) => void] {
// We don't use useReducer (which would end up with less code), because exposed
// action does not provide functional updates feature.
// Therefore we have to create and expose our own state setter with
// Therefore, we have to create and expose our own state setter with
// toggle logic.
const [state, setState] = useSafeState(initialState);
const ignoreReactEventsRef = useSyncedRef(ignoreReactEvents);

return [
state,
useCallback((nextState) => {
setState((prevState) => {
if (typeof nextState === 'undefined') {
if (
typeof nextState === 'undefined' ||
(ignoreReactEventsRef.current &&
typeof nextState === 'object' &&
(nextState.constructor.name === 'SyntheticBaseEvent' ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,no-underscore-dangle,@typescript-eslint/no-explicit-any
typeof (nextState as any)._reactName === 'string'))
) {
return !prevState;
}

Expand Down

0 comments on commit 085f711

Please sign in to comment.