Skip to content

Commit 3a51779

Browse files
authored
feat: useToggle hook (#3)
feat: useToggle hook
1 parent ce1dccb commit 3a51779

File tree

8 files changed

+192
-0
lines changed

8 files changed

+192
-0
lines changed

.eslintrc.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,33 @@ module.exports = {
5151

5252
'import/prefer-default-export': 'off',
5353
'import/no-default-export': 'error',
54+
55+
'@typescript-eslint/naming-convention': [
56+
'error',
57+
{
58+
selector: ['interface', 'typeAlias'],
59+
format: ['PascalCase'],
60+
prefix: ['I'],
61+
},
62+
{
63+
selector: 'function',
64+
format: ['camelCase'],
65+
},
66+
{
67+
selector: 'variable',
68+
format: ['camelCase', 'UPPER_CASE'],
69+
leadingUnderscore: 'allow',
70+
},
71+
{
72+
selector: 'parameter',
73+
format: ['camelCase'],
74+
leadingUnderscore: 'allow',
75+
},
76+
{
77+
selector: 'parameter',
78+
format: ['camelCase'],
79+
leadingUnderscore: 'allow',
80+
},
81+
],
5482
},
5583
};

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { useFirstMountState } from './useFirstMountState';
22
export { useMountEffect } from './useMountEffect';
33
export { useUpdateEffect } from './useUpdateEffect';
44
export { useUnmountEffect } from './useUnmountEffect';
5+
export { useToggle } from './useToggle';

src/useToggle.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useCallback, useState } from 'react';
2+
import { IInitialState, INewState, resolveHookState } from './util/resolveHookState';
3+
4+
export function useToggle(
5+
initialState: IInitialState<boolean> = false
6+
): [boolean, (nextState?: INewState<boolean>) => void] {
7+
// We dont use useReducer (which would end up with less code), because exposed
8+
// action does not provide functional updates feature.
9+
// Therefore we have to create and expose our own state setter with
10+
// toggle logic.
11+
const [state, _setState] = useState(initialState);
12+
13+
return [
14+
state,
15+
useCallback((nextState) => {
16+
_setState((prevState) => {
17+
if (typeof nextState === 'undefined') {
18+
return !prevState;
19+
}
20+
21+
return Boolean(resolveHookState(nextState, prevState));
22+
});
23+
}, []),
24+
];
25+
}

src/util/resolveHookState.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type IInitialState<S> = S | (() => S);
2+
export type INewState<S> = S | ((prevState: S) => S);
3+
4+
export function resolveHookState<S>(nextState: IInitialState<S>): S;
5+
export function resolveHookState<S>(nextState: INewState<S>, prevState: S): S;
6+
export function resolveHookState<S>(nextState: IInitialState<S> | INewState<S>, prevState?: S): S {
7+
if (typeof nextState === 'function') return (nextState as CallableFunction)(prevState);
8+
9+
return nextState;
10+
}

tests/dom/useToggle.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { act, renderHook } from '@testing-library/react-hooks/dom';
2+
import { useToggle } from '../../src';
3+
4+
describe('useToggle', () => {
5+
it('should be defined', () => {
6+
expect(useToggle).toBeDefined();
7+
});
8+
9+
it('should default to false', () => {
10+
const { result } = renderHook(() => useToggle());
11+
12+
expect(result.current[0]).toBe(false);
13+
});
14+
15+
it('should be instantiatable with value', () => {
16+
let { result } = renderHook(() => useToggle(true));
17+
expect(result.current[0]).toBe(true);
18+
19+
result = renderHook(() => useToggle(() => true)).result;
20+
expect(result.current[0]).toBe(true);
21+
22+
result = renderHook(() => useToggle(() => false)).result;
23+
expect(result.current[0]).toBe(false);
24+
});
25+
26+
it('should change state to the opposite when toggler called without args or undefined', () => {
27+
const { result } = renderHook(() => useToggle());
28+
act(() => {
29+
result.current[1]();
30+
});
31+
expect(result.current[0]).toBe(true);
32+
33+
act(() => {
34+
result.current[1](undefined);
35+
});
36+
expect(result.current[0]).toBe(false);
37+
});
38+
39+
it('should change state to one that passed to toggler', () => {
40+
const { result } = renderHook(() => useToggle());
41+
act(() => {
42+
result.current[1](false);
43+
});
44+
expect(result.current[0]).toBe(false);
45+
46+
act(() => {
47+
result.current[1](true);
48+
});
49+
expect(result.current[0]).toBe(true);
50+
51+
act(() => {
52+
result.current[1](() => false);
53+
});
54+
expect(result.current[0]).toBe(false);
55+
56+
act(() => {
57+
result.current[1](() => true);
58+
});
59+
expect(result.current[0]).toBe(true);
60+
});
61+
});

tests/dom/util/resolveHookState.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { resolveHookState } from '../../../src/util/resolveHookState';
2+
3+
describe('resolveHookState', () => {
4+
it('it should be defined', () => {
5+
expect(resolveHookState).toBeDefined();
6+
});
7+
8+
it('should return value itself if it is not function', () => {
9+
expect(resolveHookState(123)).toBe(123);
10+
11+
const obj = { foo: 'bar' };
12+
expect(resolveHookState(obj)).toBe(obj);
13+
});
14+
15+
it('should return call result in case function received', () => {
16+
expect(resolveHookState(() => 123)).toBe(123);
17+
18+
const obj = { foo: 'bar' };
19+
expect(resolveHookState(() => obj)).toBe(obj);
20+
});
21+
22+
it('should pass second parameter to received function', () => {
23+
expect(resolveHookState((state) => state, 123)).toBe(123);
24+
25+
const obj = { foo: 'bar' };
26+
expect(resolveHookState((state) => state, obj)).toBe(obj);
27+
});
28+
});

tests/ssr/useToggle.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { act, renderHook } from '@testing-library/react-hooks/server';
2+
import { useToggle } from '../../src';
3+
4+
describe('useToggle', () => {
5+
it('should be defined', () => {
6+
expect(useToggle).toBeDefined();
7+
});
8+
9+
it('should default to false', () => {
10+
const { result } = renderHook(() => useToggle());
11+
12+
expect(result.current[0]).toBe(false);
13+
});
14+
15+
it('should be instantiatable with value', () => {
16+
let { result } = renderHook(() => useToggle(true));
17+
expect(result.current[0]).toBe(true);
18+
19+
result = renderHook(() => useToggle(() => true)).result;
20+
expect(result.current[0]).toBe(true);
21+
22+
result = renderHook(() => useToggle(() => false)).result;
23+
expect(result.current[0]).toBe(false);
24+
});
25+
26+
it('should not change if toggler called', () => {
27+
const { result } = renderHook(() => useToggle());
28+
act(() => {
29+
result.current[1]();
30+
});
31+
expect(result.current[0]).toBe(false);
32+
33+
act(() => {
34+
result.current[1](true);
35+
});
36+
expect(result.current[0]).toBe(false);
37+
});
38+
});

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"compilerOptions": {
33
"module": "ESNext",
4+
"moduleResolution": "Node",
45
"target": "ESNext",
56
"lib": [
67
"ESNext",

0 commit comments

Comments
 (0)