100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
JavaScript

Local Component State Patterns

Explore idiomatic ways to structure a component's own internal reactive state before reaching for external state management like Pinia.

State ManagementIntermediate9 min readJul 9, 2026
Analogies

Local Component State Patterns

Not every piece of state in an application needs to live in a global store. Most UI state — a modal's open/closed flag, a form's current input values, a list's sort order, a tab's active index — is naturally local to a single component or a small tree of related components, and keeping it local (rather than lifting it prematurely into Pinia) keeps components easier to reason about, test, and reuse. The Composition API gives several complementary patterns for structuring this local state cleanly.

🏏

Cricket analogy: Not every stat needs to go into the team's master record book — whether a fielder's cap is on straight is purely their own concern, not something the whole squad's ledger needs to track; likewise a modal's open/closed flag belongs local to its component, not Pinia.

When several pieces of state naturally belong together — like a form's fields — grouping them into a single reactive() object can be clearer than declaring many separate ref()s, since it mirrors the shape of the data you're ultimately submitting. The tradeoff is that reactive() objects cannot be reassigned wholesale (you must mutate properties) and lose reactivity if destructured directly.

🏏

Cricket analogy: Grouping a batsman's stance, grip, and guard-mark into one reactive() "setup" object mirrors how a coach reviews them together as a single checklist — but you can't swap the whole object at once, and pulling out just "grip" alone loses its connection to live coaching feedback.

vue
<script setup>
import { reactive } from 'vue'

const form = reactive({
  email: '',
  password: '',
  rememberMe: false,
})

function resetForm() {
  form.email = ''
  form.password = ''
  form.rememberMe = false
}
</script>

Deriving State with computed() Instead of Duplicating It

A frequent anti-pattern is manually keeping a second ref in sync with a first one via a watcher, when a computed() would derive it automatically and eliminate an entire class of sync bugs. Local state should hold only the minimal 'source of truth' values; everything else that can be derived from them should be a computed property.

🏏

Cricket analogy: Manually watching a batsman's runs and updating a separate "strikeRate" ref by hand is fragile — a computed() that derives strike rate from runs and balls faced automatically stays correct, the way a scoreboard's strike-rate display should update itself, not be manually re-typed after every run.

vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([
  { name: 'Keyboard', price: 79, qty: 1 },
  { name: 'Mouse', price: 29, qty: 2 },
])

const subtotal = computed(() =>
  items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
)
</script>

Extracting Reusable Local Patterns into Composables

When a local state pattern (like managing a boolean toggle, or tracking a debounced search input) repeats across several unrelated components, it's a signal to extract it into a composable function rather than a global store — the state stays local to each component instance that calls the composable, but the logic itself is shared.

🏏

Cricket analogy: When multiple broadcast graphics — one for batting stats, one for bowling stats — each need their own "isExpanded" toggle logic repeated, extracting a useToggle() composable shares the logic while each graphic keeps its own independent open/closed state.

A useful rule of thumb: state should live at the lowest common ancestor that actually needs it. Only promote state to Pinia when multiple, non-nested parts of the component tree genuinely need to read or mutate the same data — premature global state makes components harder to test in isolation and obscures data flow.

Destructuring a reactive() object, e.g. const { email } = form, breaks reactivity for that variable because it copies the primitive value at that instant, disconnecting it from the reactive proxy. Use toRefs(form) if you need to destructure while preserving reactivity, or prefer ref() for values you plan to destructure.

  • Most UI state is naturally local; keep it in the component unless multiple unrelated components need it.
  • reactive() suits grouped, form-shaped state; ref() suits standalone primitives or values you'll reassign wholesale.
  • Prefer computed() over manually synced state + watchers for any value derivable from other local state.
  • Repeated local state logic across components is a strong signal to extract a composable, not a global store.
  • Destructuring a reactive() object loses reactivity; use toRefs() if destructuring is necessary.
  • Promote state to Pinia only when it's genuinely shared across non-nested parts of the component tree.

Practice what you learned

Was this page helpful?

Topics covered

#JavaScript#VueJsStudyNotes#WebDevelopment#LocalComponentStatePatterns#Local#Component#State#Patterns#StudyNotes#SkillVeris