# Reactive State

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Reactivity
Source: https://www.devtutorials.ai/learn/vue/reactivity/reactive-state

> What reactivity is, <script setup> and top-level bindings, ref() and .value, unwrapping in templates, reactive() and proxies, the limits of reactive() and the destructuring problem, ref vs reactive, reactive arrays, Map and Set, DOM update timing with nextTick(), and typing state in TypeScript.

## Reactivity

- **Reactivity** = when data changes, everything that uses it updates by itself.
- A spreadsheet works like this. Change one cell, and the total cell changes too.
- Vue brings this idea to web pages.
- You change the state. Vue updates the screen.
- But Vue only watches state you create in a special way.

**Simple lesson:** reactive state is data that Vue watches.

### A Real-World Example

- Meera's page shows how many plates are in the cart.
- She tried a normal `let` variable. The number on screen never moved.
- Then she used `ref()`. Now the screen follows every click.
- Run both buttons and compare.

let plainCount: number = 0;
const refCount = ref<number>(0);

function addPlain(): void {
    plainCount++;
    console.log("plainCount is now", plainCount);
}

function addRef(): void {
    refCount.value++;
}
</script>

<template>
    <p>Plain variable: {{ plainCount }}</p>
    <button @click="addPlain">Add (plain)</button>
    <p>ref: {{ refCount }}</p>
    <button @click="addRef">Add (ref)</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** the plain variable did go up. The Console proves it.
- But the screen still shows 0, because Vue does not watch plain variables.
- The `ref` number updates on every click.

### What You Will Learn

1. How `<script setup>` gives values to the template.
2. How to make state with `ref()`.
3. When you need `.value`, and when you do not.
4. How to make state with `reactive()`.
5. The limits of `reactive()`, and the destructuring trap.
6. When to choose `ref` or `reactive`.
7. How arrays, `Map` and `Set` stay reactive.
8. When the screen really updates, and `nextTick()`.
9. How to type state in TypeScript.

## script setup

- `<script setup>` is the code part of a component.
- Every **top-level** name in it can be used in the template: variables, functions and imports.
- **Top-level** = written directly in the script, not inside a function.
- Imports work too. You can use an imported function straight in the template.

const shop: string = "Meera's Kitchen";
const tables = ref<number>(6);

function openTable(): void {
    tables.value--;
}
</script>

<template>
    <h2>{{ shop }}</h2>
    <p>Free tables: {{ tables }}</p>
    <p>Deposit: {{ formatRupees(500) }}</p>
    <button @click="openTable">Seat a family</button>
</template>
`,
    "money.ts": `export function formatRupees(amount: number): string {
    return "₹" + amount.toLocaleString("en-IN");
}
`,
    "main.ts": `import { createApp } from "vue";

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

**Simple lesson:** if a name sits at the top of `<script setup>`, the template can use it.

## ref()

- `ref(value)` wraps a value in a small box that Vue watches.
- The box has one property: `.value`.
- In the script, read and change it with `.value`.
- In the template, write the name only. Vue opens the box for you. This is called **unwrapping**.
- Why `const`? The **box** never changes, only what is inside it. So `const count = ref(0)` and then `count.value++` is correct.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 190" role="img" aria-labelledby="fig-vue-ref" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-ref">Animation: a ref is a box holding a value. The script changes box.value from 0 to 1. Vue sees the change and the template shows 1</title>
  <rect x="30" y="40" width="200" height="110" rx="14" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
  <text x="130" y="30" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700 }}>const orders = ref(0)</text>
  <rect x="80" y="70" width="100" height="56" rx="10" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} />
  <text x="130" y="90" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--mono)", fontSize: 12 }}>.value</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, fill: "var(--ngc-blue)" }}>
    <text x="130" y="116" textAnchor="middle"><animate attributeName="opacity" values="1;1;0;0;1" keyTimes="0;0.4;0.41;0.97;1" dur="6s" repeatCount="indefinite" />0</text>
    <text x="130" y="116" textAnchor="middle" opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.4;0.41;0.97;1" dur="6s" repeatCount="indefinite" />1</text>
  </g>
  <text x="320" y="78" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700 }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.2;0.25;0.6;0.65" dur="6s" repeatCount="indefinite" />orders.value++</text>
  <text x="320" y="110" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.42;0.45;0.92;1" dur="6s" repeatCount="indefinite" />Vue: "it changed!"</text>
  <rect x="410" y="40" width="200" height="110" rx="14" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
  <text x="510" y="30" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700 }}>{"{{ orders }}"}</text>
  <g style={{ fontFamily: "var(--sans)", fontSize: 20, fontWeight: 700, fill: "var(--ngc-green)" }}>
    <text x="510" y="102" textAnchor="middle"><animate attributeName="opacity" values="1;1;0;0;1" keyTimes="0;0.55;0.56;0.97;1" dur="6s" repeatCount="indefinite" />Orders: 0</text>
    <text x="510" y="102" textAnchor="middle" opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.55;0.56;0.97;1" dur="6s" repeatCount="indefinite" />Orders: 1</text>
  </g>
  <text x="320" y="180" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13 }}>Script: orders.value. Template: just orders.</text>
