Skip to content

Commit f3cbade

Browse files
committed
[State Management] State containers improvements (elastic#54436)
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
1 parent a8e578f commit f3cbade

File tree

20 files changed

+304
-230
lines changed

20 files changed

+304
-230
lines changed

examples/state_containers_examples/public/todo.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
PureTransition,
4242
syncStates,
4343
getStateFromKbnUrl,
44+
BaseState,
4445
} from '../../../src/plugins/kibana_utils/public';
4546
import { useUrlTracker } from '../../../src/plugins/kibana_react/public';
4647
import {
@@ -79,7 +80,7 @@ const TodoApp: React.FC<TodoAppProps> = ({ filter }) => {
7980
const { setText } = GlobalStateHelpers.useTransitions();
8081
const { text } = GlobalStateHelpers.useState();
8182
const { edit: editTodo, delete: deleteTodo, add: addTodo } = useTransitions();
82-
const todos = useState();
83+
const todos = useState().todos;
8384
const filteredTodos = todos.filter(todo => {
8485
if (!filter) return true;
8586
if (filter === 'completed') return todo.completed;
@@ -306,22 +307,18 @@ export const TodoAppPage: React.FC<{
306307
);
307308
};
308309

309-
function withDefaultState<State>(
310+
function withDefaultState<State extends BaseState>(
310311
stateContainer: BaseStateContainer<State>,
311312
// eslint-disable-next-line no-shadow
312313
defaultState: State
313314
): INullableBaseStateContainer<State> {
314315
return {
315316
...stateContainer,
316317
set: (state: State | null) => {
317-
if (Array.isArray(defaultState)) {
318-
stateContainer.set(state || defaultState);
319-
} else {
320-
stateContainer.set({
321-
...defaultState,
322-
...state,
323-
});
324-
}
318+
stateContainer.set({
319+
...defaultState,
320+
...state,
321+
});
325322
},
326323
};
327324
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
"custom-event-polyfill": "^0.3.0",
163163
"d3": "3.5.17",
164164
"d3-cloud": "1.2.5",
165+
"deep-freeze-strict": "^1.1.1",
165166
"deepmerge": "^4.2.2",
166167
"del": "^5.1.0",
167168
"elastic-apm-node": "^3.2.0",
@@ -308,6 +309,7 @@
308309
"@types/classnames": "^2.2.9",
309310
"@types/d3": "^3.5.43",
310311
"@types/dedent": "^0.7.0",
312+
"@types/deep-freeze-strict": "^1.1.0",
311313
"@types/delete-empty": "^2.0.0",
312314
"@types/elasticsearch": "^5.0.33",
313315
"@types/enzyme": "^3.9.0",

renovate.json5

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,14 @@
340340
'@types/dedent',
341341
],
342342
},
343+
{
344+
groupSlug: 'deep-freeze-strict',
345+
groupName: 'deep-freeze-strict related packages',
346+
packageNames: [
347+
'deep-freeze-strict',
348+
'@types/deep-freeze-strict',
349+
],
350+
},
343351
{
344352
groupSlug: 'delete-empty',
345353
groupName: 'delete-empty related packages',

src/plugins/kibana_utils/demos/demos.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('demos', () => {
3838
describe('state sync', () => {
3939
test('url sync demo works', async () => {
4040
expect(await urlSyncResult).toMatchInlineSnapshot(
41-
`"http://localhost/#?_s=!((completed:!f,id:0,text:'Learning%20state%20containers'),(completed:!f,id:2,text:test))"`
41+
`"http://localhost/#?_s=(todos:!((completed:!f,id:0,text:'Learning%20state%20containers'),(completed:!f,id:2,text:test)))"`
4242
);
4343
});
4444
});

src/plugins/kibana_utils/demos/state_containers/counter.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,24 @@
1919

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

22-
const container = createStateContainer(0, {
23-
increment: (cnt: number) => (by: number) => cnt + by,
24-
double: (cnt: number) => () => cnt * 2,
25-
});
22+
interface State {
23+
count: number;
24+
}
25+
26+
const container = createStateContainer(
27+
{ count: 0 },
28+
{
29+
increment: (state: State) => (by: number) => ({ count: state.count + by }),
30+
double: (state: State) => () => ({ count: state.count * 2 }),
31+
},
32+
{
33+
count: (state: State) => () => state.count,
34+
}
35+
);
2636

2737
container.transitions.increment(5);
2838
container.transitions.double();
2939

30-
console.log(container.get()); // eslint-disable-line
40+
console.log(container.selectors.count()); // eslint-disable-line
3141

32-
export const result = container.get();
42+
export const result = container.selectors.count();

src/plugins/kibana_utils/demos/state_containers/todomvc.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,19 @@ export interface TodoItem {
2525
id: number;
2626
}
2727

28-
export type TodoState = TodoItem[];
28+
export interface TodoState {
29+
todos: TodoItem[];
30+
}
2931

30-
export const defaultState: TodoState = [
31-
{
32-
id: 0,
33-
text: 'Learning state containers',
34-
completed: false,
35-
},
36-
];
32+
export const defaultState: TodoState = {
33+
todos: [
34+
{
35+
id: 0,
36+
text: 'Learning state containers',
37+
completed: false,
38+
},
39+
],
40+
};
3741

3842
export interface TodoActions {
3943
add: PureTransition<TodoState, [TodoItem]>;
@@ -44,17 +48,34 @@ export interface TodoActions {
4448
clearCompleted: PureTransition<TodoState, []>;
4549
}
4650

51+
export interface TodosSelectors {
52+
todos: (state: TodoState) => () => TodoItem[];
53+
todo: (state: TodoState) => (id: number) => TodoItem | null;
54+
}
55+
4756
export const pureTransitions: TodoActions = {
48-
add: state => todo => [...state, todo],
49-
edit: state => todo => state.map(item => (item.id === todo.id ? { ...item, ...todo } : item)),
50-
delete: state => id => state.filter(item => item.id !== id),
51-
complete: state => id =>
52-
state.map(item => (item.id === id ? { ...item, completed: true } : item)),
53-
completeAll: state => () => state.map(item => ({ ...item, completed: true })),
54-
clearCompleted: state => () => state.filter(({ completed }) => !completed),
57+
add: state => todo => ({ todos: [...state.todos, todo] }),
58+
edit: state => todo => ({
59+
todos: state.todos.map(item => (item.id === todo.id ? { ...item, ...todo } : item)),
60+
}),
61+
delete: state => id => ({ todos: state.todos.filter(item => item.id !== id) }),
62+
complete: state => id => ({
63+
todos: state.todos.map(item => (item.id === id ? { ...item, completed: true } : item)),
64+
}),
65+
completeAll: state => () => ({ todos: state.todos.map(item => ({ ...item, completed: true })) }),
66+
clearCompleted: state => () => ({ todos: state.todos.filter(({ completed }) => !completed) }),
67+
};
68+
69+
export const pureSelectors: TodosSelectors = {
70+
todos: state => () => state.todos,
71+
todo: state => id => state.todos.find(todo => todo.id === id) ?? null,
5572
};
5673

57-
const container = createStateContainer<TodoState, TodoActions>(defaultState, pureTransitions);
74+
const container = createStateContainer<TodoState, TodoActions, TodosSelectors>(
75+
defaultState,
76+
pureTransitions,
77+
pureSelectors
78+
);
5879

5980
container.transitions.add({
6081
id: 1,
@@ -64,6 +85,6 @@ container.transitions.add({
6485
container.transitions.complete(0);
6586
container.transitions.complete(1);
6687

67-
console.log(container.get()); // eslint-disable-line
88+
console.log(container.selectors.todos()); // eslint-disable-line
6889

69-
export const result = container.get();
90+
export const result = container.selectors.todos();

src/plugins/kibana_utils/demos/state_sync/url.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919

2020
import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc';
21-
import { BaseStateContainer, createStateContainer } from '../../public/state_containers';
21+
import { BaseState, BaseStateContainer, createStateContainer } from '../../public/state_containers';
2222
import {
2323
createKbnUrlStateStorage,
2424
syncState,
@@ -55,7 +55,7 @@ export const result = Promise.resolve()
5555
return window.location.href;
5656
});
5757

58-
function withDefaultState<State>(
58+
function withDefaultState<State extends BaseState>(
5959
// eslint-disable-next-line no-shadow
6060
stateContainer: BaseStateContainer<State>,
6161
// eslint-disable-next-line no-shadow

src/plugins/kibana_utils/docs/state_containers/README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,21 @@ your services or apps.
1818
```ts
1919
import { createStateContainer } from 'src/plugins/kibana_utils';
2020

21-
const container = createStateContainer(0, {
22-
increment: (cnt: number) => (by: number) => cnt + by,
23-
double: (cnt: number) => () => cnt * 2,
24-
});
21+
const container = createStateContainer(
22+
{ count: 0 },
23+
{
24+
increment: (state: {count: number}) => (by: number) => ({ count: state.count + by }),
25+
double: (state: {count: number}) => () => ({ count: state.count * 2 }),
26+
},
27+
{
28+
count: (state: {count: number}) => () => state.count,
29+
}
30+
);
2531

2632
container.transitions.increment(5);
2733
container.transitions.double();
28-
console.log(container.get()); // 10
34+
35+
console.log(container.selectors.count()); // 10
2936
```
3037

3138

src/plugins/kibana_utils/docs/state_containers/creation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Create your a state container.
3232
```ts
3333
import { createStateContainer } from 'src/plugins/kibana_utils';
3434

35-
const container = createStateContainer<MyState>(defaultState, {});
35+
const container = createStateContainer<MyState>(defaultState);
3636

3737
console.log(container.get());
3838
```
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Consuming state in non-React setting
22

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

55
```ts
6-
store.get();
6+
stateContainer.get();
77
```
88

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

1111
```ts
12-
store.state$.subscribe(state => { ... });
12+
stateContainer.state$.subscribe(state => { ... });
1313
```

0 commit comments

Comments
 (0)