# Advanced Reactivity

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Reusability
Source: https://www.devtutorials.ai/learn/vue/reusability/advanced-reactivity

> readonly(), shallowReadonly() and isReadonly(), shallowRef(), shallowReactive() and triggerRef(), toRef(), toRefs(), isRef() and unref(), toRaw() and markRaw(), a debounced ref with customRef(), and grouping effects with effectScope().

## Beyond ref and reactive

- `ref()` and `reactive()` cover most apps.
- Some jobs need finer control:
  - stop others from changing state
  - make huge data cheaper to watch
  - keep a link when you pull out one property
  - build your own kind of ref
- Vue has small helper functions for each job.

**Simple lesson:** these helpers fine-tune how Vue watches your data.

### A Real-World Example

- Meera's search box looks up dishes as people type.
- Searching after every key wastes work.
- She wants to search only when typing pauses for 400 ms. This is called **debouncing**.
- `customRef()` builds a ref that waits.

const dishes: string[] = ["Masala Dosa", "Rava Dosa", "Idli", "Vada", "Pongal"];
const query = useDebouncedRef<string>("");

const results = computed(() => dishes.filter((d) => d.toLowerCase().includes(query.value.toLowerCase())));
watch(query, (q) => console.log("Searching for:", q));
</script>

<template>
    <input v-model="query" placeholder="Type quickly, e.g. dosa" />
    <ul>
      <li v-for="d in results" :key="d">{{ d }}</li>
    </ul>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** type fast. The Console logs one search, after you pause.
- The `set` waited 400 ms before calling `trigger()`.

### What You Will Learn

1. How to make state read-only.
2. Shallow refs and shallow reactive objects.
3. How to trigger an update by hand.
4. How to pull out properties without losing reactivity.
5. How to check and unwrap refs.
6. How to get the raw object, and how to stop Vue from watching one.
7. How `customRef()` works.
8. How to stop many watchers at once with `effectScope()`.

## Read-Only State

- `readonly(obj)` returns a copy that cannot be changed. It still updates when the original changes.
- Trying to change it gives a warning and does nothing.
- `shallowReadonly(obj)` protects only the top level.
- `isReadonly(x)` tells you if something is read-only.
- Use it to share state with code that should only read it, as in **Slots, Attributes and Provide/Inject**.

const menu = reactive({ special: "Dosa", prices: { dosa: 90 } });
const forDisplay = readonly(menu);
const shallow = shallowReadonly(menu);

function tryChanges(): void {
    (forDisplay as { special: string }).special = "Hacked";
    shallow.prices.dosa = 95;
    menu.special = "Pongal";
    console.log("isReadonly(forDisplay):", isReadonly(forDisplay), "isReadonly(menu):", isReadonly(menu));
}
</script>

<template>
    <p>Special: {{ forDisplay.special }}, Dosa: ₹{{ forDisplay.prices.dosa }}</p>
    <button @click="tryChanges">Try changes</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** the change through `forDisplay` was blocked, with a warning.
- `shallow.prices.dosa` worked, because only the top level is protected.
- The change to the original `menu` showed through `forDisplay`.

## Shallow Reactivity

- A normal ref watches **every** level inside its value. For huge data, this costs time.
- `shallowRef(x)` watches only `.value` itself. Changing deep inside does nothing.
- Replace the whole value to update: `list.value = [...]`.
- Or change it in place and call `triggerRef(list)` to update by hand.
- `shallowReactive(obj)` watches only the top-level properties.

const orders = shallowRef<number[]>([501, 502]);
const kitchen = shallowReactive({ open: true, staff: { chefs: 2 } });

function pushOnly(): void {
    orders.value.push(orders.value.length + 501);
    console.log("pushed, array has", orders.value.length);
}

function pushAndTrigger(): void {
    orders.value.push(orders.value.length + 501);
    triggerRef(orders);
}

function replace(): void {
    orders.value = [...orders.value, orders.value.length + 501];
}
</script>

<template>
    <p>Orders: {{ orders.join(", ") }}</p>
    <button @click="pushOnly">Push only</button>
    <button @click="pushAndTrigger">Push + triggerRef</button>
    <button @click="replace">Replace array</button>
    <p>Open: {{ kitchen.open }}, chefs: {{ kitchen.staff.chefs }}</p>
    <button @click="kitchen.open = !kitchen.open">Toggle open (watched)</button>
    <button @click="kitchen.staff.chefs++">Add chef (not watched)</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

Use shallow refs for big data you replace as a whole, such as a 10 000-row list from a server. You will meet this again in **Performance**.

**Simple lesson:** shallow means "watch the top only". Replace the value, or call `triggerRef()`.

## Working with Refs

- `toRef(obj, "key")` makes a ref linked to one property of a reactive object.
- `toRefs(obj)` does this for every property. Then destructuring is safe.
- `isRef(x)` checks if `x` is a ref.
- `unref(x)` gives `x.value` for a ref, or `x` itself. `toValue()` also calls getters.

