As the most starred open source, JavaScript library/framework repository on GitHub, Vue.js has become a top three front-end technology alongside React.js and Angular in terms of popularity, usage, ecosystem activity and developer satisfaction. Compared to React.js and Angular, Vue.js is incrementally adoptable and provides a declarative API that resonates with AngularJS developers. Evan You, the author of Vue.js, explained the original concept of Vue.js as extracting the best parts of AngularJS, such as directives (i.e., v-if and v-show), and building a lightweight, flexible alternative.

Building large Vue.js applications requires composing components together with state management (i.e., Vuex) and routing (i.e., Vue Router) libraries. In September 2020, the Vue.js team officially released Vue 3, which welcomed a number of improvements and new features:

  • A smaller runtime (~20kB gzipped).

  • Smaller bundle sizes via tree-shaking support for dead code elimination (global and internal APIs have been refactored to be accessible as named exports - import { ... } from 'vue'). Now you can import only what you need!

  • Suspending component rendering and rendering a fallback component via the <Suspense /> component.

  • Rendering components to a different location in the DOM tree via the <Teleport /> component. With portals, components such as modals, popups and notifications can be "teleported" outside as direct children of the <body /> element.

  • Rendering components with multiple root nodes via fragment support. This prevents the <div /> soup problem that is prevalent in component-based architectures.

  • Enhanced TypeScript support.

  • A new custom directive API.

    For example, to create a v-custom directive...

    Vue 2 Vue.directive("custom", { bind(el, binding, vnode, prevVnode) {}, inserted() {}, update() {}, componentUpdated() {}, unbind() {} });

    Vue 3
    const app = createApp(app);

    app.directive("custom", {
    created() {} // New
    beforeMount(el, binding, vnode, prevVnode) {}, // Formerly "bind"
    mounted() {} // Formerly "inserted"
    beforeUpdate() {} // New
    updated() {} // Formerly "componentUpdated"
    beforeUnmount() {} // New
    unmounted() {} // Formerly "unbind"
    // "update" removed

  • Performance gains due to virtual DOM being rewritten with a diffing algorithm that uses compiler-based optimizations. This speeds up component rendering.

  • Centralize component logic within a single option, setup, via the Composition API.

Particularly, the Composition API of Vue 3 has been met with controversy due to its resemblance to React hooks and its philosophical divergence from the Options API, which emphasizes separation of concerns by defining component logic within specific options (data, props, computed, methods, etc.). For those who prefer the Options API, unlike other major version upgrades that introduce incompatible changes, the Composition API will not break existing Vue components using Options API ("purely additive"). Although it offers similar logic composition capabilities as React hooks, the Composition API leverages Vue's reactivity system to circumvent several of React hooks' issues.

To understand why the Composition API's new approach for creating components allows developers to better reason about and maintain large components, we will need to...

  • Explore the Composition API and its trade-offs in more depth.

  • Compare the Composition API with the existing Options API via code examples.

  • Discuss the differences between the Composition API in Vue 3 and hooks in React.

What is the Composition API?#

A single file component consists of HTML-based markup (<template />), CSS-based styles (<style />) and JavaScript-based logic (<script />), all self-contained within a single .vue file.

Example:

The following component will display "Hello World!" as red text.

Note: The scoped attribute applies to CSS only to the elements within the current component.

In the above example, the data function returns an object that represents the component's data. Vue converts each data property to a Proxy, which makes the state reactive by allowing for proper this bindings and tracking/triggering changes. The object returned by data contains a single property, greeting, which has a value of "Hello World!" and is rendered within the <p /> element's mustaches ({{ ... }}). For a one-time interpolation, use the v-once directive.

Note: In previous versions of Vue, the reactivity system used Object.defineProperty, not Proxy.

Now that you are familiar with the base structure of a component, let's explore a slightly more complex component.

Open the CodeSandbox demo here. In this demo, let's look at a component that represents a simple accessible modal. When the modal is opened, the focus should be trapped within it. Elements within the modal should be "tabbable." When the modal is closed, the focus should return back to the element responsible for opening the modal.

(src/components/Modal.vue)

(src/App.vue)

Here, the <Modal /> component is defined using the Options API, which organizes component logic into specific options. The initialization of internal state (data), the passed props (props), the emitted custom events (emits), the available instance methods (methods) and lifecycle hooks (beforeMount, mounted and unmounted) are clearly separated from one another. For an option to access values/methods defined within other options, those values/methods must be referenced via the this keyword, which refers to the component instance itself.

Below is a list of commonly used options:

  • name - Specify a name for the component. Used for warning messages and as an identifier when registered globally with Vue.component.

  • components - Register child components.

  • computed - Call functions defined here from the component template. Unlike functions defined within methods, the result of these functions are cached based on their reactive dependencies. A computed function will only re-evaluate when at least one of these dependencies has changed.

  • data - Declare local data variables for the Vue instance. All properties here are converted into getters/setters and become reactive.

  • emits - Define custom events that can be emitted from a component.

  • methods - Call functions defined here from the component template.

  • props - Specify values acceptable as props from a parent component. These values can be validated, given a default value if not passed and/or marked as required.

  • watch - Observe and react to data changes. For each watcher function, there are two arguments: the new and old values of the variable.

Note: Do not use arrow functions when defining a watcher or methods/computed method. Otherwise, the this keyword will not reference the component instance.

As a component grows larger with more new features, it increasingly becomes more difficult to maintain and reason about the component, especially when the logic of each feature is fragmented over the individual options. The RFC for the Composition API provides a side-by-side comparison of how logic related to a feature (also called a logical concern in the official documentation) is distributed throughout a component's code. Each logical concern is identified by a unique color.

Options API vs. Composition API

Notice how neatly grouped each feature's logic is when using the Composition API. By restricting component logic to specific options, it adds a mental tax by requiring us to constantly switch from one block of code to another to understand/work on any single feature of a component. Also, it limits our ability to extract and reuse common logic between components.

This is the primary motivation behind the Composition API. By exposing Vue's core capabilities, such as reactivity and lifecycle hooks, as standalone functions, the Composition API can use these functions within the setup component option, which is executed before the component is created (formerly the created lifecycle hook option) and serves as the component's main entry point.

Let's revisit the simple accessible modal, but this time, create the component using the Composition API.

Open the CodeSandbox demo here.

(src/components/Modal.vue)

(src/App.vue)

All of the logic that was previously handled in multiple options of a Vue component is now contained within a single option, setup. Functions that behave similar to those options can be imported as named exports from vue and called anywhere within the setup function. For example, the lifecycle hook options beforeMount, mounted and unmounted can be replaced using the onBeforeMount, onMounted and onUnmounted methods in setup respectively. This allows component logic to be structured and arranged flexibly.

The reactive data returned by the data option can be replaced using the ref and reactive methods in setup. The ref method accepts a default value and returns a reactive ref object that can be safely passed throughout the entire application. Because this object is mutable, the value can be directly modified. To reference this value, access this object's .value property.

ref is commonly used for establishing reactivity for a single primitive value. To establish reactivity for an entire object, use the reactive method.

setup exposes two arguments, props and context. props contains the props passed from a parent component. They are reactive and automatically updated whenever new props are passed in. To properly destructure props, which means still preserving each props reactivity after destructuring, use the toRefs method.

context contains three properties:

  • attrs - A non-reactive object that contains the component's attributes.

  • slots - A non-reactive object that contains the component's slots.

  • emit - A method for emitting events.

Within the context of the <Modal /> component:

Although the component's methods are defined within the setup option, the component's template can only access these methods when they placed within the object returned by setup. This also applies to ref and reactive values, which are automatically unwrapped to allow the template to access their values without having to reference .value.

The close method and modalRef ref are made available to the <Modal /> template. The close method is set to the @click shortcut directive, so when the user clicks this button, the "close" event will be emitted by the component, which will trigger the function set to @close on the <Modal /> component within the <App /> parent component (the closeModal method). When set to the <div class="model" /> element's ref, modalRef will reference this DOM element.

The Differences Between the Composition API and Hooks in React#

A component built with the Composition API has its setup function only called once during its entire lifetime, regardless of how many times it is re-rendered. A React functional component using hooks is called whenever a re-render occurs as a result of a state/prop change, which pressures the browser's garbage collector. React provides useCallback to prevent inline functions in functional components from being garbage collected on subsequent re-renders via a referential equality check.

Additionally, these re-renders may cause unnecessary re-renders, which happens when the function representing a child functional component is called when its parent component is re-rendered, even if it is completely stateless.

Example:

When the count increases/decreases upon clicking any one of the <Counter /> component's button, the Child function is called. However, its DOM is unaffected by this state change. This is an unnecessary re-render. If this component was larger with more inline handlers, calls to hooks, etc., then any unnecessary re-render would cause these inline handlers, etc. to be garbage collected.

React provides a number of memoization-related methods (useMemo, memo, etc.) to address this. Of course, you should use your browser's profiling tool first to identify your application's actual bottlenecks before chucking in useMemo, memo and other memoization techniques.

Next Steps#

Each developer reasons about code differently. Whether you find it easier to reason about code by its low-level (Options API, which is better suited for grouping logic by a low-level attributes of a component) or high-level (Composition API, which is better suited for grouping logic by a component's high-level features) implementation, you must always evaluate and make trade-offs that best serves your needs. You should definitely read this RFC that outlines the Vue team's rationale and decisions for the Composition API, along with addressing some of the common criticisms echoed within the Vue community.

Many of these new features, such as portals and fragments, in Vue 3 are already present in React v16+. Try converting your React functional components to Vue 3 components using the Composition API.

Please read Part 2 of this blog post to learn about the main benefit of the Composition API: the capability to extract and reuse shared logic.

If you want to learn more about Vue 3, then check out Fullstack Vue:

Sources#