Skip to content

Commit 7e4e6cb

Browse files
christianalfonigitbook-bot
authored andcommitted
GitBook: [master] 4 pages modified
1 parent d6180df commit 7e4e6cb

File tree

3 files changed

+241
-28
lines changed

3 files changed

+241
-28
lines changed

SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
* [State first routing](guides-1/state-first-routing.md)
3535
* [Server Side Rendering](guides-1/server-side-rendering.md)
3636
* [Move to Typescript](guides-1/move-to-typescript.md)
37-
* [Testing](guides-1/testing.md)
37+
* [Testing](features/testing.md)
3838

3939
## API <a id="api-1"></a>
4040

core/defining-state.md

Lines changed: 211 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,12 @@ The mechanism of communicating from the application to the user interface is cal
66

77
![](../.gitbook/assets/state-ui.png)
88

9-
## The values
9+
## Core values
1010

1111
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.
1212

1313
Let us talk a little bit about what each value helps us represent in our application.
1414

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-
3015
### Objects
3116

3217
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
8570

8671
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**.
8772

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+
class LoginForm {
83+
constructor() {
84+
this.username = ''
85+
this.password = ''
86+
}
87+
isValid() {
88+
return Boolean(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+
export const state = {
103+
loginForm: new LoginForm()
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+
class User {
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+
class User {
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+
export const 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+
class User {
188+
[SERIALIZE]
189+
constructor() {
190+
this.username = ''
191+
this.jwt = ''
192+
}
193+
fromJSON(json) {
194+
return Object.assign(new User(), json)
195+
}
196+
}
197+
```
198+
{% endtab %}
199+
200+
{% tab title="overmind/actions.js" %}
201+
```typescript
202+
import { rehydrate } from 'overmind'
203+
204+
export const 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+
export const 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+
export const 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
282+
88283
## Deriving state
89284

90285
### Getter
Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@ You can also do **unit testing** of actions and effects. This will cover expecte
1111
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.
1212

1313
{% tabs %}
14-
{% tab title="overmind/index.js" %}
14+
{% tab title="overmind/index.ts" %}
1515
```typescript
1616
import { IConfig } from 'overmind'
1717
import { state } from './state'
1818

1919
export const config = {
2020
state
2121
}
22+
23+
declare module 'overmind' {
24+
interface Config extends IConfig<typeof config> {}
25+
}
2226
```
2327
{% endtab %}
2428

25-
{% tab title="index.js" %}
29+
{% tab title="index.ts" %}
2630
```typescript
2731
import { createOvermind } from 'overmind'
2832
import { config } from './overmind'
@@ -39,9 +43,11 @@ Now we are free to import our configuration without touching the application ins
3943
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.
4044

4145
{% tabs %}
42-
{% tab title="overmind/actions.js" %}
46+
{% tab title="overmind/actions.ts" %}
4347
```typescript
44-
export const getPost = async ({ state, api }, id) {
48+
import { AsyncAction } from 'overmind'
49+
50+
export const getPost: AsyncAction<string> = async ({ state, api }, id) {
4551
state.isLoadingPost = true
4652
try {
4753
state.currentPost = await api.getPost(id)
@@ -57,7 +63,7 @@ export const getPost = async ({ state, api }, id) {
5763
You might want to test if a thrown error is handled correctly here. This is an example of how you could do that:
5864

5965
{% tabs %}
60-
{% tab title="overmind/actions.test.js" %}
66+
{% tab title="overmind/actions.test.ts" %}
6167
```typescript
6268
import { createOvermindMock } from 'overmind'
6369
import { config } from './'
@@ -108,7 +114,7 @@ If your actions can result in multiple scenarios a unit test is beneficial. But
108114
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.
109115

110116
{% tabs %}
111-
{% tab title="overmind/actions.test.js" %}
117+
{% tab title="overmind/actions.test.ts" %}
112118
```typescript
113119
import { createOvermindMock } from 'overmind'
114120
import { config } from './'
@@ -156,7 +162,7 @@ In this scenario we would also ensure that the **isLoadingPost** state indeed fl
156162
The **onInitialize** hook will not trigger during testing. To test this action you have to trigger it yourself.
157163

158164
{% tabs %}
159-
{% tab title="overmind/onInitialize.test.js" %}
165+
{% tab title="overmind/onInitialize.test.ts" %}
160166
```typescript
161167
import { createOvermindMock } from 'overmind'
162168
import { config } from './'
@@ -191,17 +197,29 @@ A simple example of this is doing requests. Maybe you want to use e.g. [AXIOS](h
191197
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:
192198

193199
{% tabs %}
194-
{% tab title="overmind/effects.js" %}
200+
{% tab title="overmind/effects.ts" %}
195201
```typescript
196202
import * as axios from 'axios'
203+
import { Post } from './state'
204+
205+
interface IRequest {
206+
get<T>(url: string): Promise<T>
207+
}
208+
209+
interface IOptions {
210+
authToken: string
211+
baseUrl: string
212+
}
197213

198214
// This is the class we can create new instances of when testing
199215
export class Api {
200-
constructor(request, options) {
216+
request: IRequest
217+
options: IOptions
218+
constructor(request: IRequest, options: IOptions) {
201219
this.request = request
202220
this.options = options
203221
}
204-
async getPost(id: string) {
222+
async getPost(id: string): Promise<Post> {
205223
try {
206224
const response = await this.request.get(this.options.baseUrl + '/posts/' + id, {
207225
headers: {
@@ -229,7 +247,7 @@ export const api = new Api(axios, {
229247
Let’s see how you could write a test for it:
230248

231249
{% tabs %}
232-
{% tab title="overmind/effects.test.js" %}
250+
{% tab title="overmind/effects.test.ts" %}
233251
```typescript
234252
import { Api } from './effects'
235253

0 commit comments

Comments
 (0)