You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -6,27 +6,12 @@ The mechanism of communicating from the application to the user interface is cal
6
6
7
7

8
8
9
-
## The values
9
+
## Core values
10
10
11
11
In JavaScript we can create all sorts of abstractions to describe values, but in Overmind we lean on the core serializable values. These are **objects**, **arrays**, **strings**, **numbers**, **booleans** and **null**. Serializable values means that we can easily convert the state into a string and back again. This is fundamental for creating great developer experiences, passing state between client and server and other features. You can describe any application state with these core values.
12
12
13
13
Let us talk a little bit about what each value helps us represent in our application.
14
14
15
-
### Undefined
16
-
17
-
You might wonder why **undefined** is not part of the core value types. Well, there are two reasons:
18
-
19
-
1. It is not a serializable value. That means if you explicitly set a value to _undefined_ it will not show up in the devtools
20
-
2. Undefined values can not be tracked. That means if you were to iterate an object and look at the keys of that object, any undefined values will not be tracked. This can cause unexpected behaviour
21
-
22
-
### Naming
23
-
24
-
Each value needs to sit behind a name. Naming can be difficult, but we have some help. Even though we eventually do want to consume our application through a user interface we ideally want to avoid naming things specifically related to the environment where we show the user interface. Things like **page**, **tabs**, **modal** etc. are specific to a browser experience, maybe related to a certain size. We want to avoid those names as they should not dictate which elements are to be used with the state, that is up to the user interface to decide later. So here are some generic terms to use instead:
25
-
26
-
* page: **mode**
27
-
* tabs: **sections**
28
-
* modal: **editUser.active**
29
-
30
15
### Objects
31
16
32
17
The root value of your state tree is an object, because objects are great for holding other values. An object has keys that point to values. Most of these keys point to values that are the actual state of the application, but these keys can also represent domains of the application. A typical state structure could be:
@@ -85,6 +70,216 @@ Are things loading or not, is the user logged in or not? These are typical uses
85
70
86
71
All values, with the exception of booleans, can also be **null**. Non-existing. You can have a non-existing object, array, string or number. It means that if we haven’t selected a mode, both the string version and number version would have the value **null**.
87
72
73
+
## Class values
74
+
75
+
Overmind also supports using class instances as state values. Depending on your preference this can be a powerful tool to organize your logic. What classes provide is a way to co locate state and logic for changing and deriving that state. In functional programming the state and the logic is separated and it can be difficult to find a good way to organize the logic operating on that state.
76
+
77
+
It can be a good idea to think about your classes as models. They model some state.
78
+
79
+
{% tabs %}
80
+
{% tab title="overmind/models.js" %}
81
+
```javascript
82
+
classLoginForm {
83
+
constructor() {
84
+
this.username=''
85
+
this.password=''
86
+
}
87
+
isValid() {
88
+
returnBoolean(this.username&&this.password)
89
+
}
90
+
reset() {
91
+
this.username=''
92
+
this.password=''
93
+
}
94
+
}
95
+
```
96
+
{% endtab %}
97
+
98
+
{% tab title="overmind/state.js" %}
99
+
```javascript
100
+
import { LoginForm } from'./models'
101
+
102
+
exportconststate= {
103
+
loginForm:newLoginForm()
104
+
}
105
+
```
106
+
{% endtab %}
107
+
{% endtabs %}
108
+
109
+
You can now use this instance as normal and of course create new ones.
110
+
111
+
{% hint style="info" %}
112
+
Even though you can use **getters** as normal, they do not cache like **derived**. **Derived** is a concept of the state tree itself. It is unlikely that you need heavy computation within a single class instance though, it is typically across class instances
113
+
{% endhint %}
114
+
115
+
### Serializing class values
116
+
117
+
If you have an application that needs to serialize the state, for example to local storage or server side rendering, you can still use class instances with Overmind. Overmind exposes a symbol called **SERIALIZE** that you can attach to your class.
118
+
119
+
```typescript
120
+
import { SERIALIZE } from'overmind'
121
+
122
+
classUser {
123
+
[SERIALIZE]
124
+
constructor() {
125
+
this.username=''
126
+
this.jwt=''
127
+
}
128
+
}
129
+
```
130
+
131
+
There are two purposes to using **SERIALIZE**.
132
+
133
+
1. When using Typescript you will be able to get type safety in your rehydration of state
134
+
2. The devtools requires this to properly display values as class instances
135
+
136
+
You can also safely use **toJSON**, though the for the two reasons above you want to add **SERIALIZE**:
137
+
138
+
```typescript
139
+
import { SERIALIZE } from'overmind'
140
+
141
+
classUser {
142
+
constructor() {
143
+
this.username=''
144
+
this.jwt=''
145
+
}
146
+
toJSON() {
147
+
return {
148
+
[SERIALIZE]: true,
149
+
username: this.username
150
+
}
151
+
}
152
+
}
153
+
```
154
+
155
+
{% hint style="info" %}
156
+
The **SERIALIZE** symbol will not be part of the actual serialization done with **JSON.stringify**
157
+
{% endhint %}
158
+
159
+
### Rehydrating classes
160
+
161
+
The [**rehydrate**](../api-1/rehydrate.md)****utility of Overmind allows you to rehydrate state either by a list of mutations or a state object, like the following:
162
+
163
+
{% tabs %}
164
+
{% tab title="overmind/actions.js" %}
165
+
```typescript
166
+
import { rehydrate } from'overmind'
167
+
168
+
exportconst updateState = ({ state }) => {
169
+
rehydrate(state, {
170
+
user: {
171
+
username: 'jenny',
172
+
jwt: '123'
173
+
}
174
+
})
175
+
}
176
+
```
177
+
{% endtab %}
178
+
{% endtabs %}
179
+
180
+
Since our user is a class instance we can tell rehydrate what to do, where it is typical to give the class a static **fromJSON** method:
181
+
182
+
{% tabs %}
183
+
{% tab title="overmind/models.js" %}
184
+
```typescript
185
+
import { SERIALIZE } from'overmind'
186
+
187
+
classUser {
188
+
[SERIALIZE]
189
+
constructor() {
190
+
this.username=''
191
+
this.jwt=''
192
+
}
193
+
fromJSON(json) {
194
+
returnObject.assign(newUser(), json)
195
+
}
196
+
}
197
+
```
198
+
{% endtab %}
199
+
200
+
{% tab title="overmind/actions.js" %}
201
+
```typescript
202
+
import { rehydrate } from'overmind'
203
+
204
+
exportconst updateState = ({ state }) => {
205
+
rehydrate(
206
+
state,
207
+
{
208
+
user: {
209
+
username: 'jenny',
210
+
jwt: '123'
211
+
}
212
+
},
213
+
{
214
+
user: User.fromJSON
215
+
}
216
+
)
217
+
}
218
+
```
219
+
{% endtab %}
220
+
{% endtabs %}
221
+
222
+
It does not matter if the state is a value, an array of values or a dictionary of values, rehydrate will understand it.
223
+
224
+
That means the following will behave as expected:
225
+
226
+
{% tabs %}
227
+
{% tab title="overmind/state.js" %}
228
+
```typescript
229
+
import { User } from'./models'
230
+
231
+
exportconst state = {
232
+
user: null, // Expecting a single value
233
+
usersList: [], // Expecting an array of values
234
+
usersDictionary: {} // Expecting a dictionary of values
235
+
}
236
+
```
237
+
{% endtab %}
238
+
239
+
{% tab title="overmind/actions.js" %}
240
+
```typescript
241
+
import { rehydrate } from'overmind'
242
+
243
+
exportconst updateState = ({ state }) => {
244
+
rehydrate(
245
+
state,
246
+
{
247
+
user: {
248
+
username: 'jenny',
249
+
jwt: '123'
250
+
},
251
+
usersList: [{...}, {...}],
252
+
usersDictionary: {
253
+
'jenny': {...},
254
+
'bob': {...}
255
+
}
256
+
},
257
+
{
258
+
user: User.fromJSON,
259
+
usersList: User.fromJSON,
260
+
usersDictionary: User.fromJSON
261
+
}
262
+
)
263
+
}
264
+
```
265
+
{% endtab %}
266
+
{% endtabs %}
267
+
268
+
## Naming
269
+
270
+
Each value needs to sit behind a name. Naming can be difficult, but we have some help. Even though we eventually do want to consume our application through a user interface we ideally want to avoid naming things specifically related to the environment where we show the user interface. Things like **page**, **tabs**, **modal** etc. are specific to a browser experience, maybe related to a certain size. We want to avoid those names as they should not dictate which elements are to be used with the state, that is up to the user interface to decide later. So here are some generic terms to use instead:
271
+
272
+
* page: **mode**
273
+
* tabs: **sections**
274
+
* modal: **editUser.active**
275
+
276
+
## Undefined
277
+
278
+
You might wonder why **undefined** is not part of the core value types. Well, there are two reasons:
279
+
280
+
1. It is not a serializable value. That means if you explicitly set a value to _undefined_ it will not show up in the devtools
281
+
2. Undefined values can not be tracked. That means if you were to iterate an object and look at the keys of that object, any undefined values will not be tracked. This can cause unexpected behaviour
Copy file name to clipboardExpand all lines: features/testing.md
+29-11Lines changed: 29 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,18 +11,22 @@ You can also do **unit testing** of actions and effects. This will cover expecte
11
11
When you write tests you will create many instances of a mocked version of Overmind with the configuration you have created. To ensure that this configuration can be used many times we have to separate our configuration from the instantiation of the actual app.
12
12
13
13
{% tabs %}
14
-
{% tab title="overmind/index.js" %}
14
+
{% tab title="overmind/index.ts" %}
15
15
```typescript
16
16
import { IConfig } from'overmind'
17
17
import { state } from'./state'
18
18
19
19
exportconst config = {
20
20
state
21
21
}
22
+
23
+
declaremodule'overmind' {
24
+
interfaceConfigextendsIConfig<typeofconfig> {}
25
+
}
22
26
```
23
27
{% endtab %}
24
28
25
-
{% tab title="index.js" %}
29
+
{% tab title="index.ts" %}
26
30
```typescript
27
31
import { createOvermind } from'overmind'
28
32
import { config } from'./overmind'
@@ -39,9 +43,11 @@ Now we are free to import our configuration without touching the application ins
39
43
When testing an action you’ll want to verify that changes to state are performed as expected. To give you the best possible testing experience Overmind comes with a mocking tool called **createOvermindMock**. It takes your application configuration and allows you to run actions as if they were run from components.
40
44
41
45
{% tabs %}
42
-
{% tab title="overmind/actions.js" %}
46
+
{% tab title="overmind/actions.ts" %}
43
47
```typescript
44
-
exportconst getPost =async ({ state, api }, id) {
48
+
import { AsyncAction } from'overmind'
49
+
50
+
exportconst getPost:AsyncAction<string> =async ({ state, api }, id) {
You might want to test if a thrown error is handled correctly here. This is an example of how you could do that:
58
64
59
65
{% tabs %}
60
-
{% tab title="overmind/actions.test.js" %}
66
+
{% tab title="overmind/actions.test.ts" %}
61
67
```typescript
62
68
import { createOvermindMock } from'overmind'
63
69
import { config } from'./'
@@ -108,7 +114,7 @@ If your actions can result in multiple scenarios a unit test is beneficial. But
108
114
You do not have to explicitly write the expected state. You can also use for example [JEST](https://www.overmindjs.org/guides/intermediate/05_writingtests?view=react&typescript=true) for snapshot testing. The mock instance has a list of mutations performed. This is perfect for snapshot testing.
109
115
110
116
{% tabs %}
111
-
{% tab title="overmind/actions.test.js" %}
117
+
{% tab title="overmind/actions.test.ts" %}
112
118
```typescript
113
119
import { createOvermindMock } from'overmind'
114
120
import { config } from'./'
@@ -156,7 +162,7 @@ In this scenario we would also ensure that the **isLoadingPost** state indeed fl
156
162
The **onInitialize** hook will not trigger during testing. To test this action you have to trigger it yourself.
157
163
158
164
{% tabs %}
159
-
{% tab title="overmind/onInitialize.test.js" %}
165
+
{% tab title="overmind/onInitialize.test.ts" %}
160
166
```typescript
161
167
import { createOvermindMock } from'overmind'
162
168
import { config } from'./'
@@ -191,17 +197,29 @@ A simple example of this is doing requests. Maybe you want to use e.g. [AXIOS](h
191
197
This is just an example showing you how you can structure your code for optimal testability. You might prefer a different approach or maybe rely on integration tests for this. No worries, you do what makes most sense for your application:
192
198
193
199
{% tabs %}
194
-
{% tab title="overmind/effects.js" %}
200
+
{% tab title="overmind/effects.ts" %}
195
201
```typescript
196
202
import*asaxiosfrom'axios'
203
+
import { Post } from'./state'
204
+
205
+
interfaceIRequest {
206
+
get<T>(url:string):Promise<T>
207
+
}
208
+
209
+
interfaceIOptions {
210
+
authToken:string
211
+
baseUrl:string
212
+
}
197
213
198
214
// This is the class we can create new instances of when testing
0 commit comments