Reactivity in Vue 3: From setup() to Standalone State

JavaScript is not reactive by default. If one part of your application reads a variable and another part later reassigns it, the first part has no way of knowing the value changed. Vue solves this by wrapping state in proxies and tracking dependencies. In Vue 2, reactivity was largely implicit: props, computed, and data() were reactive as long as properties existed when the component mounted. Vue 3 keeps that model but also exposes lower-level primitives—reactive, ref, toRefs, and toRef—so you can build reactive state outside the Options API.

The shift matters because the Composition API's setup() hook runs before the component instance is created. Inside setup(), you don't have access to the familiar data object or component methods, so you need explicit tools to create reactive variables yourself. The following utilities give you that control.

Working with Objects: reactive

The reactive method is the direct successor to Vue.observable() from Vue 2.6, and it's how the Options API powers its data object internally. When you pass an object to reactive, every property on that object becomes reactive, and Vue tracks changes deeply:

import { reactive } from 'vue'

// reactive state
let user = reactive({
        "id": 1,
        "name": "Leanne Graham",
        "username": "Bret",
        "email": "[email protected]",
        "address": {
            "street": "Kulas Light",
            "suite": "Apt. 556",
            "city": "Gwenborough",
            "zipcode": "92998-3874",
            "geo": {
                "lat": "-37.3159",
                "lng": "81.1496"
            }
        },
        "phone": "1-770-736-8031 x56442",
        "website": "hildegard.org",
        "company": {
            "name": "Romaguera-Crona",
            "catchPhrase": "Multi-layered client-server neural-net",
            "bs": "harness real-time e-markets"
        },
        "cars": {
            "number": 0
        }
    })

Any component that renders user will automatically update when the object or any of its nested properties change. The conversion is deep, so a property like user.name works just as well as a top-level array or object value.

Standalone Values: ref

Objects aren't the only data type you need to make reactive. For strings, numbers, booleans, and arrays, you could wrap them inside a reactive object, but that quickly becomes awkward. The ref method exists for exactly this case: it takes a value of any type and returns a reactive container around it.

let property = {
  rooms: '4 rooms',
  garage: true,
  swimmingPool: false
}
let reactiveProperty = ref(property)
console.log(reactiveProperty)
// prints {
// value: {rooms: "4 rooms", garage: true, swimmingPool: false}
// }

Under the hood, ref converts its argument into an object with a value key. To read or reassign the data, you access variable.value. For example, a typical component that fetches a list of users and computes a total looks like this conceptually:

<template>
  <div class="home">
    <form @click.prevent="">
      <table>
        <tr>
          <th>Name</th>
          <th>Username</th>
          <th>email</th>
          <th>Edit Cars</th>
          <th>Cars</th>
        </tr>
        <tr v-for="user in users" :key="user.id">
          <td>{{ user.name }}</td>
          <td>{{ user.username }}</td>
          <td>{{ user.email }}</td>
          <td>
            <input
              type="number"
              style="width: 20px;"
              name="cars"
              id="cars"
              v-model.number="user.cars.number"
            />
          </td>
          <td>
            <cars-number :cars="user.cars" />
          </td>
        </tr>
      </table>
      <p>Total number of cars: {{ getTotalCars }}</p>
    </form>
  </div>
</template>
<script>
  // @ is an alias to /src
  import carsNumber from "@/components/cars-number.vue";
  import axios from "axios";
  import { ref } from "vue";
  export default {
    name: "Home",
    data() {
      return {};
    },
    setup() {
      let users = ref([]);
      const getUsers = async () => {
        let { data } = await axios({
          url: "data.json",
        });
        users.value = data;
      };
      return {
        users,
        getUsers,
      };
    },
    components: {
      carsNumber,
    },
    created() {
      this.getUsers();
    },
    computed: {
      getTotalCars() {
        let users = this.users;
        let totalCars = users.reduce(function(sum, elem) {
          return sum + elem.cars.number;
        }, 0);
        return totalCars;
    },
  };
</script>

Note that when ref values are unwrapped automatically in templates or in the outer component scope, an array-level ref like users is still an array at the top level. Inside setup(), however, you'll need to use .value unless the value is being accessed through a template that Vue has unwrapped.

Destructuring Props Without Losing Reactivity: toRefs

The Composition API's setup(props, context) gives you direct access to props. But destructuring a prop object—for example, pulling out a single property to use in a computed—breaks the reactive link, because the prop object itself is reactive, not the individual properties you extract. The toRefs method fixes this by converting a reactive object into a plain object where each property is its own ref:

<template>
  <p>{{ cars.number }}</p>
</template>
<script>
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      console.log(props);
   // prints {gender: "female", cars: Proxy}
    },
  };
</script>
<style></style>

If you have a prop named cars, applying toRefs on the props object yields a cars property that behaves like a ref. This lets you destructure within setup() while preserving the two-way reactivity:

{
  value: cars: {
    number: 0
  }

Once you have a ref from toRefs, you can watch it with the Composition API's watch function and respond to changes as needed. This is essential when you need to transform a prop value in isolation:

setup(props) {
      let { cars } = toRefs(props);
      watch(
        () => cars,
        (cars, prevCars) => {
          console.log("deep ", cars.value, prevCars.value);
        },
        { deep: true }
      );
    }

Targeting One Property: toRef

toRef is the single-property sibling of toRefs. Instead of converting an entire reactive object, toRef extracts one property into a ref that remains connected to its source. Both the original reactive object and the new ref stay in sync:

const cars = reactive({
  Toyota: 1,
  Honda: 0
})

const NumberOfHondas = toRef(state, 'Honda')

NumberOfHondas.value++
console.log(state.Honda) // 1

state.Honda++
console.log(NumberOfHondas.value) // 2

One distinct advantage of toRef over toRefs is that it works even when the property doesn't exist yet on the source object. If you create a ref pointing to a property that is currently null, Vue still registers a watcher on that slot, so when the property is later assigned (on props, for instance), the ref updates automatically. This pattern is useful for working with prop values that aren't guaranteed to be present at mount time:

<template>
  <p>{{ cars.number }}</p>
</template>
<script>
  import { watch, toRefs, toRef } from "vue";
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      let { cars } = toRefs(props);
      let gender = toRef(props, "gender");
      console.log(gender.value);
      watch(
        () => cars,
        (cars, prevCars) => {
          console.log("deep ", cars.value, prevCars.value);
        },
        { deep: true }
      );
    },
  };
</script>

You can also use toRef on a reactive object you've already created, which gives you a reactive alias to a single property without keeping the whole object in scope.

Understanding these four primitives covers the core of declarative state management in Vue 3. reactive handles objects with deep reactivity, ref handles primitives and single values, toRefs preserves reactivity when destructuring, and toRef lets you isolate a property while keeping it linked to the original state.