Vue 3 Composables 2026 — Design Patterns, VueUse v14, and a Reusable Architecture for Production
Posted on: 4/18/2026 6:14:32 AM
Table of contents
- 1. What is a Composable and Why Does It Matter?
- 2. Anatomy — The Standard Structure of a Composable
- 3. Core Patterns — Stateful vs Stateless vs Singleton
- 4. Advanced Patterns for Production
- 5. VueUse v14 — The Production-Ready Composables Toolkit
- 6. Dependency Injection — provide/inject with Composables
- 7. Performance — Tuning Reactivity for Scale
- 8. Vue 3.5 — New APIs for Composables
- 9. Testing Composables with Vitest
- 10. Composables Architecture in a Production Project
- 11. Checklist — Best Practices Summary
- Conclusion
The Composition API completely changed how developers write Vue. But its real power doesn't lie in ref() or computed() — it lies in composables, the reusable functions that package logic, state, and side-effects into independent modules. This article takes a deep look at design patterns, architecture, and best practices for Vue 3 composables in production 2026, from the basic structure to advanced patterns such as factory, singleton, dependency injection, and testing.
1. What is a Composable and Why Does It Matter?
A composable is a function prefixed with use that leverages the Composition API to encapsulate and reuse stateful logic. Unlike a plain utility function, a composable can contain reactive state, computed properties, watchers, and lifecycle hooks.
graph LR
A["Component A"] -->|"useAuth()"| C["Composable"]
B["Component B"] -->|"useAuth()"| C
D["Component C"] -->|"useAuth()"| C
C -->|"ref, computed"| E["Reactive State"]
C -->|"onMounted, onUnmounted"| F["Lifecycle"]
C -->|"watch, watchEffect"| G["Side Effects"]
style C fill:#e94560,stroke:#fff,color:#fff
style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style F fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style G fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style A fill:#2c3e50,stroke:#fff,color:#fff
style B fill:#2c3e50,stroke:#fff,color:#fff
style D fill:#2c3e50,stroke:#fff,color:#fff
Before the Composition API, Vue 2 used mixins to reuse logic. But mixins had three serious issues: name collisions, implicit dependencies (unclear data origins), and poor TypeScript support. Composables solve all three cleanly.
| Criterion | Mixins (Vue 2) | Composables (Vue 3) |
|---|---|---|
| Name collisions | Easy to hit, complex merge strategies | None — explicit destructuring |
| Source tracking | You don't know which mixin a field came from | Clear import paths |
| TypeScript | Limited, poor inference | Full type inference |
| Composition | Hard to combine multiple mixins | Combine freely, no limits |
| Testing | Requires mounting a component | Test directly or via withSetup |
2. Anatomy — The Standard Structure of a Composable
A production-grade composable should follow a three-part structure: Primary State → Supportive State → Methods, with internal ordering: Initialization → Refs → Computed → Methods → Lifecycle hooks → Watchers.
// composables/useUserData.ts
import { ref, computed, onMounted, watch } from 'vue'
import type { Ref } from 'vue'
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
type Status = 'idle' | 'loading' | 'success' | 'error'
export function useUserData(userId: Ref<string> | string) {
// === 1. Primary State ===
const user = ref<User | null>(null)
// === 2. Supportive State ===
const status = ref<Status>('idle')
const error = ref<Error | null>(null)
// === Computed ===
const isAdmin = computed(() => user.value?.role === 'admin')
const isLoading = computed(() => status.value === 'loading')
// === 3. Methods ===
const fetchUser = async (id: string) => {
status.value = 'loading'
error.value = null
try {
const response = await fetch(`/api/users/${id}`)
user.value = await response.json()
status.value = 'success'
} catch (e) {
error.value = e as Error
status.value = 'error'
}
}
// === Lifecycle ===
onMounted(() => {
const id = typeof userId === 'string' ? userId : userId.value
fetchUser(id)
})
// === Watchers ===
if (typeof userId !== 'string') {
watch(userId, (newId) => fetchUser(newId))
}
return { user, status, error, isAdmin, isLoading, fetchUser }
}
Naming conventions
Always use the use prefix with PascalCase: useAuth, useCart, useDarkMode. Name the file the same way: useAuth.ts. When a composable takes more than three parameters, group them into an object: useUserData({ id, fetchOnMount: true, locale: 'en' }).
3. Core Patterns — Stateful vs Stateless vs Singleton
Three fundamental patterns govern how a composable manages state:
Stateless Composable
Each call creates its own state. Suited for logic local to a component.
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
// Component A: count = 0 (separate)
// Component B: count = 0 (separate)
Singleton / Shared State
State lives at module scope — every consumer shares the same instance. Good for lightweight global state.
const currentUser = ref<User | null>(null)
const isAuthenticated = computed(
() => currentUser.value !== null
)
export function useAuth() {
const login = async (credentials: Credentials) => {
currentUser.value = await authApi.login(credentials)
}
const logout = () => { currentUser.value = null }
return {
currentUser: readonly(currentUser),
isAuthenticated,
login,
logout
}
}
// Every component sees the same currentUser
When to use Singleton vs Pinia?
Singleton composables fit simple state with few mutations (auth status, theme, locale). When state grows complex and you need devtools inspection, time-travel debugging, or SSR hydration — reach for a Pinia store. Rule of thumb: if you need $patch, $reset, or a plugin system → Pinia.
3.1 Factory Pattern — Composables that Create Composables
The factory pattern lets you create pre-configured composables, which is very useful when many modules share the same pattern but differ by base URL, headers, or transform logic:
// composables/createApiComposable.ts
function createApiComposable(baseUrl: string, defaultHeaders?: HeadersInit) {
return function useApi<T>(endpoint: string) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const isLoading = ref(false)
const execute = async (options?: RequestInit) => {
isLoading.value = true
error.value = null
try {
const res = await fetch(`${baseUrl}${endpoint}`, {
headers: { ...defaultHeaders, ...options?.headers },
...options
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
return { data, error, isLoading, execute }
}
}
// Create a composable per service
const useUserApi = createApiComposable('/api/v2/users')
const useProductApi = createApiComposable('/api/v2/products')
// In the component
const { data: users, execute } = useUserApi<User[]>('/')
const { data: product } = useProductApi<Product>('/123')
4. Advanced Patterns for Production
4.1 useAsyncState — The Standard Pattern for Async Operations
This is the most common pattern in production. Every API call, database interaction, or async operation needs loading/error/data state tracking:
// composables/useAsyncState.ts
export function useAsyncState<T>(
asyncFn: () => Promise<T>,
initialState: T,
options: { immediate?: boolean } = { immediate: true }
) {
const state = ref(initialState) as Ref<T>
const isLoading = ref(false)
const isReady = ref(false)
const error = ref<Error | null>(null)
const execute = async () => {
isLoading.value = true
error.value = null
try {
state.value = await asyncFn()
isReady.value = true
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
if (options.immediate) execute()
return { state, isLoading, isReady, error, execute }
}
// Usage
const { state: users, isLoading, error, execute: refresh } =
useAsyncState(() => fetchUsers(), [])
4.2 useEventListener — Auto-cleanup Side Effects
The most important pattern for DOM interaction: automatically detach the event listener when the component unmounts to avoid memory leaks:
export function useEventListener<K extends keyof WindowEventMap>(
target: EventTarget | Ref<EventTarget | null>,
event: K,
handler: (e: WindowEventMap[K]) => void,
options?: AddEventListenerOptions
) {
const cleanup = () => {
const el = unref(target)
el?.removeEventListener(event, handler as EventListener, options)
}
onMounted(() => {
const el = unref(target)
el?.addEventListener(event, handler as EventListener, options)
})
onUnmounted(cleanup)
// If target is a ref, watch it to re-bind
if (isRef(target)) {
watch(target, (newTarget, oldTarget) => {
oldTarget?.removeEventListener(event, handler as EventListener)
newTarget?.addEventListener(event, handler as EventListener, options)
})
}
return cleanup
}
4.3 useDebouncedRef — A Custom Reactivity Primitive
Vue lets you create custom refs via customRef(). This is the cleanest way to build a debounced input — no separate watch + setTimeout needed:
import { customRef } from 'vue'
export function useDebouncedRef<T>(initialValue: T, delay = 300) {
let timeout: ReturnType<typeof setTimeout>
return customRef<T>((track, trigger) => {
let value = initialValue
return {
get() {
track()
return value
},
set(newValue: T) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
// Template: v-model debounces itself
// <input v-model="searchQuery" />
const searchQuery = useDebouncedRef('', 400)
4.4 Cancellable Requests with AbortController
When a component unmounts or a user changes input quickly, the old request must be cancelled to avoid race conditions:
export function useCancellableFetch<T>(url: Ref<string>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
let controller: AbortController | null = null
const execute = async () => {
controller?.abort()
controller = new AbortController()
try {
const res = await fetch(url.value, {
signal: controller.signal
})
data.value = await res.json()
} catch (e) {
if ((e as Error).name !== 'AbortError') {
error.value = e as Error
}
}
}
watch(url, execute, { immediate: true })
onUnmounted(() => controller?.abort())
return { data, error }
}
sequenceDiagram
participant U as User
participant C as Component
participant F as useCancellableFetch
participant S as Server
U->>C: Types "vue"
C->>F: url = "/search?q=vue"
F->>S: GET /search?q=vue
U->>C: Types "vue compos" (fast)
C->>F: url changes
F->>F: abort() old request
F->>S: GET /search?q=vue+compos
S-->>F: ❌ Old request cancelled
S-->>F: ✅ New response
F-->>C: data = new result
5. VueUse v14 — The Production-Ready Composables Toolkit
VueUse is the largest composables library for Vue 3, shipping more than 300 ready-to-use composables. Version v14 (2025–2026) requires Vue 3.5+ and brings several important improvements:
| Composable | Purpose | What's new in v14 |
|---|---|---|
useIntersectionObserver | Track elements entering the viewport | Reactive rootMargin — changing margin no longer recreates the observer |
useDraggable | Drag-and-drop for elements | Auto-scroll inside scrollable containers |
useDropZone | Drop zone for files/elements | Validation function for file type/size |
useSortable | Drag-to-reorder lists | watchElement — auto re-init when the DOM changes |
useCssSupports | Detect CSS feature support | Brand new — reactive CSS feature detection |
useWebSocket | WebSocket connection | Function support for autoConnect.delay |
useElementVisibility | Whether an element is visible | New initialValue option |
An example of using VueUse in production:
import {
useIntersectionObserver,
useDebounceFn,
useLocalStorage,
useMediaQuery,
useOnline,
useTitle
} from '@vueuse/core'
export function useSmartList() {
// Persist scroll position in localStorage
const scrollPos = useLocalStorage('list-scroll', 0)
// Responsive breakpoint
const isMobile = useMediaQuery('(max-width: 768px)')
const pageSize = computed(() => isMobile.value ? 10 : 20)
// Offline detection
const isOnline = useOnline()
// Infinite scroll trigger
const loadMoreRef = ref<HTMLElement | null>(null)
const isLoadMoreVisible = ref(false)
useIntersectionObserver(loadMoreRef, ([{ isIntersecting }]) => {
isLoadMoreVisible.value = isIntersecting
if (isIntersecting && isOnline.value) {
loadNextPage()
}
}, { rootMargin: '200px' })
// Debounced search
const search = useDebounceFn(async (query: string) => {
// fetch results...
}, 300)
// Dynamic page title
const totalItems = ref(0)
useTitle(computed(() => `List (${totalItems.value}) | App`))
const loadNextPage = async () => { /* ... */ }
return {
scrollPos, isMobile, pageSize, isOnline,
loadMoreRef, search, totalItems
}
}
6. Dependency Injection — provide/inject with Composables
When a composable needs an external service (API client, logger, config), dependency injection via provide/inject is the cleanest pattern — it avoids direct imports and makes mocking in tests trivial:
graph TB
A["App Plugin
provide(apiKey, apiClient)"] --> B["Page Component"]
B --> C["Child Component"]
C --> D["Deep Nested Component"]
B -->|"useProducts()"| E["Composable
inject(apiKey)"]
C -->|"useProducts()"| E
D -->|"useProducts()"| E
style A fill:#2c3e50,stroke:#fff,color:#fff
style E fill:#e94560,stroke:#fff,color:#fff
style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50
// injection-keys.ts — Typed injection keys
import type { InjectionKey } from 'vue'
export interface ApiClient {
get<T>(url: string): Promise<T>
post<T>(url: string, body: unknown): Promise<T>
}
export const apiClientKey: InjectionKey<ApiClient> = Symbol('apiClient')
export const loggerKey: InjectionKey<Logger> = Symbol('logger')
// plugins/api.ts — Plugin provides the service
export const apiPlugin = {
install(app: App) {
const client: ApiClient = {
async get(url) {
const res = await fetch(url)
return res.json()
},
async post(url, body) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
return res.json()
}
}
app.provide(apiClientKey, client)
}
}
// composables/useProducts.ts — Composable injects the service
export function useProducts() {
const api = inject(apiClientKey)
if (!api) throw new Error('ApiClient not provided. Did you install apiPlugin?')
const products = ref<Product[]>([])
const fetchAll = async () => {
products.value = await api.get<Product[]>('/api/products')
}
const create = async (product: CreateProductDto) => {
const created = await api.post<Product>('/api/products', product)
products.value.push(created)
return created
}
return { products, fetchAll, create }
}
When to use provide/inject vs direct import?
Use provide/inject when: (1) you need to swap the implementation in tests, (2) service config changes per environment, (3) you want to avoid circular dependencies. Use a direct import when the composable is a pure utility that doesn't need mocking (for instance, useDebouncedRef, useCounter).
7. Performance — Tuning Reactivity for Scale
7.1 shallowRef for Large Datasets
With large lists (thousands of items), ref() creates a deep reactive proxy for every property of every object — expensive in memory and CPU. shallowRef only tracks changes to .value:
import { shallowRef, triggerRef } from 'vue'
export function useDataGrid<T>() {
// ✅ shallowRef — no deep proxy over 10,000 rows
const rows = shallowRef<T[]>([])
const setData = (newRows: T[]) => {
rows.value = newRows // triggers re-render
}
const updateRow = (index: number, patch: Partial<T>) => {
Object.assign(rows.value[index] as object, patch)
triggerRef(rows) // force trigger since mutations aren't auto-detected
}
// ❌ Do NOT use ref() for large datasets
// const rows = ref([]) // Deep proxy over 10,000 objects = slow
return { rows: readonly(rows), setData, updateRow }
}
7.2 watchEffect Cleanup — Avoiding Memory Leaks
watchEffect receives an onCleanup callback that runs before each re-execution and on component unmount. This pattern is crucial for async operations:
export function useRealtimeData(channel: Ref<string>) {
const messages = ref<Message[]>([])
watchEffect((onCleanup) => {
const ws = new WebSocket(`wss://api.example.com/${channel.value}`)
ws.onmessage = (e) => {
messages.value.push(JSON.parse(e.data))
}
// Cleanup: close the old connection when channel changes
// or when the component unmounts
onCleanup(() => {
ws.close()
})
})
return { messages }
}
7.3 Computed Caching vs Method Calls
| Approach | When it recomputes | Best for |
|---|---|---|
computed(() => ...) | Only when a dependency changes | Derived state used multiple times in the template |
function filter() { ... } | On every template re-render | Logic that needs different arguments each call |
export function useFilteredList<T>(
items: Ref<T[]>,
predicate: (item: T) => boolean
) {
// ✅ computed — recomputes only when items change
const filtered = computed(() => items.value.filter(predicate))
const count = computed(() => filtered.value.length)
return { filtered, count }
}
Don't destructure reactive objects
const { x, y } = reactive({ x: 1, y: 2 }) — x and y lose reactivity. Use toRefs(): const { x, y } = toRefs(state). With a Pinia store: const { count } = storeToRefs(store).
8. Vue 3.5 — New APIs for Composables
Vue 3.5 (the baseline for VueUse v14) adds two important built-in composables:
8.1 useTemplateRef — Safer Template Refs
// Vue 3.4 and earlier — the ref name had to match the variable
const inputEl = ref<HTMLInputElement | null>(null)
// <input ref="inputEl" /> — variable name = ref name
// Vue 3.5+ — useTemplateRef with a string ID
const inputEl = useTemplateRef<HTMLInputElement>('my-input')
// <input ref="my-input" /> — ref name is a string, independent of the variable
// Supports dynamic refs
const currentRef = useTemplateRef<HTMLElement>(activeTab)
8.2 useId — SSR-safe Unique IDs
export function useFormField(label: string) {
const id = useId() // Unique and stable across SSR/client
return {
inputId: `field-${id}`,
labelId: `label-${id}`,
errorId: `error-${id}`,
attrs: computed(() => ({
id: `field-${id}`,
'aria-labelledby': `label-${id}`,
'aria-describedby': `error-${id}`
}))
}
}
// Each instance receives a distinct ID, identical on SSR and client hydration
9. Testing Composables with Vitest
Composable tests fall into two categories: direct invocation (no lifecycle required) and withSetup (needs onMounted, provide/inject).
9.1 Direct Test — Reactivity-only Composables
// useCounter.test.ts
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('initializes with the correct value', () => {
const { count } = useCounter(10)
expect(count.value).toBe(10)
})
it('increments and decrements correctly', () => {
const { count, increment, decrement } = useCounter()
increment()
increment()
expect(count.value).toBe(2)
decrement()
expect(count.value).toBe(1)
})
})
9.2 withSetup Helper — Composables with Lifecycle
// test-utils.ts
import { createApp, type App } from 'vue'
export function withSetup<T>(composable: () => T): [T, App] {
let result!: T
const app = createApp({
setup() {
result = composable()
return () => {}
}
})
app.mount(document.createElement('div'))
return [result, app]
}
// Test a composable with lifecycle hooks
import { describe, it, expect, afterEach } from 'vitest'
import { useWindowSize } from './useWindowSize'
describe('useWindowSize', () => {
let app: App
afterEach(() => app?.unmount())
it('tracks window resize', async () => {
const [result, _app] = withSetup(() => useWindowSize())
app = _app
// Simulate resize
window.innerWidth = 1024
window.dispatchEvent(new Event('resize'))
expect(result.width.value).toBe(1024)
})
})
9.3 Testing with Dependency Injection
export function withSetupAndProvide<T>(
composable: () => T,
provides: Record<string | symbol, unknown>
): [T, App] {
let result!: T
const app = createApp({
setup() {
for (const [key, val] of Object.entries(provides)) {
provide(key, val)
}
// Handle Symbol keys
for (const sym of Object.getOwnPropertySymbols(provides)) {
provide(sym, provides[sym as unknown as string])
}
result = composable()
return () => {}
}
})
app.mount(document.createElement('div'))
return [result, app]
}
// Test useProducts with a mocked API
describe('useProducts', () => {
it('fetches products via the injected API client', async () => {
const mockApi: ApiClient = {
get: vi.fn().mockResolvedValue([{ id: 1, name: 'Test' }]),
post: vi.fn()
}
const [result, app] = withSetupAndProvide(
() => useProducts(),
{ [apiClientKey as unknown as string]: mockApi }
)
await result.fetchAll()
expect(result.products.value).toHaveLength(1)
expect(mockApi.get).toHaveBeenCalledWith('/api/products')
app.unmount()
})
})
pie title Composables Testing Strategy 2026
"Integration Tests (Vitest Browser)" : 70
"Composable Unit Tests" : 20
"Visual/A11y Regression" : 10
10. Composables Architecture in a Production Project
10.1 Functional Core, Imperative Shell
Split pure logic (pure functions, easy to test) from Vue reactivity (side effects, lifecycle):
// core/pricing.ts — Pure functions, NO Vue imports
export const calculateDiscount = (price: number, tier: 'bronze' | 'silver' | 'gold') => {
const rates = { bronze: 0, silver: 0.1, gold: 0.2 }
return price * (1 - rates[tier])
}
export const formatCurrency = (amount: number, locale = 'en-US') =>
new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }).format(amount)
// composables/usePricing.ts — Vue shell wrapping the pure core
import { calculateDiscount, formatCurrency } from '@/core/pricing'
export function usePricing(tier: Ref<'bronze' | 'silver' | 'gold'>) {
const rawPrice = ref(0)
const discountedPrice = computed(() =>
calculateDiscount(rawPrice.value, tier.value)
)
const displayPrice = computed(() =>
formatCurrency(discountedPrice.value)
)
return { rawPrice, discountedPrice, displayPrice }
}
Benefits of this pattern
calculateDiscount and formatCurrency are pure functions — testable with simple unit tests, no withSetup, no Vue context needed. The usePricing composable is just a thin wrapper bridging pure logic with reactivity.
10.2 Recommended Directory Structure
src/
├── composables/ # Application-level composables
│ ├── useAuth.ts
│ ├── useProducts.ts
│ └── useNotifications.ts
├── composables/shared/ # Generic, reusable across projects
│ ├── useAsyncState.ts
│ ├── useEventListener.ts
│ ├── useDebouncedRef.ts
│ └── useCancellableFetch.ts
├── core/ # Pure functions (no Vue dependency)
│ ├── pricing.ts
│ ├── validation.ts
│ └── formatting.ts
├── injection-keys.ts # Typed InjectionKey definitions
└── plugins/
├── api.ts # Provides ApiClient
└── logger.ts # Provides Logger
10.3 Composable Composition — Combining Small Composables into Domain Logic
// Small, single-responsibility composables
export function usePagination(pageSize = 20) {
const page = ref(1)
const total = ref(0)
const totalPages = computed(() => Math.ceil(total.value / pageSize))
const hasNext = computed(() => page.value < totalPages.value)
const offset = computed(() => (page.value - 1) * pageSize)
return { page, total, totalPages, hasNext, offset, pageSize }
}
// A domain composable combining several small composables
export function usePaginatedProducts() {
const { page, total, totalPages, hasNext, offset, pageSize } = usePagination(12)
const { state: products, isLoading, execute } = useAsyncState(
() => fetchProducts({ offset: offset.value, limit: pageSize }),
[]
)
const searchQuery = useDebouncedRef('', 400)
watch([page, searchQuery], () => execute())
return {
products, isLoading,
page, totalPages, hasNext,
searchQuery
}
}
11. Checklist — Best Practices Summary
| # | Practice | Details |
|---|---|---|
| 1 | use prefix | Always name useSomething, with the file matching the name |
| 2 | Return an object, not a tuple | return { data, error } — consumers destructure by name, not position |
| 3 | Accept Ref or plain value | Use MaybeRef<T> + unref() for flexible input |
| 4 | Expose readonly state | return { count: readonly(count) } — prevents unintended mutation |
| 5 | Clean up side effects | onUnmounted, watchEffect(onCleanup) — avoid memory leaks |
| 6 | Always surface error state | Don't swallow errors — expose an error ref for components to handle |
| 7 | shallowRef for large data | Lists of 100+ items: use shallowRef instead of ref |
| 8 | Single Responsibility | One purpose per composable — combine via composition |
| 9 | Pure core, reactive shell | Separate pure business logic from Vue reactivity |
| 10 | Typed injection keys | InjectionKey<T> for type safety when using provide/inject |
Conclusion
Composables aren't just a way to organize code — they are the architectural foundation of modern Vue 3 applications. With patterns like factory, singleton, dependency injection, and functional core / imperative shell, you can build a codebase that is flexible, testable, and maintainable. VueUse v14 ships more than 300 ready-made composables, but knowing how to write a composable correctly remains a core skill that no library can replace.
Start by refactoring a mixin or a repeated chunk of logic in your current project into a composable — you'll immediately see the difference in code quality and developer experience.
Micro-Frontend 2026: Divide and Conquer the Frontend with Module Federation 2.0
Tailwind CSS 4 and the Oxide Engine — When a CSS Framework Is Rewritten in Rust
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.