State containers (#52384) (#52421)

* feat: 🎸 add state containers

* docs: ✏️ add state container demos

* docs: ✏️ refrech state container docs

* chore: 🤖 install default comparator

* chore: 🤖 remove old state container implementation

* feat: 🎸 add selectors

* chore: 🤖 move Ensure tyep to type utils

* fix: 🐛 fix useSelector() types and demo CLI command

* test: 💍 add tests for state container demos

* feat: 🎸 add ReacursiveReadonly to kbn-utility-types

* feat: 🎸 shallow freeze state when not in production

* test: 💍 fix Jest tests

* refactor: 💡 remove .state and use BehaviourSubject
This commit is contained in:
Vadim Dalecky 2019-12-06 12:51:12 -08:00 committed by GitHub
parent 7671ce3eb4
commit db342e9e05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1275 additions and 750 deletions

View file

@ -163,6 +163,7 @@
"encode-uri-query": "1.0.1",
"execa": "^3.2.0",
"expiry-js": "0.1.7",
"fast-deep-equal": "^3.1.1",
"file-loader": "4.2.0",
"font-awesome": "4.7.0",
"getos": "^3.1.0",
@ -226,6 +227,7 @@
"react-resize-detector": "^4.2.0",
"react-router-dom": "^4.3.1",
"react-sizeme": "^2.3.6",
"react-use": "^13.10.2",
"reactcss": "1.2.3",
"redux": "4.0.0",
"redux-actions": "2.2.1",

View file

@ -18,7 +18,9 @@ type B = UnwrapPromise<A>; // string
## Reference
- `UnwrapPromise<T>` &mdash; Returns wrapped type of a promise.
- `UnwrapObservable<T>` &mdash; Returns wrapped type of an observable.
- `ShallowPromise<T>` &mdash; Same as `Promise` type, but it flat maps the wrapped type.
- `Ensure<T, X>` &mdash; Makes sure `T` is of type `X`.
- `ObservableLike<T>` &mdash; Minimal interface for an object resembling an `Observable`.
- `RecursiveReadonly<T>` &mdash; Like `Readonly<T>`, but freezes object recursively.
- `ShallowPromise<T>` &mdash; Same as `Promise` type, but it flat maps the wrapped type.
- `UnwrapObservable<T>` &mdash; Returns wrapped type of an observable.
- `UnwrapPromise<T>` &mdash; Returns wrapped type of a promise.

View file

@ -42,3 +42,19 @@ export type UnwrapObservable<T extends ObservableLike<any>> = T extends Observab
* Converts a type to a `Promise`, unless it is already a `Promise`. Useful when proxying the return value of a possibly async function.
*/
export type ShallowPromise<T> = T extends Promise<infer U> ? Promise<U> : Promise<T>;
/**
* Ensures T is of type X.
*/
export type Ensure<T, X> = T extends X ? T : never;
// If we define this inside RecursiveReadonly TypeScript complains.
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RecursiveReadonlyArray<T> extends Array<RecursiveReadonly<T>> {}
export type RecursiveReadonly<T> = T extends (...args: any) => any
? T
: T extends any[]
? RecursiveReadonlyArray<T[number]>
: T extends object
? Readonly<{ [K in keyof T]: RecursiveReadonly<T[K]> }>
: T;

View file

@ -2,4 +2,4 @@
Utilities for building Kibana plugins.
- [Store reactive serializable app state in state containers, `createStore`](./docs/store/README.md).
- [State containers](./docs/state_containers/README.md).

View file

@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { result as counterResult } from './state_containers/counter';
import { result as todomvcResult } from './state_containers/todomvc';
describe('demos', () => {
describe('state containers', () => {
test('counter demo works', () => {
expect(counterResult).toBe(10);
});
test('TodoMVC demo works', () => {
expect(todomvcResult).toEqual([
{ id: 0, text: 'Learning state containers', completed: true },
{ id: 1, text: 'Learning transitions...', completed: true },
]);
});
});
});

View file

@ -17,26 +17,16 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { Store as ReduxStore } from 'redux';
import { createStateContainer } from '../../public/state_containers';
export interface AppStore<
State extends {},
StateMutators extends Mutators<PureMutators<State>> = {}
> {
redux: ReduxStore;
get: () => State;
set: (state: State) => void;
state$: Observable<State>;
createMutators: <M extends PureMutators<State>>(pureMutators: M) => Mutators<M>;
mutators: StateMutators;
}
const container = createStateContainer(0, {
increment: (cnt: number) => (by: number) => cnt + by,
double: (cnt: number) => () => cnt * 2,
});
export type PureMutator<State extends {}> = (state: State) => (...args: any[]) => State;
export type Mutator<M extends PureMutator<any>> = (...args: Parameters<ReturnType<M>>) => void;
container.transitions.increment(5);
container.transitions.double();
export interface PureMutators<State extends {}> {
[name: string]: PureMutator<State>;
}
console.log(container.get()); // eslint-disable-line
export type Mutators<M extends PureMutators<any>> = { [K in keyof M]: Mutator<M[K]> };
export const result = container.get();

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createStateContainer, PureTransition } from '../../public/state_containers';
export interface TodoItem {
text: string;
completed: boolean;
id: number;
}
export type TodoState = TodoItem[];
export const defaultState: TodoState = [
{
id: 0,
text: 'Learning state containers',
completed: false,
},
];
export interface TodoActions {
add: PureTransition<TodoState, [TodoItem]>;
edit: PureTransition<TodoState, [TodoItem]>;
delete: PureTransition<TodoState, [number]>;
complete: PureTransition<TodoState, [number]>;
completeAll: PureTransition<TodoState, []>;
clearCompleted: PureTransition<TodoState, []>;
}
export const pureTransitions: TodoActions = {
add: state => todo => [...state, todo],
edit: state => todo => state.map(item => (item.id === todo.id ? { ...item, ...todo } : item)),
delete: state => id => state.filter(item => item.id !== id),
complete: state => id =>
state.map(item => (item.id === id ? { ...item, completed: true } : item)),
completeAll: state => () => state.map(item => ({ ...item, completed: true })),
clearCompleted: state => () => state.filter(({ completed }) => !completed),
};
const container = createStateContainer<TodoState, TodoActions>(defaultState, pureTransitions);
container.transitions.add({
id: 1,
text: 'Learning transitions...',
completed: false,
});
container.transitions.complete(0);
container.transitions.complete(1);
console.log(container.get()); // eslint-disable-line
export const result = container.get();

View file

