Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
4.5 KiB
Markdown
166 lines
4.5 KiB
Markdown
---
|
|
title: Use shallowRef Pattern for External State Libraries
|
|
impact: MEDIUM
|
|
impactDescription: External state systems (Immer, XState, Redux) should use shallowRef to avoid double-wrapping in proxies
|
|
type: efficiency
|
|
tags: [vue3, reactivity, shallowRef, external-state, immer, xstate, integration]
|
|
---
|
|
|
|
# Use shallowRef Pattern for External State Libraries
|
|
|
|
**Impact: MEDIUM** - When integrating Vue with external state management libraries (Immer, XState, Redux, MobX), use `shallowRef()` to hold the external state. This prevents Vue from deep-wrapping the external state in proxies, which can cause conflicts and performance issues.
|
|
|
|
The pattern: hold external state in a `shallowRef`, and replace `.value` entirely when the external system updates. This gives Vue reactivity while letting the external library manage state internals.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Use `shallowRef()` to hold external library state
|
|
- [ ] Replace `.value` entirely when external state changes (don't mutate)
|
|
- [ ] Integrate update functions that produce new state objects
|
|
- [ ] Consider this pattern for immutable data structures
|
|
|
|
**Integrating with Immer:**
|
|
```javascript
|
|
import { produce } from 'immer'
|
|
import { shallowRef } from 'vue'
|
|
|
|
export function useImmer(baseState) {
|
|
const state = shallowRef(baseState)
|
|
|
|
function update(updater) {
|
|
// Immer produces a new immutable state
|
|
// Replace shallowRef value entirely to trigger reactivity
|
|
state.value = produce(state.value, updater)
|
|
}
|
|
|
|
return [state, update]
|
|
}
|
|
|
|
// Usage
|
|
const [todos, updateTodos] = useImmer([
|
|
{ id: 1, text: 'Learn Vue', done: false }
|
|
])
|
|
|
|
// Update with Immer's mutable API (produces immutable result)
|
|
updateTodos(draft => {
|
|
draft[0].done = true
|
|
draft.push({ id: 2, text: 'Use Immer', done: false })
|
|
})
|
|
```
|
|
|
|
**Integrating with XState:**
|
|
```javascript
|
|
import { createMachine, interpret } from 'xstate'
|
|
import { shallowRef, onUnmounted } from 'vue'
|
|
|
|
export function useMachine(options) {
|
|
const machine = createMachine(options)
|
|
const state = shallowRef(machine.initialState)
|
|
|
|
const service = interpret(machine)
|
|
.onTransition((newState) => {
|
|
// Replace state entirely on each transition
|
|
state.value = newState
|
|
})
|
|
.start()
|
|
|
|
const send = (event) => service.send(event)
|
|
|
|
onUnmounted(() => service.stop())
|
|
|
|
return { state, send }
|
|
}
|
|
|
|
// Usage
|
|
const { state, send } = useMachine({
|
|
id: 'toggle',
|
|
initial: 'inactive',
|
|
states: {
|
|
inactive: { on: { TOGGLE: 'active' } },
|
|
active: { on: { TOGGLE: 'inactive' } }
|
|
}
|
|
})
|
|
|
|
// In template: state.value.matches('active')
|
|
send('TOGGLE')
|
|
```
|
|
|
|
**Integrating with Redux-style stores:**
|
|
```javascript
|
|
import { shallowRef, readonly } from 'vue'
|
|
|
|
export function createStore(reducer, initialState) {
|
|
const state = shallowRef(initialState)
|
|
|
|
function dispatch(action) {
|
|
state.value = reducer(state.value, action)
|
|
}
|
|
|
|
function getState() {
|
|
return state.value
|
|
}
|
|
|
|
return {
|
|
state: readonly(state), // Prevent direct mutations
|
|
dispatch,
|
|
getState
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const store = createStore(
|
|
(state, action) => {
|
|
switch (action.type) {
|
|
case 'INCREMENT':
|
|
return { ...state, count: state.count + 1 }
|
|
default:
|
|
return state
|
|
}
|
|
},
|
|
{ count: 0 }
|
|
)
|
|
|
|
store.dispatch({ type: 'INCREMENT' })
|
|
console.log(store.state.value.count) // 1
|
|
```
|
|
|
|
**Why NOT use ref() for external state:**
|
|
```javascript
|
|
import { ref } from 'vue'
|
|
import { produce } from 'immer'
|
|
|
|
// WRONG: ref() deep-wraps the state
|
|
const state = ref({ nested: { value: 1 } })
|
|
|
|
// This creates double-proxying:
|
|
// 1. Vue wraps state in Proxy
|
|
// 2. External library may also wrap/expect raw objects
|
|
// 3. Causes identity issues and potential conflicts
|
|
|
|
// WRONG: Mutating ref with Immer
|
|
state.value = produce(state.value, draft => {
|
|
draft.nested.value = 2
|
|
})
|
|
// Vue's deep proxy on state.value may interfere with Immer's proxies
|
|
```
|
|
|
|
**Correct pattern with shallowRef:**
|
|
```javascript
|
|
import { shallowRef } from 'vue'
|
|
|
|
// CORRECT: shallowRef only tracks .value replacement
|
|
const state = shallowRef({ nested: { value: 1 } })
|
|
|
|
// External library works with raw objects inside
|
|
state.value = produce(state.value, draft => {
|
|
draft.nested.value = 2
|
|
})
|
|
// Clean separation: Vue tracks outer ref, library manages inner state
|
|
```
|
|
|
|
## Reference
|
|
- [Vue.js Reactivity in Depth - Integration with External State](https://vuejs.org/guide/extras/reactivity-in-depth.html#integration-with-external-state-systems)
|
|
- [Vue.js shallowRef API](https://vuejs.org/api/reactivity-advanced.html#shallowref)
|
|
- [Immer Documentation](https://immerjs.github.io/immer/)
|
|
- [XState Documentation](https://xstate.js.org/)
|