Skip to content

Commit

Permalink
[State Management] State containers improvements (elastic#54436)
Browse files Browse the repository at this point in the history
Some maintenance and minor fixes to state containers based on experience while working with them in elastic#53582

Patch unit tests to use current "terminology" (e.g. "transition" vs "mutation")
Fix docs where "store" was used instead of "state container"
Allow to create state container without transition.
Fix freeze function to deeply freeze objects.
Restrict State to BaseState with extends object.
in set() function, make sure the flow goes through dispatch to make sure middleware see this update
Improve type inference for useTransition()
Improve type inference for createStateContainer().

Other issues noticed, but didn't fix in reasonable time:
Can't use addMiddleware without explicit type casting elastic#54438
Transitions and Selectors allow any state, not bind to container's state elastic#54439
  • Loading branch information
Dosant authored and jkelastic committed Jan 17, 2020
1 parent 22f3e48 commit 64b3f50
Show file tree
Hide file tree
Showing 20 changed files with 304 additions and 230 deletions.
17 changes: 7 additions & 10 deletions examples/state_containers_examples/public/todo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
PureTransition,
syncStates,
getStateFromKbnUrl,
BaseState,
} from '../../../src/plugins/kibana_utils/public';
import { useUrlTracker } from '../../../src/plugins/kibana_react/public';
import {
Expand Down Expand Up @@ -79,7 +80,7 @@ const TodoApp: React.FC<TodoAppProps> = ({ filter }) => {
const { setText } = GlobalStateHelpers.useTransitions();
const { text } = GlobalStateHelpers.useState();
const { edit: editTodo, delete: deleteTodo, add: addTodo } = useTransitions();
const todos = useState();
const todos = useState().todos;
const filteredTodos = todos.filter(todo => {
if (!filter) return true;
if (filter === 'completed') return todo.completed;
Expand Down Expand Up @@ -306,22 +307,18 @@ export const TodoAppPage: React.FC<{
);
};

function withDefaultState<State>(
function withDefaultState<State extends BaseState>(
stateContainer: BaseStateContainer<State>,
// eslint-disable-next-line no-shadow
defaultState: State
): INullableBaseStateContainer<State> {
return {
...stateContainer,
set: (state: State | null) => {
if (Array.isArray(defaultState)) {
stateContainer.set(state || defaultState);
} else {
stateContainer.set({
...defaultState,
...state,
});
}
stateContainer.set({
...defaultState,
...state,
});
},
};
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"custom-event-polyfill": "^0.3.0",
"d3": "3.5.17",
"d3-cloud": "1.2.5",
"deep-freeze-strict": "^1.1.1",
"deepmerge": "^4.2.2",
"del": "^5.1.0",
"elastic-apm-node": "^3.2.0",
Expand Down Expand Up @@ -314,6 +315,7 @@
"@types/classnames": "^2.2.9",
"@types/d3": "^3.5.43",
"@types/dedent": "^0.7.0",
"@types/deep-freeze-strict": "^1.1.0",
"@types/delete-empty": "^2.0.0",
"@types/elasticsearch": "^5.0.33",
"@types/enzyme": "^3.9.0",
Expand Down
8 changes: 8 additions & 0 deletions renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@
'@types/dedent',
],
},
{
groupSlug: 'deep-freeze-strict',
groupName: 'deep-freeze-strict related packages',
packageNames: [
'deep-freeze-strict',
'@types/deep-freeze-strict',
],
},
{
groupSlug: 'delete-empty',
groupName: 'delete-empty related packages',
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/kibana_utils/demos/demos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('demos', () => {
describe('state sync', () => {
test('url sync demo works', async () => {
expect(await urlSyncResult).toMatchInlineSnapshot(
`"http://localhost/#?_s=!((completed:!f,id:0,text:'Learning%20state%20containers'),(completed:!f,id:2,text:test))"`
`"http://localhost/#?_s=(todos:!((completed:!f,id:0,text:'Learning%20state%20containers'),(completed:!f,id:2,text:test)))"`
);
});
});
Expand Down
22 changes: 16 additions & 6 deletions src/plugins/kibana_utils/demos/state_containers/counter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,24 @@

import { createStateContainer } from '../../public/state_containers';

const container = createStateContainer(0, {
increment: (cnt: number) => (by: number) => cnt + by,
double: (cnt: number) => () => cnt * 2,
});
interface State {
count: number;
}

const container = createStateContainer(
{ count: 0 },
{
increment: (state: State) => (by: number) => ({ count: state.count + by }),
double: (state: State) => () => ({ count: state.count * 2 }),
},
{
count: (state: State) => () => state.count,
}
);

container.transitions.increment(5);
container.transitions.double();

console.log(container.get()); // eslint-disable-line
console.log(container.selectors.count()); // eslint-disable-line

export const result = container.get();
export const result = container.selectors.count();
57 changes: 39 additions & 18 deletions src/plugins/kibana_utils/demos/state_containers/todomvc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,19 @@ export interface TodoItem {
id: number;
}

export type TodoState = TodoItem[];
export interface TodoState {
todos: TodoItem[];
}

export const defaultState: TodoState = [
{
id: 0,
text: 'Learning state containers',
completed: false,
},
];
export const defaultState: TodoState = {
todos: [
{
id: 0,
text: 'Learning state containers',
completed: false,
},
],
};

export interface TodoActions {
add: PureTransition<TodoState, [TodoItem]>;
Expand All @@ -44,17 +48,34 @@ export interface TodoActions {
clearCompleted: PureTransition<TodoState, []>;
}

export interface TodosSelectors {
todos: (state: TodoState) => () => TodoItem[];
todo: (state: TodoState) => (id: number) => TodoItem | null;
}

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),
add: state => todo => ({ todos: [...state.todos, todo] }),
edit: state => todo => ({
todos: state.todos.map(item => (item.id === todo.id ? { ...item, ...todo } : item)),
}),
delete: state => id => ({ todos: state.todos.filter(item => item.id !== id) }),
complete: state => id => ({
todos: state.todos.map(item => (item.id === id ? { ...item, completed: true } : item)),
}),
completeAll: state => () => ({ todos: state.todos.map(item => ({ ...item, completed: true })) }),
clearCompleted: state => () => ({ todos: state.todos.filter(({ completed }) => !completed) }),
};

export const pureSelectors: TodosSelectors = {
todos: state => () => state.todos,
todo: state => id => state.todos.find(todo => todo.id === id) ?? null,
};

const container = createStateContainer<TodoState, TodoActions>(defaultState, pureTransitions);
const container = createStateContainer<TodoState, TodoActions, TodosSelectors>(
defaultState,
pureTransitions,
pureSelectors
);

container.transitions.add({
id: 1,
Expand All @@ -64,6 +85,6 @@ container.transitions.add({
container.transitions.complete(0);
container.transitions.complete(1);

console.log(container.get()); // eslint-disable-line
console.log(container.selectors.todos()); // eslint-disable-line

export const result = container.get();
export const result = container.selectors.todos();
4 changes: 2 additions & 2 deletions src/plugins/kibana_utils/demos/state_sync/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc';
import { BaseStateContainer, createStateContainer } from '../../public/state_containers';
import { BaseState, BaseStateContainer, createStateContainer } from '../../public/state_containers';
import {
createKbnUrlStateStorage,
syncState,
Expand Down Expand Up @@ -55,7 +55,7 @@ export const result = Promise.resolve()
return window.location.href;
});

function withDefaultState<State>(
function withDefaultState<State extends BaseState>(
// eslint-disable-next-line no-shadow
stateContainer: BaseStateContainer<State>,
// eslint-disable-next-line no-shadow
Expand Down
17 changes: 12 additions & 5 deletions src/plugins/kibana_utils/docs/state_containers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@ your services or apps.
```ts
import { createStateContainer } from 'src/plugins/kibana_utils';

const container = createStateContainer(0, {
increment: (cnt: number) => (by: number) => cnt + by,
double: (cnt: number) => () => cnt * 2,
});
const container = createStateContainer(
{ count: 0 },
{
increment: (state: {count: number}) => (by: number) => ({ count: state.count + by }),
double: (state: {count: number}) => () => ({ count: state.count * 2 }),
},
{
count: (state: {count: number}) => () => state.count,
}
);

container.transitions.increment(5);
container.transitions.double();
console.log(container.get()); // 10

console.log(container.selectors.count()); // 10
```


Expand Down
2 changes: 1 addition & 1 deletion src/plugins/kibana_utils/docs/state_containers/creation.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Create your a state container.
```ts
import { createStateContainer } from 'src/plugins/kibana_utils';

const container = createStateContainer<MyState>(defaultState, {});
const container = createStateContainer<MyState>(defaultState);

console.log(container.get());
```
6 changes: 3 additions & 3 deletions src/plugins/kibana_utils/docs/state_containers/no_react.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Consuming state in non-React setting

To read the current `state` of the store use `.get()` method.
To read the current `state` of the store use `.get()` method or `getState()` alias method.

```ts
store.get();
stateContainer.get();
```

To listen for latest state changes use `.state$` observable.

```ts
store.state$.subscribe(state => { ... });
stateContainer.state$.subscribe(state => { ... });
```
2 changes: 1 addition & 1 deletion src/plugins/kibana_utils/docs/state_containers/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
```ts
import { createStateContainer, createStateContainerReactHelpers } from 'src/plugins/kibana_utils';

const container = createStateContainer({}, {});
const container = createStateContainer({});
export const {
Provider,
Consumer,
Expand Down
Loading

0 comments on commit 64b3f50

Please sign in to comment.