useMediatedState implementation, tests and docs;

This commit is contained in:
xobotyi 2019-10-14 20:30:07 +03:00
parent 7955c3e1e0
commit cb4a735b84
6 changed files with 172 additions and 0 deletions

View File

@ -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)
<br />
<br />

42
docs/useMediatedState.md Normal file
View File

@ -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 (
<div>
<div>You will not be able to enter more than one space</div>
<input
type="number"
min="0"
max="10"
value={state}
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
setState(ev.target.value);
}}
/>
</div>
);
};
```
## Reference
```ts
const [state, setState] = useMediatedState<S=any>(
mediator: StateMediator<S>,
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._

View File

@ -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 (
<div>
<div>You will not be able to enter more than one space</div>
<input
type="text"
min="0"
max="10"
value={state}
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
setState(ev.target.value);
}}
/>
</div>
);
};
storiesOf('State|useMediatedState', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useMediatedState.md')} />)
.add('Demo', () => <Demo />);

View File

@ -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<number> = jest.fn(newState => newState / 2)
): [jest.Mock | StateMediator, RenderHookResult<any, UseMediatedStateReturn<number>>] {
return [fn, renderHook(() => useMediatedState<number>(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<SetStateAction<number>>): void => {
setState(newState * 2);
}) as unknown) as StateMediator<number>;
const [, hook] = getHook(1, spy);
act(() => hook.result.current[1](3));
expect(hook.result.current[0]).toBe(6);
});
});

View File

@ -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';

32
src/useMediatedState.ts Normal file
View File

@ -0,0 +1,32 @@
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react';
export interface StateMediator<S = any> {
(newState: any): S;
(newState: any, dispatch: Dispatch<SetStateAction<S>>): void;
}
export type UseMediatedStateReturn<S = any> = [S, Dispatch<SetStateAction<S>>];
export function useMediatedState<S = undefined>(
mediator: StateMediator<S | undefined>
): UseMediatedStateReturn<S | undefined>;
export function useMediatedState<S = any>(mediator: StateMediator<S>, initialState: S): UseMediatedStateReturn<S>;
export function useMediatedState<S = any>(mediator: StateMediator<S>, initialState?: S): UseMediatedStateReturn<S> {
const mediatorFn = useRef(mediator);
const [state, setMediatedState] = useState<S>(initialState!);
const setState = useCallback(
(newState: any) => {
if (mediatorFn.current.length === 2) {
mediatorFn.current(newState, setMediatedState);
} else {
setMediatedState(mediatorFn.current(newState));
}
},
[state]
);
return [state, setState];
}