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
mixinoption with its existing options.Example:
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.
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.
<Modal /> component instance contains a
setup option that houses all of its data, methods, lifecycle hooks, etc.
Here's a brief overview of the code within the
previouslyFocusedwill contain a reference to the element that was last focused prior to the opening of the modal.
modalRefwill contain a reference to the
<div class="modal" />element.
prevRefprop is destructured from the
toRefspreserves the reactivity of props destructured from the
Define two inline methods:
closeemits the custom event
close, which will execute the function
@closeis set to on the
<Modal />component in the parent
handleKeydownchecks 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
closemethod 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 (
handleKeydownevent handler is bound to the
When Vue unmounts the modal from the DOM (
handleKeydownevent handler is unbound from the
keydownevent. Resume the focus of the user back to the element that triggered the opening of the modal.
closeto 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,
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.
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,
closeDialog. To allow a component to access these functions and this ref, return them in an object.
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
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.
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.
Because the call to the
useDialog function within the
<App /> component does not need to pass
context, we should isolate this logic within a function
initDialog, which will be exposed to the
<Modal /> component and accept these arguments when called.