From bfede03ddd28e180e4813a24e02cec09bcb8682d Mon Sep 17 00:00:00 2001 From: xobotyi Date: Thu, 30 Jun 2022 23:43:29 +0300 Subject: [PATCH] feat: implement `useFunctionalState` close #530 --- README.md | 2 ++ src/index.ts | 4 +-- src/useFunctionalState/__docs__/story.mdx | 19 ++++++++++++ src/useFunctionalState/__tests__/dom.ts | 31 ++++++++++++++++++++ src/useFunctionalState/__tests__/ssr.ts | 19 ++++++++++++ src/useFunctionalState/useFunctionalState.ts | 25 ++++++++++++++++ src/useSafeState/__docs__/story.mdx | 4 +-- src/useSafeState/__tests__/dom.ts | 2 +- 8 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/useFunctionalState/__docs__/story.mdx create mode 100644 src/useFunctionalState/__tests__/dom.ts create mode 100644 src/useFunctionalState/__tests__/ssr.ts create mode 100644 src/useFunctionalState/useFunctionalState.ts diff --git a/README.md b/README.md index 0ca53903..427f250a 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ Coming from `react-use`? Check out our — Tracks a numeric value and offers functions for manipulating it. - [**`useDebouncedState`**](https://react-hookz.github.io/web/?path=/docs/state-usedebouncedstate--example) — Like `useSafeState` but its state setter is debounced. + - [**`useFunctionalState`**](https://react-hookz.github.io/web/?path=/docs/state-usefunctionalstate--page) + — Like `useState` but instead of raw state, state getter returned. - [**`useList`**](https://react-hookz.github.io/web/?path=/docs/state-uselist--example) — Tracks a list and offers functions for manipulating it. - [**`useMap`**](https://react-hookz.github.io/web/?path=/docs/state-usemap--example) — Tracks the diff --git a/src/index.ts b/src/index.ts index 877e665e..32597b10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ export { useIntervalEffect } from './useIntervalEffect/useIntervalEffect'; // State export { useDebouncedState } from './useDebouncedState/useDebouncedState'; +export { useFunctionalState } from './useFunctionalState/useFunctionalState'; export { useList } from './useList/useList'; export { useMap } from './useMap/useMap'; export { useMediatedState } from './useMediatedState/useMediatedState'; @@ -58,6 +59,7 @@ export { useVibrate } from './useVibrate/useVibrate'; // Miscellaneous export { useSyncedRef } from './useSyncedRef/useSyncedRef'; +export { useHookableRef, HookableRefHandler } from './useHookableRef/useHookableRef'; // SideEffect export { useLocalStorageValue } from './useLocalStorageValue/useLocalStorageValue'; @@ -109,5 +111,3 @@ export { useWindowSize, WindowSize } from './useWindowSize/useWindowSize'; export { truthyAndArrayPredicate, truthyOrArrayPredicate } from './util/const'; export { IEffectCallback, IEffectHook } from './util/misc'; - -export { useHookableRef } from './useHookableRef/useHookableRef'; diff --git a/src/useFunctionalState/__docs__/story.mdx b/src/useFunctionalState/__docs__/story.mdx new file mode 100644 index 00000000..bc09611a --- /dev/null +++ b/src/useFunctionalState/__docs__/story.mdx @@ -0,0 +1,19 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# useFunctionalState + +Like `useState` but instead of raw state, state getter returned. `useSafeState` is used underneath. + +## Reference + +```ts +export function useFunctionalState( + initialState: S | (() => S) +): [() => S, React.Dispatch>]; +export function useFunctionalState(): [ + () => S | undefined, + React.Dispatch> +]; +``` diff --git a/src/useFunctionalState/__tests__/dom.ts b/src/useFunctionalState/__tests__/dom.ts new file mode 100644 index 00000000..d592c333 --- /dev/null +++ b/src/useFunctionalState/__tests__/dom.ts @@ -0,0 +1,31 @@ +import { act, renderHook } from '@testing-library/react-hooks/dom'; +import { useFunctionalState } from '../..'; + +describe('useFunctionalState', () => { + it('should be defined', () => { + expect(useFunctionalState).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useFunctionalState()); + expect(result.error).toBeUndefined(); + }); + + it('should return proper values', () => { + const { result } = renderHook(() => useFunctionalState(1)); + expect(result.current[1]).toBeInstanceOf(Function); + expect(result.current[0]).toBeInstanceOf(Function); + }); + + it('should return state getter', () => { + const { result } = renderHook(() => useFunctionalState(1)); + + expect(result.current[0]()).toBe(1); + + act(() => { + result.current[1](2); + }); + + expect(result.current[0]()).toBe(2); + }); +}); diff --git a/src/useFunctionalState/__tests__/ssr.ts b/src/useFunctionalState/__tests__/ssr.ts new file mode 100644 index 00000000..c19e65c1 --- /dev/null +++ b/src/useFunctionalState/__tests__/ssr.ts @@ -0,0 +1,19 @@ +import { renderHook } from '@testing-library/react-hooks/server'; +import { useFunctionalState } from '../..'; + +describe('useFunctionalState', () => { + it('should be defined', () => { + expect(useFunctionalState).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useFunctionalState()); + expect(result.error).toBeUndefined(); + }); + + it('should return proper values', () => { + const { result } = renderHook(() => useFunctionalState(1)); + expect(result.current[1]).toBeInstanceOf(Function); + expect(result.current[0]).toBeInstanceOf(Function); + }); +}); diff --git a/src/useFunctionalState/useFunctionalState.ts b/src/useFunctionalState/useFunctionalState.ts new file mode 100644 index 00000000..c289f71f --- /dev/null +++ b/src/useFunctionalState/useFunctionalState.ts @@ -0,0 +1,25 @@ +import { Dispatch, SetStateAction, useCallback } from 'react'; +import { useSafeState } from '../useSafeState/useSafeState'; +import { useSyncedRef } from '../useSyncedRef/useSyncedRef'; + +export function useFunctionalState( + initialState: S | (() => S) +): [() => S, Dispatch>]; +export function useFunctionalState(): [ + () => S | undefined, + Dispatch> +]; + +/** + * Like `useState` but instead of raw state, state getter returned. `useSafeState` is + * used underneath. + */ +export function useFunctionalState( + initialState?: S | (() => S) +): [() => S | undefined, Dispatch>] { + const [state, setState] = useSafeState(initialState); + const stateRef = useSyncedRef(state); + + // eslint-disable-next-line react-hooks/exhaustive-deps + return [useCallback(() => stateRef.current, []), setState]; +} diff --git a/src/useSafeState/__docs__/story.mdx b/src/useSafeState/__docs__/story.mdx index 954381b3..211c8807 100644 --- a/src/useSafeState/__docs__/story.mdx +++ b/src/useSafeState/__docs__/story.mdx @@ -16,8 +16,8 @@ action if component is unmounted, otherwise it is the same hook as common `useSt #### Example -Sadly we can't provide an example since this documentation built in `production` mode and warning -are only shown in `development` mode. +Sadly we can't provide an example since this documentation built in `production` mode and +warnings are only shown in `development` mode. ## Reference diff --git a/src/useSafeState/__tests__/dom.ts b/src/useSafeState/__tests__/dom.ts index 7dd9dee8..79ddcf73 100644 --- a/src/useSafeState/__tests__/dom.ts +++ b/src/useSafeState/__tests__/dom.ts @@ -11,7 +11,7 @@ describe('useSafeState', () => { expect(result.error).toBeUndefined(); }); - it('should not call', () => { + it('should not cause state change after component unmount', () => { const consoleSpy = jest.spyOn(console, 'error'); consoleSpy.mockImplementationOnce(() => {});