DEV Community

Cover image for How I Build Vue 3 Applications (Part 3): When Logic Belongs in a Composable
Luka Jioshvili
Luka Jioshvili

Posted on

How I Build Vue 3 Applications (Part 3): When Logic Belongs in a Composable

After deciding where components belong, the next question is:

Should this logic become a composable?

Vue makes extracting logic incredibly easy.

Unfortunately, that also makes it easy to extract too much.

Before long, composables/ becomes another dumping ground full of useSomething.ts files with unclear responsibilities.

Over the years, I've settled on a simple decision process that helps me avoid that.

This article isn't about using composables.

It's about knowing when to create one.


Why does composables/ become a dumping ground?

Just like components/, the composables/ folder usually starts out clean.

A few composables solve obvious problems:

  • useDebounce
  • useMediaQuery
  • useClipboard

Everything feels organized.

As the project grows, though, another pattern starts to appear.

Composable Structure

Nothing looks technically wrong.

But eventually, every piece of extracted logic becomes another useSomething.ts.

The folder grows, responsibilities become blurred, and it's no longer obvious what deserves to be a composable.

The problem isn't having a lot of composables.

It's extracting logic without a clear reason.

Many developers start with a simple idea:

"This component is getting big."

So they move code into a composable.

The component becomes shorter.

But the application doesn't necessarily become simpler.

Instead of moving responsibility, they only moved lines of code.

That's an important distinction.

A composable should exist because it owns a single, well-defined behavior, not because the component reached 300 lines.

Every composable should own one clear behavior.

If I can't describe that behavior in a single sentence, it's usually a sign that the composable is doing too much.


So before I extract any logic, I don't ask:

"Can I move this into a composable?"

Instead, I ask something much more important:

"Does this logic have its own responsibility?"

That's the question that drives almost every decision I make.

A composable shouldn't exist simply because a component is getting bigger.

It should exist because it owns one well-defined behavior.

For me, that's where the Single Responsibility Principle naturally applies.

Every composable should be focused, predictable, and easy to describe in a single sentence.

Once I can clearly explain what it owns, deciding whether it deserves its own composable becomes much easier.

That simple shift changes the question from:

"Can I extract this?"

to

"Should this behavior have its own home?"


What logic should stay inside the component?

Not every piece of logic deserves its own composable.

In fact, most of it doesn't.

A common mistake is assuming that every ref, computed, or watch should be extracted.

I don't see composables as a way to reduce line count.

I see them as a way to separate responsibility.

If the logic only exists to support a single piece of UI, I usually leave it where it is.

For example:

  • Is a dialog open?
  • Which tab is selected?
  • Which row is hovered?
  • Which accordion is expanded?

The component is the owner of that state.

Here's a typical example:

Local UI state that remains owned by the UserSettings component

Those values belong to the component because the component owns that interaction.

They don't have their own responsibility.

They're simply part of how this component behaves.


When does logic deserve a composable?

If the logic shouldn't stay inside the component, I ask another question:

Does this logic have its own responsibility?

This is where I stop thinking about file size.

I start thinking about behavior.

A composable shouldn't exist because the component became bigger.

It should exist because the behavior deserves its own home.

That's the decision process I follow:

Decision Tree

The number of lines isn't the deciding factor.

A small piece of logic may deserve a composable if it owns a clear responsibility, while a much larger block of tightly coupled UI logic may still belong inside the component.

The goal isn't to make components shorter.

It's to make responsibilities easier to understand.


Generic vs Feature Composables

Once I've decided that the behavior deserves its own composable, the next question becomes:

Who owns it?

Not every composable has the same owner.

Some are generic and reusable across the entire application.

Others exist because of one specific business feature.

Generic composables

Generic composables are business-agnostic.

They solve reusable technical problems such as:

  • useDebounce
  • useClipboard
  • useMediaQuery
  • useInterval
  • useWindowSize

They should be easy to move into another Vue project without changing their behavior.

Feature composables

Feature composables understand one business domain.

Examples include:

  • useBillingFilters
  • useInvoiceValidation
  • useProductSearch
  • useSessionTimer

They may depend on domain types, services, stores, or business rules because that feature owns them.

The distinction is simple:

Generic composables own reusable behavior.

Feature composables own domain-specific behavior.

Both are valid.

The important part is that their ownership is clear.

Tree


Services vs Composables

Creating a composable doesn't mean every piece of logic belongs inside it.

One convention I follow consistently is keeping network requests inside services.

A composable may coordinate reactive state such as loading, errors, or pagination, but the actual request belongs somewhere else.

For example:

┌────────────┬──────────────────────────────────────┐
│ Component  │ Render the UI                        │
├────────────┼──────────────────────────────────────┤
│ Composable │ Manage reactive behaviour            │
├────────────┼──────────────────────────────────────┤
│ Service    │ Perform API / business operations    │
└────────────┴──────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

This keeps responsibilities clear.

  • Components render the UI.
  • Composables manage reactive behavior.
  • Services perform operations.

Knowing where to look for each responsibility makes the codebase much easier to navigate.


Composable or Store?

A question I often ask is:

Does this state belong to one workflow or the entire application?

If the behavior belongs to one component or one feature, a composable is usually enough.

If unrelated parts of the application need to share the same state, it probably belongs in a store.

The distinction isn't about complexity.

It's about ownership.

A composable coordinates behavior.

A store coordinates shared application state.


Conclusion

There isn't a magic number of lines that tells you when to create a composable.

What matters is responsibility.

Some logic belongs inside the component.

Some deserves its own composable.

Some belongs in a service.

Some belongs in a store.

The important part is knowing why.

The goal isn't to create more abstractions.

It's to make every piece of your application have a clear owner.

Extract responsibility, not lines of code.

That's the rule I come back to whenever I'm deciding where new logic belongs.

Top comments (0)