Vue.js 26 🟢 Vue 3 Component Lifecycle Hooks Overview (onMounted, onUpdated, onUnmounted)
A Vue component is not a static piece of markup. It is created, mounted into the DOM, updated when its reactive state changes, and eventually unmounted. Each of these phases is a moment when the component may need to do something: fetch data after it appears, react to a prop change, clean up a timer before it disappears. Vue’s lifecycle hooks are the API for those moments. They are functions that the component registers, and Vue calls them at the corresponding phase of the component’s life.
In Vue 3’s Composition API, lifecycle hooks are imported from vue and called inside setup(). onMounted runs after the component’s DOM has been created and inserted. onUpdated runs after the component re-renders because of a reactive change. onUnmounted runs after the component has been removed from the DOM. Each hook can be called multiple times in a single component, and the callbacks run in the order they were registered. They are the Composition API’s replacement for the Options API’s mounted, updated, and unmounted options, but with a key difference: the hooks are not methods on a component instance. They are standalone functions that register a callback, and the callback runs at the right time.
The lifecycle is not just three moments. Vue 3 has a full sequence: onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, plus the activation hooks for <KeepAlive> and the error and render-tracking hooks. The three hooks in this chapter’s title — onMounted, onUpdated, onUnmounted — are the ones that cover the most common cases: initialization, reaction to changes, and cleanup. Understanding them is the prerequisite for understanding the rest.
This chapter covers three areas. First, why lifecycle hooks exist — the problem of running code at specific moments in a component’s life, and the difference between the Composition API and the Options API. Second, how the three core hooks work — onMounted for DOM access and initialization, onUpdated for reacting to re-renders, and onUnmounted for cleanup. Third, how to use them correctly — the timing of each hook relative to the DOM, the common patterns for each, and the pitfalls that come from using the wrong hook for the job. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the lifecycle sequence.
Key point: onMounted runs after the component’s DOM is inserted; use it for DOM access, data fetching, and initialization. onUpdated runs after the component re-renders; use it sparingly, because it runs on every update. onUnmounted runs after the component is removed; use it for cleanup. All three are registered inside setup() and can be called multiple times. They do not run during server-side rendering.
Why lifecycle hooks exist
The timing problem. A component’s code runs at different times, and not all code is valid at all times. Code that reads ref values is valid during setup. Code that accesses the DOM is not valid during setup, because the DOM does not exist yet. Code that fetches data should run after the component is mounted, so the user sees the loading state. Code that cleans up a timer should run before the component is destroyed. Without lifecycle hooks, there would be no way to express “run this at this moment.” The hooks provide the timing.
The Options-API problem. In Vue 2 and the Options API, lifecycle hooks are options on the component object: mounted(), updated(), beforeDestroy(). They are methods, they have access to this, and they are declared once. In the Composition API, a component may be composed of multiple functions, each with its own logic. A useTimer composable needs to register its own onUnmounted cleanup, and a useFetch composable needs to register its own onMounted fetch. The Options API cannot express this, because there is only one mounted option per component. The Composition API solves this by making the hooks functions that register callbacks. Each composable calls onMounted with its own callback, and all of them run in order.
The multiple-callbacks problem. A component that uses three composables, each of which registers an onMounted callback, has three callbacks that run at mount time. The Composition API supports this: onMounted can be called multiple times, and the callbacks run in the order they were registered. This is what makes composables possible. Each composable is self-contained: it registers its own hooks, and the component does not need to know about them. The lifecycle hooks are the mechanism that makes composition work.
The cleanup problem. Some resources must be released when a component is unmounted. A timer must be cleared. A WebSocket must be closed. An event listener must be removed. A subscription must be canceled. If these are not released, they leak: the timer keeps firing, the socket stays open, the listener keeps the component’s closure alive. onUnmounted is the hook for cleanup. It runs after the component is removed from the DOM, and it is the last chance to release resources.
The SSR problem. Lifecycle hooks do not run during server-side rendering. The server renders the component to a string and sends it; there is no DOM to mount into, no re-render to update, no unmount to clean up. This means code that accesses the DOM or window must be inside onMounted, not inside setup(). The onMounted hook runs only in the browser, so it is the safe place for browser-only code. The onUnmounted hook also runs only in the browser, so it is the safe place for cleanup that references browser APIs.
The trade-off. Lifecycle hooks are imperative. They run code at specific moments, and the code is not tied to the component’s reactive state in the way that computed and watch are. A developer must remember which hook runs when, and must choose the right one for the job. Using onUpdated for logic that should run on a specific state change is a common mistake: onUpdated runs on every re-render, which is often too often. The trade-off is between the precision of running code at a specific moment and the reactivity of running code in response to a specific state change.
a. onMounted — after the DOM is created
onMounted registers a callback that runs after the component has been mounted: its DOM has been created, inserted into the parent, and its child components have also been mounted. It is the hook for DOM access, data fetching, and initialization .
<script setup>
import { ref, onMounted } from 'vue';
const inputRef = ref(null);
const data = ref(null);
onMounted(() => {
inputRef.value.focus();
});
onMounted(async () => {
const response = await fetch('/api/data');
data.value = await response.json();
});
</script>
<template>
<input ref="inputRef" />
<div v-if="data">{{ data }}</div>
</template>
The inputRef template ref is null during setup. It is populated by the time onMounted runs, because the DOM has been created and the ref has been bound. The onMounted callback can access inputRef.value and call focus() on it. The data fetch also runs in onMounted, and the data ref is updated when the fetch resolves, triggering a re-render.
onMounted does not run during server-side rendering. Code inside onMounted is browser-only. This is why browser APIs like document, window, and localStorage should be accessed inside onMounted, not inside setup().
onMounted can be called multiple times, and the callbacks run in registration order. A component that uses a useTimer composable and a useFetch composable will have two onMounted callbacks, one from each composable, and they run in the order the composables were called.
b. onUpdated — after a re-render
onUpdated registers a callback that runs after the component has re-rendered because of a reactive state change. The DOM has been updated by the time the callback runs, so it can read the new DOM. It is the hook for reacting to DOM changes that cannot be handled by watch or computed .
<script setup>
import { ref, onUpdated } from 'vue';
const count = ref(0);
const listRef = ref(null);
onUpdated(() => {
// The DOM has been updated. Scroll to the bottom.
if (listRef.value) {
listRef.value.scrollTop = listRef.value.scrollHeight;
}
});
</script>
<template>
<button @click="count++">Increment</button>
<div ref="listRef" style="height: 100px; overflow: auto;">
<p v-for="i in count" :key="i">Item {{ i }}</p>
</div>
</template>
The onUpdated callback runs after every re-render. In this example, the callback scrolls the list to the bottom after new items are added. The listRef is populated, and the DOM reflects the new items.
onUpdated is a blunt instrument. It runs on every re-render, not on a specific state change. If a component has three reactive values, and only one of them affects the DOM that the callback cares about, the callback still runs when the other two change. The recommended pattern is to use watch for logic that should run on a specific state change, and to use onUpdated only when the logic genuinely needs to run after every render, or when the logic depends on the DOM state after the render.
The onUpdated callback should not mutate reactive state unconditionally. If it does, it can trigger another re-render, which triggers onUpdated again, which mutates the state again, and so on. This is an infinite loop. The Vue documentation explicitly warns against this pattern.
c. onUnmounted — after the component is removed
onUnmounted registers a callback that runs after the component has been unmounted: its DOM has been removed, and its child components have also been unmounted. It is the hook for cleanup: clearing timers, closing sockets, removing event listeners, and canceling subscriptions .
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const seconds = ref(0);
let timerId = null;
onMounted(() => {
timerId = setInterval(() => {
seconds.value++;
}, 1000);
});
onUnmounted(() => {
clearInterval(timerId);
timerId = null;
});
</script>
<template>
<p>Seconds: {{ seconds }}</p>
</template>
The onMounted callback starts an interval, and the onUnmounted callback clears it. Without the cleanup, the interval would continue firing after the component is removed, leaking memory and updating a ref that is no longer rendered.
The same pattern applies to event listeners, WebSocket connections, and subscriptions. The onUnmounted callback is the last chance to release resources. It runs only in the browser, so it is the safe place for cleanup that references browser APIs.
onUnmounted does not run during server-side rendering, and it does not run if the component is kept alive by <KeepAlive>. For components inside <KeepAlive>, the onActivated and onDeactivated hooks run instead of onMounted and onUnmounted when the component is switched in and out.
Complete Example Session
<!-- ============================================
PART 1: onMounted — DOM ACCESS
============================================ -->
<script setup>
import { ref, onMounted } from 'vue';
const inputRef = ref(null);
onMounted(() => {
inputRef.value.focus();
});
</script>
<template>
<input ref="inputRef" />
</template>
<!-- ============================================
PART 2: onMounted — DATA FETCHING
============================================ -->
<script setup>
import { ref, onMounted } from 'vue';
const users = ref([]);
const loading = ref(true);
const error = ref(null);
onMounted(async () => {
try {
const response = await fetch('/api/users');
users.value = await response.json();
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
});
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error.message }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</template>
<!-- ============================================
PART 3: onUpdated — SCROLLING AFTER UPDATE
============================================ -->
<script setup>
import { ref, onUpdated } from 'vue';
const messages = ref([]);
const listRef = ref(null);
function addMessage() {
messages.value.push({ id: Date.now(), text: 'New message' });
}
onUpdated(() => {
if (listRef.value) {
listRef.value.scrollTop = listRef.value.scrollHeight;
}
});
</script>
<template>
<button @click="addMessage">Add</button>
<div ref="listRef" style="height: 100px; overflow: auto;">
<p v-for="msg in messages" :key="msg.id">{{ msg.text }}</p>
</div>
</template>
<!-- ============================================
PART 4: onUnmounted — TIMER CLEANUP
============================================ -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const seconds = ref(0);
let timerId = null;
onMounted(() => {
timerId = setInterval(() => seconds.value++, 1000);
});
onUnmounted(() => {
clearInterval(timerId);
});
</script>
<template>
<p>Seconds: {{ seconds }}</p>
</template>
<!-- ============================================
PART 5: onUnmounted — EVENT LISTENER CLEANUP
============================================ -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const width = ref(window.innerWidth);
function handleResize() {
width.value = window.innerWidth;
}
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
</script>
<template>
<p>Window width: {{ width }}</p>
</template>
<!-- ============================================
PART 6: MULTIPLE onMounted CALLBACKS
============================================ -->
<script setup>
import { onMounted } from 'vue';
onMounted(() => console.log('first'));
onMounted(() => console.log('second'));
onMounted(() => console.log('third'));
// Output: first, second, third
</script>
<!-- ============================================
PART 7: A COMPOSABLE THAT REGISTERS HOOKS
============================================ -->
<script>
// composables/useTimer.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useTimer(interval = 1000) {
const seconds = ref(0);
let timerId = null;
onMounted(() => {
timerId = setInterval(() => seconds.value++, interval);
});
onUnmounted(() => {
clearInterval(timerId);
});
return { seconds };
}
</script>
<script setup>
import { useTimer } from './composables/useTimer';
const { seconds } = useTimer();
</script>
<template>
<p>Seconds: {{ seconds }}</p>
</template>
<!-- ============================================
PART 8: onUpdated vs watch
============================================ -->
<script setup>
import { ref, onUpdated, watch } from 'vue';
const count = ref(0);
const name = ref('');
// onUpdated runs on EVERY re-render
onUpdated(() => {
console.log('component updated');
});
// watch runs only when count changes
watch(count, (newVal) => {
console.log(`count changed to ${newVal}`);
});
// Changing name triggers onUpdated but not watch
</script>
<!-- ============================================
PART 9: CLEANUP FOR A WEBSOCKET
============================================ -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const messages = ref([]);
let socket = null;
onMounted(() => {
socket = new WebSocket('wss://example.com/chat');
socket.onmessage = (event) => {
messages.value.push(JSON.parse(event.data));
};
});
onUnmounted(() => {
if (socket) {
socket.close();
socket = null;
}
});
</script>
<template>
<ul>
<li v-for="msg in messages" :key="msg.id">{{ msg.text }}</li>
</ul>
</template>
<!-- ============================================
PART 10: THE COMPLETE COMPONENT
============================================ -->
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue';
const data = ref(null);
const loading = ref(true);
const listRef = ref(null);
let timerId = null;
onMounted(async () => {
const response = await fetch('/api/items');
data.value = await response.json();
loading.value = false;
timerId = setInterval(() => {
// Poll for updates
}, 5000);
});
onUpdated(() => {
if (listRef.value) {
listRef.value.scrollTop = listRef.value.scrollHeight;
}
});
onUnmounted(() => {
clearInterval(timerId);
});
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else ref="listRef" style="height: 200px; overflow: auto;">
<p v-for="item in data" :key="item.id">{{ item.name }}</p>
</div>
</template>
The ten parts show onMounted for DOM access and data fetching, onUpdated for scrolling after an update, onUnmounted for timer and event listener cleanup, multiple onMounted callbacks, a composable that registers hooks, onUpdated vs watch, WebSocket cleanup, and a complete component that uses all three hooks.
Quick Reference
The Three Core Hooks
| Hook | Runs when | Common use |
|---|---|---|
onMounted | After DOM is inserted | DOM access, data fetching, initialization |
onUpdated | After re-render | DOM reads after update, scrolling |
onUnmounted | After DOM is removed | Cleanup: timers, sockets, listeners |
Full Lifecycle Order
| Order | Hook | Phase |
|---|---|---|
| 1 | setup() | Component instance created |
| 2 | onBeforeMount | Before DOM insertion |
| 3 | onMounted | After DOM insertion |
| 4 | onBeforeUpdate | Before re-render |
| 5 | onUpdated | After re-render |
| 6 | onBeforeUnmount | Before removal |
| 7 | onUnmounted | After removal |
Registration Rules
| Rule | Behavior |
|---|---|
Called inside setup() | Required |
| Called multiple times | All callbacks run in order |
Called outside setup() | Warning; no effect |
| During SSR | Do not run |
KeepAlive Hooks
| Hook | Runs when |
|---|---|
onActivated | Component is inserted into KeepAlive |
onDeactivated | Component is removed from KeepAlive |
Best Practices
✅ Do This:
<script setup>
// Access DOM in onMounted
onMounted(() => { inputRef.value.focus(); }); // ✅
// Fetch data in onMounted
onMounted(async () => { data.value = await fetchData(); }); // ✅
// Clean up in onUnmounted
onUnmounted(() => { clearInterval(timerId); }); // ✅
// Use watch for specific state changes
watch(count, (val) => { /* ... */ }); // ✅
// Register hooks in composables
export function useTimer() {
onMounted(() => { /* ... */ });
onUnmounted(() => { /* ... */ });
} // ✅
</script>
❌ Don’t Do This:
<script setup>
// Don't access DOM in setup()
const width = window.innerWidth; // SSR breaks // ❌
// Don't mutate state unconditionally in onUpdated
onUpdated(() => { count.value++; }); // infinite loop // ❌
// Don't use onUpdated for specific state changes
onUpdated(() => { if (count.value > 10) { /* ... */ } }); // use watch // ❌
// Don't forget cleanup for timers
onMounted(() => { setInterval(() => {}, 1000); }); // leaks // ❌
// Don't call hooks outside setup()
function doSomething() { onMounted(() => {}); } // warning // ❌
</script>
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
ref is null in setup | DOM not created yet | Access in onMounted |
SSR error: window is not defined | Browser API in setup | Move to onMounted |
| Infinite loop | State mutation in onUpdated | Guard the mutation or use watch |
onUpdated runs too often | Every re-render | Use watch for specific changes |
| Timer keeps running after unmount | No cleanup | Clear in onUnmounted |
| Cleanup does not run in KeepAlive | onUnmounted not called | Use onDeactivated |
| Hook called outside setup | Wrong scope | Call inside setup() |
Real-World Examples
1. Focus an Input
onMounted(() => { inputRef.value.focus(); });
2. Fetch Data
onMounted(async () => { data.value = await fetch('/api').then(r => r.json()); });
3. Scroll After Update
onUpdated(() => { listRef.value.scrollTop = listRef.value.scrollHeight; });
4. Clear a Timer
onUnmounted(() => { clearInterval(timerId); });
5. Remove an Event Listener
onUnmounted(() => { window.removeEventListener('resize', handleResize); });
6. Close a WebSocket
onUnmounted(() => { socket.close(); });
7. Multiple onMounted Callbacks
onMounted(() => console.log('first'));
onMounted(() => console.log('second'));
8. Composable Timer
export function useTimer() {
onMounted(() => { /* start */ });
onUnmounted(() => { /* stop */ });
}
9. Resize Listener
onMounted(() => window.addEventListener('resize', handleResize));
onUnmounted(() => window.removeEventListener('resize', handleResize));
10. onUpdated vs watch
onUpdated(() => { /* every re-render */ });
watch(count, () => { /* only when count changes */ });
Visual
The Lifecycle Sequence
┌──────────────────────────────────────────────────────────────┐
│ LIFECYCLE SEQUENCE │
│ │
│ setup() │
│ │ │
│ │ Component instance created │
│ │ Refs declared, computed defined │
│ ▼ │
│ onBeforeMount │
│ │ │
│ │ Before DOM insertion │
│ ▼ │
│ onMounted │
│ │ │
│ │ DOM created and inserted │
│ │ Template refs populated │
│ │ Child components mounted │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Reactive state changes → re-render │ │
│ │ │ │ │
│ │ ├── onBeforeUpdate │ │
│ │ │ │ │
│ │ └── onUpdated │ │
│ │ │ │
│ │ (repeats on every state change) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ onBeforeUnmount │
│ │ │
│ │ Before DOM removal │
│ ▼ │
│ onUnmounted │
│ │ │
│ │ DOM removed │
│ │ Cleanup runs │
│ ▼ │
│ Component destroyed │
│ │
└──────────────────────────────────────────────────────────────┘
onMounted Timing
┌──────────────────────────────────────────────────────────────┐
│ onMounted TIMING │
│ │
│ setup() onMounted() │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ │ │ │ │
│ │ inputRef = null │ │ inputRef = <input> │ │
│ │ │ │ │ │
│ │ Template refs are │ │ DOM is created │ │
│ │ NOT populated yet │ │ Refs are populated │ │
│ │ │ │ │ │
│ │ DOM does not exist │ │ DOM exists │ │
│ │ │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ // In setup() — inputRef is null │ │
│ │ console.log(inputRef.value); // null │ │
│ │ │ │
│ │ // In onMounted — inputRef is the DOM element │ │
│ │ console.log(inputRef.value); // <input> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ This is why DOM access must be in onMounted, not setup. │
│ │
└──────────────────────────────────────────────────────────────┘
onUpdated vs watch
┌──────────────────────────────────────────────────────────────┐
│ onUpdated vs watch │
│ │
│ Reactive state: count = 0, name = '' │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ count++ → re-render │ │
│ │ onUpdated runs │ │
│ │ watch(count) runs │ │
│ │ │ │
│ │ name = 'Alice' → re-render │ │
│ │ onUpdated runs │ │
│ │ watch(count) does NOT run │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ onUpdated runs on EVERY re-render. │
│ watch runs only when the watched source changes. │
│ │
│ Use onUpdated when the logic depends on the DOM state │
│ after any render. │
│ Use watch when the logic depends on a specific value. │
│ │
└──────────────────────────────────────────────────────────────┘
Cleanup Pattern
┌──────────────────────────────────────────────────────────────┐
│ CLEANUP PATTERN │
│ │
│ onMounted(() => { │
│ timerId = setInterval(...); ← acquire resource │
│ window.addEventListener(...); ← acquire resource │
│ socket = new WebSocket(...); ← acquire resource │
│ }); │
│ │
│ onUnmounted(() => { │
│ clearInterval(timerId); ← release resource │
│ window.removeEventListener(...); ← release resource │
│ socket.close(); ← release resource │
│ }); │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ For every resource acquired in onMounted, │ │
│ │ release it in onUnmounted. │ │
│ │ │ │
│ │ Without cleanup, the resource leaks: │ │
│ │ - Timer keeps firing │ │
│ │ - Listener keeps closure alive │ │
│ │ - Socket stays open │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
onMounted | Runs after DOM insertion; DOM access, data fetching |
onUpdated | Runs after re-render; DOM reads, scrolling |
onUnmounted | Runs after DOM removal; cleanup |
| Registration | Inside setup(), can be called multiple times |
| Callback order | Registration order |
| SSR | Hooks do not run on the server |
| KeepAlive | onActivated/onDeactivated instead of mount/unmount |
onUpdated vs watch | onUpdated every render; watch specific source |
| Cleanup | Release every resource acquired in onMounted |
Key takeaways:
onMountedruns after the component’s DOM is inserted. It is the hook for DOM access, data fetching, and initialization. Template refs are populated by this point, but not before. The hook does not run during server-side rendering, so browser-only code belongs here .onUpdatedruns after every re-render. It is the hook for reading the DOM after an update, such as scrolling to the bottom of a list. It is a blunt instrument: it runs on every re-render, not on a specific state change. Usewatchfor logic that should run on a specific source .onUnmountedruns after the component is removed. It is the hook for cleanup: clearing timers, closing sockets, removing event listeners, and canceling subscriptions. Every resource acquired inonMountedshould be released here .- Hooks can be called multiple times. A component that uses multiple composables will have multiple
onMountedcallbacks, one per composable. They run in registration order. This is what makes composables self-contained . - Do not mutate reactive state unconditionally in
onUpdated. It triggers another re-render, which triggersonUpdatedagain, and so on. This is an infinite loop. If the mutation is necessary, guard it with a condition that becomes false after the first run . onUnmounteddoes not run for components inside<KeepAlive>. UseonActivatedandonDeactivatedinstead. The component is not destroyed; it is deactivated, and its state is preserved.- Lifecycle hooks do not run during server-side rendering. The server renders the component to a string and sends it. There is no DOM to mount, update, or unmount. Browser-only code must be inside
onMountedoronUnmounted.
Remember: Vue’s lifecycle hooks are the API for running code at specific moments in a component’s life. onMounted runs after the DOM is created — use it for DOM access and initialization. onUpdated runs after every re-render — use it sparingly, because it is often too broad. onUnmounted runs after the component is removed — use it for cleanup. The hooks are registered inside setup() and can be called multiple times, which is what allows composables to manage their own lifecycle. The full sequence is setup → onBeforeMount → onMounted → onBeforeUpdate → onUpdated → onBeforeUnmount → onUnmounted, with the update hooks repeating on every state change. The hooks do not run during server-side rendering, and components inside <KeepAlive> use activation hooks instead of mount and unmount. Understanding the timing of each hook is the difference between code that works and code that runs at the wrong moment.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!