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];
+}