</svg>
<figcaption>A ref is a box. Change what is inside with .value, and Vue updates the screen.</figcaption>
</figure>

const customer = ref<string>("Asha");
const plates = ref<number>(1);
const order = ref({ dish: "Dosa", spicy: false });

function next(): void {
    customer.value = "Ravi";
    plates.value += 2;
    order.value.spicy = true;
    console.log(customer.value, plates.value, order.value);
}
</script>

<template>
    <p>{{ customer }} ordered {{ plates }} plates of {{ order.dish }}</p>
    <p>Spicy: {{ order.spicy ? "yes" : "no" }}</p>
    <button @click="next">Next customer</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** the script used `.value` every time.
- The template used `customer`, `plates` and `order.dish` with no `.value`.
- A ref can hold an object too. Changes deep inside it are watched.

Template unwrapping only works for top-level refs. A ref stored inside a plain object, such as `const box = { count: ref(1) }`, is not unwrapped in an expression like `{{ box.count + 1 }}`. Keep refs at the top level.

**Simple lesson:** `ref()` makes a watched box. Use `.value` in the script, not in the template.

## reactive()

- `reactive(object)` makes a whole object reactive.
- There is no `.value`. You use the object's properties directly.
- It works by wrapping the object in a **proxy**.
- **Proxy** = a stand-in object that notices every read and every change.
- Always change the object **through the proxy** that `reactive()` returns. Changing the original plain object is not noticed.

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

function addPlate(): void {
    order.plates++;
}
</script>

<template>
    <p>Table {{ order.table }}: {{ order.plates }} × {{ order.dish }}</p>
    <button @click="addPlate">One more plate</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

### Limits of reactive()

- It only works with objects, arrays, `Map` and `Set`. Not with a plain number or text.
- You cannot replace the whole object. `order = { ... }` breaks the link with the screen.
- **Destructuring** breaks reactivity. `let { plates } = order` copies the number out of the proxy.
- After that, changing `plates` does not change `order` or the screen.

const order = reactive({ plates: 1 });
let { plates } = order;

function addCopy(): void {
    plates++;
    console.log("copy:", plates, "order:", order.plates);
}

function addReal(): void {
    order.plates++;
}
</script>

<template>
    <p>Plates: {{ order.plates }}</p>
    <button @click="addCopy">Add to the copy</button>
    <button @click="addReal">Add to the order</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** "Add to the copy" changed only the copy. The Console shows it.
- "Add to the order" changed the real state, so the screen updated.

Need separate variables from a reactive object? Use `toRefs(order)`. It turns each property into a ref that stays linked. You will meet it again in **Advanced Reactivity**.

**Simple lesson:** `reactive()` makes an object watched. Do not destructure it or replace it.

## ref vs reactive

<div className="tscroll">

| Point | `ref()` | `reactive()` |
|---|---|---|
| Holds | Any value: number, text, object, array | Objects, arrays, Map, Set |
| In the script | `count.value` | `order.plates` |
| In the template | `count` | `order.plates` |
| Replace the whole value | Yes: `list.value = []` | No |
| Destructure safely | Yes | No (use `toRefs`) |
| Official advice | Use as the main tool | Use for grouped values, carefully |

</div>

- The Vue docs advise `ref()` as your main tool, because it has fewer traps.
- This course uses `ref()` most of the time.

**Simple lesson:** when in doubt, use `ref()`.

## Arrays, Map and Set

- Array methods such as `push`, `splice` and `sort` are watched.
- Replacing the array with `list.value = [...]` is watched too.
- A reactive `Map` and a reactive `Set` are watched as well.
- `Set` keeps only unique values. Adding "spicy" twice keeps one "spicy".

const dishes = ref<string[]>(["Dosa", "Idli"]);
const stock = reactive(new Map<string, number>([["Dosa", 5]]));
const tags = reactive(new Set<string>());

