mirror of
https://github.com/streamich/react-use.git
synced 2025-12-08 18:02:14 +00:00
useMediatedState implementation, tests and docs;
This commit is contained in:
parent
7955c3e1e0
commit
cb4a735b84
@ -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
42
docs/useMediatedState.md
Normal 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._
|
||||
28
src/__stories__/useMediatedState.story.tsx
Normal file
28
src/__stories__/useMediatedState.story.tsx
Normal 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 />);
|
||||
68
src/__tests__/useMediatedState.test.ts
Normal file
68
src/__tests__/useMediatedState.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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
32
src/useMediatedState.ts
Normal 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];
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user