Vue 3 - The Composition API - Reusability (Part 2)

Vue's Options API constrains the reusability of logic across multiple components. Patterns involving mixins and higher-order components (HOCs) have been established to create reusable blocks of code and consolidate repeated state and functionality. However, each pattern has a drawback:

  • Higher-order components introduce more layers to a component hierarchy. The larger the component tree, the more higher-order component instances exist, which impacts performance.

  • Vue merges the properties of every mixin added to a component's mixin option with its existing options.Example:

    (src/mixin.js)

    (src/components/Component.vue)

    Resulting Component Definition:

    If a component's options object contains a property that already exists in a mixin, then there will be namespace clashing when merging these options with the mixin. Vue's default merge strategy keeps the property in the component's options over the one in the mixin. If both contain the same lifecycle hooks, then they will be ran sequentially. In larger applications, it can be difficult to track these individual properties throughout multiple components and mixins. In fact, Dan Abramov wrote an article explaining why using mixins with components is considered an anti-pattern.

With Vue 3's Composition API, component logic can be extracted, shared and reused amongst multiple components. Exposing Vue's core capabilities (reactivity and lifecycle hooks) as standalone, globally available functions allows developers to create custom hooks that are decoupled from any UI, but yet, can be consumed by any component. This can greatly improve the maintainability and flexibility of a large application composed of hundreds of components.

Let's walkthrough an example demo to demonstrate the simplicity of writing reusable code with the Composition API.

Demo Overview#

In this CodeSandbox demo, clicking the "Open Modal" button pops open a basic accessible modal that displays a list of links to sections in the official Vue.js documentation site.

Vue 3 Composition API Demo - Modal

The <Modal /> component instance contains a setup option that houses all of its data, methods, lifecycle hooks, etc.

(src/components/Modal.vue)

Here's a brief overview of the code within the setup option:

  • Define two refs: previouslyFocused and modalRef.

    • previouslyFocused will contain a reference to the element that was last focused prior to the opening of the modal.

    • modalRef will contain a reference to the <div class="modal" /> element.

  • The prevRef prop is destructured from the props object. toRefs preserves the reactivity of props destructured from the props object.

  • Define two inline methods: close and handleKeydown.

    • Calling close emits the custom event close, which will execute the function @close is set to on the <Modal /> component in the parent <App /> component.

    • handleKeydown checks if the user has pressed either the tab or ESC key.

      • When the tab key is pressed, move the focus to the next focusable element in the modal. Because the focus is trapped within the modal, tabbing on the modal's last focusable element will move the focus to the modal's first focusable element.

      • When the ESC key is pressed, the component's close method is executed.

  • Before Vue mounts the modal to the DOM (onBeforeMount), determine which element is focused prior to the modal opening and cache it. Usually, this will be the element that triggered the opening of the modal. When the modal is closed, the focus will resume back to this element.

  • When Vue mounts the modal to the DOM (onMounted), the handleKeydown event handler is bound to the window's keydown event.

  • When Vue unmounts the modal from the DOM (onUnmounted), the handleKeydown event handler is unbound from the window's keydown event. Resume the focus of the user back to the element that triggered the opening of the modal.

  • setup returns modalRef and close to make them accessible to the modal's template.

In a web application, a modal acts as a subordinate window to the browser window. Because modals can be used to communicate information to a user, they can be categorized as dialogs. Another component that behaves similarly to modals is toast notifications, which briefly notify users of certain events before disappearing. If you are not familiar with toast notifications, then have a look at an easy-to-use toast notifications library, toastr.

Using the Composition API, we can implement a toast notification component that uses the same accessibility and closing logic as our modal component. Let's refactor this logic into a separate hook that can be used in both components.

Refactoring for Reusability#

First, create a new directory, hooks, within the src directory. Then, create a new file useDialog.js within this new directory.

Inside of src/hooks/useDialog.js, create a function called useDialog that declares a ref, isDialogOpened, and has two methods responsible for updating this ref's value, openDialog and closeDialog. To allow a component to access these functions and this ref, return them in an object.

(src/hooks/useDialog.js)

The useDialog function is created outside of the context of components as a standalone piece of functionality.

Notice how this logic resembles the logic within the setup option of the <App /> component:

Let's refactor this logic by replacing it with a call to the useDialog function.

(src/App.vue)

Since isDialogOpened is reactive, any updates to this value will be reflected wherever it is used in the component template.

Examining at the <Modal /> component, we should extract out the methods and lifecycle hooks for re-use in a <ToastNotification /> component.

Because the close method relies on the context object, the handleKeydown method relies on the modalRef ref and the onBeforeMount lifecycle hook relies on the prevRef ref, all of these values need to passed to the useDialog method when we migrate these methods and lifecycle hooks.

(src/hooks/useDialog.js)

Because the call to the useDialog function within the <App /> component does not need to pass prevRef, dialogRef (formerly modalRef) and context, we should isolate this logic within a function initDialog, which will be exposed to the <Modal /> component and accept these arguments when called.

(src/hooks/useDialog.js)

We will also need to slightly adjust the <Modal /> component's template to call emitClose when the <button class="modal__close-btn" /> element is clicked.

(src/components/Modal.vue)

Because modalRef and prevRef are reactive, their updated values will be available to the methods and lifecycle hooks within useDialog whenever they are called.

Try out these changes! When you click on the "Open Modal" button, the modal pops open, and focus is trapped within it. Once the modal is closed, the focus resumes back to the "Open Modal" button.

Let's create a simple toast notification component. Add a new file to the src/components directory named ToastNotification.vue. Plug-in the same logic that is now used in the setup option of the <Modal /> component.

(src/components/ToastNotification.vue)

Let's add an example toast notification and a button that pops open this notification when clicked to the <App /> component.

(src/App.vue)