@pythoughts/vue-skills-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/index.mjs +139 -0
- package/package.json +34 -0
- package/skills/create-adaptable-composable/SKILL.md +76 -0
- package/skills/vue-best-practices/SKILL.md +154 -0
- package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
- package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
- package/skills/vue-best-practices/references/component-async.md +97 -0
- package/skills/vue-best-practices/references/component-data-flow.md +350 -0
- package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
- package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
- package/skills/vue-best-practices/references/component-slots.md +216 -0
- package/skills/vue-best-practices/references/component-suspense.md +228 -0
- package/skills/vue-best-practices/references/component-teleport.md +108 -0
- package/skills/vue-best-practices/references/component-transition-group.md +128 -0
- package/skills/vue-best-practices/references/component-transition.md +125 -0
- package/skills/vue-best-practices/references/composables.md +290 -0
- package/skills/vue-best-practices/references/directives.md +162 -0
- package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
- package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
- package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
- package/skills/vue-best-practices/references/plugins.md +166 -0
- package/skills/vue-best-practices/references/reactivity.md +346 -0
- package/skills/vue-best-practices/references/render-functions.md +201 -0
- package/skills/vue-best-practices/references/sfc.md +310 -0
- package/skills/vue-best-practices/references/state-management.md +135 -0
- package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
- package/skills/vue-debug-guides/SKILL.md +205 -0
- package/skills/vue-debug-guides/reference/animation-key-for-rerender.md +160 -0
- package/skills/vue-debug-guides/reference/animation-transitiongroup-performance.md +241 -0
- package/skills/vue-debug-guides/reference/async-component-error-handling.md +115 -0
- package/skills/vue-debug-guides/reference/async-component-keepalive-ref-issue.md +112 -0
- package/skills/vue-debug-guides/reference/async-component-suspense-control.md +84 -0
- package/skills/vue-debug-guides/reference/async-component-vue-router.md +109 -0
- package/skills/vue-debug-guides/reference/attrs-event-listener-merging.md +205 -0
- package/skills/vue-debug-guides/reference/checkbox-true-false-value-form-submission.md +118 -0
- package/skills/vue-debug-guides/reference/cleanup-side-effects.md +172 -0
- package/skills/vue-debug-guides/reference/click-events-on-components.md +180 -0
- package/skills/vue-debug-guides/reference/component-naming-conflicts.md +159 -0
- package/skills/vue-debug-guides/reference/component-ref-requires-defineexpose.md +176 -0
- package/skills/vue-debug-guides/reference/composable-avoid-hidden-side-effects.md +208 -0
- package/skills/vue-debug-guides/reference/composable-call-location-restrictions.md +141 -0
- package/skills/vue-debug-guides/reference/composable-naming-return-pattern.md +139 -0
- package/skills/vue-debug-guides/reference/composable-tovalue-inside-watcheffect.md +182 -0
- package/skills/vue-debug-guides/reference/composition-api-not-functional-programming.md +120 -0
- package/skills/vue-debug-guides/reference/composition-api-script-setup-async-context.md +203 -0
- package/skills/vue-debug-guides/reference/composition-api-vs-react-hooks-differences.md +156 -0
- package/skills/vue-debug-guides/reference/computed-array-mutation.md +148 -0
- package/skills/vue-debug-guides/reference/computed-conditional-dependencies.md +147 -0
- package/skills/vue-debug-guides/reference/computed-no-parameters.md +159 -0
- package/skills/vue-debug-guides/reference/computed-no-side-effects.md +107 -0
- package/skills/vue-debug-guides/reference/computed-return-value-readonly.md +160 -0
- package/skills/vue-debug-guides/reference/configure-app-before-mount.md +89 -0
- package/skills/vue-debug-guides/reference/declare-emits-for-documentation.md +212 -0
- package/skills/vue-debug-guides/reference/define-expose-before-await.md +192 -0
- package/skills/vue-debug-guides/reference/define-model-default-value-sync.md +139 -0
- package/skills/vue-debug-guides/reference/defineEmits-must-be-top-level.md +164 -0
- package/skills/vue-debug-guides/reference/defineEmits-no-runtime-and-type-mixed.md +170 -0
- package/skills/vue-debug-guides/reference/definemodel-object-mutation-no-emit.md +148 -0
- package/skills/vue-debug-guides/reference/dom-update-timing-nexttick.md +90 -0
- package/skills/vue-debug-guides/reference/dynamic-argument-constraints.md +146 -0
- package/skills/vue-debug-guides/reference/dynamic-component-registration-vite.md +147 -0
- package/skills/vue-debug-guides/reference/event-modifier-order-matters.md +101 -0
- package/skills/vue-debug-guides/reference/exact-modifier-for-precise-shortcuts.md +155 -0
- package/skills/vue-debug-guides/reference/fallthrough-attrs-overwrite-vue3.md +159 -0
- package/skills/vue-debug-guides/reference/in-dom-template-parsing-caveats.md +149 -0
- package/skills/vue-debug-guides/reference/inheritattrs-false-for-wrapper-components.md +230 -0
- package/skills/vue-debug-guides/reference/keepalive-router-nested-double-mount.md +222 -0
- package/skills/vue-debug-guides/reference/keepalive-transition-memory-leak.md +144 -0
- package/skills/vue-debug-guides/reference/keyup-modifier-timing.md +137 -0
- package/skills/vue-debug-guides/reference/lifecycle-dom-access-timing.md +216 -0
- package/skills/vue-debug-guides/reference/lifecycle-hooks-synchronous-registration.md +156 -0
- package/skills/vue-debug-guides/reference/lifecycle-ssr-awareness.md +184 -0
- package/skills/vue-debug-guides/reference/local-components-not-in-descendants.md +151 -0
- package/skills/vue-debug-guides/reference/mount-return-value.md +88 -0
- package/skills/vue-debug-guides/reference/multi-root-component-class-attrs.md +93 -0
- package/skills/vue-debug-guides/reference/native-event-collision-with-emits.md +162 -0
- package/skills/vue-debug-guides/reference/no-passive-with-prevent.md +141 -0
- package/skills/vue-debug-guides/reference/no-v-if-with-v-for.md +136 -0
- package/skills/vue-debug-guides/reference/perf-computed-object-stability.md +157 -0
- package/skills/vue-debug-guides/reference/perf-props-stability-update-optimization.md +140 -0
- package/skills/vue-debug-guides/reference/plugin-global-properties-sparingly.md +109 -0
- package/skills/vue-debug-guides/reference/plugin-install-before-mount.md +124 -0
- package/skills/vue-debug-guides/reference/plugin-prefer-provide-inject-over-global-properties.md +120 -0
- package/skills/vue-debug-guides/reference/plugin-typescript-type-augmentation.md +157 -0
- package/skills/vue-debug-guides/reference/prop-defineprops-scope-limitation.md +161 -0
- package/skills/vue-debug-guides/reference/provide-inject-debugging-challenges.md +203 -0
- package/skills/vue-debug-guides/reference/provide-inject-default-value-factory.md +244 -0
- package/skills/vue-debug-guides/reference/provide-inject-reactivity-not-automatic.md +226 -0
- package/skills/vue-debug-guides/reference/provide-inject-synchronous-setup.md +235 -0
- package/skills/vue-debug-guides/reference/reactive-destructuring.md +89 -0
- package/skills/vue-debug-guides/reference/reactivity-debugging-hooks.md +132 -0
- package/skills/vue-debug-guides/reference/reactivity-markraw-for-non-reactive.md +149 -0
- package/skills/vue-debug-guides/reference/reactivity-proxy-identity-hazard.md +96 -0
- package/skills/vue-debug-guides/reference/reactivity-same-tick-batching.md +166 -0
- package/skills/vue-debug-guides/reference/ref-value-access.md +61 -0
- package/skills/vue-debug-guides/reference/refs-in-collections-need-value.md +81 -0
- package/skills/vue-debug-guides/reference/render-function-avoid-internal-vnode-properties.md +151 -0
- package/skills/vue-debug-guides/reference/render-function-vnodes-must-be-unique.md +133 -0
- package/skills/vue-debug-guides/reference/rendering-render-function-h-import-vue3.md +148 -0
- package/skills/vue-debug-guides/reference/rendering-render-function-return-from-setup.md +148 -0
- package/skills/vue-debug-guides/reference/rendering-render-function-slots-as-functions.md +168 -0
- package/skills/vue-debug-guides/reference/rendering-resolve-component-for-string-names.md +231 -0
- package/skills/vue-debug-guides/reference/select-initial-value-ios-bug.md +91 -0
- package/skills/vue-debug-guides/reference/self-referencing-component-name.md +157 -0
- package/skills/vue-debug-guides/reference/sfc-named-exports-forbidden.md +184 -0
- package/skills/vue-debug-guides/reference/sfc-scoped-css-child-component-styling.md +156 -0
- package/skills/vue-debug-guides/reference/sfc-scoped-css-dynamic-content.md +193 -0
- package/skills/vue-debug-guides/reference/sfc-scoped-css-slot-content.md +242 -0
- package/skills/vue-debug-guides/reference/sfc-script-setup-reactivity.md +195 -0
- package/skills/vue-debug-guides/reference/slot-forwarding-to-child-components.md +143 -0
- package/skills/vue-debug-guides/reference/slot-implicit-default-content.md +155 -0
- package/skills/vue-debug-guides/reference/slot-name-reserved-prop.md +109 -0
- package/skills/vue-debug-guides/reference/slot-named-scoped-explicit-default.md +95 -0
- package/skills/vue-debug-guides/reference/slot-render-scope-parent-only.md +135 -0
- package/skills/vue-debug-guides/reference/slot-v-slot-on-components-or-templates-only.md +122 -0
- package/skills/vue-debug-guides/reference/ssr-hydration-mismatch-causes.md +280 -0
- package/skills/vue-debug-guides/reference/ssr-platform-specific-apis.md +256 -0
- package/skills/vue-debug-guides/reference/state-ssr-cross-request-pollution.md +276 -0
- package/skills/vue-debug-guides/reference/suspense-no-builtin-error-handling.md +127 -0
- package/skills/vue-debug-guides/reference/suspense-ssr-hydration-issues.md +159 -0
- package/skills/vue-debug-guides/reference/tailwind-dynamic-class-generation.md +144 -0
- package/skills/vue-debug-guides/reference/teleport-scoped-styles-limitation.md +191 -0
- package/skills/vue-debug-guides/reference/teleport-ssr-hydration.md +152 -0
- package/skills/vue-debug-guides/reference/teleport-target-must-exist.md +113 -0
- package/skills/vue-debug-guides/reference/template-expressions-restrictions.md +114 -0
- package/skills/vue-debug-guides/reference/template-functions-no-side-effects.md +187 -0
- package/skills/vue-debug-guides/reference/template-ref-null-with-v-if.md +123 -0
- package/skills/vue-debug-guides/reference/template-ref-unwrapping-top-level.md +104 -0
- package/skills/vue-debug-guides/reference/template-ref-v-for-order.md +172 -0
- package/skills/vue-debug-guides/reference/textarea-no-interpolation.md +72 -0
- package/skills/vue-debug-guides/reference/transition-group-flip-inline-elements.md +152 -0
- package/skills/vue-debug-guides/reference/transition-group-move-animation-position-absolute.md +130 -0
- package/skills/vue-debug-guides/reference/transition-group-no-default-wrapper-vue3.md +152 -0
- package/skills/vue-debug-guides/reference/transition-js-hooks-done-callback.md +251 -0
- package/skills/vue-debug-guides/reference/transition-nested-duration.md +182 -0
- package/skills/vue-debug-guides/reference/transition-reusable-scoped-style.md +245 -0
- package/skills/vue-debug-guides/reference/transition-router-view-appear.md +193 -0
- package/skills/vue-debug-guides/reference/transition-type-when-mixed.md +172 -0
- package/skills/vue-debug-guides/reference/transition-unmount-hook-timing.md +149 -0
- package/skills/vue-debug-guides/reference/ts-defineprops-boolean-default-false.md +225 -0
- package/skills/vue-debug-guides/reference/ts-defineprops-imported-types-limitations.md +281 -0
- package/skills/vue-debug-guides/reference/ts-event-handler-explicit-typing.md +213 -0
- package/skills/vue-debug-guides/reference/ts-reactive-no-generic-argument.md +196 -0
- package/skills/vue-debug-guides/reference/ts-shallowref-for-dynamic-components.md +218 -0
- package/skills/vue-debug-guides/reference/ts-template-ref-null-handling.md +249 -0
- package/skills/vue-debug-guides/reference/ts-template-type-casting.md +214 -0
- package/skills/vue-debug-guides/reference/ts-withdefaults-mutable-factory-function.md +171 -0
- package/skills/vue-debug-guides/reference/undeclared-emits-double-firing.md +195 -0
- package/skills/vue-debug-guides/reference/use-template-ref-vue35.md +158 -0
- package/skills/vue-debug-guides/reference/v-else-must-follow-v-if.md +136 -0
- package/skills/vue-debug-guides/reference/v-for-component-props.md +95 -0
- package/skills/vue-debug-guides/reference/v-for-computed-reverse-sort.md +86 -0
- package/skills/vue-debug-guides/reference/v-for-key-attribute.md +90 -0
- package/skills/vue-debug-guides/reference/v-for-range-starts-at-one.md +66 -0
- package/skills/vue-debug-guides/reference/v-if-null-check-order.md +171 -0
- package/skills/vue-debug-guides/reference/v-model-ignores-html-attributes.md +83 -0
- package/skills/vue-debug-guides/reference/v-model-ime-composition.md +83 -0
- package/skills/vue-debug-guides/reference/v-model-number-modifier-behavior.md +124 -0
- package/skills/vue-debug-guides/reference/v-show-template-limitation.md +124 -0
- package/skills/vue-debug-guides/reference/watch-async-cleanup.md +180 -0
- package/skills/vue-debug-guides/reference/watch-async-creation-memory-leak.md +176 -0
- package/skills/vue-debug-guides/reference/watch-deep-same-object-reference.md +165 -0
- package/skills/vue-debug-guides/reference/watch-flush-timing.md +189 -0
- package/skills/vue-debug-guides/reference/watch-reactive-property-getter.md +108 -0
- package/skills/vue-debug-guides/reference/watcheffect-async-dependency-tracking.md +173 -0
- package/skills/vue-debug-guides/reference/watcheffect-flush-post-for-refs.md +176 -0
- package/skills/vue-jsx-best-practices/SKILL.md +12 -0
- package/skills/vue-jsx-best-practices/reference/render-function-jsx-vue-vs-react.md +141 -0
- package/skills/vue-options-api-best-practices/SKILL.md +23 -0
- package/skills/vue-options-api-best-practices/reference/no-arrow-functions-in-lifecycle-hooks.md +95 -0
- package/skills/vue-options-api-best-practices/reference/no-arrow-functions-in-methods.md +68 -0
- package/skills/vue-options-api-best-practices/reference/stateful-methods-lifecycle.md +61 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-arrow-functions-validators.md +141 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-computed-return-types.md +192 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-proptype-complex-types.md +212 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-provide-inject-limitations.md +135 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-type-event-handlers.md +202 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-use-definecomponent.md +172 -0
- package/skills/vue-options-api-best-practices/reference/ts-strict-mode-options-api.md +197 -0
- package/skills/vue-pinia-best-practices/SKILL.md +21 -0
- package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
- package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
- package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
- package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
- package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
- package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
- package/skills/vue-router-best-practices/SKILL.md +23 -0
- package/skills/vue-router-best-practices/reference/router-beforeenter-no-param-trigger.md +167 -0
- package/skills/vue-router-best-practices/reference/router-beforerouteenter-no-this.md +176 -0
- package/skills/vue-router-best-practices/reference/router-guard-async-await-pattern.md +227 -0
- package/skills/vue-router-best-practices/reference/router-navigation-guard-infinite-loop.md +187 -0
- package/skills/vue-router-best-practices/reference/router-navigation-guard-next-deprecated.md +150 -0
- package/skills/vue-router-best-practices/reference/router-param-change-no-lifecycle.md +181 -0
- package/skills/vue-router-best-practices/reference/router-simple-routing-cleanup.md +209 -0
- package/skills/vue-router-best-practices/reference/router-use-vue-router-for-production.md +183 -0
- package/skills/vue-testing-best-practices/SKILL.md +29 -0
- package/skills/vue-testing-best-practices/reference/async-component-testing.md +163 -0
- package/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md +158 -0
- package/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md +175 -0
- package/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md +208 -0
- package/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md +144 -0
- package/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md +238 -0
- package/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md +242 -0
- package/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md +197 -0
- package/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md +228 -0
- package/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md +229 -0
- package/skills/vue-testing-best-practices/reference/testing-vitest-recommended-for-vue.md +204 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Prevent Cross-Request State Pollution in SSR Applications
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Singleton stores in SSR share state across all server requests, potentially leaking user data between requests
|
|
5
|
+
type: gotcha
|
|
6
|
+
tags: [vue3, ssr, state-management, pinia, vuex, security, server-side-rendering, nuxt]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Prevent Cross-Request State Pollution in SSR Applications
|
|
10
|
+
|
|
11
|
+
**Impact: CRITICAL** - In Server-Side Rendering (SSR) applications, a singleton store pattern creates a single instance that is shared across all server requests. This means data from one user's request could leak into another user's response, causing serious security and data integrity issues.
|
|
12
|
+
|
|
13
|
+
This is one of the most critical gotchas in Vue state management that can have severe production consequences.
|
|
14
|
+
|
|
15
|
+
## Task Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Never use a singleton store pattern in SSR applications
|
|
18
|
+
- [ ] Create a fresh store instance per request when using SSR
|
|
19
|
+
- [ ] Use Pinia which handles SSR state management correctly
|
|
20
|
+
- [ ] Test SSR state isolation with concurrent requests
|
|
21
|
+
- [ ] Review any global reactive state for SSR compatibility
|
|
22
|
+
|
|
23
|
+
## The Problem: Singleton State in SSR
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// store.js - DANGEROUS for SSR
|
|
27
|
+
import { reactive } from 'vue'
|
|
28
|
+
|
|
29
|
+
// This is a singleton - same instance for ALL requests
|
|
30
|
+
export const store = reactive({
|
|
31
|
+
user: null,
|
|
32
|
+
cart: [],
|
|
33
|
+
preferences: {}
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**What happens in SSR:**
|
|
38
|
+
|
|
39
|
+
1. Request A comes in for User A
|
|
40
|
+
2. Server sets `store.user = userA`
|
|
41
|
+
3. Before response completes, Request B arrives for User B
|
|
42
|
+
4. Request B sees `store.user = userA` (User A's data leaked!)
|
|
43
|
+
5. Server sets `store.user = userB`
|
|
44
|
+
6. Request A's response might now contain User B's data
|
|
45
|
+
|
|
46
|
+
This creates unpredictable behavior and potential security vulnerabilities.
|
|
47
|
+
|
|
48
|
+
## Solution 1: Use Pinia (Recommended)
|
|
49
|
+
|
|
50
|
+
Pinia handles SSR correctly by creating fresh store instances per request:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
// stores/user.js
|
|
54
|
+
import { defineStore } from 'pinia'
|
|
55
|
+
|
|
56
|
+
export const useUserStore = defineStore('user', {
|
|
57
|
+
state: () => ({
|
|
58
|
+
user: null,
|
|
59
|
+
preferences: {}
|
|
60
|
+
}),
|
|
61
|
+
actions: {
|
|
62
|
+
setUser(user) {
|
|
63
|
+
this.user = user
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
// main.js (or entry-server.js)
|
|
71
|
+
import { createPinia } from 'pinia'
|
|
72
|
+
import { createApp } from 'vue'
|
|
73
|
+
import App from './App.vue'
|
|
74
|
+
|
|
75
|
+
// For SSR: Create fresh instances per request
|
|
76
|
+
export function createAppInstance() {
|
|
77
|
+
const app = createApp(App)
|
|
78
|
+
const pinia = createPinia()
|
|
79
|
+
|
|
80
|
+
app.use(pinia)
|
|
81
|
+
|
|
82
|
+
return { app, pinia }
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
// entry-server.js
|
|
88
|
+
import { createAppInstance } from './main'
|
|
89
|
+
import { renderToString } from 'vue/server-renderer'
|
|
90
|
+
|
|
91
|
+
export async function render(url, context) {
|
|
92
|
+
// Fresh app and store instance per request
|
|
93
|
+
const { app, pinia } = createAppInstance()
|
|
94
|
+
|
|
95
|
+
// ... setup router, fetch data, etc.
|
|
96
|
+
|
|
97
|
+
const html = await renderToString(app)
|
|
98
|
+
|
|
99
|
+
// Serialize state for client hydration
|
|
100
|
+
const state = pinia.state.value
|
|
101
|
+
|
|
102
|
+
return { html, state }
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
// entry-client.js - Hydrate from serialized state
|
|
108
|
+
import { createAppInstance } from './main'
|
|
109
|
+
|
|
110
|
+
const { app, pinia } = createAppInstance()
|
|
111
|
+
|
|
112
|
+
// Restore server state before mounting
|
|
113
|
+
if (window.__PINIA_STATE__) {
|
|
114
|
+
pinia.state.value = window.__PINIA_STATE__
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
app.mount('#app')
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Solution 2: Factory Pattern for Hand-Rolled State
|
|
121
|
+
|
|
122
|
+
If not using Pinia, create a factory function:
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
// store.js - SSR-safe with factory
|
|
126
|
+
import { reactive, readonly } from 'vue'
|
|
127
|
+
|
|
128
|
+
// Factory function creates fresh state per call
|
|
129
|
+
export function createStore() {
|
|
130
|
+
const state = reactive({
|
|
131
|
+
user: null,
|
|
132
|
+
cart: [],
|
|
133
|
+
preferences: {}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
state: readonly(state),
|
|
138
|
+
setUser(user) {
|
|
139
|
+
state.user = user
|
|
140
|
+
},
|
|
141
|
+
addToCart(item) {
|
|
142
|
+
state.cart.push(item)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
// entry-server.js
|
|
150
|
+
import { createStore } from './store'
|
|
151
|
+
import { provide } from 'vue'
|
|
152
|
+
|
|
153
|
+
export async function render(url) {
|
|
154
|
+
const app = createApp(App)
|
|
155
|
+
|
|
156
|
+
// Fresh store instance for this request only
|
|
157
|
+
const store = createStore()
|
|
158
|
+
app.provide('store', store)
|
|
159
|
+
|
|
160
|
+
// ... render
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Solution 3: Context-Based State (Advanced)
|
|
165
|
+
|
|
166
|
+
For frameworks like Nuxt, use request context:
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
// composables/useRequestState.js
|
|
170
|
+
import { useSSRContext } from 'vue'
|
|
171
|
+
|
|
172
|
+
export function useRequestState(key, initialValue) {
|
|
173
|
+
if (import.meta.env.SSR) {
|
|
174
|
+
const ctx = useSSRContext()
|
|
175
|
+
ctx.state = ctx.state || {}
|
|
176
|
+
|
|
177
|
+
if (!(key in ctx.state)) {
|
|
178
|
+
ctx.state[key] = initialValue()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return ctx.state[key]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Client-side: use regular reactive state
|
|
185
|
+
return reactive(initialValue())
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Nuxt.js Handles This Automatically
|
|
190
|
+
|
|
191
|
+
In Nuxt 3, state isolation is handled automatically:
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
// Nuxt automatically creates fresh Pinia instance per request
|
|
195
|
+
// You can use stores normally
|
|
196
|
+
|
|
197
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
198
|
+
const userStore = useUserStore()
|
|
199
|
+
await userStore.fetchUser()
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Testing for State Pollution
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
// test/ssr-state-isolation.test.js
|
|
207
|
+
import { describe, it, expect } from 'vitest'
|
|
208
|
+
import { render } from './entry-server'
|
|
209
|
+
|
|
210
|
+
describe('SSR State Isolation', () => {
|
|
211
|
+
it('should not leak state between concurrent requests', async () => {
|
|
212
|
+
// Simulate concurrent requests
|
|
213
|
+
const [result1, result2] = await Promise.all([
|
|
214
|
+
render('/user/1', { userId: '1' }),
|
|
215
|
+
render('/user/2', { userId: '2' })
|
|
216
|
+
])
|
|
217
|
+
|
|
218
|
+
// Each should have their own user data
|
|
219
|
+
expect(result1.html).toContain('User 1')
|
|
220
|
+
expect(result2.html).toContain('User 2')
|
|
221
|
+
|
|
222
|
+
// State should not be mixed
|
|
223
|
+
expect(result1.html).not.toContain('User 2')
|
|
224
|
+
expect(result2.html).not.toContain('User 1')
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
// Alternative: Test store isolation directly
|
|
231
|
+
import { createApp } from './app.js'
|
|
232
|
+
|
|
233
|
+
test('requests do not share state', async () => {
|
|
234
|
+
// Simulate two concurrent requests
|
|
235
|
+
const { app: app1, store: store1 } = createApp()
|
|
236
|
+
const { app: app2, store: store2 } = createApp()
|
|
237
|
+
|
|
238
|
+
store1.user = { id: 1, name: 'Alice' }
|
|
239
|
+
store2.user = { id: 2, name: 'Bob' }
|
|
240
|
+
|
|
241
|
+
// Each should have its own state
|
|
242
|
+
expect(store1.user.name).toBe('Alice')
|
|
243
|
+
expect(store2.user.name).toBe('Bob')
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Red Flags to Watch For
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
// ANY module-level reactive state is dangerous in SSR
|
|
251
|
+
|
|
252
|
+
// BAD: Module-level reactive
|
|
253
|
+
export const globalUser = ref(null)
|
|
254
|
+
|
|
255
|
+
// BAD: Module-level reactive object
|
|
256
|
+
export const appState = reactive({})
|
|
257
|
+
|
|
258
|
+
// BAD: Shared Map/Set
|
|
259
|
+
export const cache = new Map()
|
|
260
|
+
|
|
261
|
+
// BAD: Even plain objects can be problematic
|
|
262
|
+
let requestCount = 0 // Shared across requests
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Why Pinia is Recommended for SSR
|
|
266
|
+
|
|
267
|
+
1. **Automatic request isolation** - Fresh store instances per request
|
|
268
|
+
2. **Built-in state serialization** - Easy hydration on client
|
|
269
|
+
3. **DevTools support** - Debug state on both server and client
|
|
270
|
+
4. **TypeScript support** - Type-safe state management
|
|
271
|
+
5. **Tested patterns** - Battle-tested SSR handling
|
|
272
|
+
|
|
273
|
+
## Reference
|
|
274
|
+
- [Vue.js State Management - SSR Considerations](https://vuejs.org/guide/scaling-up/state-management.html#ssr-considerations)
|
|
275
|
+
- [Pinia SSR Guide](https://pinia.vuejs.org/ssr/)
|
|
276
|
+
- [Vue SSR Guide](https://vuejs.org/guide/scaling-up/ssr.html)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Suspense Has No Built-in Error Handling
|
|
2
|
+
|
|
3
|
+
## Rule
|
|
4
|
+
|
|
5
|
+
`<Suspense>` does not provide error handling via the component itself. You must implement error handling using `errorCaptured` option or `onErrorCaptured()` hook in a parent component to catch async errors.
|
|
6
|
+
|
|
7
|
+
## Why This Matters
|
|
8
|
+
|
|
9
|
+
Without explicit error handling, async errors in suspended components will propagate uncaught, potentially crashing the application or leaving users stuck on loading states. Unlike React's Error Boundaries, Vue's Suspense requires manual error boundary implementation.
|
|
10
|
+
|
|
11
|
+
## Bad Code
|
|
12
|
+
|
|
13
|
+
```vue
|
|
14
|
+
<script setup>
|
|
15
|
+
// No error handling - async errors will propagate uncaught
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<Suspense>
|
|
20
|
+
<AsyncComponent />
|
|
21
|
+
<template #fallback>
|
|
22
|
+
Loading...
|
|
23
|
+
</template>
|
|
24
|
+
</Suspense>
|
|
25
|
+
</template>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Good Code
|
|
29
|
+
|
|
30
|
+
```vue
|
|
31
|
+
<script setup>
|
|
32
|
+
import { ref, onErrorCaptured } from 'vue'
|
|
33
|
+
import AsyncComponent from './AsyncComponent.vue'
|
|
34
|
+
|
|
35
|
+
const error = ref(null)
|
|
36
|
+
|
|
37
|
+
onErrorCaptured((err) => {
|
|
38
|
+
error.value = err
|
|
39
|
+
return false // Prevent error from propagating further
|
|
40
|
+
})
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<div v-if="error" class="error-state">
|
|
45
|
+
<p>Something went wrong: {{ error.message }}</p>
|
|
46
|
+
<button @click="error = null">Retry</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<Suspense v-else>
|
|
50
|
+
<AsyncComponent />
|
|
51
|
+
<template #fallback>
|
|
52
|
+
Loading...
|
|
53
|
+
</template>
|
|
54
|
+
</Suspense>
|
|
55
|
+
</template>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Reusable Error Boundary Pattern
|
|
59
|
+
|
|
60
|
+
```vue
|
|
61
|
+
<!-- ErrorBoundary.vue -->
|
|
62
|
+
<script setup>
|
|
63
|
+
import { ref, onErrorCaptured } from 'vue'
|
|
64
|
+
|
|
65
|
+
const props = defineProps({
|
|
66
|
+
fallback: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: 'Something went wrong'
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const emit = defineEmits(['error'])
|
|
73
|
+
|
|
74
|
+
const error = ref(null)
|
|
75
|
+
|
|
76
|
+
onErrorCaptured((err, instance, info) => {
|
|
77
|
+
error.value = { err, instance, info }
|
|
78
|
+
emit('error', { err, instance, info })
|
|
79
|
+
return false
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const reset = () => {
|
|
83
|
+
error.value = null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
defineExpose({ reset })
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<slot v-if="!error" />
|
|
91
|
+
<slot v-else name="error" :error="error" :reset="reset">
|
|
92
|
+
<div class="error-boundary">
|
|
93
|
+
{{ fallback }}
|
|
94
|
+
<button @click="reset">Retry</button>
|
|
95
|
+
</div>
|
|
96
|
+
</slot>
|
|
97
|
+
</template>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```vue
|
|
101
|
+
<!-- Usage -->
|
|
102
|
+
<template>
|
|
103
|
+
<ErrorBoundary @error="logError">
|
|
104
|
+
<Suspense>
|
|
105
|
+
<AsyncDashboard />
|
|
106
|
+
<template #fallback>Loading dashboard...</template>
|
|
107
|
+
</Suspense>
|
|
108
|
+
|
|
109
|
+
<template #error="{ error, reset }">
|
|
110
|
+
<DashboardError :error="error" @retry="reset" />
|
|
111
|
+
</template>
|
|
112
|
+
</ErrorBoundary>
|
|
113
|
+
</template>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Key Points
|
|
117
|
+
|
|
118
|
+
1. Always wrap `<Suspense>` with error handling logic in production
|
|
119
|
+
2. Use `onErrorCaptured` for Composition API or `errorCaptured` option for Options API
|
|
120
|
+
3. Return `false` from the error handler to stop propagation
|
|
121
|
+
4. Consider creating a reusable `ErrorBoundary` component to reduce boilerplate
|
|
122
|
+
5. Provide a way for users to retry failed operations
|
|
123
|
+
|
|
124
|
+
## References
|
|
125
|
+
|
|
126
|
+
- [Vue.js Suspense Documentation](https://vuejs.org/guide/built-ins/suspense#error-handling)
|
|
127
|
+
- [Vue.js onErrorCaptured](https://vuejs.org/api/composition-api-lifecycle#onerrorcaptured)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Suspense SSR Hydration Issues and Workarounds
|
|
2
|
+
|
|
3
|
+
## Rule
|
|
4
|
+
|
|
5
|
+
`<Suspense>` has known issues with SSR hydration, particularly with async components. During initial hydration, Suspense may not properly include child components within its "cloak of suspense," leading to hydration mismatches, flickering, or runtime crashes.
|
|
6
|
+
|
|
7
|
+
## Why This Matters
|
|
8
|
+
|
|
9
|
+
In SSR applications, hydration mismatches cause:
|
|
10
|
+
- Visual flickering as the client re-renders
|
|
11
|
+
- Loss of state in affected components
|
|
12
|
+
- Console warnings in development (silent failures in production)
|
|
13
|
+
- Potential runtime crashes in edge cases
|
|
14
|
+
- Poor user experience, especially on slower networks
|
|
15
|
+
|
|
16
|
+
## Bad Code
|
|
17
|
+
|
|
18
|
+
```vue
|
|
19
|
+
<template>
|
|
20
|
+
<!-- Async component directly in Suspense can fail hydration -->
|
|
21
|
+
<Suspense>
|
|
22
|
+
<AsyncDashboard />
|
|
23
|
+
<template #fallback>
|
|
24
|
+
Loading...
|
|
25
|
+
</template>
|
|
26
|
+
</Suspense>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup>
|
|
30
|
+
import { defineAsyncComponent } from 'vue'
|
|
31
|
+
|
|
32
|
+
const AsyncDashboard = defineAsyncComponent(
|
|
33
|
+
() => import('./Dashboard.vue')
|
|
34
|
+
)
|
|
35
|
+
</script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Good Code
|
|
39
|
+
|
|
40
|
+
### Solution 1: Wrap Async Components with Suspense
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<template>
|
|
44
|
+
<!-- Each async component wrapped in its own Suspense -->
|
|
45
|
+
<div class="dashboard">
|
|
46
|
+
<Suspense>
|
|
47
|
+
<AsyncHeader />
|
|
48
|
+
<template #fallback><HeaderSkeleton /></template>
|
|
49
|
+
</Suspense>
|
|
50
|
+
|
|
51
|
+
<Suspense>
|
|
52
|
+
<AsyncContent />
|
|
53
|
+
<template #fallback><ContentSkeleton /></template>
|
|
54
|
+
</Suspense>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Solution 2: Use ClientOnly Wrapper (Nuxt/SSR Frameworks)
|
|
60
|
+
|
|
61
|
+
```vue
|
|
62
|
+
<template>
|
|
63
|
+
<!-- Prevent SSR for problematic async components -->
|
|
64
|
+
<ClientOnly>
|
|
65
|
+
<Suspense>
|
|
66
|
+
<AsyncDashboard />
|
|
67
|
+
<template #fallback>
|
|
68
|
+
Loading dashboard...
|
|
69
|
+
</template>
|
|
70
|
+
</Suspense>
|
|
71
|
+
|
|
72
|
+
<template #fallback>
|
|
73
|
+
<DashboardSkeleton />
|
|
74
|
+
</template>
|
|
75
|
+
</ClientOnly>
|
|
76
|
+
</template>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Solution 3: Prefetch with Proper Stale Time (with TanStack Query)
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<script setup>
|
|
83
|
+
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
|
84
|
+
|
|
85
|
+
// IMPORTANT: All useQuery calls must be BEFORE any await
|
|
86
|
+
const { data, suspense } = useQuery({
|
|
87
|
+
queryKey: ['dashboard'],
|
|
88
|
+
queryFn: fetchDashboardData,
|
|
89
|
+
staleTime: 1000 * 60 * 5, // 5 minutes - prevents refetch after hydration
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Wait for suspense AFTER all useQuery calls
|
|
93
|
+
await suspense()
|
|
94
|
+
|
|
95
|
+
// Now safe to use data
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Solution 4: Handle Hydration Errors Gracefully
|
|
100
|
+
|
|
101
|
+
```vue
|
|
102
|
+
<script setup>
|
|
103
|
+
import { ref, onErrorCaptured, onMounted } from 'vue'
|
|
104
|
+
|
|
105
|
+
const hydrationError = ref(false)
|
|
106
|
+
const isClient = ref(false)
|
|
107
|
+
|
|
108
|
+
onMounted(() => {
|
|
109
|
+
isClient.value = true
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
onErrorCaptured((err) => {
|
|
113
|
+
if (err.message?.includes('hydration')) {
|
|
114
|
+
hydrationError.value = true
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<template>
|
|
121
|
+
<div v-if="hydrationError" class="hydration-recovery">
|
|
122
|
+
<!-- Force client-only re-render -->
|
|
123
|
+
<Suspense v-if="isClient">
|
|
124
|
+
<AsyncContent />
|
|
125
|
+
<template #fallback>Recovering...</template>
|
|
126
|
+
</Suspense>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<Suspense v-else>
|
|
130
|
+
<AsyncContent />
|
|
131
|
+
<template #fallback>Loading...</template>
|
|
132
|
+
</Suspense>
|
|
133
|
+
</template>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Common SSR + Suspense Issues
|
|
137
|
+
|
|
138
|
+
| Issue | Cause | Solution |
|
|
139
|
+
|-------|-------|----------|
|
|
140
|
+
| Hydration mismatch | Async chunk not loaded in time | Wrap with Suspense or use ClientOnly |
|
|
141
|
+
| Empty flash on Safari | Slow chunk loading | Preload critical chunks, use skeleton |
|
|
142
|
+
| useQuery after await error | Vue context lost after await | Put all useQuery calls before any await |
|
|
143
|
+
| Immediate refetch after hydration | staleTime too low | Set appropriate staleTime value |
|
|
144
|
+
|
|
145
|
+
## Key Points
|
|
146
|
+
|
|
147
|
+
1. Suspense + SSR has known edge cases - test thoroughly
|
|
148
|
+
2. Safari has slower chunk loading that triggers more hydration issues
|
|
149
|
+
3. With data-fetching libraries, ensure queries are set up before awaiting suspense
|
|
150
|
+
4. Consider ClientOnly wrappers for non-critical async content
|
|
151
|
+
5. Set appropriate staleTime to prevent unnecessary refetches after hydration
|
|
152
|
+
6. Use skeleton screens that match server-rendered content structure
|
|
153
|
+
|
|
154
|
+
## References
|
|
155
|
+
|
|
156
|
+
- [Vue.js Suspense Documentation](https://vuejs.org/guide/built-ins/suspense)
|
|
157
|
+
- [Vue Issue #6638 - Suspense hydration](https://github.com/vuejs/core/issues/6638)
|
|
158
|
+
- [Vue Issue #7672 - defineAsyncComponent SSR](https://github.com/vuejs/core/issues/7672)
|
|
159
|
+
- [TanStack Query SSR Discussion](https://github.com/TanStack/query/discussions/4870)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Tailwind CSS Dynamic Class Generation
|
|
2
|
+
|
|
3
|
+
## Rule
|
|
4
|
+
|
|
5
|
+
Never construct Tailwind CSS class names dynamically using string concatenation or template literals. Tailwind's build process cannot detect dynamically generated class names, causing styles to be missing in production.
|
|
6
|
+
|
|
7
|
+
## Why This Matters
|
|
8
|
+
|
|
9
|
+
- Tailwind uses static analysis at build time to determine which CSS classes to include
|
|
10
|
+
- Dynamically constructed class names (e.g., `bg-${color}-500`) are invisible to Tailwind's scanner
|
|
11
|
+
- Classes work in development with JIT but fail silently in production builds
|
|
12
|
+
- This is a common source of "it works locally but not in production" bugs
|
|
13
|
+
|
|
14
|
+
## Bad Code
|
|
15
|
+
|
|
16
|
+
```vue
|
|
17
|
+
<script setup>
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
color: String, // 'red', 'blue', 'green'
|
|
20
|
+
size: String // 'sm', 'md', 'lg'
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<!-- WRONG: Tailwind cannot detect these classes -->
|
|
26
|
+
<div :class="`bg-${color}-500 text-${size}`">
|
|
27
|
+
Content
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- WRONG: String concatenation -->
|
|
31
|
+
<div :class="'p-' + padding">
|
|
32
|
+
Content
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- WRONG: Template literal in array -->
|
|
36
|
+
<div :class="[`gap-x-${spacing}`]">
|
|
37
|
+
Content
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Good Code
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<script setup>
|
|
46
|
+
const props = defineProps({
|
|
47
|
+
color: String,
|
|
48
|
+
size: String
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Use a mapping object with complete class names
|
|
52
|
+
const colorClasses = {
|
|
53
|
+
red: 'bg-red-500',
|
|
54
|
+
blue: 'bg-blue-500',
|
|
55
|
+
green: 'bg-green-500'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sizeClasses = {
|
|
59
|
+
sm: 'text-sm p-2',
|
|
60
|
+
md: 'text-base p-4',
|
|
61
|
+
lg: 'text-lg p-6'
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<!-- CORRECT: Full class names that Tailwind can detect -->
|
|
67
|
+
<div :class="[colorClasses[color], sizeClasses[size]]">
|
|
68
|
+
Content
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Using Conditional Objects
|
|
74
|
+
|
|
75
|
+
```vue
|
|
76
|
+
<script setup>
|
|
77
|
+
const props = defineProps({
|
|
78
|
+
variant: String // 'primary', 'secondary', 'danger'
|
|
79
|
+
})
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<!-- CORRECT: All class names are complete strings -->
|
|
84
|
+
<button :class="{
|
|
85
|
+
'bg-blue-500 hover:bg-blue-600': variant === 'primary',
|
|
86
|
+
'bg-gray-500 hover:bg-gray-600': variant === 'secondary',
|
|
87
|
+
'bg-red-500 hover:bg-red-600': variant === 'danger'
|
|
88
|
+
}">
|
|
89
|
+
Click me
|
|
90
|
+
</button>
|
|
91
|
+
</template>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Safelist for Truly Dynamic Classes
|
|
95
|
+
|
|
96
|
+
If you must use dynamic classes, add them to Tailwind's safelist:
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
// tailwind.config.js
|
|
100
|
+
module.exports = {
|
|
101
|
+
safelist: [
|
|
102
|
+
'bg-red-500',
|
|
103
|
+
'bg-blue-500',
|
|
104
|
+
'bg-green-500',
|
|
105
|
+
// Or use patterns (use sparingly - increases bundle size)
|
|
106
|
+
{
|
|
107
|
+
pattern: /bg-(red|blue|green)-(100|500|900)/
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Alternative: CSS Custom Properties
|
|
114
|
+
|
|
115
|
+
For truly dynamic values, use CSS custom properties:
|
|
116
|
+
|
|
117
|
+
```vue
|
|
118
|
+
<script setup>
|
|
119
|
+
const props = defineProps({
|
|
120
|
+
customColor: String // Any hex color
|
|
121
|
+
})
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<template>
|
|
125
|
+
<!-- Use CSS variable for truly dynamic values -->
|
|
126
|
+
<div
|
|
127
|
+
class="dynamic-bg"
|
|
128
|
+
:style="{ '--dynamic-color': customColor }"
|
|
129
|
+
>
|
|
130
|
+
Content
|
|
131
|
+
</div>
|
|
132
|
+
</template>
|
|
133
|
+
|
|
134
|
+
<style>
|
|
135
|
+
.dynamic-bg {
|
|
136
|
+
background-color: var(--dynamic-color);
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## References
|
|
142
|
+
|
|
143
|
+
- [Tailwind CSS Dynamic Class Names](https://tailwindcss.com/docs/content-configuration#dynamic-class-names)
|
|
144
|
+
- [Tailwind Safelist](https://tailwindcss.com/docs/content-configuration#safelisting-classes)
|