From cb4a735b84efeaee711de97b0cf00a0db8ee7193 Mon Sep 17 00:00:00 2001 From: xobotyi Date: Mon, 14 Oct 2019 20:30:07 +0300 Subject: [PATCH] useMediatedState implementation, tests and docs; --- README.md | 1 + docs/useMediatedState.md | 42 +++++++++++++ src/__stories__/useMediatedState.story.tsx | 28 +++++++++ src/__tests__/useMediatedState.test.ts | 68 ++++++++++++++++++++++ src/index.ts | 1 + src/useMediatedState.ts | 32 ++++++++++ 6 files changed, 172 insertions(+) create mode 100644 docs/useMediatedState.md create mode 100644 src/__stories__/useMediatedState.story.tsx create mode 100644 src/__tests__/useMediatedState.test.ts create mode 100644 src/useMediatedState.ts diff --git a/README.md b/README.md index ebe6c734..50286451 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ - [`useList`](./docs/useList.md) and [`useUpsert`](./docs/useUpsert.md) — tracks state of an array. [![][img-demo]](https://codesandbox.io/s/wonderful-mahavira-1sm0w) - [`useMap`](./docs/useMap.md) — tracks state of an object. [![][img-demo]](https://codesandbox.io/s/quirky-dewdney-gi161) - [`useStateValidator`](./docs/useStateValidator.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo) + - [`useMedisatedState`](./docs/useMediatedState.md) — like the regular `useState` but with mediation by custom function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemediatedstate--demo)

diff --git a/docs/useMediatedState.md b/docs/useMediatedState.md new file mode 100644 index 00000000..34a5cdb5 --- /dev/null +++ b/docs/useMediatedState.md @@ -0,0 +1,42 @@ +# `useMediatedState` + +A lot like the standard `useState`, but with mediation process. + +## Usage +```ts +import * as React from 'react'; +import { useMediatedState } from '../useMediatedState'; + +const InputMediator = s => s.replace(/[\s]+/, ' '); +const Demo = () => { + const [state, setState] = useMediatedState('', InputMediator); + + return ( +
+
You will not be able to enter more than one space
+ ) => { + setState(ev.target.value); + }} + /> +
+ ); +}; +``` + +## Reference +```ts +const [state, setState] = useMediatedState( + mediator: StateMediator, + initialState?: S +); +``` + +> Initial state will be set as-is. + +In case mediator expects 2 arguments it will receive the `setState` function as second argument, it is useful for async mediators. +>This hook will not cancel previous mediation when new one been invoked, you have to handle it yourself._ diff --git a/src/__stories__/useMediatedState.story.tsx b/src/__stories__/useMediatedState.story.tsx new file mode 100644 index 00000000..d3c946f6 --- /dev/null +++ b/src/__stories__/useMediatedState.story.tsx @@ -0,0 +1,28 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { useMediatedState } from '../useMediatedState'; +import ShowDocs from './util/ShowDocs'; + +const InputMediator = s => s.replace(/[\s]+/, ' '); +const Demo = () => { + const [state, setState] = useMediatedState(InputMediator, ''); + + return ( +
+
You will not be able to enter more than one space
+ ) => { + setState(ev.target.value); + }} + /> +
+ ); +}; + +storiesOf('State|useMediatedState', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/src/__tests__/useMediatedState.test.ts b/src/__tests__/useMediatedState.test.ts new file mode 100644 index 00000000..ef9838a9 --- /dev/null +++ b/src/__tests__/useMediatedState.test.ts @@ -0,0 +1,68 @@ +import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { Dispatch, SetStateAction } from 'react'; +import { useMediatedState } from '../'; +import { StateMediator, UseMediatedStateReturn } from '../useMediatedState'; + +describe('useMediatedState', () => { + it('should be defined', () => { + expect(useMediatedState).toBeDefined(); + }); + + function getHook( + initialState: number = 2, + fn: StateMediator = jest.fn(newState => newState / 2) + ): [jest.Mock | StateMediator, RenderHookResult>] { + return [fn, renderHook(() => useMediatedState(fn, initialState))]; + } + + it('should return array of two elements', () => { + const [, hook] = getHook(); + + expect(Array.isArray(hook.result.current)).toBe(true); + expect(hook.result.current[0]).toBe(2); + expect(typeof hook.result.current[1]).toBe('function'); + }); + + it('should act like regular useState but with mediator call on each setState', () => { + const [spy, hook] = getHook(); + + expect(hook.result.current[0]).toBe(2); + + act(() => hook.result.current[1](3)); + expect(hook.result.current[0]).toBe(1.5); + expect(spy).toHaveBeenCalledTimes(1); + + act(() => hook.result.current[1](4)); + expect(hook.result.current[0]).toBe(2); + expect(spy).toHaveBeenCalledTimes(2); + }); + + it('should not call mediator on init', () => { + const [spy] = getHook(); + + expect(spy).toHaveBeenCalledTimes(0); + }); + + it('mediator should receive setState argument as first argument', () => { + let val; + const spy = jest.fn(newState => { + val = newState; + return newState * 2; + }); + const [, hook] = getHook(1, spy); + + act(() => hook.result.current[1](3)); + expect(val).toBe(3); + expect(hook.result.current[0]).toBe(6); + }); + + it('if mediator expects 2 args, second should be a function setting the state', () => { + const spy = (jest.fn((newState: number, setState: Dispatch>): void => { + setState(newState * 2); + }) as unknown) as StateMediator; + const [, hook] = getHook(1, spy); + + act(() => hook.result.current[1](3)); + expect(hook.result.current[0]).toBe(6); + }); +}); diff --git a/src/index.ts b/src/index.ts index 65872134..e1e9b337 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,7 @@ export { default as useLogger } from './useLogger'; export { default as useMap } from './useMap'; export { default as useMedia } from './useMedia'; export { default as useMediaDevices } from './useMediaDevices'; +export { useMediatedState } from './useMediatedState'; export { default as useMotion } from './useMotion'; export { default as useMount } from './useMount'; export { default as useMountedState } from './useMountedState'; diff --git a/src/useMediatedState.ts b/src/useMediatedState.ts new file mode 100644 index 00000000..708b2ecf --- /dev/null +++ b/src/useMediatedState.ts @@ -0,0 +1,32 @@ +import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'; + +export interface StateMediator { + (newState: any): S; + + (newState: any, dispatch: Dispatch>): void; +} + +export type UseMediatedStateReturn = [S, Dispatch>]; + +export function useMediatedState( + mediator: StateMediator +): UseMediatedStateReturn; +export function useMediatedState(mediator: StateMediator, initialState: S): UseMediatedStateReturn; + +export function useMediatedState(mediator: StateMediator, initialState?: S): UseMediatedStateReturn { + const mediatorFn = useRef(mediator); + + const [state, setMediatedState] = useState(initialState!); + const setState = useCallback( + (newState: any) => { + if (mediatorFn.current.length === 2) { + mediatorFn.current(newState, setMediatedState); + } else { + setMediatedState(mediatorFn.current(newState)); + } + }, + [state] + ); + + return [state, setState]; +}