@ -0,0 +1,50 @@
# State containers
State containers are Redux-store-like objects meant to help you manage state in
your services or apps.
- State containers are strongly typed, you will get TypeScript autocompletion suggestions from
your editor when accessing state, executing transitions and using React helpers.
- State containers can be easily hooked up with your React components.
- State containers can be used without React, too.
- State containers provide you central place where to store state, instead of spreading
state around multiple RxJs observables, which you need to coordinate. With state
container you can always access the latest state snapshot synchronously.
- Unlike Redux, state containers are less verbose, see example below.
## Example
```ts
import { createStateContainer } from 'src/plugins/kibana_utils';
const container = createStateContainer(0, {
increment: (cnt: number) => (by: number) => cnt + by,
double: (cnt: number) => () => cnt * 2,
});
container.transitions.increment(5);
container.transitions.double();
console.log(container.get()); // 10
```
## Demos
See demos [here](../../demos/state_containers/).
You can run them with
```
npx -q ts-node src/plugins/kibana_utils/demos/state_containers/counter.ts
npx -q ts-node src/plugins/kibana_utils/demos/state_containers/todomvc.ts
```
## Reference
- [Creating a state container](./creation.md).
- [State transitions](./transitions.md).
- [Using with React](./react.md).
- [Using without React`](./no_react.md).
- [Parallels with Redux](./redux.md).

View file

@ -17,7 +17,7 @@ interface MyState {
}
```
Create default state of your *store*.
Create default state of your container.
```ts
const defaultState: MyState = {
@ -27,17 +27,12 @@ const defaultState: MyState = {
};
```
Create your state container, i.e *store*.
Create your a state container.
```ts
import { createStore } from 'kibana-utils';
import { createStateContainer } from 'src/plugins/kibana_utils';
const store = createStore<MyState>(defaultState);
console.log(store.get());
const container = createStateContainer<MyState>(defaultState, {});
console.log(container.get());
```
> ##### N.B.
>
> State must always be an object `{}`.
>
> You cannot create a store out of an array, e.g ~~`createStore([])`~~.

View file

@ -1,4 +1,4 @@
# Reading state
# Consuming state in non-React setting
To read the current `state` of the store use `.get()` method.

View file

@ -0,0 +1,41 @@
# React
`createStateContainerReactHelpers` factory allows you to easily use state containers with React.
## Example
```ts
import { createStateContainer, createStateContainerReactHelpers } from 'src/plugins/kibana_utils';
const container = createStateContainer({}, {});
export const {
Provider,
Consumer,
context,
useContainer,
useState,
useTransitions,
useSelector,
connect,
} = createStateContainerReactHelpers<typeof container>();
```
Wrap your app with `<Provider>`.
```tsx
<Provider value={container}>
<MyApplication />
</Provider>
```
## Reference
- [`useContainer()`](./react/use_container.md)
- [`useState()`](./react/use_state.md)
- [`useSelector()`](./react/use_selector.md)
- [`useTransitions()`](./react/use_transitions.md)
- [`connect()()`](./react/connect.md)
- [Context](./react/context.md)

View file

@ -0,0 +1,22 @@
# `connect()()` higher order component
Use `connect()()` higher-order-component to inject props from state into your component.
```tsx
interface Props {
name: string;
punctuation: '.' | ',' | '!',
}
const Demo: React.FC<Props> = ({ name, punctuation }) =>
<div>Hello, {name}{punctuation}</div>;
const store = createStateContainer({ userName: 'John' });
const { Provider, connect } = createStateContainerReactHelpers(store);
const mapStateToProps = ({ userName }) => ({ name: userName });
const DemoConnected = connect<Props, 'name'>(mapStateToProps)(Demo);
<Provider>
<DemoConnected punctuation="!" />
</Provider>
```

View file

@ -0,0 +1,24 @@
# React context
`createStateContainerReactHelpers` returns `<Provider>` and `<Consumer>` components
as well as `context` React context object.
```ts
export const {
Provider,
Consumer,
context,
} = createStateContainerReactHelpers<typeof container>();
```
`<Provider>` and `<Consumer>` are just regular React context components.
```tsx
<Provider value={container}>
<div>
<Consumer>{container =>
<pre>{JSON.stringify(container.get())}</pre>
}</Consumer>
</div>
</Provider>
```

View file

@ -0,0 +1,10 @@
# `useContainer` hook
`useContainer` React hook will simply return you `container` object from React context.
```tsx
const Demo = () => {
const store = useContainer();
return <div>{store.get().isDarkMode ? '🌑' : '☀️'}</div>;
};
```

View file

@ -0,0 +1,20 @@
# `useSelector()` hook
With `useSelector` React hook you specify a selector function, which will pick specific
data from the state. *Your component will update only when that specific part of the state changes.*
```tsx
const selector = state => state.isDarkMode;
const Demo = () => {
const isDarkMode = useSelector(selector);
return <div>{isDarkMode ? '🌑' : '☀️'}</div>;
};
```
As an optional second argument for `useSelector` you can provide a `comparator` function, which
compares currently selected value with the previous and your component will re-render only if
`comparator` returns `true`. By default it uses [`fast-deep-equal`](https://github.com/epoberezkin/fast-deep-equal).
```
useSelector(selector, comparator?)
```

View file

@ -0,0 +1,11 @@
# `useState()` hook
- `useState` hook returns you directly the state of the container.
- It also forces component to re-render every time state changes.
```tsx
const Demo = () => {
const { isDarkMode } = useState();
return <div>{isDarkMode ? '🌑' : '☀️'}</div>;
};
```

View file

@ -0,0 +1,17 @@
# `useTransitions` hook
Access [state transitions](../transitions.md) by `useTransitions` React hook.
```tsx
const Demo = () => {
const { isDarkMode } = useState();
const { setDarkMode } = useTransitions();
return (
<>
<div>{isDarkMode ? '🌑' : '☀️'}</div>
<button onClick={() => setDarkMode(true)}>Go dark</button>
<button onClick={() => setDarkMode(false)}>Go light</button>
</>
);
};
```

View file

@ -0,0 +1,40 @@
# Redux
State containers similar to Redux stores but without the boilerplate.
State containers expose Redux-like API:
```js
container.getState()
container.dispatch()
container.replaceReducer()
container.subscribe()
container.addMiddleware()
```
State containers have a reducer and every time you execute a state transition it
actually dispatches an "action". For example, this
```js
container.transitions.increment(25);
```
is equivalent to
```js
container.dispatch({
type: 'increment',
args: [25],
});
```
Because all transitions happen through `.dispatch()` interface, you can add middleware&mdash;similar how you
would do with Redux&mdash;to monitor or intercept transitions.
For example, you can add `redux-logger` middleware to log in console all transitions happening with your store.
```js
import logger from 'redux-logger';
container.addMiddleware(logger);
```

View file

@ -0,0 +1,61 @@
# State transitions
*State transitions* describe possible state changes over time. Transitions are pure functions which
receive `state` object and other&mdash;optional&mdash;arguments and must return a new `state` object back.
```ts
type Transition = (state: State) => (...args) => State;
```
Transitions must not mutate `state` object in-place, instead they must return a
shallow copy of it, e.g. `{ ...state }`. Example:
```ts
const setUiMode: PureTransition = state => uiMode => ({ ...state, uiMode });
```
You provide transitions as a second argument when you create your state container.
```ts
import { createStateContainer } from 'src/plugins/kibana_utils';
const container = createStateContainer(0, {
increment: (cnt: number) => (by: number) => cnt + by,
double: (cnt: number) => () => cnt * 2,
});
```
Now you can execute the transitions by calling them with only optional parameters (`state` is
provided to your transitions automatically).
```ts
container.transitions.increment(25);
container.transitions.increment(5);
container.state; // 30
```
Your transitions are bound to the container so you can treat each of them as a
standalone function for export.
```ts
const defaultState = {
uiMode: 'light',
};
const container = createStateContainer(defaultState, {
setUiMode: state => uiMode => ({ ...state, uiMode }),
resetUiMode: state => () => ({ ...state, uiMode: defaultState.uiMode }),
});
export const {
setUiMode,
resetUiMode
} = container.transitions;
```
You can add TypeScript annotations for your transitions as the second generic argument
to `createStateContainer()` function.
```ts
const container = createStateContainer<MyState, MyTransitions>(defaultState, pureTransitions);
```

View file

@ -1,9 +0,0 @@
# State containers
- State containers for holding serializable state.
- [Each plugin/app that needs runtime state will create a *store* using `store = createStore()`](./creation.md).
- [*Store* can be updated using mutators `mutators = store.createMutators({ ... })`](./mutators.md).
- [*Store* can be connected to React `{Provider, connect} = createContext(store)`](./react.md).
- [In no-React setting *store* is consumed using `store.get()` and `store.state$`](./getters.md).
- [Under-the-hood uses Redux `store.redux`](./redux.md) (but you should never need it explicitly).
- [See idea doc with samples and rationale](https://docs.google.com/document/d/18eitHkcyKSsEHUfUIqFKChc8Pp62Z4gcRxdu903hbA0/edit#heading=h.iaxc9whxifl5).

View file

@ -1,70 +0,0 @@
# Mutators
State *mutators* are pure functions which receive `state` object and other&mdash;optional&mdash;arguments
and must return a new `state` object back.
```ts
type Mutator = (state: State) => (...args) => State;
```
Mutator must not mutate `state` object in-place, instead it should return a
shallow copy of it, e.g. `{ ...state }`.
```ts
const setUiMode: Mutator = state => uiMode => ({ ...state, uiMode });
```
You create mutators using `.createMutator(...)` method.
```ts
const store = createStore({uiMode: 'light'});
const mutators = store.createMutators({
setUiMode: state => uiMode => ({ ...state, uiMode }),
});
```
Now you can use your mutators by calling them with only optional parameters (`state` is
provided to your mutator automatically).
```ts
mutators.setUiMode('dark');
```
Your mutators are bound to the `store` so you can treat each of them as a
standalone function for export.
```ts
const { setUiMode, resetUiMode } = store.createMutators({
setUiMode: state => uiMode => ({ ...state, uiMode }),
resetUiMode: state => () => ({ ...state, uiMode: 'light' }),
});
export {
setUiMode,
resetUiMode,
};
```
The mutators you create are also available on the `store` object.
```ts
const store = createStore({ cnt: 0 });
store.createMutators({
add: state => value => ({ ...state, cnt: state.cnt + value }),
});
store.mutators.add(5);
store.get(); // { cnt: 5 }
```
You can add TypeScript annotations to your `.mutators` property of `store` object.
```ts
const store = createStore<{
cnt: number;
}, {
add: (value: number) => void;
}>({
cnt: 0
});
```

View file

@ -1,101 +0,0 @@
# React
`createContext` factory allows you to easily use state containers with React.
```ts
import { createStore, createContext } from 'kibana-utils';
const store = createStore({});
const {
Provider,
Consumer,
connect,
context,
useStore,
useState,
useMutators,
useSelector,
} = createContext(store);
```
Wrap your app with `<Provider>`.
```tsx
<Provider>
<MyApplication />
</Provider>
```
Use `connect()()` higer-order-component to inject props from state into your component.
```tsx
interface Props {
name: string;
punctuation: '.' | ',' | '!',
}
const Demo: React.FC<Props> = ({ name, punctuation }) =>
<div>Hello, {name}{punctuation}</div>;
const store = createStore({ userName: 'John' });
const { Provider, connect } = createContext(store);
const mapStateToProps = ({ userName }) => ({ name: userName });
const DemoConnected = connect<Props, 'name'>(mapStateToProps)(Demo);
<Provider>
<DemoConnected punctuation="!" />
</Provider>
```
`useStore` React hook will fetch the `store` object from the context.
```tsx
const Demo = () => {
const store = useStore();
return <div>{store.get().isDarkMode ? '🌑' : '☀️'}</div>;
};
```
If you want your component to always re-render when the state changes use `useState` React hook.
```tsx
const Demo = () => {
const { isDarkMode } = useState();
return <div>{isDarkMode ? '🌑' : '☀️'}</div>;
};
```
For `useSelector` React hook you specify a selector function, which will pick specific
data from the state. *Your component will update only when that specific part of the state changes.*
```tsx
const selector = state => state.isDarkMode;
const Demo = () => {
const isDarkMode = useSelector(selector);
return <div>{isDarkMode ? '🌑' : '☀️'}</div>;
};
```
As an optional second argument for `useSelector` you can provide a `comparator` function, which
compares currently selected value with the previous and your component will re-render only if
`comparator` returns `true`. By default, it simply uses tripple equals `===` comparison.
```
useSelector(selector, comparator?)
```
Access state mutators by `useMutators` React hook.
```tsx
const Demo = () => {
const { isDarkMode } = useState();
const { setDarkMode } = useMutators();
return (
<>
<div>{isDarkMode ? '🌑' : '☀️'}</div>
<button onClick={() => setDarkMode(true)}>Go dark</button>
<button onClick={() => setDarkMode(false)}>Go light</button>
</>
);
};
```

View file

@ -1,19 +0,0 @@
# Redux
Internally `createStore()` uses Redux to manage the state. When you call `store.get()`
it is actually calling the Redux `.getState()` method. When you execute a mutation
it is actually dispatching a Redux action.
You can access Redux *store* using `.redux`.
```ts
store.redux;
```
But you should never need it, if you think you do, consult with Kibana App Architecture team.
We use Redux internally for 3 main reasons:
- We can reuse `react-redux` library to easily connect state containers to React.
- We can reuse Redux devtools.
- We can reuse battle-tested Redux library and action/reducer paradigm.

View file

@ -17,9 +17,9 @@
* under the License.
*/
import { createStore, createContext } from '.';
import { createStateContainer, createStateContainerReactHelpers } from '.';
test('exports store methods', () => {
expect(typeof createStore).toBe('function');
expect(typeof createContext).toBe('function');
expect(typeof createStateContainer).toBe('function');
expect(typeof createStateContainerReactHelpers).toBe('function');
});

View file

@ -19,13 +19,12 @@
export * from './core';
export * from './errors';
export * from './store';
export * from './parse';
export * from './resize_checker';
export * from './render_complete';
export * from './store';
export * from './errors';
export * from './field_mapping';
export * from './parse';
export * from './render_complete';
export * from './resize_checker';
export * from './state_containers';
export * from './storage';
export * from './storage/hashed_item_store';
export * from './state_management/state_hash';

View file

@ -0,0 +1,303 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createStateContainer } from './create_state_container';
const create = <S, T extends object>(state: S, transitions: T = {} as T) => {
const pureTransitions = {
set: () => (newState: S) => newState,
...transitions,
};
const store = createStateContainer<typeof state, typeof pureTransitions>(state, pureTransitions);
return { store, mutators: store.transitions };
};
test('can create store', () => {
const { store } = create({});
expect(store).toMatchObject({
getState: expect.any(Function),
state$: expect.any(Object),
transitions: expect.any(Object),
dispatch: expect.any(Function),
subscribe: expect.any(Function),
replaceReducer: expect.any(Function),
addMiddleware: expect.any(Function),
});
});
test('can set default state', () => {
const defaultState = {
foo: 'bar',
};
const { store } = create(defaultState);
expect(store.get()).toEqual(defaultState);
expect(store.getState()).toEqual(defaultState);
});
test('can set state', () => {
const defaultState = {
foo: 'bar',
};
const newState = {
foo: 'baz',
};
const { store, mutators } = create(defaultState);
mutators.set(newState);
expect(store.get()).toEqual(newState);
expect(store.getState()).toEqual(newState);
});
test('does not shallow merge states', () => {
const defaultState = {
foo: 'bar',
};
const newState = {
foo2: 'baz',
};
const { store, mutators } = create(defaultState);
mutators.set(newState as any);
expect(store.get()).toEqual(newState);
expect(store.getState()).toEqual(newState);
});
test('can subscribe and unsubscribe to state changes', () => {
const { store, mutators } = create({});
const spy = jest.fn();
const subscription = store.state$.subscribe(spy);
mutators.set({ a: 1 });
mutators.set({ a: 2 });
subscription.unsubscribe();
mutators.set({ a: 3 });
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0]).toEqual({ a: 1 });
expect(spy.mock.calls[1][0]).toEqual({ a: 2 });
});
test('multiple subscribers can subscribe', () => {
const { store, mutators } = create({});
const spy1 = jest.fn();
const spy2 = jest.fn();
const subscription1 = store.state$.subscribe(spy1);
const subscription2 = store.state$.subscribe(spy2);
mutators.set({ a: 1 });
subscription1.unsubscribe();
mutators.set({ a: 2 });
subscription2.unsubscribe();
mutators.set({ a: 3 });
expect(spy1).toHaveBeenCalledTimes(1);
expect(spy2).toHaveBeenCalledTimes(2);
expect(spy1.mock.calls[0][0]).toEqual({ a: 1 });
expect(spy2.mock.calls[0][0]).toEqual({ a: 1 });
expect(spy2.mock.calls[1][0]).toEqual({ a: 2 });
});
test('creates impure mutators from pure mutators', () => {
const { mutators } = create(
{},
{
setFoo: () => (bar: any) => ({ foo: bar }),
}
);
expect(typeof mutators.setFoo).toBe('function');
});
test('mutators can update state', () => {
const { store, mutators } = create(
{
value: 0,
foo: 'bar',
},
{
add: (state: any) => (increment: any) => ({ ...state, value: state.value + increment }),
setFoo: (state: any) => (bar: any) => ({ ...state, foo: bar }),
}
);
expect(store.get()).toEqual({
value: 0,
foo: 'bar',
});
mutators.add(11);
mutators.setFoo('baz');
expect(store.get()).toEqual({
value: 11,
foo: 'baz',
});
mutators.add(-20);
mutators.setFoo('bazooka');
expect(store.get()).toEqual({
value: -9,
foo: 'bazooka',
});
});
test('mutators methods are not bound', () => {
const { store, mutators } = create(
{ value: -3 },
{
add: (state: { value: number }) => (increment: number) => ({
...state,
value: state.value + increment,
}),
}
);
expect(store.get()).toEqual({ value: -3 });
mutators.add(4);
expect(store.get()).toEqual({ value: 1 });
});
test('created mutators are saved in store object', () => {
const { store, mutators } = create(
{ value: -3 },
{
add: (state: { value: number }) => (increment: number) => ({
...state,
value: state.value + increment,
}),
}
);
expect(typeof store.transitions.add).toBe('function');
mutators.add(5);
expect(store.get()).toEqual({ value: 2 });
});
test('throws when state is modified inline - 1', () => {
const container = createStateContainer({ a: 'b' }, {});
let error: TypeError | null = null;
try {
(container.get().a as any) = 'c';
} catch (err) {
error = err;
}
expect(error).toBeInstanceOf(TypeError);
});
test('throws when state is modified inline - 2', () => {
const container = createStateContainer({ a: 'b' }, {});
let error: TypeError | null = null;
try {
(container.getState().a as any) = 'c';
} catch (err) {
error = err;
}
expect(error).toBeInstanceOf(TypeError);
});
test('throws when state is modified inline in subscription', done => {
const container = createStateContainer({ a: 'b' }, { set: () => (newState: any) => newState });
container.subscribe(value => {
let error: TypeError | null = null;
try {
(value.a as any) = 'd';
} catch (err) {
error = err;
}
expect(error).toBeInstanceOf(TypeError);
done();
});
container.transitions.set({ a: 'c' });
});
describe('selectors', () => {
test('can specify no selectors, or can skip them', () => {
createStateContainer({}, {});
createStateContainer({}, {}, {});
});
test('selector object is available on .selectors key', () => {
const container1 = createStateContainer({}, {}, {});
const container2 = createStateContainer({}, {}, { foo: () => () => 123 });
const container3 = createStateContainer({}, {}, { bar: () => () => 1, baz: () => () => 1 });
expect(Object.keys(container1.selectors).sort()).toEqual([]);
expect(Object.keys(container2.selectors).sort()).toEqual(['foo']);
expect(Object.keys(container3.selectors).sort()).toEqual(['bar', 'baz']);
});
test('selector without arguments returns correct state slice', () => {
const container = createStateContainer(
{ name: 'Oleg' },
{
changeName: (state: { name: string }) => (name: string) => ({ ...state, name }),
},
{ getName: (state: { name: string }) => () => state.name }
);
expect(container.selectors.getName()).toBe('Oleg');
container.transitions.changeName('Britney');
expect(container.selectors.getName()).toBe('Britney');
});
test('selector can accept an argument', () => {
const container = createStateContainer(
{
users: {
1: {
name: 'Darth',
},
},
},
{},
{
getUser: (state: any) => (id: number) => state.users[id],
}
);
expect(container.selectors.getUser(1)).toEqual({ name: 'Darth' });
expect(container.selectors.getUser(2)).toBe(undefined);
});
test('selector can accept multiple arguments', () => {
const container = createStateContainer(
{
users: {
5: {
name: 'Darth',
surname: 'Vader',
},
},
},
{},
{
getName: (state: any) => (id: number, which: 'name' | 'surname') => state.users[id][which],
}
);
expect(container.selectors.getName(5, 'name')).toEqual('Darth');
expect(container.selectors.getName(5, 'surname')).toEqual('Vader');
});
});

