Master Vue 3 Ref: One‑Way Data Flow, Component Communication & Custom useRefState
This article explains how to use Vue 3's ref and shallowRef for reactive state, demonstrates one‑way data flow between parent and child components via props and events, and introduces a custom useRefState composable that mimics React's useState for cleaner state management.
In Vue 3, reactive variables are defined using ref , e.g., const name = ref(""); name.value = "test" . After defining, changes update the UI.
This article discusses managing Ref state across components and better encapsulating Ref read/write.
One‑Way Data Flow
All props follow one‑way binding: they change when the parent updates and flow down to children, preventing children from unintentionally modifying parent state.
Example: a header with a menu button toggles a sidebar; clicking close hides it.
<code><script>
import { ref } from "vue";
import Header from "./components/Header.vue";
import Nav from "./components/Nav.vue";
export default {
components: { Header, Nav },
setup() {
const isOpen = ref(false);
const handToggle = () => isOpen.value = !isOpen.value;
return { isOpen, handToggle };
},
};
</script>
<template>
<Header @toggle="handToggle" />
<Nav :isOpen="isOpen" @toggle="handToggle" />
</template></code>header.vue
<code><script>
export default {
setup(props, { emit }) {
const handleMenu = () => {
emit("toggle");
};
return { handleMenu };
},
};
</script>
<template>
<header>
<nav>
<button @click="handleMenu">menu</button>
</nav>
</header>
</template></code>nav.vue
<code><script>
export default {
props: {
isOpen: { type: Boolean, default: false },
},
setup(props, { emit }) {
const handleMenu = () => {
emit("toggle");
};
return { props, handleMenu };
},
};
</script>
<template>
<div :class="['nav', { open: props.isOpen }]">
<a @click="handleMenu">close</a>
</div>
</template></code>The flow: Header emits toggle , the parent updates isOpen , passes it as a prop to Nav, Nav's close link emits toggle , the parent updates isOpen again, completing the cycle.
Although functional, this adds extra event communication; a simpler approach removes the event layer.
<code><script>
// ... omitted for brevity
export default {
setup() {
const isOpen = ref(false);
const handleOpenMenu = () => isOpen.value = !isOpen.value;
return { isOpen, handleOpenMenu };
},
};
</script>
<template>
<Header :handleOpenMenu="handleOpenMenu" />
<Nav :isOpen="isOpen" :handleOpenMenu="handleOpenMenu" />
</template></code>Passing the state‑modifying function directly to child components simplifies the data flow.
One‑way data flow ensures data moves from parent to child via props, making the flow clear and predictable for large applications.
React has a similar "Lifting State Up" pattern.
Encapsulation
Encapsulating state‑modifying functions hides implementation details, similar to React's useState .
<code>import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [address, setAddress] = useState('Taiwan');
}</code>In Vue we can create a useRefState composable to manage state like React's useState .
<code>import { shallowRef } from "vue";
export function useRefState(baseState) {
const state = shallowRef(baseState);
const update = (newValue) => {
state.value = newValue;
};
return [state, update];
}
</code>Usage example:
<code><script setup>
import { useRefState } from "./composables/useRefState.js";
const [name, setName] = useRefState("mike");
const [info, setInfo] = useRefState({
name: "mike",
age: 12,
});
</script>
<template>
<h2>name: {{ name }}</h2>
<pre>info: {{ info }}</pre>
<input type="text" v-model="name" />
<button @click="setName('jacky')">set name</button>
<button @click="setInfo({ name: 'andy', age: 20 })">set info</button>
</template>
</code>This reduces boilerplate by avoiding separate set functions for each ref while preserving reactivity.
shallowRef differs from ref : it only makes the top‑level property reactive, improving performance for large or deep structures.
When using useRefState , updates replace the whole object, similar to React's state handling.
<code>const [info, setInfo] = useState({
name: "mike",
age: 12,
});
// Replace the entire object
setInfo({
...info,
address: addr,
});
</code>Thus we avoid unnecessary ref usage for large objects.
Conclusion
Vue 3's reactivity offers flexible ways to manage cross‑component state, whether via event emission or direct method passing. Encapsulating state with a custom useRefState composable provides a React‑like experience, promoting clear data flow and maintainable code as projects grow.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.