Empower growth and innovation with the latest Front-end Dev insights

It's 2026, two dialogs in Vue share the same composable—one fails to submit and the other also shows an error. Is state leaking?

Oct 11, 2026 Read: 32

In Vue, after extracting request logic into a shared composable, two dialogs sharing the same error message or loading state is usually not because the hook was extracted badly, but because the state variable was written in module scope: the module is evaluated only once on first import, so that ref becomes the single instance for the entire app, and whoever references it shares it. Based on 2026 project delivery experience, mutable loading, error messages, form drafts, and temporary lists should be written inside the function body and created per call, while module scope should keep only constants, pure functions, and stateless utilities.

Why an extracted hook can leak state

Vue 3's ref and reactive are just ordinary JS objects with a proxy layer; they do not inherently carry the concept of 'which component they belong to.' What really determines sharing versus isolation is which scope the declaration executes in and how many times it executes in total.

Module top-level code executes only once when the module is first imported; after that, no matter how many components reference it, they all get the same object reference. When a component calls a composable, however, the function body re-executes each time, so a ref written inside the function body naturally becomes one per component. This is also why the same code placed in different locations can behave in completely opposite ways.

  • Declaration location determines instance count: one outside the function, one per call inside the function.
  • Execution timing determines sharing scope: module first load, or component mount time.
  • Whether it is returned only determines whether the outside can access it, not whether it is shared.

Which patterns are inherently shared, and which are inherently isolated

To judge whether a hook will leak state, just look at the variable's ownership level; there is no need to first suspect the framework version or the reactivity system.

  • Module-level const loading = ref(false): inherently shared, one for the entire app; suitable for globally unique state such as theme and logged-in user.
  • Declared inside the function body and returned: inherently isolated, one per caller; suitable for loading, drafts, and temporary lists.
  • Provided by provide / inject: shared within the same subtree, which is intentional sharing.
  • State library (such as Pinia): shared across pages, at the cost of maintaining store structure and reset timing.

The pass/fail line is straightforward: if two pages opened at the same time should not affect each other, write it inside the function; only when synchronization is truly needed should it go into a shared layer. If you cannot tell the difference, the more you extract, the more you leak.

A nameable troubleshooting path: the 'four checks'

When you see the phenomenon of 'changing page A makes page B change too,' checking four things in a fixed order is much faster than digging through files one by one.

  1. Check declaration location: whether the suspicious variable is outside or inside the function; if it is outside, you can basically locate the root cause first.
  2. Check call count: when the same component calls the hook twice, does it get the same object? If so, the instance is being reused.
  3. Check whether sharing is intended: if the business truly needs two places synchronized, that is not a bug but a misplaced level.
  4. Check the change scope: after moving the declaration into the function body, does any other page depend on that sharing? Avoid fixing one and breaking another.

The order of the four checks should not be reversed: the first two locate the root cause, and the last two decide the fix. Skipping the third check and changing code directly can easily turn intentionally shared login state into one per component, creating new inconsistencies instead.

The common delivery-scene incident

A common situation in projects: the schedule is two to three weeks, assets and API docs are not fully finalized, and two dialogs both need to reuse the same 'selector + form submit' logic, so you extract a useDialog directly. The constraint is tight time and no extra manpower for a split review; the approach is to quickly put loading, error messages, and selected items at module top level. As a result, when dialog A fails to submit, dialog B also shows the same error message, and you discover it the day before launch. Temporarily moving the declarations back inside the function and adding regression self-tests: the typical rework time for this kind of issue is half a day to one day, and it also crowds out the original integration testing time. This kind of rework is usually not because the framework is hard, but because when extracting shared logic, no one first asked, 'Should this state be shared?'

How to choose among three strategies, and where the costs differ