const order = reactive({ dish: "Idli", plates: 2 });

const platesRef = toRef(order, "plates");
const { dish } = toRefs(order);

function more(): void {
    platesRef.value++;
    console.log("order.plates is now", order.plates);
    console.log("isRef(dish):", isRef(dish), "unref(dish):", unref(dish), "unref(5):", unref(5));
}
</script>

<template>
    <p>{{ order.plates }} × {{ dish }}</p>
    <button @click="more">More (through toRef)</button>
    <button @click="dish = 'Vada'">Change dish (through toRefs)</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** `toRefs()` makes destructuring safe. `toRef()` links one property.

Composables often accept a reactive object and use `toRefs()` on it, so callers can destructure the result. You saw composables return refs in **Composables, Directives and Plugins**.

## Raw Objects

- `toRaw(proxy)` gives back the plain object behind a reactive proxy. Changes to it are not seen.
- `markRaw(obj)` tells Vue to never make that object reactive.
- Use `markRaw` for big objects from other libraries, such as a map or chart instance.

class Printer {
    jobs = 0;
    print(text: string): void {
      this.jobs++;
      console.log("PRINT:", text);
    }
}

const original = { table: 4 };
const state = reactive({ bill: original, printer: markRaw(new Printer()) });

function check(): void {
    console.log("toRaw(state.bill) === original:", toRaw(state.bill) === original);
    console.log("printer is reactive:", isReactive(state.printer));
    state.printer.print("Bill for table 4");
}
</script>

<template>
    <button @click="check">Check</button>
    <p>Jobs printed: {{ state.printer.jobs }} (not watched)</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

Changing data through `toRaw()` does not update the screen. Use it only to read, or to hand data to code outside Vue.

## customRef()

- `customRef((track, trigger) => ({ get, set }))` builds your own ref.
- Call `track()` in `get`, so Vue knows who reads it.
- Call `trigger()` in `set`, when you want the readers to update.
- You decide **when** to trigger. That is how the debounced search box above works.

## effectScope()

- Watchers and computed values are **effects**.
- In a component, Vue stops them on unmount.
- Outside a component, you must stop them yourself.
- `effectScope()` collects every effect made inside `scope.run(...)`.
- `scope.stop()` stops all of them at once.

const plates = ref<number>(1);
const log = ref<string[]>([]);
const scope = effectScope();

scope.run(() => {
    const doubled = computed(() => plates.value * 2);
    watch(plates, (p) => log.value.push(\`plates \${p}\`));
    watch(doubled, (d) => log.value.push(\`doubled \${d}\`));
});
</script>

<template>
    <p>Plates: {{ plates }}</p>
    <button @click="plates++">More</button>
    <button @click="scope.stop()">Stop the scope</button>
    <p>Log: {{ log.join(" | ") }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** "More" added two log lines each time.
- After "Stop the scope", `plates` still changes, but nothing is logged.

**Simple lesson:** `customRef()` controls when a ref updates. `effectScope()` stops many effects together.

## TL;DR

- **`readonly()`** = a read-only view that still follows the original.
- **`shallowReadonly()`** = protects the top level only.
- **`shallowRef()`** = watches `.value` only. Replace it, or call `triggerRef()`.
- **`shallowReactive()`** = watches top-level properties only.
- **`toRef()` / `toRefs()`** = refs linked to reactive properties.
- **`isRef()` / `unref()`** = check a ref / read it or the plain value.
- **`toRaw()` / `markRaw()`** = the plain object behind a proxy / never reactive.
- **`customRef()`** = your own ref with `track()` and `trigger()`.
- **`effectScope()`** = collects effects. `scope.stop()` stops them all.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Changing a `readonly` view | It changes the original | Change the original | A warning, nothing changes |
| `push` on a `shallowRef` array | The list updates | Replace the array, or `triggerRef()` | The screen stays the same |
| Destructuring `reactive()` | Values stay linked | Use `toRefs()` | Values freeze |
| Changing data through `toRaw()` | The screen updates | Change the reactive version | The screen is out of date |
| Forgetting `track()` in `customRef` | Vue tracks it by itself | Call `track()` in `get` | Readers never update |
| Watchers outside components, never stopped | Vue stops them | Use `effectScope()` and `stop()` | Memory leaks and repeated work |

</div>

## Check Yourself

## Exercises

## What to Remember

- `readonly()` shares state that others may only read.
- Shallow refs and objects watch only the top. Replace values or use `triggerRef()`.
- `toRefs()` keeps destructured properties linked.
- `markRaw()` keeps big library objects out of reactivity.
- `customRef()` decides when a ref updates. `effectScope()` stops many effects at once.

Next: **TypeScript with Vue**