function update(): void {
    dishes.value.push("Vada");
    stock.set("Dosa", (stock.get("Dosa") ?? 0) - 1);
    tags.add("spicy");
    tags.add("spicy");
}
</script>

<template>
    <p>Dishes: {{ dishes.join(", ") }}</p>
    <p>Dosa left: {{ stock.get("Dosa") }}</p>
    <p>Tags: {{ [...tags].join(", ") || "none" }} ({{ tags.size }})</p>
    <button @click="update">Update</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

**Simple lesson:** arrays, Maps and Sets stay reactive when you change them with their own methods.

## When the Screen Updates

- Vue does not update the screen at the exact moment you change state.
- It waits until your function finishes, then updates once.
- This saves work when you change many values in a row.
- To read the screen after the update, `await nextTick()`.

const status = ref<string>("Closed");

async function open(): Promise<void> {
    status.value = "Open";
    const p = document.getElementById("status");
    console.log("Right away:", p?.textContent);
    await nextTick();
    console.log("After nextTick:", p?.textContent);
}
</script>

<template>
    <p id="status">{{ status }}</p>
    <button @click="open">Open the kitchen</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** right after the change, the page still said "Closed".
- After `await nextTick()`, the page said "Open".

**Simple lesson:** Vue updates the screen a moment later, all at once. `nextTick()` waits for it.

## Typing State in TypeScript

- `ref` usually guesses the type from the starting value: `ref(0)` is a number.
- Give the type yourself when the guess is not enough: `ref<string | null>(null)`.
- For a list of objects, describe one object with an `interface`.
- `reactive` takes its type from the object, or from an interface you give it.
- In the JavaScript flavour, the types are simply removed.

interface Dish {
    name: string;
    price: number;
}

interface Bill {
    table: number;
    paid: boolean;
}

const menu = ref<Dish[]>([{ name: "Dosa", price: 90 }]);
const selected = ref<Dish | null>(null);
const bill: Bill = reactive({ table: 2, paid: false });

function pick(): void {
    selected.value = menu.value[0];
    bill.paid = true;
}
</script>

<template>
    <p>Selected: {{ selected ? selected.name : "nothing" }}</p>
    <p>Table {{ bill.table }} paid: {{ bill.paid }}</p>
    <button @click="pick">Pick the first dish</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

Real projects check these types with `vue-tsc` when you build. In this lab, types are removed before the code runs, and the editor does not check types in `.vue` files. So a wrong type does not stop the preview. Run `vue-tsc` in a real project to catch it.

**Simple lesson:** add types with `ref<Type>()` and interfaces. JavaScript just leaves them out.

## TL;DR

- **Reactivity** = when state changes, the screen updates by itself.
- **`<script setup>`** = every top-level name is usable in the template.
- **`ref(x)`** = a watched box. Use `.value` in the script, not in the template.
- **`reactive(obj)`** = a watched object (a proxy). No `.value`.
- **Destructuring trap** = `let { a } = reactiveObj` copies the value and loses reactivity.
- **ref vs reactive** = prefer `ref()`. It can be replaced and destructured safely.
- **Arrays, Map, Set** = stay reactive when changed with their own methods.
- **`nextTick()`** = waits until Vue has updated the screen.
- **Typing** = `ref<Dish[]>([])`, `ref<Dish | null>(null)`.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Plain `let` for state | Any variable updates the screen | Use `ref()` or `reactive()` | The value changes, the screen does not |
| No `.value` in the script | `count++` works on a ref | Write `count.value++` | Nothing happens, or a type error |
| Destructuring `reactive()` | `let { plates } = order` stays linked | Use `order.plates` or `toRefs` | Changes do not reach the screen |
| Replacing a reactive object | `order = { ... }` is fine | Change its properties, or use a ref | The screen keeps the old object |
| `reactive(0)` | reactive works with any value | Use `ref(0)` for single values | A warning, and nothing is reactive |
| Reading the DOM right after a change | The screen updates at once | `await nextTick()` first | You read the old text |
| A ref inside a plain object in `{{ }}` | All refs are unwrapped | Keep refs at the top level | You see `[object Object]` or wrong maths |

</div>

## Check Yourself

## Exercises

## What to Remember

- Only `ref()` and `reactive()` state updates the screen.
- Refs use `.value` in the script. The template unwraps them.
- Never destructure or replace a `reactive()` object.
- Prefer `ref()`. It works for every kind of value.
- Vue updates the screen after your code finishes. `nextTick()` waits for it.

Next: **Computed Properties and Watchers**