View file

@ -0,0 +1,89 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { BehaviorSubject } from 'rxjs';
import { skip } from 'rxjs/operators';
import { RecursiveReadonly } from '@kbn/utility-types';
import {
PureTransitionsToTransitions,
PureTransition,
ReduxLikeStateContainer,
PureSelectorsToSelectors,
} from './types';
const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable';
const freeze: <T>(value: T) => RecursiveReadonly<T> =
process.env.NODE_ENV !== 'production'
? <T>(value: T): RecursiveReadonly<T> => {
if (!value) return value as RecursiveReadonly<T>;
if (value instanceof Array) return value as RecursiveReadonly<T>;
if (typeof value === 'object') return Object.freeze({ ...value }) as RecursiveReadonly<T>;
else return value as RecursiveReadonly<T>;
}
: <T>(value: T) => value as RecursiveReadonly<T>;
export const createStateContainer = <
State,
PureTransitions extends object,
PureSelectors extends object = {}
>(
defaultState: State,
pureTransitions: PureTransitions,
pureSelectors: PureSelectors = {} as PureSelectors
): ReduxLikeStateContainer<State, PureTransitions, PureSelectors> => {
const data$ = new BehaviorSubject<RecursiveReadonly<State>>(freeze(defaultState));
const state$ = data$.pipe(skip(1));
const get = () => data$.getValue();
const container: ReduxLikeStateContainer<State, PureTransitions, PureSelectors> = {
get,
state$,
getState: () => data$.getValue(),
set: (state: State) => {
data$.next(freeze(state));
},
reducer: (state, action) => {
const pureTransition = (pureTransitions as Record<string, PureTransition<State, any[]>>)[
action.type
];
return pureTransition ? freeze(pureTransition(state)(...action.args)) : state;
},
replaceReducer: nextReducer => (container.reducer = nextReducer),
dispatch: action => data$.next(container.reducer(get(), action)),
transitions: Object.keys(pureTransitions).reduce<PureTransitionsToTransitions<PureTransitions>>(
(acc, type) => ({ ...acc, [type]: (...args: any) => container.dispatch({ type, args }) }),
{} as PureTransitionsToTransitions<PureTransitions>
),
selectors: Object.keys(pureSelectors).reduce<PureSelectorsToSelectors<PureSelectors>>(
(acc, selector) => ({
...acc,
[selector]: (...args: any) => (pureSelectors as any)[selector](get())(...args),
}),
{} as PureSelectorsToSelectors<PureSelectors>
),
addMiddleware: middleware =>
(container.dispatch = middleware(container as any)(container.dispatch)),
subscribe: (listener: (state: RecursiveReadonly<State>) => void) => {
const subscription = state$.subscribe(listener);
return () => subscription.unsubscribe();
},
[$$observable]: state$,
};
return container;
};