To solve the same state-leak problem, the three approaches differ in cost and applicability. Based on common 2026 project rhythms, they are roughly:

  • Ref inside the function (can be the default): small change, typical range is ten-plus minutes to one or two hours; clean isolation, suitable for state that is used and discarded within a page.
  • Module-level singleton: almost zero change cost, but the largest sharing scope; only suitable for truly unique state such as theme, logged-in user, and global notifications.
  • State library (such as Pinia): requires defining a store, agreeing on naming and reset timing, typically half a day to one day or more; suitable for data that needs synchronization across pages and routes.

The selection criterion is not 'which one is more advanced,' but how many consumers this state needs and how long its lifecycle is. Putting state used only by the current page into a state library instead adds the burden of reset and cleanup.

After the fix, what counts as passing

Acceptance should not only check whether the component runs; it is recommended to go through each item:

  • Open two dialogs that use the hook at the same time; operating one does not affect the other's loading, error message, or form values.
  • After the component unmounts, loading resets and no intermediate state from the previous run remains.
  • Under abnormal operations such as fast switching and repeated submit clicks, state does not become disordered.
  • The code contains no same-named state that is both declared at module top level and used as a local variable.

A few common pitfalls

  • Only changing the return from an object to an array without moving the declaration; the problem remains.
  • Returning a reactive object; once the caller destructures it, reactivity is lost, showing as 'the value changed but the page does not update.'
  • A shared request-cancellation object is reused by multiple dialogs; canceling in one page also aborts the other page's request.
  • Directly emptying shared state in an unmount hook, affecting other pages still using it.

Applicable scenarios and boundaries

Extracting a shared hook is suitable when multiple pages reuse the same shape of logic and their runtime states are independent of each other, such as list pagination, dialog forms, and upload progress. In these scenarios, extraction can significantly reduce duplicate code, and one change takes effect everywhere. When organizing the component layer according to enterprise project delivery habits, teams usually list these types of logic first before extracting.

The unsuitable cases should also be made clear: logic used by only one or two pages and still changing frequently is often easier to write directly in the page than to extract a layer first and then modify it. Login state, global messages, and theme that truly need cross-page synchronization should not be stuffed into a page-level hook, but placed in a state library or global layer. The boundary sentence can be remembered as: who owns the state matters more than which file the logic lives in.

FAQ

If I write a ref inside the function and return it, will two dialogs definitely each get their own copy?

As long as each call re-executes the function body, they each get their own copy. But if the return value is cached in a module variable as a 'singleton' return, it becomes shared again.

If a ref returned from a composable stops updating after a child component destructures it, is reactivity lost?

Yes. Destructuring takes the value at that moment; a common practice is to access the whole returned object, or use toRefs on the caller side to preserve the reactive reference.

Should the submit-failure error message go in the hook or in a state library?

For page-local, use-and-discard messages, putting it inside the hook function is lighter; only when multiple pages need to see the same message at the same time is it worth putting into a state library for unified management.

After the dialog closes, the request callback still changes state—will that cause problems?

It may trigger meaningless updates or errors. A common 2026 practice is to abort the request on close, or check in the callback whether the current instance is still valid.


Implementation advice: before extracting a shared hook next time, first use the 'four checks' to confirm each state's consumer count and lifecycle. Default to isolation inside the function; only move it to a higher layer if it truly needs to be shared. If the team already has state library conventions, follow them; there is no need to introduce a new dependency for a small feature. The applicable boundary remains: state that serves only a single page should not be forcibly shared just to look consistent.

Have a similar project in mind?
Contact us for a one-to-one project reference proposal
Obtain Proposal
Interested in this topic?
10-year tech team — reference proposal within 24 hours
Obtain Proposal
Are you ready?
Then reach out to us!
+86-13370032918
Discover more services, feel free to contact us anytime.
Please fill in your requirements
What services would you like us to provide for you?
Your Budget
ct.
Our WeChat
Professional technical solutions
Phone
+86-13370032918 (Manager Jin)
The phone is busy or unavailable; feel free to add me on WeChat.
E-mail
349077570@qq.com
Submitted successfully
Thank you for your trust. We will contact you soon!
Recommended projects for you