Skip to content

Commit 6f06a44

Browse files
committed
state management
1 parent 9a87ffc commit 6f06a44

File tree

5 files changed

+201
-85
lines changed

5 files changed

+201
-85
lines changed

src/.vitepress/config.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ const nav = [
8080
text: 'Contribution Guide',
8181
link: '/about/contribution-guide'
8282
},
83-
{ text: 'Code of Conduct', link: '/about/coc' }
83+
{ text: 'Code of Conduct', link: '/about/coc' },
84+
{
85+
text: 'The Documentary',
86+
link: 'https://www.youtube.com/watch?v=OrxmtDw4pVI'
87+
}
8488
]
8589
}
8690
]
@@ -198,7 +202,11 @@ export const sidebar = {
198202
link: '/guide/scaling-up/state-management'
199203
},
200204
{ text: 'Testing', link: '/guide/scaling-up/testing' },
201-
{ text: 'TypeScript', link: '/guide/scaling-up/typescript' }
205+
{ text: 'TypeScript', link: '/guide/scaling-up/typescript' },
206+
{
207+
text: 'Server-Side Rendering (SSR)',
208+
link: '/guide/scaling-up/ssr'
209+
}
202210
]
203211
},
204212
{
@@ -245,10 +253,6 @@ export const sidebar = {
245253
text: 'Render Functions & JSX',
246254
link: '/guide/extras/render-function'
247255
},
248-
{
249-
text: 'Server-Side Rendering (SSR)',
250-
link: '/guide/extras/ssr'
251-
},
252256
{
253257
text: 'Vue and Web Components',
254258
link: '/guide/extras/web-components'

src/guide/extras/reactivity-in-depth.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,6 @@ const obj = reactive({
304304
console.log(obj.count === 0) // true
305305
```
306306

307-
308307
### Retaining Reactivity
309308

310309
When we want to use a few properties of the large reactive object, it could be tempting to use destructuring to get properties we want. However, the destructured property would lose the reactivity connection to the original object:
@@ -444,3 +443,13 @@ watchEffect(async (onInvalidate) => {
444443
An async function implicitly returns a Promise, but the cleanup function needs to be registered immediately before the Promise resolves. In addition, Vue relies on the returned Promise to automatically handle potential errors in the Promise chain.
445444

446445
</div>
446+
447+
## Integration with External State Systems
448+
449+
### State Machines
450+
451+
// TODO `useMachine()` example
452+
453+
### RxJS
454+
455+
The [VueUse](https://vueuse.org/) library provides the [`@vueuse/rxjs`](https://vueuse.org/rxjs/readme.html) add-on for connecting RxJS streams with Vue's reactivity system.
17.8 KB
Loading

src/guide/extras/ssr.md renamed to src/guide/scaling-up/ssr.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Server-Side Rendering <Badge text="WIP" />
22

3+
## Cross-Request State Pollution
4+
5+
In the State Management chapter, we introduced a [simple state management pattern using Reactivity APIs](state-management.html#simple-state-management-with-reactivity-api). In an SSR context, this pattern requires some additional adjustments.
6+
7+
The pattern declares shared state as **singletons**. This means there is only once instance of the reactive object throughout the entire lifecycle of our application. This works as expected in a pure client-side Vue application, since the our application code is initialized fresh for each browser page visit.
8+
9+
However, in an SSR context, the application code is typically initialized only once on the server, when the server boots up. In such case, singletons in our application will be shared across multiple requests handled by the server! If we mutate the shared singleton store with data specific to one user, it can be accidentally leaked to a request from another user. We call this **cross-request state pollution.**
10+
11+
// TODO finish
12+
313
## Higher Level Solutions
414

515
### Nuxt.js

src/guide/scaling-up/state-management.md

Lines changed: 171 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,130 +2,223 @@
22

33
## What is State Management?
44

5-
## Simple State Management from Scratch
5+
Technically, every Vue component instance already "manages" its own reactive state. Take a simple counter component as an example:
66

7-
It is often overlooked that the source of truth in Vue applications is the reactive `data` object - a component instance only proxies access to it. Therefore, if you have a piece of state that should be shared by multiple instances, you can use the [reactive](/api/reactivity-core.html#reactive) method to make an object reactive:
7+
<div class="composition-api">
88

9-
```js
10-
const { createApp, reactive } = Vue
11-
12-
const sourceOfTruth = reactive({
13-
message: 'Hello'
14-
})
15-
16-
const appA = createApp({
17-
data() {
18-
return sourceOfTruth
19-
}
20-
}).mount('#app-a')
9+
```vue
10+
<script setup>
11+
import { ref } from 'vue'
2112
22-
const appB = createApp({
23-
data() {
24-
return sourceOfTruth
25-
}
26-
}).mount('#app-b')
27-
```
13+
// state
14+
const count = ref(0)
2815
29-
```vue-html
30-
<div id="app-a">App A: {{ message }}</div>
16+
// actions
17+
function increment() {
18+
count.value++
19+
}
20+
</script>
3121
32-
<div id="app-b">App B: {{ message }}</div>
22+
<!-- view -->
23+
<template>{{ count }}</template>
3324
```
3425

35-
Now whenever `sourceOfTruth` is mutated, both `appA` and `appB` will update their views automatically. We have a single source of truth now, but debugging would be a nightmare. Any piece of data could be changed by any part of our app at any time, without leaving a trace.
26+
</div>
27+
<div class="options-api">
3628

37-
```js
38-
const appB = createApp({
29+
```vue
30+
<script>
31+
export default {
32+
// state
3933
data() {
40-
return sourceOfTruth
34+
return {
35+
count: 0
36+
}
4137
},
42-
mounted() {
43-
sourceOfTruth.message = 'Goodbye' // both apps will render 'Goodbye' message now
38+
// actions
39+
methods: {
40+
increment() {
41+
this.count++
42+
}
4443
}
45-
}).mount('#app-b')
44+
}
45+
</script>
46+
47+
<!-- view -->
48+
<template>{{ count }}</template>
4649
```
4750

48-
To help solve this problem, we can adopt a **store pattern**:
51+
</div>
4952

50-
```js
51-
const store = {
52-
debug: true,
53+
It is a self-contained unit with the following parts:
5354

54-
state: reactive({
55-
message: 'Hello!'
56-
}),
55+
- The **state**, the source of truth that drives our app;
56+
- The **view**, a declarative mapping of the **state**;
57+
- The **actions**, the possible ways the state could change in reaction to user inputs from the **view**.
5758

58-
setMessageAction(newValue) {
59-
if (this.debug) {
60-
console.log('setMessageAction triggered with', newValue)
61-
}
59+
This is a simple representation of the concept of "one-way data flow":
6260

63-
this.state.message = newValue
64-
},
61+
<p style="text-align: center">
62+
<img alt="state flow diagram" src="./images/state-flow.png" width="252px" style="margin: 40px auto">
63+
</p>
6564

66-
clearMessageAction() {
67-
if (this.debug) {
68-
console.log('clearMessageAction triggered')
69-
}
65+
However, the simplicity starts to break down when we have **multiple components that share a common state**:
7066

71-
this.state.message = ''
72-
}
73-
}
67+
1. Multiple views may depend on the same piece of state.
68+
2. Actions from different views may need to mutate the same piece of state.
69+
70+
For case one, a possible workaround is by "lifting" the shared state up to a common ancestor component, and then pass it down as props. However, this quickly gets tedious in component trees with deep hierarchies, leading to another problem known as [Props Drilling](/guide/components/provide-inject.html#props-drilling).
71+
72+
For case two, we often find ourselves resorting to solutions such as reaching for direct parent / child instances via template refs, or trying to mutate and synchronize multiple copies of the state via emitted events. Both of these patterns are brittle and quickly lead to unmaintainable code.
73+
74+
A simpler and more straightforward solution is to extract the shared state out of the components, and manage it in a global singleton. With this, our component tree becomes a big "view", and any component can access the state or trigger actions, no matter where they are in the tree!
75+
76+
## Simple State Management with Reactivity API
77+
78+
<div class="options-api">
79+
80+
In Options API, reactive data is declared using the `data()` option. Internally, the object returned by `data()` is made reactive via the [`reactive()`](/api/reactivity-core.html#reactive) function, which is also available as a public API.
81+
82+
</div>
83+
84+
If you have a piece of state that should be shared by multiple instances, you can use [`reactive()`](/api/reactivity-core.html#reactive) to create a reactive object, and then import it from multiple components:
85+
86+
```js
87+
// store.js
88+
import { reactive } from 'vue'
89+
90+
export const store = reactive({
91+
count: 0
92+
})
7493
```
7594

76-
Notice all actions that mutate the store's state are put inside the store itself. This type of centralized state management makes it easier to understand what type of mutations could happen and how they are triggered. Now when something goes wrong, we'll also have a log of what happened leading up to the bug.
95+
<div class="composition-api">
7796

78-
In addition, each instance/component can still own and manage its own private state:
97+
```vue
98+
<!-- ComponentA.vue -->
99+
<script setup>
100+
import { store } from './store.js'
101+
</script>
102+
103+
<template>From A: {{ store.count }}</template>
104+
```
79105

80-
```vue-html
81-
<div id="app-a">{{sharedState.message}}</div>
106+
```vue
107+
<!-- ComponentB.vue -->
108+
<script setup>
109+
import { store } from './store.js'
110+
</script>
82111
83-
<div id="app-b">{{sharedState.message}}</div>
112+
<template>From B: {{ store.count }}</template>
84113
```
85114

86-
```js
87-
const appA = createApp({
115+
</div>
116+
<div class="options-api">
117+
118+
```vue
119+
<!-- ComponentA.vue -->
120+
<script>
121+
import { store } from './store.js'
122+
123+
export default {
88124
data() {
89125
return {
90-
privateState: {},
91-
sharedState: store.state
126+
store
92127
}
93-
},
94-
mounted() {
95-
store.setMessageAction('Goodbye!')
96128
}
97-
}).mount('#app-a')
129+
}
130+
</script>
131+
132+
<template>From A: {{ store.count }}</template>
133+
```
134+
135+
```vue
136+
<!-- ComponentB.vue -->
137+
<script>
138+
import { store } from './store.js'
98139
99-
const appB = createApp({
140+
export default {
100141
data() {
101142
return {
102-
privateState: {},
103-
sharedState: store.state
143+
store
104144
}
105145
}
106-
}).mount('#app-b')
146+
}
147+
</script>
148+
149+
<template>From B: {{ store.count }}</template>
150+
```
151+
152+
</div>
153+
154+
Now whenever the `store` object is mutated, both `<ComponentA>` and `<ComponentB>` will update their views automatically - we have a single source of truth now.
155+
156+
However, this also means any component importing `store` can mutate it however they want:
157+
158+
```vue-html{2}
159+
<template>
160+
<button @click="store.count++">
161+
From B: {{ store.count }}
162+
</button>
163+
</template>
164+
```
165+
166+
While this works in simple cases, global state that can be arbitrarily mutated by any component is not going to be very maintainable for the long run. To ensure the state-mutating logic is centralized like the state itself, it is recommended to define methods on the store with names that express the intention of the actions:
167+
168+
```js{6-8}
169+
// store.js
170+
import { reactive } from 'vue'
171+
172+
export const store = reactive({
173+
count: 0,
174+
increment() {
175+
this.count++
176+
}
177+
})
178+
```
179+
180+
```vue-html{2}
181+
<template>
182+
<button @click="store.increment()">
183+
From B: {{ store.count }}
184+
</button>
185+
</template>
107186
```
108187

109-
![State Management](/images/state.png)
188+
<div class="composition-api">
189+
190+
[Try it in the Playground](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCBDb21wb25lbnRBIGZyb20gJy4vQ29tcG9uZW50QS52dWUnXG5pbXBvcnQgQ29tcG9uZW50QiBmcm9tICcuL0NvbXBvbmVudEIudnVlJ1xuPC9zY3JpcHQ+XG5cbjx0ZW1wbGF0ZT5cbiAgPENvbXBvbmVudEEgLz5cbiAgPENvbXBvbmVudEIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIkNvbXBvbmVudEEudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHN0b3JlIH0gZnJvbSAnLi9zdG9yZS5qcydcbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxkaXY+XG4gICAgPGJ1dHRvbiBAY2xpY2s9XCJzdG9yZS5pbmNyZW1lbnQoKVwiPlxuICAgICAgRnJvbSBBOiB7eyBzdG9yZS5jb3VudCB9fVxuICAgIDwvYnV0dG9uPlxuICA8L2Rpdj5cbjwvdGVtcGxhdGU+IiwiQ29tcG9uZW50Qi52dWUiOiI8c2NyaXB0IHNldHVwPlxuaW1wb3J0IHsgc3RvcmUgfSBmcm9tICcuL3N0b3JlLmpzJ1xuPC9zY3JpcHQ+XG5cbjx0ZW1wbGF0ZT5cbiAgPGRpdj5cbiAgICA8YnV0dG9uIEBjbGljaz1cInN0b3JlLmluY3JlbWVudCgpXCI+XG4gICAgICBGcm9tIEI6IHt7IHN0b3JlLmNvdW50IH19XG4gICAgPC9idXR0b24+XG4gIDwvZGl2PlxuPC90ZW1wbGF0ZT4iLCJzdG9yZS5qcyI6ImltcG9ydCB7IHJlYWN0aXZlIH0gZnJvbSAndnVlJ1xuXG5leHBvcnQgY29uc3Qgc3RvcmUgPSByZWFjdGl2ZSh7XG4gIGNvdW50OiAwLFxuICBpbmNyZW1lbnQoKSB7XG4gICAgdGhpcy5jb3VudCsrXG4gIH1cbn0pIn0=)
110191

111-
::: tip
112-
You should never replace the original state object in your actions - the components and the store need to share reference to the same object in order for mutations to be observed.
192+
</div>
193+
<div class="options-api">
194+
195+
[Try it in the Playground](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdD5cbmltcG9ydCBDb21wb25lbnRBIGZyb20gJy4vQ29tcG9uZW50QS52dWUnXG5pbXBvcnQgQ29tcG9uZW50QiBmcm9tICcuL0NvbXBvbmVudEIudnVlJ1xuICBcbmV4cG9ydCBkZWZhdWx0IHtcbiAgY29tcG9uZW50czoge1xuICAgIENvbXBvbmVudEEsXG4gICAgQ29tcG9uZW50QlxuICB9XG59XG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8Q29tcG9uZW50QSAvPlxuICA8Q29tcG9uZW50QiAvPlxuPC90ZW1wbGF0ZT4iLCJpbXBvcnQtbWFwLmpzb24iOiJ7XG4gIFwiaW1wb3J0c1wiOiB7XG4gICAgXCJ2dWVcIjogXCJodHRwczovL3NmYy52dWVqcy5vcmcvdnVlLnJ1bnRpbWUuZXNtLWJyb3dzZXIuanNcIlxuICB9XG59IiwiQ29tcG9uZW50QS52dWUiOiI8c2NyaXB0PlxuaW1wb3J0IHsgc3RvcmUgfSBmcm9tICcuL3N0b3JlLmpzJ1xuXG5leHBvcnQgZGVmYXVsdCB7XG4gIGRhdGEoKSB7XG4gICAgcmV0dXJuIHtcbiAgICAgIHN0b3JlXG4gICAgfVxuICB9XG59XG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8ZGl2PlxuICAgIDxidXR0b24gQGNsaWNrPVwic3RvcmUuaW5jcmVtZW50KClcIj5cbiAgICAgIEZyb20gQToge3sgc3RvcmUuY291bnQgfX1cbiAgICA8L2J1dHRvbj5cbiAgPC9kaXY+XG48L3RlbXBsYXRlPiIsIkNvbXBvbmVudEIudnVlIjoiPHNjcmlwdD5cbmltcG9ydCB7IHN0b3JlIH0gZnJvbSAnLi9zdG9yZS5qcydcblxuZXhwb3J0IGRlZmF1bHQge1xuICBkYXRhKCkge1xuICAgIHJldHVybiB7XG4gICAgICBzdG9yZVxuICAgIH1cbiAgfVxufVxuPC9zY3JpcHQ+XG5cbjx0ZW1wbGF0ZT5cbiAgPGRpdj5cbiAgICA8YnV0dG9uIEBjbGljaz1cInN0b3JlLmluY3JlbWVudCgpXCI+XG4gICAgICBGcm9tIEI6IHt7IHN0b3JlLmNvdW50IH19XG4gICAgPC9idXR0b24+XG4gIDwvZGl2PlxuPC90ZW1wbGF0ZT4iLCJzdG9yZS5qcyI6ImltcG9ydCB7IHJlYWN0aXZlIH0gZnJvbSAndnVlJ1xuXG5leHBvcnQgY29uc3Qgc3RvcmUgPSByZWFjdGl2ZSh7XG4gIGNvdW50OiAwLFxuICBpbmNyZW1lbnQoKSB7XG4gICAgdGhpcy5jb3VudCsrXG4gIH1cbn0pIn0=)
196+
197+
</div>
198+
199+
:::tip
200+
Note the click handler uses `store.increment()` with the parenthesis - this is necessary to call the method with the proper `this` context since it's not a component method.
113201
:::
114202

115-
As we continue developing the convention, where components are never allowed to directly mutate state that belongs to a store but should instead dispatch events that notify the store to perform actions, we eventually arrive at the [Flux](https://facebook.github.io/flux/) architecture. The benefit of this convention is we can record all state mutations happening to the store and implement advanced debugging helpers such as mutation logs, snapshots, and history re-rolls / time travel.
203+
Although here we are using a single reactive object as a store, you can also share reactive state created using other [Reactivity APIs](/api/reactivity-core.html) such as `ref()` or `computed()`. The fact that Vue's reactivity system is decoupled from the component model makes it extremely flexible.
204+
205+
## SSR Considerations
116206

117-
This brings us full circle back to [Vuex](https://next.vuex.vuejs.org/), so if you've read this far it's probably time to try it out!
207+
If you are building an application that leverages [Server-Side Rendering (SSR)](./ssr), the above pattern can lead to issues due to the store being a singleton shared across multiple requests. This is discussed in [more details](./ssr#cross-request-state-pollution) in the SSR guide.
118208

119209
## Pinia
120210

121-
## Vuex
211+
While our hand-rolled state management solution will suffice in simple scenarios, there are many more things to consider in large-scale production applications:
122212

123-
Large applications can often grow in complexity, due to multiple pieces of state scattered across many components and the interactions between them. To solve this problem, Vue offers [Vuex](https://next.vuex.vuejs.org/), our own Elm-inspired state management library. It even integrates into [vue-devtools](https://github.com/vuejs/vue-devtools), providing zero-setup access to [time travel debugging](https://raw.githubusercontent.com/vuejs/vue-devtools/legacy/media/demo.gif).
213+
- Stronger conventions for team collaboration
214+
- Integrating with the Vue DevTools, including timeline, in-component inspection, and time-travel debugging.
215+
- Hot Module Replacement
216+
- Server-Side Rendering support
124217

125-
## Integration with External State Systems
218+
[Pinia](https://pinia.vuejs.org) is a state management library that implements all of the above. It is maintained by the Vue core team, and works with both Vue 2 and Vue 3.
126219

127-
### State Machines
220+
Existing users may be familiar with [Vuex](https://vuex.vuejs.org/), the previous official state management library for Vue. With Pinia serving the same role in the ecosystem, Vuex is now in maintenance mode. It still works, but will no longer receive new features. It is recommended to use Pinia for new applications.
128221

129-
### RxJS
222+
Pinia in fact started as an exploration for how the next iteration of Vuex would look like, incorporating many ideas from core team discussions for Vuex 5. Eventually, we realized that Pinia already implements most of what we wanted in Vuex 5, and decided to make it the new recommendation instead.
130223

131-
### Redux
224+
Compared to Vuex, Pinia provides a simpler API with less ceremony, offers Composition-API-style APIs, and most importantly, has solid type inference support when used with TypeScript.

0 commit comments

Comments
 (0)