View file

@ -20,8 +20,17 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { act, Simulate } from 'react-dom/test-utils';
import { createStore } from './create_store';
import { createContext } from './react';
import { createStateContainer } from './create_state_container';
import { createStateContainerReactHelpers } from './create_state_container_react_helpers';
const create = <S, T extends object>(state: S, transitions: T = {} as T) => {
const pureTransitions = {
set: () => (newState: S) => newState,
...transitions,
};
const store = createStateContainer<typeof state, typeof pureTransitions>(state, pureTransitions);
return { store, mutators: store.transitions };
};
let container: HTMLDivElement | null;
@ -36,27 +45,23 @@ afterEach(() => {
});
test('can create React context', () => {
const store = createStore({ foo: 'bar' });
const context = createContext(store);
const context = createStateContainerReactHelpers();
expect(context).toMatchObject({
Provider: expect.any(Function),
Consumer: expect.any(Function),
Provider: expect.any(Object),
Consumer: expect.any(Object),
connect: expect.any(Function),
context: {
Provider: expect.any(Object),
Consumer: expect.any(Object),
},
context: expect.any(Object),
});
});
test('<Provider> passes state to <Consumer>', () => {
const store = createStore({ hello: 'world' });
const { Provider, Consumer } = createContext(store);
const { store } = create({ hello: 'world' });
const { Provider, Consumer } = createStateContainerReactHelpers<typeof store>();
ReactDOM.render(
<Provider>
<Consumer>{({ hello }) => hello}</Consumer>
<Provider value={store}>
<Consumer>{(s: typeof store) => s.get().hello}</Consumer>
</Provider>,
container
);
@ -74,8 +79,8 @@ interface Props1 {
}
test('<Provider> passes state to connect()()', () => {
const store = createStore<State1>({ hello: 'Bob' });
const { Provider, connect } = createContext(store);
const { store } = create({ hello: 'Bob' });
const { Provider, connect } = createStateContainerReactHelpers();
const Demo: React.FC<Props1> = ({ message, stop }) => (
<>
@ -87,7 +92,7 @@ test('<Provider> passes state to connect()()', () => {
const DemoConnected = connect<Props1, 'message'>(mergeProps)(Demo);
ReactDOM.render(
<Provider>
<Provider value={store}>
<DemoConnected stop="?" />
</Provider>,
container
@ -97,13 +102,13 @@ test('<Provider> passes state to connect()()', () => {
});
test('context receives Redux store', () => {
const store = createStore({ foo: 'bar' });
const { Provider, context } = createContext(store);
const { store } = create({ foo: 'bar' });
const { Provider, context } = createStateContainerReactHelpers<typeof store>();
ReactDOM.render(
/* eslint-disable no-shadow */
<Provider>
<context.Consumer>{({ store }) => store.getState().foo}</context.Consumer>
<Provider value={store}>
<context.Consumer>{store => store.get().foo}</context.Consumer>
</Provider>,
/* eslint-enable no-shadow */
container
@ -117,16 +122,16 @@ xtest('can use multiple stores in one React app', () => {});
describe('hooks', () => {
describe('useStore', () => {
test('can select store using useStore hook', () => {
const store = createStore({ foo: 'bar' });
const { Provider, useStore } = createContext(store);
const { store } = create({ foo: 'bar' });
const { Provider, useContainer } = createStateContainerReactHelpers<typeof store>();
const Demo: React.FC<{}> = () => {
// eslint-disable-next-line no-shadow
const store = useStore();
const store = useContainer();
return <>{store.get().foo}</>;
};
ReactDOM.render(
<Provider>
<Provider value={store}>
<Demo />
</Provider>,
container
@ -138,15 +143,15 @@ describe('hooks', () => {
describe('useState', () => {
test('can select state using useState hook', () => {
const store = createStore({ foo: 'qux' });
const { Provider, useState } = createContext(store);
const { store } = create({ foo: 'qux' });
const { Provider, useState } = createStateContainerReactHelpers<typeof store>();
const Demo: React.FC<{}> = () => {
const { foo } = useState();
return <>{foo}</>;
};
ReactDOM.render(
<Provider>
<Provider value={store}>
<Demo />
</Provider>,
container
@ -156,18 +161,23 @@ describe('hooks', () => {
});
test('re-renders when state changes', () => {
const store = createStore({ foo: 'bar' });
const { setFoo } = store.createMutators({
setFoo: state => foo => ({ ...state, foo }),
});
const { Provider, useState } = createContext(store);
const {
store,
mutators: { setFoo },
} = create(
{ foo: 'bar' },
{
setFoo: (state: { foo: string }) => (foo: string) => ({ ...state, foo }),
}
);
const { Provider, useState } = createStateContainerReactHelpers<typeof store>();
const Demo: React.FC<{}> = () => {
const { foo } = useState();
return <>{foo}</>;
};
ReactDOM.render(
<Provider>
<Provider value={store}>
<Demo />
</Provider>,
container
@ -181,26 +191,31 @@ describe('hooks', () => {
});
});
describe('useMutations', () => {
test('useMutations hook returns mutations that can update state', () => {
const store = createStore<
describe('useTransitions', () => {
test('useTransitions hook returns mutations that can update state', () => {
const { store } = create<
{
cnt: number;
},
any
>(
{
increment: (value: number) => void;
cnt: 0,
},
{
increment: (state: { cnt: number }) => (value: number) => ({
...state,
cnt: state.cnt + value,
}),
}
>({
cnt: 0,
});
store.createMutators({
increment: state => value => ({ ...state, cnt: state.cnt + value }),
});
);
const { Provider, useState, useMutators } = createContext(store);
const { Provider, useState, useTransitions } = createStateContainerReactHelpers<
typeof store
>();
const Demo: React.FC<{}> = () => {
const { cnt } = useState();
const { increment } = useMutators();
const { increment } = useTransitions();
return (
<>
<strong>{cnt}</strong>
@ -210,7 +225,7 @@ describe('hooks', () => {
};
ReactDOM.render(
<Provider>
<Provider value={store}>
<Demo />
</Provider>,
container
@ -230,7 +245,7 @@ describe('hooks', () => {
describe('useSelector', () => {
test('can select deeply nested value', () => {
const store = createStore({
const { store } = create({
foo: {
bar: {
baz: 'qux',
@ -238,14 +253,14 @@ describe('hooks', () => {
},
});
const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz;
const { Provider, useSelector } = createContext(store);
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
const Demo: React.FC<{}> = () => {
const value = useSelector(selector);
return <>{value}</>;
};
ReactDOM.render(
<Provider>
<Provider value={store}>
<Demo />
</Provider>,
container
@ -255,7 +270,7 @@ describe('hooks', () => {
});
test('re-renders when state changes', () => {
const store = createStore({
const { store, mutators } = create({
foo: {
bar: {
baz: 'qux',
@ -263,14 +278,14 @@ describe('hooks', () => {
},
});
const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz;
const { Provider, useSelector } = createContext(store);
const { Provider, useSelector } = createStateContainerReactHelpers();
const Demo: React.FC<{}> = () => {
const value = useSelector(selector);
return <>{value}</>;
};
ReactDOM.render(
<Provider>
<Provider value={store}>
<Demo />
</Provider>,
container
@ -278,7 +293,7 @@ describe('hooks', () => {
expect(container!.innerHTML).toBe('qux');
act(() => {
store.set({
mutators.set({
foo: {
bar: {
baz: 'quux',
@ -290,9 +305,9 @@ describe('hooks', () => {
});
test("re-renders only when selector's result changes", async () => {
const store = createStore({ a: 'b', foo: 'bar' });
const { store, mutators } = create({ a: 'b', foo: 'bar' });
const selector = (state: { foo: string }) => state.foo;
const { Provider, useSelector } = createContext(store);
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
let cnt = 0;
const Demo: React.FC<{}> = () => {
@ -301,7 +316,7 @@ describe('hooks', () => {
return <>{value}</>;
};
ReactDOM.render(
<Provider>
<Provider value={store}>
<Demo />
</Provider>,
container
@ -311,24 +326,24 @@ describe('hooks', () => {
expect(cnt).toBe(1);
act(() => {
store.set({ a: 'c', foo: 'bar' });
mutators.set({ a: 'c', foo: 'bar' });
});
await new Promise(r => setTimeout(r, 1));
expect(cnt).toBe(1);
act(() => {
store.set({ a: 'd', foo: 'bar 2' });
mutators.set({ a: 'd', foo: 'bar 2' });
});
await new Promise(r => setTimeout(r, 1));
expect(cnt).toBe(2);
});
test('re-renders on same shape object', async () => {
const store = createStore({ foo: { bar: 'baz' } });
test('does not re-render on same shape object', async () => {
const { store, mutators } = create({ foo: { bar: 'baz' } });
const selector = (state: { foo: any }) => state.foo;
const { Provider, useSelector } = createContext(store);
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
let cnt = 0;
const Demo: React.FC<{}> = () => {
@ -337,7 +352,7 @@ describe('hooks', () => {
return <>{JSON.stringify(value)}</>;
};
ReactDOM.render(
<Provider>
<Provider value={store}>
<Demo />
</Provider>,
container
@ -347,7 +362,14 @@ describe('hooks', () => {
expect(cnt).toBe(1);
act(() => {
store.set({ foo: { bar: 'baz' } });
mutators.set({ foo: { bar: 'baz' } });
});
await new Promise(r => setTimeout(r, 1));
expect(cnt).toBe(1);
act(() => {
mutators.set({ foo: { bar: 'qux' } });
});
await new Promise(r => setTimeout(r, 1));
@ -355,10 +377,15 @@ describe('hooks', () => {
});
test('can set custom comparator function to prevent re-renders on deep equality', async () => {
const store = createStore({ foo: { bar: 'baz' } });
const { store, mutators } = create(
{ foo: { bar: 'baz' } },
{
set: () => (newState: { foo: { bar: string } }) => newState,
}
);
const selector = (state: { foo: any }) => state.foo;
const comparator = (prev: any, curr: any) => JSON.stringify(prev) === JSON.stringify(curr);
const { Provider, useSelector } = createContext(store);
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
let cnt = 0;
const Demo: React.FC<{}> = () => {
@ -367,7 +394,7 @@ describe('hooks', () => {
return <>{JSON.stringify(value)}</>;
};
ReactDOM.render(
<Provider>
<Provider value={store}>
<Demo />
</Provider>,
container
@ -377,7 +404,7 @@ describe('hooks', () => {
expect(cnt).toBe(1);
act(() => {
store.set({ foo: { bar: 'baz' } });
mutators.set({ foo: { bar: 'baz' } });
});
await new Promise(r => setTimeout(r, 1));

View file

@ -0,0 +1,77 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import useObservable from 'react-use/lib/useObservable';
import defaultComparator from 'fast-deep-equal';
import { Comparator, Connect, StateContainer, UnboxState } from './types';
const { useContext, useLayoutEffect, useRef, createElement: h } = React;
export const createStateContainerReactHelpers = <Container extends StateContainer<any, any>>() => {
const context = React.createContext<Container>(null as any);
const useContainer = (): Container => useContext(context);
const useState = (): UnboxState<Container> => {
const { state$, get } = useContainer();
const value = useObservable(state$, get());
return value;
};
const useTransitions = () => useContainer().transitions;
const useSelector = <Result>(
selector: (state: UnboxState<Container>) => Result,
comparator: Comparator<Result> = defaultComparator
): Result => {
const { state$, get } = useContainer();
const lastValueRef = useRef<Result>(get());
const [value, setValue] = React.useState<Result>(() => {
const newValue = selector(get());
lastValueRef.current = newValue;
return newValue;
});
useLayoutEffect(() => {
const subscription = state$.subscribe((currentState: UnboxState<Container>) => {
const newValue = selector(currentState);
if (!comparator(lastValueRef.current, newValue)) {
lastValueRef.current = newValue;
setValue(newValue);
}
});
return () => subscription.unsubscribe();
}, [state$, comparator]);
return value;
};
const connect: Connect<UnboxState<Container>> = mapStateToProp => component => props =>
h(component, { ...useSelector(mapStateToProp), ...props } as any);
return {
Provider: context.Provider,
Consumer: context.Consumer,
context,
useContainer,
useState,
useTransitions,
useSelector,
connect,
};
};

View file

@ -17,5 +17,6 @@
* under the License.
*/
export * from './create_store';
export * from './react';
export * from './types';
export * from './create_state_container';
export * from './create_state_container_react_helpers';

View file

@ -0,0 +1,99 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Observable } from 'rxjs';
import { Ensure, RecursiveReadonly } from '@kbn/utility-types';
export interface TransitionDescription<Type extends string = string, Args extends any[] = any[]> {
type: Type;
args: Args;
}
export type Transition<State, Args extends any[]> = (...args: Args) => State;
export type PureTransition<State, Args extends any[]> = (
state: RecursiveReadonly<State>
) => Transition<State, Args>;
export type EnsurePureTransition<T> = Ensure<T, PureTransition<any, any>>;
export type PureTransitionToTransition<T extends PureTransition<any, any>> = ReturnType<T>;
export type PureTransitionsToTransitions<T extends object> = {
[K in keyof T]: PureTransitionToTransition<EnsurePureTransition<T[K]>>;
};
export interface BaseStateContainer<State> {
get: () => RecursiveReadonly<State>;
set: (state: State) => void;
state$: Observable<RecursiveReadonly<State>>;
}
export interface StateContainer<
State,
PureTransitions extends object,
PureSelectors extends object = {}
> extends BaseStateContainer<State> {
transitions: Readonly<PureTransitionsToTransitions<PureTransitions>>;
selectors: Readonly<PureSelectorsToSelectors<PureSelectors>>;
}
export interface ReduxLikeStateContainer<
State,
PureTransitions extends object,
PureSelectors extends object = {}
> extends StateContainer<State, PureTransitions, PureSelectors> {
getState: () => RecursiveReadonly<State>;
reducer: Reducer<RecursiveReadonly<State>>;
replaceReducer: (nextReducer: Reducer<RecursiveReadonly<State>>) => void;
dispatch: (action: TransitionDescription) => void;
addMiddleware: (middleware: Middleware<RecursiveReadonly<State>>) => void;
subscribe: (listener: (state: RecursiveReadonly<State>) => void) => () => void;
}
export type Dispatch<T> = (action: T) => void;
export type Middleware<State = any> = (
store: Pick<ReduxLikeStateContainer<State, any>, 'getState' | 'dispatch'>
) => (
next: (action: TransitionDescription) => TransitionDescription | any
) => Dispatch<TransitionDescription>;
export type Reducer<State> = (state: State, action: TransitionDescription) => State;
export type UnboxState<
Container extends StateContainer<any, any>
> = Container extends StateContainer<infer T, any> ? T : never;
export type UnboxTransitions<
Container extends StateContainer<any, any>
> = Container extends StateContainer<any, infer T> ? T : never;
export type Selector<Result, Args extends any[] = []> = (...args: Args) => Result;
export type PureSelector<State, Result, Args extends any[] = []> = (
state: State
) => Selector<Result, Args>;
export type EnsurePureSelector<T> = Ensure<T, PureSelector<any, any, any>>;
export type PureSelectorToSelector<T extends PureSelector<any, any, any>> = ReturnType<
EnsurePureSelector<T>
>;
export type PureSelectorsToSelectors<T extends object> = {
[K in keyof T]: PureSelectorToSelector<EnsurePureSelector<T[K]>>;
};
export type Comparator<Result> = (previous: Result, current: Result) => boolean;
export type MapStateToProps<State, StateProps extends object> = (state: State) => StateProps;
export type Connect<State> = <Props extends object, StatePropKeys extends keyof Props>(
mapStateToProp: MapStateToProps<State, Pick<Props, StatePropKeys>>
) => (component: React.ComponentType<Props>) => React.FC<Omit<Props, StatePropKeys>>;

View file

@ -1,177 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createStore } from './create_store';
test('can create store', () => {
const store = createStore({});
expect(store).toMatchObject({
get: expect.any(Function),
set: expect.any(Function),
state$: expect.any(Object),
createMutators: expect.any(Function),
mutators: expect.any(Object),
redux: {
getState: expect.any(Function),
dispatch: expect.any(Function),
subscribe: expect.any(Function),
},
});
});
test('can set default state', () => {
const defaultState = {
foo: 'bar',
};
const store = createStore(defaultState);
expect(store.get()).toEqual(defaultState);
expect(store.redux.getState()).toEqual(defaultState);
});
test('can set state', () => {
const defaultState = {
foo: 'bar',
};
const newState = {
foo: 'baz',
};
const store = createStore(defaultState);
store.set(newState);
expect(store.get()).toEqual(newState);
expect(store.redux.getState()).toEqual(newState);
});
test('does not shallow merge states', () => {
const defaultState = {
foo: 'bar',
};
const newState = {
foo2: 'baz',
};
const store = createStore<any>(defaultState);
store.set(newState);
expect(store.get()).toEqual(newState);
expect(store.redux.getState()).toEqual(newState);
});
test('can subscribe and unsubscribe to state changes', () => {
const store = createStore<any>({});
const spy = jest.fn();
const subscription = store.state$.subscribe(spy);
store.set({ a: 1 });
store.set({ a: 2 });
subscription.unsubscribe();
store.set({ a: 3 });
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0]).toEqual({ a: 1 });
expect(spy.mock.calls[1][0]).toEqual({ a: 2 });
});
test('multiple subscribers can subscribe', () => {
const store = createStore<any>({});
const spy1 = jest.fn();
const spy2 = jest.fn();
const subscription1 = store.state$.subscribe(spy1);
const subscription2 = store.state$.subscribe(spy2);
store.set({ a: 1 });
subscription1.unsubscribe();
store.set({ a: 2 });
subscription2.unsubscribe();
store.set({ a: 3 });
expect(spy1).toHaveBeenCalledTimes(1);
expect(spy2).toHaveBeenCalledTimes(2);
expect(spy1.mock.calls[0][0]).toEqual({ a: 1 });
expect(spy2.mock.calls[0][0]).toEqual({ a: 1 });
expect(spy2.mock.calls[1][0]).toEqual({ a: 2 });
});
test('creates impure mutators from pure mutators', () => {
const store = createStore<any>({});
const mutators = store.createMutators({
setFoo: _ => bar => ({ foo: bar }),
});
expect(typeof mutators.setFoo).toBe('function');
});
test('mutators can update state', () => {
const store = createStore<any>({
value: 0,
foo: 'bar',
});
const mutators = store.createMutators({
add: state => increment => ({ ...state, value: state.value + increment }),
setFoo: state => bar => ({ ...state, foo: bar }),
});
expect(store.get()).toEqual({
value: 0,
foo: 'bar',
});
mutators.add(11);
mutators.setFoo('baz');
expect(store.get()).toEqual({
value: 11,
foo: 'baz',
});
mutators.add(-20);
mutators.setFoo('bazooka');
expect(store.get()).toEqual({
value: -9,
foo: 'bazooka',
});
});
test('mutators methods are not bound', () => {
const store = createStore<any>({ value: -3 });
const { add } = store.createMutators({
add: state => increment => ({ ...state, value: state.value + increment }),
});
expect(store.get()).toEqual({ value: -3 });
add(4);
expect(store.get()).toEqual({ value: 1 });
});
test('created mutators are saved in store object', () => {
const store = createStore<
any,
{
add: (increment: number) => void;
}
>({ value: -3 });
store.createMutators({
add: state => increment => ({ ...state, value: state.value + increment }),
});
expect(typeof store.mutators.add).toBe('function');
store.mutators.add(5);
expect(store.get()).toEqual({ value: 2 });
});

View file

@ -1,85 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createStore as createReduxStore, Reducer } from 'redux';
import { Subject, Observable } from 'rxjs';
import { AppStore, Mutators, PureMutators } from './types';
const SET = '__SET__';
export const createStore = <
State extends {},
StateMutators extends Mutators<PureMutators<State>> = {}
>(
defaultState: State
): AppStore<State, StateMutators> => {
const pureMutators: PureMutators<State> = {};
const mutators: StateMutators = {} as StateMutators;
const reducer: Reducer = (state, action) => {
const pureMutator = pureMutators[action.type];
if (pureMutator) {
return pureMutator(state)(...action.args);
}
switch (action.type) {
case SET:
return action.state;
default:
return state;
}
};
const redux = createReduxStore<State, any, any, any>(reducer, defaultState as any);
const get = redux.getState;
const set = (state: State) =>
redux.dispatch({
type: SET,
state,
});
const state$ = new Subject();
redux.subscribe(() => {
state$.next(get());
});
const createMutators: AppStore<State>['createMutators'] = newPureMutators => {
const result: Mutators<any> = {};
for (const type of Object.keys(newPureMutators)) {
result[type] = (...args) => {
redux.dispatch({
type,
args,
});
};
}
Object.assign(pureMutators, newPureMutators);
Object.assign(mutators, result);
return result;
};
return {
get,
set,
redux,
state$: (state$ as unknown) as Observable<State>,
createMutators,
mutators,
};
};

View file

@ -1,47 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Observable, BehaviorSubject } from 'rxjs';
export type Selector<State, Result> = (state: State) => Result;
export type Comparator<Result> = (previous: Result, current: Result) => boolean;
export type Unsubscribe = () => void;
const defaultComparator: Comparator<any> = (previous, current) => previous === current;
export const observableSelector = <State extends {}, Result>(
state: State,
state$: Observable<State>,
selector: Selector<State, Result>,
comparator: Comparator<Result> = defaultComparator
): [Observable<Result>, Unsubscribe] => {
let previousResult: Result = selector(state);
const result$ = new BehaviorSubject<Result>(previousResult);
const subscription = state$.subscribe(value => {
const result = selector(value);
const isEqual: boolean = comparator(previousResult, result);
if (!isEqual) {
result$.next(result);
}
previousResult = result;
});
return [(result$ as unknown) as Observable<Result>, subscription.unsubscribe];
};

View file

@ -1,126 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import { Provider as ReactReduxProvider, connect as reactReduxConnect } from 'react-redux';
import { Store } from 'redux';
import { AppStore, Mutators, PureMutators } from './types';
import { observableSelector, Selector, Comparator } from './observable_selector';
// TODO: Below import is temporary, use `react-use` lib instead.
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { useObservable } from '../../../kibana_react/public/util/use_observable';
const { useMemo, useLayoutEffect, useContext, createElement, Fragment } = React;
/**
* @note
* Types in `react-redux` seem to be quite off compared to reality
* that's why a lot of `any`s below.
*/
export interface ConsumerProps<State> {
children: (state: State) => React.ReactChild;
}
export type MapStateToProps<State extends {}, StateProps extends {}> = (state: State) => StateProps;
// TODO: `Omit` is generally part of TypeScript, but it currently does not exist in our build.
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
export type Connect<State extends {}> = <Props extends {}, StatePropKeys extends keyof Props>(
mapStateToProp: MapStateToProps<State, Pick<Props, StatePropKeys>>
) => (component: React.ComponentType<Props>) => React.FC<Omit<Props, StatePropKeys>>;
interface ReduxContextValue {
store: Store;
}
const mapDispatchToProps = () => ({});
const mergeProps: any = (stateProps: any, dispatchProps: any, ownProps: any) => ({
...ownProps,
...stateProps,
...dispatchProps,
});
export const createContext = <
State extends {},
StateMutators extends Mutators<PureMutators<State>> = {}
>(
store: AppStore<State, StateMutators>
) => {
const { redux } = store;
(redux as any).__appStore = store;
const context = React.createContext<ReduxContextValue>({ store: redux });
const useStore = (): AppStore<State, StateMutators> => {
// eslint-disable-next-line no-shadow
const { store } = useContext(context);
return (store as any).__appStore;
};
const useState = (): State => {
const { state$, get } = useStore();
const state = useObservable(state$, get());
return state;
};
const useMutators = (): StateMutators => useStore().mutators;
const useSelector = <Result>(
selector: Selector<State, Result>,
comparator?: Comparator<Result>
): Result => {
const { state$, get } = useStore();
/* eslint-disable react-hooks/exhaustive-deps */
const [observable$, unsubscribe] = useMemo(
() => observableSelector(get(), state$, selector, comparator),
[state$]
);
/* eslint-enable react-hooks/exhaustive-deps */
useLayoutEffect(() => unsubscribe, [observable$, unsubscribe]);
const value = useObservable(observable$, selector(get()));
return value;
};
const Provider: React.FC<{}> = ({ children }) =>
createElement(ReactReduxProvider, {
store: redux,
context,
children,
} as any);
const Consumer: React.FC<ConsumerProps<State>> = ({ children }) => {
const state = useState();
return createElement(Fragment, { children: children(state) });
};
const options: any = { context };
const connect: Connect<State> = mapStateToProps =>
reactReduxConnect(mapStateToProps, mapDispatchToProps, mergeProps, options) as any;
return {
Provider,
Consumer,
connect,
context,
useStore,
useState,
useMutators,
useSelector,
};
};

164
yarn.lock
View file

@ -6776,6 +6776,11 @@ bounce@1.x.x:
boom "7.x.x"
hoek "5.x.x"
bowser@^1.7.3:
version "1.9.4"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a"
integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==
boxen@^1.2.1, boxen@^1.2.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
@ -8686,6 +8691,13 @@ copy-to-clipboard@^3.0.8:
dependencies:
toggle-selection "^1.0.3"
copy-to-clipboard@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467"
integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==
dependencies:
toggle-selection "^1.0.6"
copy-webpack-plugin@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.0.4.tgz#c78126f604e24f194c6ec2f43a64e232b5d43655"
@ -9053,6 +9065,14 @@ css-color-keywords@^1.0.0:
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=
css-in-js-utils@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99"
integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==
dependencies:
hyphenate-style-name "^1.0.2"
isobject "^3.0.1"
css-loader@2.1.1, css-loader@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea"
@ -9138,6 +9158,14 @@ css-tree@1.0.0-alpha.29:
mdn-data "~1.1.0"
source-map "^0.5.3"
css-tree@^1.0.0-alpha.28:
version "1.0.0-alpha.39"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
dependencies:
mdn-data "2.0.6"
source-map "^0.6.1"
css-url-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec"
@ -9209,7 +9237,7 @@ cssstyle@^2.0.0:
dependencies:
cssom "~0.3.6"
csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7:
csstype@^2.2.0, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5"
integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==
@ -10955,6 +10983,13 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-stack-parser@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.4.tgz#a757397dc5d9de973ac9a5d7d4e8ade7cfae9101"
integrity sha512-fZ0KkoxSjLFmhW5lHbUT3tLwy3nX1qEzMYo8koY1vrsAco53CMT1djnBSeC/wUjTEZRhZl9iRw7PaMaxfJ4wzQ==
dependencies:
stackframe "^1.1.0"
error@^7.0.0, error@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02"
@ -12131,6 +12166,11 @@ fast-deep-equal@^2.0.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
fast-deep-equal@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
fast-diff@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
@ -12197,6 +12237,11 @@ fast-stream-to-buffer@^1.0.0:
dependencies:
end-of-stream "^1.4.1"
fastest-stable-stringify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028"
integrity sha1-kSLUBtTJ2YvqZEpraFPVh0uHsCg=
fastparse@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
@ -15110,6 +15155,11 @@ hyperlinker@^1.0.0:
resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e"
integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==
hyphenate-style-name@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
i18n-iso-countries@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-4.3.1.tgz#f110a8824ce14edbb0eb8f3b0bd817ff950af37c"
@ -15389,6 +15439,14 @@ ini@^1.2.0, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
inline-style-prefixer@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911"
integrity sha512-N8nVhwfYga9MiV9jWlwfdj1UDIaZlBFu4cJSJkIr7tZX7sHpHhGR5su1qdpW+7KPL8ISTvCIkcaFi/JdBknvPg==
dependencies:
bowser "^1.7.3"
css-in-js-utils "^2.0.0"
inline-style@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/inline-style/-/inline-style-2.0.0.tgz#2fa9cf624596a8109355b925094e138bbd5ea29b"
@ -18922,6 +18980,11 @@ mdast-add-list-metadata@1.0.1:
dependencies:
unist-util-visit-parents "1.1.2"
mdn-data@2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
mdn-data@~1.1.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01"
@ -19792,6 +19855,20 @@ nan@^2.10.0, nan@^2.9.2:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==
nano-css@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.2.1.tgz#73b8470fa40b028a134d3393ae36bbb34b9fa332"
integrity sha512-T54okxMAha0+de+W8o3qFtuWhTxYvqQh2ku1cYEqTTP9mR62nWV2lLK9qRuAGWmoaYWhU7K4evT9Lc1iF65wuw==
dependencies:
css-tree "^1.0.0-alpha.28"
csstype "^2.5.5"
fastest-stable-stringify "^1.0.1"
inline-style-prefixer "^4.0.0"
rtl-css-js "^1.9.0"
sourcemap-codec "^1.4.1"
stacktrace-js "^2.0.0"
stylis "3.5.0"
nano-time@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
@ -23507,6 +23584,21 @@ react-transition-group@^2.2.1:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-use@^13.10.2:
version "13.10.2"
resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.10.2.tgz#4250d258ca9068662943299c01794a136408c8e9"
integrity sha512-z3VFSiPHW6arViGVnajO7YKY5OD+Z9LWcImoJdYHkau23cLSoTctxM3XENLpGxjhJlHaYiQZ6pPgq7pwGTqSZA==
dependencies:
copy-to-clipboard "^3.2.0"
nano-css "^5.2.1"
react-fast-compare "^2.0.4"
resize-observer-polyfill "^1.5.1"
screenfull "^5.0.0"
set-harmonic-interval "^1.0.1"
throttle-debounce "^2.1.0"
ts-easing "^0.2.0"
tslib "^1.10.0"
react-virtualized@^9.18.5:
version "9.20.1"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.20.1.tgz#02dc08fe9070386b8c48e2ac56bce7af0208d22d"
@ -24869,6 +24961,13 @@ rsvp@^4.8.4:
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
rtl-css-js@^1.9.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.13.1.tgz#80deabf6e8f36d6767d495cd3eb60fecb20c67e1"
integrity sha512-jgkIDj6Xi25kAEm5oYM3ZMFiOQhpLEcXi2LY/6bVr91cVz73hciHKneL5AMVPxOcks/JuizSaaNsvNRkeAWe3w==
dependencies:
"@babel/runtime" "^7.1.2"
run-async@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
@ -25156,6 +25255,11 @@ scoped-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
integrity sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=
screenfull@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.0.tgz#5c2010c0e84fd4157bf852877698f90b8cbe96f6"
integrity sha512-yShzhaIoE9OtOhWVyBBffA6V98CDCoyHTsp8228blmqYy1Z5bddzE/4FPiJKlr8DVR4VBiiUyfPzIQPIYDkeMA==
script-loader@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/script-loader/-/script-loader-0.7.2.tgz#2016db6f86f25f5cf56da38915d83378bb166ba7"
@ -25393,6 +25497,11 @@ set-getter@^0.1.0:
dependencies:
to-object-path "^0.3.0"
set-harmonic-interval@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249"
integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==
set-immediate-shim@^1.0.0, set-immediate-shim@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
@ -25869,6 +25978,11 @@ source-map@0.1.32:
dependencies:
amdefine ">=0.0.4"
source-map@0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
"source-map@>= 0.1.2":
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
@ -25905,6 +26019,11 @@ source-map@~0.2.0:
dependencies:
amdefine ">=0.0.4"
sourcemap-codec@^1.4.1:
version "1.4.6"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9"
integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==
space-separated-tokens@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412"
@ -26106,6 +26225,13 @@ stable@^0.1.8:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
stack-generator@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.4.tgz#027513eab2b195bbb43b9c8360ba2dd0ab54de09"
integrity sha512-ha1gosTNcgxwzo9uKTQ8zZ49aUp5FIUW58YHFxCqaAHtE0XqBg0chGFYA1MfmW//x1KWq3F4G7Ug7bJh4RiRtg==
dependencies:
stackframe "^1.1.0"
stack-trace@0.0.10, stack-trace@0.0.x:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
@ -26116,6 +26242,11 @@ stack-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
integrity sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=
stackframe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.0.tgz#e3fc2eb912259479c9822f7d1f1ff365bd5cbc83"
integrity sha512-Vx6W1Yvy+AM1R/ckVwcHQHV147pTPBKWCRLrXMuPrFVfvBUc3os7PR1QLIWCMhPpRg5eX9ojzbQIMLGBwyLjqg==
stackman@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/stackman/-/stackman-4.0.0.tgz#3ccdc8682fee36373ed2492dc3dad546eb44647d"
@ -26127,6 +26258,23 @@ stackman@^4.0.0:
error-callsites "^2.0.2"
load-source-map "^1.0.0"
stacktrace-gps@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.3.tgz#b89f84cc13bb925b96607e737b617c8715facf57"
integrity sha512-51Rr7dXkyFUKNmhY/vqZWK+EvdsfFSRiQVtgHTFlAdNIYaDD7bVh21yBHXaNWAvTD+w+QSjxHg7/v6Tz4veExA==
dependencies:
source-map "0.5.6"
stackframe "^1.1.0"
stacktrace-js@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.1.tgz#ebdb0e9a16e6f171f96ca7878404e7f15c3d42ba"
integrity sha512-13oDNgBSeWtdGa4/2BycNyKqe+VktCoJ8VLx4pDoJkwGGJVtiHdfMOAj3aW9xTi8oR2v34z9IcvfCvT6XNdNAw==
dependencies:
error-stack-parser "^2.0.4"
stack-generator "^2.0.4"
stacktrace-gps "^3.0.3"
state-toggle@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425"
@ -26649,6 +26797,11 @@ stylis-rule-sheet@^0.0.10:
resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430"
integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==
stylis@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1"
integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw==
stylus-lookup@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-3.0.2.tgz#c9eca3ff799691020f30b382260a67355fefdddd"
@ -27475,7 +27628,7 @@ to-through@^2.0.0:
dependencies:
through2 "^2.0.3"
toggle-selection@^1.0.3:
toggle-selection@^1.0.3, toggle-selection@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
@ -27621,6 +27774,11 @@ ts-dedent@^1.1.0:
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-1.1.0.tgz#67983940793183dc7c7f820acb66ba02cdc33c6e"
integrity sha512-CVCvDwMBWZKjDxpN3mU/Dx1v3k+sJgE8nrhXcC9vRopRfoa7vVzilNvHEAUi5jQnmFHpnxDx5jZdI1TpG8ny2g==
ts-easing@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
ts-invariant@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.2.1.tgz#3d587f9d6e3bded97bf9ec17951dd9814d5a9d3f"
@ -27676,7 +27834,7 @@ tsd@^0.7.4:
typescript "^3.0.1"
update-notifier "^2.5.0"
tslib@^1:
tslib@^1, tslib@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==