skip to content

Vue 3 removed filters like `{{ price | currency }}`; what replaces a local filter and a globally registered one?

level: juniorimportance: should knowfreq 45%

answer

  1. templates are just JavaScript now
  2. the pipe is bitwise OR
  3. method call or computed locally
  4. globalProperties for global helpers

basics

~20 s

Vue 3 dropped filters, so a local filter becomes a method call or computed property and a global Vue.filter becomes a function you import, or one exposed as app.config.globalProperties.$filters. The pipe in a template is now JavaScript's bitwise OR.

solid answer

~40 s

Filters were a Vue 2-only template syntax, `{{ price | currency }}` or `:title="date | fmt"`, registered locally in a `filters` option or globally with `Vue.filter`. Vue 3 removed them because they made template expressions not quite JavaScript. A local filter becomes a **method call** (`{{ currency(price) }}`) or a **computed property** when it formats one known value. In `<script setup>` you can simply import a plain `formatCurrency` function and call it in the template. A global filter becomes a shared utility module, or, as the migration guide suggests for many templates, an object on `app.config.globalProperties.$filters` called as `{{ $filters.currency(price) }}`. Watch the leftovers: without the compat build the `|` is parsed as bitwise OR, so the old expression renders a wrong number and warns about an undefined property instead of failing loudly.

go deeper

for a junior

Recall that filters are gone in Vue 3 and that {{ price | currency }} becomes {{ currency(price) }}, a method call or an imported function.

for a middle

Know both replacements, method or computed locally, globalProperties.$filters or a utility module globally, and why filters broke the just-JavaScript template rule.

for a senior

Catch the silent failure: without the compat build the pipe is bitwise OR, so a missed filter renders a truncated number with only a dev warning.

for a principal

Treat formatting as shared code, not template syntax: centralise it in typed utility modules so the migration also removes a global registry.

## What filters were In Vue 2 a **filter** was a formatting function applied with a pipe inside interpolations and `v-bind` expressions: - `{{ price | currency }}` called `currency(price)`; - `{{ price | currency('EUR') }}` called `currency(price, 'EUR')`, with the piped value always first; - `{{ text | trim | capitalize }}` chained, meaning `capitalize(trim(text))`; - filters were registered **locally** in a component's `filters` option or **globally** with `Vue.filter('currency', fn)`. ## Why Vue 3 removed them The migration guide's reason is that filters required a custom syntax that broke the assumption that expressions inside curly braces are **just JavaScript**. That had a learning cost and an implementation cost, and it duplicated what a function call already does. So Vue 3 removed filters entirely; the migration build can emulate them temporarily under the `FILTERS` and `COMPILER_FILTERS` flags. ## Translating the syntax | Vue 2 filter expression | Vue 3 equivalent | |---|---| | `{{ price \| currency }}` | `{{ currency(price) }}` | | `{{ price \| currency('EUR') }}` | `{{ currency(price, 'EUR') }}` | | `{{ text \| trim \| capitalize }}` | `{{ capitalize(trim(text)) }}` | | `:title="date \| fmt"` | `:title="fmt(date)"` | Keep the argument order: the value that was piped in goes **first**, then the filter's own arguments. ## Replacing a local filter Two options, depending on the shape of the use: - **A method call** when the same formatter is applied to several values or inside a `v-for`: `{{ currency(item.price) }}`. - **A computed property** when the component formats one known value, such as a `balanceInUsd` derived from a prop; it is cached until its inputs change. In `<script setup>` the simplest replacement is an import, because top-level imports are usable directly in the template: ```vue <script setup lang="ts"> import { formatCurrency } from '@/utils/format' defineProps<{ price: number }>() </script> <template> <p>{{ formatCurrency(price, 'EUR') }}</p> </template> ``` ## Replacing a global filter Global filters were used across many templates, so editing every component to add a method is tedious. The migration guide's suggestion: ```ts const app = createApp(App) app.config.globalProperties.$filters = { currency(value: number) { return '$' + value } } // template: {{ $filters.currency(accountBalance) }} ``` Two notes on that approach: 1. It only works with **methods**, not computed properties, because a computed belongs to a single component instance. 2. It is a bridge. For new code, and anywhere `<script setup>` or TypeScript is involved, a plain utility module you import is clearer and is type-checked without extra declarations. ## A worked migration 1. **Inventory** every `filters:` option and every `Vue.filter(...)` registration; that is the full list of names to replace. 2. **Move the function bodies** into a plain utility module, one exported function per filter, keeping the value-first argument order. 3. **Rewrite each pipe** as a function call, inside out for chains. 4. **Bridge if needed**: while hundreds of templates still use a global filter, expose the functions on `app.config.globalProperties.$filters` and rewrite pipes to `$filters.name(value)` first, then to imports later. 5. **Delete the registrations** and, if you used the migration build, confirm the filter warnings are gone before disabling its compat flags. ## The silent failure to look for Without the migration build, Vue 3 does not reject a leftover filter. It compiles `{{ price | currency }}` as ordinary JavaScript, where `|` is the **bitwise OR** operator: - in the Options API, `currency` is looked up on the component, is `undefined`, and Vue warns in development that the property was accessed during render but is not defined on the instance; - `price | undefined` converts `price` to a 32-bit integer, so `19.99` renders as `19`. The page renders a plausible but wrong number. That is why a search for `|` inside `{{ }}` and bound attributes belongs in the upgrade checklist, and why the migration build's filter warnings are worth reading before you switch it off.

  • What does a leftover `{{ price | currency }}` render in Vue 3 without the migration build?
    It is compiled as JavaScript, so `|` is bitwise OR. If `currency` is not defined on the instance, Vue warns in development that the property was accessed during render but is not defined, and `price | undefined` truncates `price` to a 32-bit integer: `19.99` renders as `19`. No error is thrown, so the bug can ship unnoticed.
  • When would you choose a computed property over a method to replace a Vue 2 filter?
    Use a computed when the component formats one known reactive value, such as a prop-derived balance: it is cached and recomputes only when its dependencies change. Use a method, or an imported function, when the formatter takes arguments or runs per item inside `v-for`, since a computed cannot take per-call arguments.

saying these in an interview costs you the question

  • Vue 3 still supports filters but only in text interpolations
  • A leftover filter makes the Vue 3 template compiler fail
  • app.filter() registers global filters in a standard Vue 3 build
  • A computed property can be shared through globalProperties like a filter
  • Filter arguments come before the piped value when converted