---
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/)
