--- title: Fixing JavaScript observability, one library at a time description: Sentry is adding TracingChannel support to 44 JavaScript libraries upstream, replacing fragile monkey-patching with native observability that works across all runtimes. tags: ['javascript', 'observability'] --- # Fixing JavaScript observability, one library at a time --- --- title: The Curious Case of Vue's Function Props description: A curious look at Vue's function props and their unexpected advantages over emits. tags: ['frontend', 'vuejs'] --- # The Curious Case of Vue's Function Props --- --- title: Wait for that tick description: Learn how Vue's nextTick works, why DOM updates are batched, and how to avoid timing issues when working with refs and animations. tags: ['frontend', 'vuejs'] --- # Wait for that tick --- --- title: Falling With Style description: Learn how Vue.js fall-through attributes work, when they're useful, and common pitfalls to avoid. Master class, style, and event handling in components. tags: ['frontend', 'vuejs'] --- # Falling With Style --- --- title: Vue.js Magical Reactivity Has Some Quirks description: Why does Vue's reactivity sometimes behave unexpectedly?! tags: ['frontend', 'vuejs'] --- # Vue.js Magical Reactivity Has Some Quirks --- --- title: Just Use useTemplateRef for Template Refs description: Learn how to use useTemplateRef for easier template refs in Vue.js. tags: ['frontend', 'vuejs'] --- # Just Use useTemplateRef for Template Refs --- --- title: Getting Started With Provide/Inject In Vue.js description: Explore what's beyond props and events with Vue's powerful Provide/Inject API. tags: ['frontend', 'vuejs'] --- # Getting Started With Provide/Inject In Vue.js --- --- title: Understanding Vue.js Directives description: learn to create custom directives, and discover practical use cases and the best practices of using them in your projects. tags: ['frontend', 'vuejs'] --- # Understanding Vue.js Directives --- --- title: Handling Async Components' loading errors description: What happens when the component fails to load? Let's see how to handle this situation. tags: ['frontend', 'vue', 'ux'] --- # Handling Async Components' loading errors
So having worked at Rasayel for a while now, we've built the most complex web app I have ever worked on. It's a huge project with many moving parts, and it's been a great learning experience. One of the things I've learned is how to handle errors, but they come in various shapes. One aspect I have been ignoring for a while is the importance of handling errors when loading async components, and this is what this article is about.
## When do we use async components? One of the great things about Vue is how easy it made marking a component as "lazy-loaded" or "async" (I will be using these terms exchangeably). This allows for a more performant application, as we don't need to load the component until required. This is especially useful when we have a large application with many components, and we want to reduce the initial load time. This has been straightforward to do in Vue since the days of Vue 2 and is still the same in Vue 3. You would need to use the `defineAsyncComponent` function to define a component that will be loaded asynchronously. Here's an example: ```js const AsyncComp = defineAsyncComponent( () => import('./components/MyComponent.vue'), ); ``` The `import()` function is a dynamic import that returns a promise. When the promise resolves, the component is loaded and can be used. So most of the time you do it explicitly like this, but one of the main use cases for async components is when you use it to define a route component in Vue Router. Here's an example: ```js const router = createRouter({ // ... routes: [ { path: '/settings', component: () => import('./pages/UserSettings.vue'), }, ], }); ``` This is particularly important as apps with client-side routing or SPAs (Single Page Applications) will need to load the component when the route is visited. Otherwise, you would be forcing the users to download the entire app with all of its pages, and sub-components all at once. Suffice it to say, this will make your app slower to load initially and potentially unresponsive. Certain frameworks and configurations even do it for you under the hood by default because it is such a critical good practice. For example, Nuxt.js and [unplugin-vue-router](https://github.com/posva/unplugin-vue-router). So far this sounds good and in most cases, you don't need to worry about anything beyond what I showed you. But what happens when the component fails to load? What could even occur that would cause the component to fail to load? and How would your app behave in such a situation? ## What could go wrong? Lazy loading a component is essentially a network request that fetches what the component requires to be rendered. This means it will load a JS file, maybe a CSS file, or maybe some other assets. And because it is a network request, it can fail for the same reasons a `fetch` call may fail: - Maybe the device is offline. - Maybe the server is down. - Maybe the file(s) doesn't/don't exist. - Your CI pipeline could have failed to build the app correctly or the file was deleted. - The app might be trying to load an older version of the file that the new deployment has since been renamed/removed. These are all good reasons, but they are exceptions. Many things need to go wrong for any of those issues to happen which is why many developers don't feel the need to handle them. I won't try to convince you otherwise, but the next one is more common than you think. So at Rasayel, in 3 years of building and shipping countless features that involve tons of lazy loading, we can add one more reason to this list, adblockers. Adblockers are known to block requests that contain certain keywords like "ad" "banner" "popup" "dialog" or most of the market-y words you can think of. In our case, it blocked any component that had the word "campaign" in its name and started blocking "dialog" components as well recently with some adblockers. While this is outside of your control, it is something you can handle gracefully and in an informative way to the user. This is what the UX of errors is all about: **if you cannot recover, inform**. ## The Problem You know now what could go wrong, but what happens when it does? What happens with your app? At best nothing happens. At worst the app crashes. Neither of these is a good user experience. To make matters a bit harder, when any of the above happens. They all throw the exact same error, you cannot tell by just looking at the error why it happened. You can only guess using the context of the error and with the help of some browser APIs. Here's an example of what the error looks like in the console: ``` Failed to fetch dynamically imported module https://.... ``` So you can see that the error message is not very informative, and it doesn't tell you why it failed. So trying to tell your user what went wrong is harder than you think. Let's first see if we can prevent the app from crashing or if we can do something when the component fails to load. ## onError and errorComponent Luckily, Vue's `defineAsyncComponent` has a couple of APIs you can use to handle errors. The first one is `errorComponent` which is an alternative component that renders when the async target component fails to load, you can specify it using the extended object definition for an async component. Here is an example: ```js // You need to import it synchronously, otherwise what's the point? const AsyncComp = defineAsyncComponent({ // the loader function, whatever we had before loader: () => import('./Foo.vue'), // Will render this if the `Foo.vue` fails to load for whatever reason errorComponent: ErrorComponent, }); ``` The error component will receive the error that caused the async component to fail to load as a prop, so you can use it to inform the user about what went wrong. Here is a quick definition of an error component: ```vue ``` Here is a full example in action, we are loading a component that fails to load on purpose: This is good for loading in content sections of the app, but not that great for overlay components like dialogs or modals because there is the matter of positioning, but the main thing is you need to create an error component for the different kinds of async components you have around your apps. Another API we could use is `onError`, which is a callback that is called when the async component fails to load. This is useful if you want to log the error or send it to a logging service, among other things. Here is an example: ```ts const AsyncComp = defineAsyncComponent({ // the loader function, whatever we had before loader: () => import('./Foo.vue'), // Will render this if the `Foo.vue` fails to load for whatever reason onError(error, retry, fail, attempts) { // Do stuff... }, }); ``` We have a few cool arguments to play with here, let me explain them: - `error`: The error that caused the async component to fail to load. - `retry`: A function that you can call to retry loading the component. - `fail`: A function that you can call to re-throw the error up the chain. - If there is a global error handler or boundary, it will catch it. - If there isn't, the app will crash which is what was happening without this API. - `attempts`: The number of times the component has tried to load. Here is a simple example that doesn't do much, we just tell the user that we failed to do something and that they should try again: A few bits are going on here, so let me try to break it down: - We have a global `` component that we will trigger via a global event whenever any async error happens. - We use DOM events and `CustomEvent` object to create a custom event that we can listen to globally. - We use the `onError` callback to trigger the event when the async component fails to load from anywhere in our app. This while a bit more complex than the previous example, is more flexible and gives you more options and control over what you want to do with the error. Even tho the example is a bit lacking, this API allows you to do more than render an error component. You can retry loading the component, or do something else entirely. To improve upon this we need to dig more into the error and try to inform the user about what went wrong. ## Disambiguating the error The error itself doesn't tell us much, but we can go over the reasons we mentioned earlier and try to confirm or rule out each one of them. Let's start with the user connection. The next few examples will be simplified for brevity, but you can expand upon them with the previous examples to make them more useful. At the end of the article, I will show you a more complete example. ### Network The `navigator` object has a property called `onLine` that tells you if the user is online or not. This is not a perfect solution, but it's a good start. Here's how you can use it: ```js const AsyncComp = defineAsyncComponent({ loader: () => import('./Foo.vue'), onError() { if (!navigator.onLine) { console.error('The user is offline'); return; } }, }); ``` The `navigator.onLine` reports its value based on the user's device. So if you disconnect from the Wifi or turn on the airplane mode, it will return `false`. However it doesn't work well with the low-fi situation where the user device is connected to a network, but the network itself is down or spotty. Another thing you can do is to try to send a simple request to a server, it needs to be something you know is always up. A static page is perfect for this. Here's an example: ```js {9-14} const AsyncComp = defineAsyncComponent({ loader: () => import('./Foo.vue'), async onError() { if (!navigator.onLine) { console.error('The user is offline'); return; } try { await fetch('/online.html'); } catch (err) { console.error('The user is offline or has a bad network'); return; } }, }); ``` This is where I like the `fetch` behavior, it only throws if the request is never made due to a network error. If the request is made but the server responds with an error, it doesn't throw. This is perfect for our use case. Even if our static page is down, the request will still be made and the error will not be thrown. However, if the user's device is offline or the user has some other spotty network issue, the request will not be made and the error will be thrown. Notice that I'm still checking if the user is offline using `navigator.onLine`, because if the device is disconnected then it is a bit redundant to make the request. `navigator.onLine` is accurate in this case, but if the user is connected to a network that's spotty or down, then it's not as reliable. You can optimize this. We don't need to make a `GET` request. Instead, we could make a `HEAD` request, which is a lot faster and doesn't download the entire page. This ensures we are conscious of the user bandwidth and gives us what we need. Here's how you can do it: ```js const AsyncComp = defineAsyncComponent({ loader: () => import('./Foo.vue'), async onError() { if (!navigator.onLine) { console.error('The user is offline'); return; } try { await fetch('/online.html'); // [!code --] await fetch('/online.html', { method: 'HEAD' }); // [!code ++] } catch (err) { console.error('The user is offline or has a bad network'); return; } }, }); ``` You can dig down further if you want and try to figure out what kind of network error it is by inspecting the error itself. But this here is enough for us to know if the user is offline or has a bad network. Here is how it works: before you try it out, make sure you are offline, you can do so by opening the devtools, and going to the network tab, and choosing "Offline" from the throttle dropdown, or just disconnect your device. { const module = window.__modules__[key]; const blob = new Blob([module], { type: 'text/javascript' }); const url = URL.createObjectURL(blob).replace('blob:', ''); await (await fetch(url)).text(); return Promise.resolve(window.__modules__[key]); };`} files={{ 'App.vue': 'network-check', 'ErrorDialog.vue': 'global-error-dialog', 'utils.ts': 'utils', 'AsyncDemo.vue': 'happy-network', }} client:visible /> In the demo, we had a chance to recover automatically by waiting for the user to come back online. But usually, you don't want to block their UI while they are offline, also retrial isn't always the best solution. You can let them do the action again instead after informing them. I just included this bit to show you how much flexibility we have here. ### Component Assets Existence The request we made earlier doesn't tell us if the component's assets exist or not, but we can try to confirm that by doing a similar `fetch` on our component's JS file and checking the response status code. We have a few cases to handle: - **Non-200**: - **4xx**: 404 if the file doesn't exist, the server may return 403 or 401 if the file is protected. - **5xx**: Ok wow, we have a serious issue at hand here. - **200**: The file exists and can be downloaded, this is a weird one. The handling here is up to you, but in my opinion, there is not much difference between a 4xx and a 5xx. What we are doing here is checking the downloadability of the file, in either case, the user cannot download the file and that's what matters. A message telling your user to reload the app and try again is usually good enough here, you could also show them a retry button. If the file is off-limits for one reason or another you cannot recover from this but what is crucial is you send the error to your logging service with the exact details so you or your team can debug it later. ```js const AsyncComp = defineAsyncComponent({ loader: () => import('./Foo.vue'), async onError(error, retry, _, attempts) { //... try { // We don't have the URL that failed directly, so regex it is. // Some browsers might not give you the URL in the error message. const url = error.message.match(/https?:\/\/[^ ]+/)?.[0]; const response = await fetch(url, { method: 'HEAD' }); // Hmm, got 200. This is weird. if (response.ok && attempts < 2) { // Might as well retry the import, could've been a blip on the network. // If it works, then it works, the user won't notice it. retry(); return; } // Non-200 status code, we have a problem. dispatchAsyncError({ message: 'Could not download the component, looks like a 404', retry, }); // Send this to your logging service, this is crucial. logError({ message: 'Failed to load async component', error, status: response.status, }); } catch (error) { // Fetch errors mean their network is bad, 4xx and 5xx won't be caught here. // same handling as before... } }, }); ``` This is a bit simpler, we do some sort of auto recovery here by checking if the file is downloadable, and if it is then we retry on the spot so the user won't be aware of it. However, if it fails we inform the user and log the error. Here is an example of a component 404ing: This situation is a bit rare, if you try to load in a component that you don't have, your bundler will likely complain. I know `vite` will scream at me if I do that. This is more for some weird cases where maybe file name case sensitivity is at play or your CI pipeline replaced the files with a fresh deployment or some other weird case. For us, this was too common until we fixed our deployment pipeline to keep all old assets for long user sessions. The main thing here, if something happens you will know exactly what went wrong which puts you on the path to fixing it. ### AdBlockers Ok, we all use AdBlockers, right? But at Rasayel some customers had such an aggressive adblocker that it blocked any component that had the word "campaign" in its name. Campaigns are a big part of our app, so this was a big issue for us. We considered obfuscating the component names during the build, but this will come back to bite us if an error happens regarding that component since we won't be able to tell which one it is. But if that works for you then go for it! Still, there is no telling what an adblocker may decide to block and how they might evolve in the future to keep doing so. So we need to handle this gracefully, telling the user that "if they have an adblocker, they should disable it" is more than enough here. We just need to detect it. We can start by inspecting the error message or error name given to us in the `onError` callback: ```js const AsyncComp = defineAsyncComponent({ loader: () => import('./Ads.vue'), async onError(error) { console.log(error.message, error.name); }, }); ``` But remember what I said at the start of this article? The error is always... ``` Failed to fetch dynamically imported module: http://.... ``` So it is not very helpful, and indeed we only used it to pick up the file URL which will be useful here. We could try to fetch the file with our `HEAD` method and inspect the response just like what we did before with non-200 responses. In this case, you will get no response because the error is thrown just like the network error. ```js const AsyncComp = defineAsyncComponent({ loader: () => import('./components/Ads.vue'), async onError(error) { try { const url = error.message.match(/https?:\/\/[^ ]+/)?.[0]; const response = await fetch(url, { method: 'HEAD' }); } catch (err) { // The request didn't go through, this is either a network error or an adblocker. } }, }); ``` This makes distinguishing between a network error and an adblocker a bit harder, but remember that we already checked for network errors. So if the request fails, then it is likely an adblocker. And we can inform the user about it. Here is an example, if you are using an adblocker add the following to your blocklist: ``` /BigAd.vue ``` We can combine this with the downloadability check we did earlier to avoid making a request twice. ```js const AsyncComp = defineAsyncComponent({ loader: () => import('./Foo.vue'), async onError(error, retry, fail, attempts) { //... try { const url = error.message.match(/https?:\/\/[^ ]+/)?.[0]; const response = await fetch(url, { method: 'HEAD' }); if (response.ok && attempts < 2) { retry(); return; } // Non-200 status code, we have a problem. dispatchAsyncError({ message: 'Could not download the component, looks like a 404', retry, }); // Log error to your logging service // ... } catch (error) { dispatchAsyncError({ // [!code ++] message: 'Looks like you have an adblocker on', // [!code ++] retry, // [!code ++] fail, // [!code ++] }); // [!code ++] } }, }); ``` ## When to call `fail`? So you may have felt that we are not making use of the `fail` function in the previous examples. However, if you check the `ErrorDialog.vue` component code we used earlier, you will find that it calls it whenever the user dismisses the dialog without resolving the issue. ```js function onExitErrorHandling() { dialogEl.value?.close(); // You should use `fail` to tell Vue the error handler wasn't successful in recovering callbackProps.value.fail?.(); } ``` This is not required in my opinion but it appears to be a good practice, there aren't many resources on this so I'm not sure myself. I tested a few scenarios with or without calling it, and it seems like Vue assumes the component error was resolved if you don't call it. So semantically, I would call it if the user dismisses the dialog as the issue has not been resolved. This means you will need to ignore this error from popping up in your logging service since you already have a better handling for it which is a win. ## All together now Putting everything together and cleaning it up will give us a nice error handling system for our async components. It's all a mess however and we can clean it up by breaking up the logic into smaller functions and creating a `defineAsyncComponent` wrapper that incorporates these error handler strategies. This way we can use it across our app without repeating ourselves. Here it is all in action, try changing the name of the async component to any of the following: - `/BigAd.vue` if you want to test adblocker detection, make sure to add it to your blocklist. - `/AnythingWeird.vue` if you want to test 404 and non-200 responses. - If you want to test offline detection, disconnect or simulate an offline connection before clicking the button. If you want to reset the example, reload this article. { const module = window.__modules__[key]; const blob = new Blob([module], { type: 'text/javascript' }); const url = URL.createObjectURL(blob).replace('blob:', ''); await (await fetch(url)).text(); return Promise.resolve(window.__modules__[key]); };`} client:visible /> The `utils.ts` is where the magic happens, but overall we now have a sound strategy for handling these kinds of errors. You can expand upon this by adding more checks or more error handling strategies. ## Other Ideas and Explorations I have considered other ways to handle these errors, sadly you won't get much from the error object itself. Because whenever this error is thrown, you only get the obscure one that is a TypeError, however the fetch error that caused it won't be thrown and won't be connected to this event typically. This is the main problem we have here. So one thing you can probably explore to connect the fetch error itself with the error event is to use a service worker to make that connection for you. But I found this a bit too advanced to cover here. Another limitation is you cannot at the moment use both `errorComponent` and `onError` together, you can only use one of them. This is a bit limiting as I imagine where you want to perform some action and then render an error component. An example here is you have a tab/accordion system that lazy loads its contents, you want to run our error handling logic and render a component in-place of the tab/accordion that failed to load. One way to do this is to move some of the logic we have written here to the error component itself, but remember you don't have access to `retry` or `fail` or the `attempts` count. One more thing before I finish. You may want to choose a better message copy to display for the user, I used plain ones here so you know what happened but your users may appreciate a different tone and wording. ## Conclusion This might look a bit niche or an overkill, but this will make it easier not to waste time on debugging these kinds of issues where the user device is either blocking the file or their network is dodgy, and allows us to focus on the real issue where everything looks fine but the component isn't loading and even then, we have more info to work with. I believe this is one of the least explored APIs in Vue.js and with this article, I hope you can make your apps more robust and user-friendly and evolve this API if need be. --- --- title: Fix Your Annoying Popups with the CloseWatcher API description: Interesting new API to help us build better and more accessible UIs tags: ['frontend', 'web-api', 'platform'] --- # Fix Your Annoying Popups with the CloseWatcher API
So my product designer came to me the other day and asked me to implement something to cancel an action whenever the user hits the Esc key. I was like "Sure, no problem" and I went on my way to implement it but then I noticed the way I was doing it was fundamentally flawed. The [CloseWatcher](https://web.dev/blog/web-platform-12-2023) is a new API that has been recently introduced in Chrome 120. The main job of this API is for listening and responding to "close requests", but what are they and why is this even a problem? And why is it related to my short story earlier?
## What is a close request? A close request is when the user using your web app wants to close something, just about anything. This can range from sidebars, menus, popovers, modals, an accordion, or whatever UI that has an open/close interaction. The [proposal](https://github.com/WICG/close-watcher) defines it as: > platform-mediated interaction that's intended to close an in-page component. This is distinct from page-mediated interactions, such as clicking on an "x" or "Done" button, or clicking on the backdrop outside of the modal. Usually, those close requests are triggered by hitting the Esc key, specifically "pressing down" the key. But that's just for the desktop. On mobile and specifically on Android, the "back" key is also another source for triggering those close requests. On iOS there is no such way to do that without the [VoiceOver assistive "Z" gesture](https://support.apple.com/en-eg/guide/iphone/iph3e2e2281/ios#:~:text=To%20dismiss%20the%20Item%20Chooser,making%20a%20%E2%80%9Cz%E2%80%9D). ## Why is this a problem? We've been closing modals and opening menus for more than a decade on the web now, so why is this suddenly a problem? One reason is we are in the golden area of web apps and the platform is trying to normalize, standardize, and offer as much help as possible to the developers to write simpler and better web apps through more standard APIs for complex web UI needs. But also recently, the `popover` API was introduced, and not so recently the `dialog` element was also introduced, and both defined some interesting interaction and accessibility behaviors when it comes to closing stuff that has been well established in the web for a while now across multiple UI frameworks and libraries, but never standardized. Some of those behaviors are: - Clicking outside the popover closes it automatically. - Pressing the Esc key closes the popover automatically. - Pressing the Esc closes the last open popover, if there are multiple popovers open. - Continually pressing the Esc will close the popovers one by one, starting from the last opened one until none are left. We've been doing click-outside detection for a while now, also detecting a key press on the Esc key is not that hard, but the last two behaviors are a bit tricky to implement and it is not always clear how to do it. Let's say you are building a floating component with any framework of your choice. That can be a menu or a modal or whatever, and so you want to support the last two behaviors I just listed, how would you do it? Better yet, let's go through a few examples. ### Example: Sidebar This is a simple sidebar that shows up when you press the button, the sidebar happens to have an input of type `search`. - Vue.js, but the API is framework agnostic and can be used with any framework or even vanilla JS. - `@vueuse/core` for the `onClickOutside` and `onKeyDown` functions. - `tailwindcss` for styling. If you open the sidebar, click outside, or press Esc it closes as expected. However, there is a thing about `input[type="search"]` that you may not be aware of. When pressing the Esc key, the [input will clear itself](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/search#differences_between_search_and_text_types), so if you have some text in the input and you press Esc it **_should_** clear it. But here, we close the sidebar altogether so that may be unexpected to the user, especially if they prefer to use the keyboard to navigate around the UI, this is the first problem here. Merely listing to the Esc key is not enough, we need to consider if other parts of the UI are also interested in the same key-down event. You can try handling it yourself by checking the currently active element and if it happens to be an input text of type search, then you ignore the event until it is safe to close the sidebar. ```ts onKeyDown('Escape', (e) => { // Skip if the active element is a search input if ( document.activeElement?.tagName === 'INPUT' && document.activeElement?.type === 'search' ) { return; } isOpen.value = false; }); ``` This would work nicely, but I don't think you would be building a web app with just a sidebar and a search input. You would have modals, other inputs, pickers, menus, and more stuff that could be interested in that precious Esc keystroke, so are you going to conditionally filter out every other thing? Say you are building another component, are you going to check if the sidebar is open before you decide to close that other component? Wait, that means all components that "open", need to be aware of each other somehow so they can manage their close behavior. ### Example: Context Menu Here is another example with a different kind of issue. We have this context menu component that has a list of items. If the user wants to close the current open item then they need to hit the Esc key or click outside the menu. That is all straightforward, but let me throw a wrench in there and add another requirement. The menu can be nested. Now immediately you would ask a few questions about the behavior here, one of them is "Does the Esc key close the currently open menu item or all the menu items currently open?". We can argue whether that is an expected behavior or not. But this is the same behavior you can observe with nested popovers or even multiple popovers at the same time and if the menu is to be fully keyboard-navigable then it is reasonable to implement that behavior. How can we go about doing that? A shared state or a store could help those to become aware of one another's state or if it should be closing on that Esc keystroke or not. Or if you like Vue.js's `provide/inject` then the components can inform one another of their hierarchy and their current "open" state, I use that latter approach in my work. Going with the shared state idea, you could have a state that behaves like a stack, the last item is the last one to respond to the `keydown` event. ```js [closeStack.js] export const closeStack = shallowRef([]); ``` ```vue ``` This is a bit clever, but again. These menus will NOT be the only closable component to be be present in your app, you want to have a way to uniformly and reliably close any kind of closable UI element in order. This is how I implement the desired behavior today, but remembering to add the `closeStack` to every component that can be closed is a bit of a hassle could be a major UX problem. This is where the `CloseWatcher` API comes in. ## The CloseWatcher API So all this presentation on the issues and we have not yet talked about the API itself. I will try my best to summarize the API: ```js let watcher = new CloseWatcher(); // Listen for close request watcher.onclose = () => { // Add your close logic }; // Trigger the close event and handler watcher.close(); // Dispose of the watcher instance watcher.destroy(); ``` There are some other things that the watcher can do but let's leave that till later when we are more familiar with the API. The close watcher API takes into consideration many things, all of which are done for you. That includes native UI elements with pickers, special behaviors with full-screen, dialogs, popovers, and inputs and it even cleans up our code by centralizing the closure logic of the UI component into a single place. This is a significant shift in mindset when handling these kinds of things, you no longer think about key presses, or back button presses. Those are now "close requests" and you are **_handling or issuing close requests_**. One implication of this mindset is your UI component close logic is no longer scattered around, it should only exist in the `close` event handler on the watcher instance. This also means we no longer need to listen for the Esc key or the back button press, we just need to listen for the `onclose` event and handle it accordingly. Here is a quick rundown of a typical implementation of a component that opens/closes. ```js let watcher; function onOpen() { // Change state or however your component is meant to be "open". // ... // Initialize a new instance every time we open the component watcher = new CloseWatcher(); watcher.onclose = () => { // Close the component or however your component is meant to be "closed". // Dispose of the watcher instance watcher.destroy(); }; } function onCloseClick() { // Close the component by triggering the handler. watcher?.close(); } ``` This may look a bit confusing at first, but it will all make sense once you see how it can "fix" the examples earlier. ### Sidebar - Revisited The changes are minor, we just need to create a new instance of the `CloseWatcher` and listen for the `onclose` event, here is a snippet of the logic. ```js let watcher; const isOpen = ref(false); function onOpenClick() { isOpen.value = true; // Initialize a new instance every time we open the sidebar watcher = new CloseWatcher(); // Closure logic is handled by the watcher watcher.onclose = () => { // Dispose of the instance watcher.destroy(); // Close the sidebar isOpen.value = false; }; } onClickOutside(sidebar, () => { // Close that sidebar by the watcher watcher?.close(); }); ``` That looks simple enough, but one weird part that I didn't wrap my head around initially was the need to create a new watcher every time the component is "opened". This is because the watcher is a one-time use instance, once it receives a `close` event it cannot be used again. So we need to create a new one every time we open the sidebar. This makes sense because of a couple of things: - You cannot close a closed UI, the `onclose` shouldn't trigger by then. - What if you have multiple things that can be opened and closed, you cannot share watchers here and you want to only close the last opened one. - No need to tell the watcher itself that something has opened. Creating the instance means "I've opened something, be on the lookout when it should be closed." Here is the full example with the `CloseWatcher` API. Notice that now if you have a search input with a value, pressing the Esc key will clear it first without closing the sidebar. The next Esc keystroke will close the sidebar and we didn't have to code in any kind of exceptions or filtration of the events. ### Nested Menus - Revisited Similarly with the nested menu, we do not need to do a lot of work to make it work as expected. We just need to create a new instance of the `CloseWatcher` every time we open a menu item and close it when we close it. We don't need to maintain a state of stacks for nested items or any other UI component. It is one of those things that truly "just works". The amazing thing here to me is this `ContextMenu` component is a recursive nested component and they don't care about which one is open and which one is whose child. They just open and close themselves in order all thanks to the `CloseWatcher` API which routes the close request to only one watcher instance at a time, the last one typically. ## Interrupting Close Requests There is one more thing that the `CloseWatcher` API can do for us, and that is interrupting close requests. This is useful when you want to prevent the close request from happening, maybe you want to show a confirmation dialog or the user has forgotten to save their work in a closeable UI and you want to make sure they know what they are doing. This is where the `cancel` and `requestClose` come in, you want to ask nicely if the current UI can close and if it cannot then you can cancel the close request. ```js watcher.oncancel = (e) => { e.preventDefault(); // decide to close or not if (something) { watcher.close(); } }; // Ask nicely to trigger the "cancel" event watcher.requestClose(); ``` Here is a rather complicated example. This example has a toggle form that can be opened and closed, but if closed then the user will lose any changes done to their text input. So we want to ask them nicely if they are sure that they want to close the form or not. Take your time to go through the code and understand the moving pieces and play around with the example, here are a few scenarios to try: 1. Open the form, type a value, and **_click_** "Save" The form closes and the value is saved. 2. Open the form, change the value and **_click_** "Cancel" and then **_click_** confirm: Form closes and the value is lost. 3. Open the form, change the value and **_click_** "Cancel" and then **_click_** cancel: Form stays open. 4. Open the form, change the value and **_press_** Esc: Confirmation dialog appears, **_press_** Esc again: Form stays open. - We are exposing a Vue component through a composable so we get a nice async API for showing/closing the confirmation. - We have two close watchers at play here, one for the form and one for the confirmation dialog. - All the closure logic is handled by the watchers - We are using `requestClose` to trigger the confirmation dialog to show up. - We are using `close` to forcibly close the form if the user confirms or if they save their changes. - We are listening for `oncancel` to prevent the form from closing until the user confirms their action. ## Behavior on mobile The `CloseWatcher` API is not only for the desktop, it also works on mobile. On Android, the back button is the main way to trigger close requests, and on iOS it is the VoiceOver "Z" gesture. Let's focus on the Android use case because it does have a few interesting behaviors that are worth mentioning. Consider the last sidebar example, on the mobile, a sliding menu like that could potentially cover most of the screen. So to the user, it may as well be a new page. So clicking the back button is expected to close the menu or the sidebar. An issue I faced in my full-time work while working on a hybrid web app version using Capacitor, is that the back button doubles as a navigation command to "go back". With modern frameworks that use the history API and client-side routing, you can easily confuse the browser into navigating to the previous page rather than just closing the current open UI. Unless you are using native popovers or dialog elements, the browser will navigate to the previous page which is frustrating. I handled it by listening to the back button press and preventing the default behavior, then triggering a global event that would propagate to the router and all components that were meant to be closed. Once a component handles it, it is marked as "handled" and the back navigation is skipped. So doesn't work well especially if there are a lot of UIs interested in that back button press, and it is not a very elegant solution. Consider the nested menu on mobile, it would typically cover most of the screen and with each nested item opening, it would slide over covering the previous one. ## Review You can use the `CloseWatcher` API with any kind of closable UI, and not only popovers and menus. Here is an API summary: ```ts // initialize a new instance, usually in the UI open handler let watcher = new CloseWatcher(); // listen for close requests // All the logic for closure and cleanup belongs here watcher.onclose = () => { // close the UI by toggling the state or whatever // Dispose of the watcher instance. watcher.destroy(); }; // Listen for cancel requests that precede close requests // Use this if you want to interrupt the close request watcher.oncancel = (e) => { // You can prevent the close request by calling `preventDefault` e.preventDefault(); // decide to close or not... if (something) { watcher.close(); } }; // Trigger a close request manually // This will go through the cancel handler first if there is one // Then the close handler. watcher.requestClose(); // Triggers the close handler, bypassing the cancel handler. watcher.close(); ``` This API is still experimental and is subject to change, it is only available in Chrome 120+. However, it is a good time to start thinking about how you can use it in your apps and how it can help you build better components. The standardization of this behavior and being able to hook into it is a big deal, we've seen a lot of recently introduced APIs and new web platform features that can take advantage of this API. The proposal menu mentions a few of them: - a `` element, especially the showModal() API. - a sidebar menu. - a lightbox. - a custom picker input (e.g. date picker). - a custom context menu. - fullscreen mode. I know I will make use of it once it gets enough browser support or as I like to call it: let it "marinate" for a little while. The API being fresh means TypeScript won't like seeing you use it. Until it is added to the `dom` standard typings, you can add it yourself like so: ```sh npm i -D @types/dom-close-watcher ``` And add it to your `tsconfig.json` file: ```json5 { compilerOptions: { //... types: ['@types/dom-close-watcher'], }, } ``` ## Conclusion The `CloseWatcher` API can help us build better and more accessible UIs. It is a small API that can have a big impact on the web platform and how we build web apps. It standardizes an area that has been left to the developers to figure out on their own for a very long time and brings order to the chaos there. --- --- title: Better Vue.js inputs with Generics: The Select description: Creating better select inputs with generic types in Vue.js tags: ['vuejs', 'frontend', 'typescript'] --- # Better Vue.js inputs with Generics: The Select
It's been a while since I talked about generic types in Vue components, especially since they were last introduced in Vue 3.3.0. I've been using them a lot lately and I wanted to share some of the use cases where I think they can be really useful, especially with Input components. We will be covering 3 main types of input components and how can you use generic types to make them more type-safe and pleasant to use. In this article, I will talk about the Select input and how to craft a strict and clear API.
## The problem Often when you are building a select input, you pass in the options as a prop to avoid having to enumerate the objects declaratively in the template. But something you may have done or noticed with some 3rd party libraries is you are limited to having options satisfying a certain shape or value type. It mostly revolves around presenting the option item, it must have a value and it also should have a label to show to the user. So there are many ways to model the options array, a couple of those could be: - An array of primitive values, like strings, here the string item serves as both the value and the label. - Array of objects, each object always has distinct properties serving as a label and value, commonly they are named `label` and `value` but it may vary depending on which 3rd party library you are using. Both approaches have their cons, the first one is very limited as you must pass a value that is both presentable to the user and can be used within your data models as a value. Unless you are working with a simple form, this is not a good approach. The second approach has more promise here, but you limit the developer who is using your component to always map their objects to the shape you are expecting. Since we are going with clarity through stricter types, this could be a great starting point, let's start with that. Such a component could initially look like this: ```vue ``` I've baked in some nice defaults like a placeholder and a default value to keep the empty option selected. However, there are a few problems with such a component in terms of developer usability. The first problem is: **it emits `string` as the selected value type**, which is not always ideal. Another issue is **your options must conform to a specific shape**, this is fine for many cases but limits and forces the developer to always map their objects to the shape you are expecting. Here is a typical case where you have a collection of items of a certain shape, and you are forced to map it to be able to use it with the component: ```ts const users = [ { id: 1, name: 'John' }, // [!code highlight] { id: 2, name: 'Jane' }, // [!code ++] ]; const options = users.map((user) => ({ label: user.name, value: user.id, })); ``` Both of these issues make our component a bit of a pain to use, and we can do better with generic types. ## Giving flexibility to props Let's bring generic types into the mix and see how we can utilize them to improve this component. First, I would like to make our component a bit flexible, so it can accept either an array of strings OR an array of objects of any shape. ```vue {4} ``` The template will break because we have no idea what the shape of the object is. But the is, we don't have to know. We can let the user of our component decide how to present the option and how to map it to a value.
Let's tackle option labels first, we can introduce an `optionValue` prop which is a function that takes in an option and returns a string to be used as the option label. ```vue {9,18} ``` When you try to pass the `optionLabel` to the props notice that you get type checks for the option argument. So regardless of what type of options you pass in, it will be inferred correctly and piped back to your props. ```vue ``` Let's tackle the option's value next. We don't want to force the user to choose a single prop to be used as the value, for example, maybe they want to select the `id` as the value. Alternatively, they may want a different property or even the entire option object. So how do we solve this? Since the model value could be different than the option value, that means we need another generic type to represent that relationship. That means we have another generic type to introduce, let's call it `TValue`. ```vue {6,15} ``` The `TValue` generic type by default is the same as the `TOption`, this allows some flexibility where if the user didn't specify an `optionValue` prop, then the value is the item itself. To fix the reset of the component we need to make some changes to how we render the options and how we get each iteration keys, this is one way to do that: ```vue {10,15-21,25-27,35,36} ``` With that out of the way, now if you pass an `optionValue` prop to the component, you will get type checks for the model value argument and you won't be able to bind it to an incorrect `ref` type. In the following snippet, I created a `selectedValue` ref that is of type `string` and tried to bind it to the component while specifying the `id` as the value, and it errors out as expected. ```vue ``` ## Bonus: a bit of UI Let's spice our UI up a bit, ideally, we would like to be able to style our options and the popup menu to give the component a bit of style. Since the `select` element is very limited when it comes to styling, we could use [Open UI's ``](https://open-ui.org/components/selectlist/). If you are on the latest Chrome production release, open the [chrome://flags](chrome://flags) page and enable the "Experimental Web Platform features" flag to get the `` element to render correctly. You can re-build the input with `div`s and floating libraries like tippy or floating-UI for better cross-browser support but I chose the path of least resistance here. The same principles still apply in the next sections. Adding the entirety of the component here will be massive, so here is a working example of the component with the UI and all the changes we made so far: Now we have a good-looking component that is also type-safe, I went through the trouble of doing all that UI work just so we can now move to the next step and allow the user complete customization of the option content, which means slots are in order. ## Typed Slots for the option content Having an overridable option slot allows for complex rendering of the options, the user can choose to display an image, icon, or whatever they want based on the option value. Simply displaying string labels is not enough in many cases. This means we need to add an overridable option slot. We can do that by wrapping our option content with `` tag, and we will give it a `name=option` to make it clear that it overrides the option content. here is how it would look: ```vue {8-11,14} ``` This allows us to keep the current render behavior of the options as a default while at the same time allowing the consumers of the component to override the content as they see fit. Here is an example with some countries' flags! And the best part is we get the selected option preview for free because of the selectlist's `selectedoption` element, so no extra work is needed there. Notice that the `option` slot prop is strictly typed, and you get auto-completion for the option object properties. ## Conclusion We've created a select input that takes any shape of options and allows the user to customize the option content. We've also made the component airtight in terms of type safety, and we've done all that with the help of generic types. Generic types usefulness is not only limited to props, but you can also use them with slots and events and quite commonly with `v-model` events. And you saw a glimpse of the promise of Open UI and how it can help us with powerful and less JavaScripty components. I hope this article was helpful and you learned something new. I will be covering more input components next week, so stay tuned for that. --- --- title: Three ways to expose internal Vue components API description: And when to use each one of them tags: ['vuejs', 'frontend'] --- # Three ways to expose internal Vue components API
We've all been there, you got a component with an internal API (function or state) that you want to expose to the parent component, but with a lot of options to do so, which one is the best?
I recently answered a question on Twitter about this, and I thought it would be a good idea to write an article about it. Especially since we have more options in Vue 3 than we had in Vue 2 to tackle this problem. ## Component API First, let's define what a "component internal API" is. A component API usually consists of: - State (props/data) - Functions or methods You are familiar with passing props to components to share state from parent to child, but what about the other way around? How can the child component pass back the state to the parent component? You are also familiar with emitting events from child components to the parent, but what if the parent wants the child to execute a function? How can the parent component call a function on the child component? So what we are discussing is the reverse of the "props-down events-up" principle, we want somehow to pass the state upwards and sort of emit events downwards but not exactly like that. ## Different nails, different hammers There are a few ways we can expose an internal API to the parent component in Vue. Each excels in some cases and falls short in others. This is just my opinion on the features/patterns I'm going to discuss, you might have a different opinion and that's fine. I'm just sharing my experience with these patterns and when I think they are best used. ### Slot props This is the most popular one by far and has a lot of patterns associated with it. Whenever you find a library that advertises itself as "headless" or "renderless" it's probably using slot props. Here is an example of a headless `` component. ```vue ``` This component exposes the `duration` object to the parent component, which can then use it to render whatever it wants. Here is an example of how you would use it: ```vue ``` This is where slot props work best, the component has no idea how are you planning to present it. So the component just does the heavy lifting and provides you with a state or functions that you can use to render whatever you want. It doesn't have to be "renderless" like the example, you can have a list component that lets you render each item however you want and it renders the rest of the component. ### Provide/Inject shenanigans This pattern became more relevant with the composition API and the typescript enhancements in Vue 3. If you are using plain JavaScript then I don't recommend using this one at all because it is hard to reason about with. So this pattern relies on the parent component providing a mutable object to the child component, and the child component injects it and mutates it (directly or indirectly) to share stuff back to the parent component. This example might be familiar if you know my work. ```vue ``` And for this to make any sense, here is the parent component: ```vue ``` Not the most ideal form component system but it shows cases where this pattern is ideal. This is how a lot of libraries implement hierarchy-sensitive components, you might have seen the following in the wild: ```vue-html ``` So while it is ugly, it can be a very powerful pattern to use in your project. But I would only recommend it if you are using TypeScript and the composition API and you can justify the complexity it adds to your teammates. If you want to learn how to use typescript with provide/inject you can check [this article](/blog/making-the-most-out-of-vuejs-injections/#use-typescript) where I covered some best practices for it. I have no idea what to call this pattern but you can do anything with `provide` and `inject` so "shenanigans" seem to be most suitable here. No point in trying to implement this using slot props because while possible (I won't ever show you how because it is very ugly and I don't want to be responsible for that), let's just say it is not worth the effort. ### Template Refs First, let's recap what ["template refs"](https://vuejs.org/guide/essentials/template-refs.html) are. Whenever you want access to a DOM element or a Vue component instance in your script, you assign a `ref` attribute to it. This populates the `$refs` property if you are using the options API or the ref you created if you are using the composition API. Here is a quick example for both: Options API: ```vue ``` Composition API: ```vue ``` So the example component auto-focuses the input field whenever the component is mounted. Given we have an `InputText` component, we want to be able to do some stuff with it other than capturing user input. For example let's say you want to programmatically focus the input on demand, very much like how the native `` element works. Here is a quick base component that we can use as a start: ```vue ``` Then in your parent component, you use it like this: ```vue ``` This is a very cool functionality and it works similarly to native HTML elements and that's when it works best. Whenever you have a component with DOM-like API, and **especially functions**. To further drive this point home, consider using any of the previous patterns for this example, starting with slot props: ```vue-html ``` This is just confusing to any reader, also what if you want to call `focus` in your script? There is no reasonable way to do that. In larger templates, you will do some serious scoping gymnastics to get this to work. It is a different story with the `provide/inject` pattern, you only have to add a `focus` function to the `field` object. ```js {1,4,6-8,14} const form = inject('form'); const input = ref(); function focus() { input.value?.focus(); } const field = reactive({ value: '', touched: false, name: props.name, focus, }); form.register(field); ``` I think that makes sense if you are building that sort of component system that's meant to be used frequently also it scales well. But if it is a one-off situation, template refs are much more straightforward. ## Conclusion To recap, I've summarized when to use each pattern: - Sharing state and functions in template. - Renderless/Headless components or components that allow overriding some of its content via slots. - Sharing state/functions in script, no good way to get the state across to the script without hacks. - When the component scope becomes confusing, like a button inside an input component. The exposed props should be relevant to the component itself and what the slot is going to render. - You have some sort of a "controller" component or a composable that needs awareness of specific child components and manages them under the hood. - You have a component that needs to be aware of its siblings. To sum it up, "hierarchal-awareness". vee-validate uses this pattern and so many other popular libraries in the Vue ecosystem. - Not using TypeScript. Blindly injecting untyped stuff is a nightmare to maintain and explain. - Setting up a provide/inject context just for a single component that's only used once. Too much of an overkill. Will leave it to you to judge. - You have one-off functions you want to execute on a component. - When you want to expose a DOM-like API to the parent component, like focusing an input, scrolling to an element, or proxying other DOM element functions. Another example where I use this personally is a `ScrollableContainer` component with custom scrollbars (because each OS has its ugly ones), so that component has a few interesting functions exposed like `scrollToEnd` and `scrollToTop` and `isAtEnd`. - When exposing state (hot take?). - When you have a lot of components that you need to interact with. Having a lot of `ref` attributes could make a lot of noise in the composition API, if you are using the options API then it is fine. I don't like using this pattern with state because reactivity becomes a dodgy subject but works if you know what you are doing. However, the other patterns are much better at this and are easier. If you are exposing state that's meant to be used in the template then use slot props, if you want to share state that's meant to be used in the script then use `provide/inject`. I hope you found this useful to pick out the best pattern fitting your needs. Remember that no one way is better than the others, look at what you are trying to do and pick the best tool possible for the job. --- --- title: Reducing component noise with Composition API description: An approach to reducing a component wirings with the composition API tags: ['vuejs', 'frontend', 'composition', 'typescript', 'refactoring', 'DX'] --- # Reducing component noise with Composition API
Some components require a lot of wiring to get working correctly, the wiring being Passing props, listening to events. However, some components can get out of hand and produce a lot of "noise". In this article you will come to understand what component noise is and how to reduce it with the composition API.
Do not get disoriented if you see me using `script` then `template` blocks. You can have them in any order you prefer, but we mostly focus on script issues so it makes sense to show it first. ## A Modal walked into the bar Let's say you want to create a modal component that will be used to display some kind of confirmation dialog, you may want to use it to confirm deleting stuff. At first glance, you may create a modal component that looks something like this: ```vue [components/ModalDialog.vue] ``` You may want to externalize the `visible` prop and move the `v-if` to the parent component, but I prefer to keep it internal because this is within the Modal's component purview and responsibility. So using this component usually looks something like this: ```vue [App.vue] ``` This is simple but it leaves something to be desired. We need to setup a value binding and an event listener. The issue here is if you forget to listen for the `close` event or forget to set `isOpen` back to `false` the modal won't close. This the first problem we have, and the first noise. Such components cannot control their state freely as they require the parent to show them but they can't close/hide themselves without telling the parent and the parent doing it properly. We could make it slightly better with `v-model` support so let's add that: ```vue {3,7,12,18} [components/ModalDialog.vue] ``` Now we can use `v-model` with it, it feels much nicer as we have to write even less code than before: ```vue [App.vue] ``` Now the component somewhat owns the `close` behavior, it can close itself at any time for any reason and the parent can open it or close at any given time. It can't get better than this, no? ## The noise So the previous example feels nice and simple. However, it falls short once you have a slightly complicated scenario. Here is a common one: You have a list of items, you want to mark an item for deletion and want the modal to confirm that action. If you don't see how that complicates things, I have a couple of issues that irk me whenever I get into this situation: - The visibility state is a boolean, so now I need to track both a visibility state and the item to mark for deletion - We need to sync both states whenever the modal opens/closes Here is a quick example to show how these two issues make things a little annoying: ```vue [App.vue] ``` To me, this is getting a little disgusting. Can you see how much wiring do we have to set up just to delete an item from the list? You have a couple of states to worry about (`isOpen` and `itemToDelete`), and need to keep them synced at all times. Then we have a bunch (three) of events to handle and we are back to having both a value binding and an event handler, so back to square one in that regard. Another issue is the modal `update:modelValue` doesn't make any sense, in theory it could emit `true` so what happens then? We can't really handle that case, so its not really updating a model value, the item to delete is actually the state that decides the visibility. All that required wiring makes the component "noisy" to me, and that almost always means that the component is very brittle and can fail if you don't wire something correctly. If you miss any of the two states or the three handlers, it no longer works. We can make this slightly better making `isOpen` depend on `itemToDelete` so we can use `v-model` again. And some typing changes to give us some leeway. ```vue [App.vue] ``` This is slightly better, we managed to reduce the number of states we need to keep track of. However we still have that annoying `isOpen.value = true` issue. We really cannot handle that case. Furthermore, if you are building some confirm-heavy UI, like a dashboard or CRUD of sorts then you will need to do all that stuff just to use the `ModalDialog` component and the API isn't perfect and there is a lot of room for mistakes. Here is a little trick I do to figure out how much noisy they are. Describe what is going on in a "dev story" format. Here is an example for a dev story for the previous snippet: > "When I select an item for deletion, mark the item for deletion and show a modal to confirm it. If it is cancelled then unmark the item and if it is confirmed then remove the item and close the modal" This feels a little verbose, almost as verbose as the code you wrote for this thing to work. This may not be an issue at all, but I can't help but think of a better way to do things, something to mute this noise away. A more technical description of the issue is that the modal component is leaking too much and delegating too much. ## Using composition API to create components I have covered this in a different topic [here](/blog/generic-type-components-with-composition-api#defining-components-in-setup). But the takeaways are: - We can construct components on the fly in the `setup` function and use it in our template. - We can create component wrappers that act as generic components This helps in a few ways ways: - Predefine some Props - Expose clearer APIs with explicit behavior - Hide some complexity away First, let's see how can we convert the modal dialog into a composable. We can use the initial version of our modal dialog as it's API is more clear in terms of prop names and events. We won't need the `v-model` support anymore. Usually, I organize my composable functions/APIs in a `features/{name}` fashion. So let's create a `features/modal.ts` file: ```ts [features/modal.ts] function useModalDialog() { // TODO: } ``` Now, let us have a thinking moment about how we want it to work. We know at least that opening/closing menus isn't just about a boolean state anymore. It can be really any kind of data, we can think of this as the modal dialog opens in a contextual data of sorts, like the item we want to delete. So building it as a generic makes a lot of sense: ```ts {3} [features/modal.ts] function useModalDialog() { // TODO: } ``` We typed `TData` as `unknown` by default because we don't really know what kind of data it is, and there are no restrictions we can assume, this component is really dumb in that regard and that is great, it means it is very flexible. You can use `any` if you prefer but `unknown` is more type safe. Next step is building the logic. We want the consumer to be able to open the modal with contextual data and close it. There are a few ways to go about this, so let's take it one step at a time. First let's handle the state. We will need to create a wrapper component and internalize the showing and hiding logic. ```ts [features/modal.ts] function useModalDialog() { // The contextual data const data = ref(); function onClose() { data.value = undefined; } const DialogComponent = defineComponent({ inheritAttrs: false, setup(_, { slots, emit }) { function onConfirmed() { if (data.value !== undefined) { emit('confirmed', data.value); } } return () => h(ModalDialog, { onClose, onConfirmed, visible: data.value !== undefined, }); }, }); return { DialogComponent, }; } ``` However, we have no way to open it now or close it. So let's add that, we can expose really clear `show` and `hide` functions: ```ts {6-8,10-12,40,41} [features/modal.ts] function useModalDialog() { // ... function show(value: TData) { data.value = value; } function hide() { data.value = undefined; } const DialogComponent = defineComponent({ // .. }); return { DialogComponent, show, hide, }; } ``` Let's see how well this works in a consuming component. Let's use the previous delete item example: ```vue [App.vue] ``` We are very close to what we need, the component is much cleaner already. We've removed all listeners but one and we no longer have any state to worry about. But we no longer have access to `itemToDelete` state anymore, which we could need in a lot of cases like this one. Now we could in theory pass the ref ourselves and have the `useModalDialog` not internalize that state, something like this: ```ts const itemToDelete = ref(); const { show } = useModalDialog(itemToDelete); ``` This could work really well, however we can solve it by using slots here. We can have the `ModalDialog` expose the contextual data on its default slot, giving us access to it: ```ts {19,26-38} [features/modal.ts] export function useModalDialog() { // ... const DialogComponent = defineComponent({ inheritAttrs: false, setup(_, { slots, emit }) { //... return () => h( ModalDialog, { onClose: hide, onConfirmed, visible: data.value !== undefined, }, { default: () => slots.default?.({ data: data.value }), }, ); }, }); return { DialogComponent: DialogComponent as typeof DialogComponent & { // we augment the wrapper type with a constructor type that overrides/adds // the slots type information by adding a `$slots` object with slot functions defined as properties new (): { $emit: { (e: 'confirmed', data: TData): void; }; $slots: { default: (arg: { data: TData }) => VNode[]; }; }; }, show, hide, }; } ``` Aside from the weird syntax for giving the slot types to the component, we just added `data` on the component slot props. I admit it feels a little complicated and out of no where, but AFAIK there not a lot of ways you can define slots manually without using an SFC. Here is how it is used now after the changes: ```vue [App.vue] ``` Now to me, this is really simple and clean and doesn't have any room for errors. You no longer concerned in the parent component about how the Modal component works. Let's write another dev story after all of that. > "When I click an item, open a dialog for it, and when the action is confirmed let me know so I can delete the item". The code to me reads like that. I may have cheated a little but the story itself is more explicit than before. It is no longer a `ModalDialog` component, it is a `DeleteItemDialog` and that specialization allows you to omit a lot of details away. Here it is in action: What's even better here is you can re-use `useModalDialog` to create as many dialogs as you need in the same component without complicating or increasing the code much: ```ts // Multiple modals const { show: onDeleteClick, DialogComponent: DeleteItemDialog, } = useModalDialog(); const { show: onUpdateClick, DialogComponent: UpdateItemDialog, } = useModalDialog(); ``` ## Further improvements Where do you go from here? Well, I have a couple of improvements that could be worthwhile. ### Injections, anybody? You could create some sort of a "shared" modal dialog that many components can reach out to and use. Maybe with the `provide/inject` API: ```js // In a parent page or component const { DialogComponent, ...modalApi } = useModalDialog(); // make modal api available to child components provide('modal', modalApi); // Somewhere else in a child component: const modal = inject('modal'); modal.show(data); ``` Perhaps you can bake the injection in the previous example inside the `useModalDialog` so that it injects that modal context and any child component can inject and show/hide the modal. This makes it handy to use one modal for a repeated list of complex items if you need to make each item component show the dialog. ### No events Another improvement is you can reduce your template code further by offloading the `onConfirmed` handling to be passed to the composable function instead. ```ts {4-6,13} [features/modal.ts] export function useModalDialog( onConfirmProp: (data: TData) => void, ) { // ... const DialogComponent = defineComponent({ inheritAttrs: false, setup(_, { slots, emit }) { function onConfirmed() { if (data.value !== undefined) { onConfirmProp(data); } } return () => h( ModalDialog, { onClose, onConfirmed, visible: data.value !== undefined, }, { default: () => slots.default?.({ data: data.value }), }, ); }, }); // .... } ``` And it would allow us to drop `@confirmed` bit from our consuming component. ```vue {18-23} [App.vue] ``` But that really doesn't affect our experience much, so I will leave it up to you to decide how much wiring this component needs now. ## Conclusion This pattern is really useful to use with components with similar nature. ToolTips, Context menus, Panels, and so on. Such components are often noisy and need a lot of wiring to get them working correctly. But using the composition API to hide some complexities and those wires. We actually use this pattern in production in Rasayel and it worked really well for us. In my opinion this pattern improves the DX of such components and cleans up a lot of your consuming/parent components. Try it out and let me know how well it works for you. --- --- title: Juggling Vue.js Refs description: Tips for building better Vue.js composable APIs tags: ['vuejs', 'frontend', 'composition', 'typescript', 'ergonomics'] --- # Juggling Vue.js Refs
When you build a composable API in Vue.js you don't usually think about the ergonomics of it. Especially if you have other developers in your team or plan to open-source your API for everybody. But having worked for so long with the composition API exclusively I found some simple yet significant improvements around passing refs that can be useful for you to use in your next app.
## What is "Passing Refs" To get a better idea of what I mean by "passing refs". Imagine you have a `useProduct` composable that accepts a product id to fetch it. ```ts async function fetchProduct(id: number) { // returns products from API return fetch(`/api/products/${id}`).then((res) => res.json()); } export function useProduct(id: number) { const product = ref(null); onMounted(async () => { product.value = await fetchProduct(id); }); return product; } ``` While having a function like this is very neat for encapsulating that logic, it is not very flexible because the id value can only be initiated once and only once. If it changes, the product won't refetch. We can fix that by allowing the id value to be a reactive ref instead and watching for changes. ```ts async function fetchProduct(id: number) { // returns products from API return fetch(`/api/products/${id}`).then((res) => res.json()); } export function useProduct(id: Ref) { const product = ref(null); onMounted(async () => { product.value = await fetchProduct(id.value); }); watch(id, async (newId) => { product.value = await fetchProduct(newId); }); return product; } ``` Much better, and it looks good but let's see how ergonomic that is when being used by a consumer: ```ts const route = useRoute(); const product = useProduct( computed(() => route.params.productId), ); ``` This is completely fine, however, there are a few ergonomic issues with it. This is what this article is going to address. ## 1. Make reactivity optional First off, what if the consumer is perfectly fine with not having the value watched? Requiring a ref may seem a bit aggressive, but here is the problematic scenario. And by "problematic" I mostly mean "annoying to use". ```ts // ❌ will error out because the raw value "1" isn't reactive. const product = useProduct(1); ``` Instead, let's make it optional by utilizing a very cool utility type used by a lot of Vue libraries, I have mentioned it before in a previous article but as a refresher here is the `MaybeRef` type: ```ts type MaybeRef = Ref | T; // example usage // βœ… Valid const raw: MaybeRef = 1; // βœ… Valid const reffed: MaybeRef = ref(1); ``` Vue 3.3 added the `MaybeRef` as a built-in type, so you can import it directly from `vue` and avoid defining it. Now that we can declare arguments as optionally reactive, we need a way to extract the real value regardless if it is a ref or not. Luckily Vue exports an `unref` function that makes this a breeze. ```ts // example usage // βœ… Valid const raw: MaybeRef = 1; // βœ… Valid const reffed: MaybeRef = ref(1); unref(raw); // 1 unref(reffed); // 1 ``` Lastly, since we watch the values using Vue's `watch()` API we need to know if the passed value is a ref or not so we can set up a watcher or skip it if it is a raw value. Again Vue has an `isRef` function that tells you just that. ```ts const raw: MaybeRef = 1; const reffed: MaybeRef = ref(1); isRef(raw); // false isRef(reffed); // true ``` Using these few ideas we end up with the following improvements for our `useProduct` function: ```ts async function fetchProduct(id: number) { // returns products from API return fetch(`/api/products/${id}`).then((res) => res.json()); } export function useProduct(id: MaybeRef) { const product = ref(null); onMounted(async () => { product.value = await fetchProduct(unref(id)); }); if (isRef(id)) { watch(id, async (newId) => { product.value = await fetchProduct(newId); }); } return product; } ``` Now it is up to the consumer to decide how to use that function, if they plan to fire it off just once then they can pass the raw id as a number and if they expect it to be in sync with a given id then they should pass it as a ref. ```ts const route = useRoute(); // Fetched only once on mount const product = useProduct(route.params.productId); // In sync whenever the param changes const product = useProduct( computed(() => route.params.productId), ); ``` It's no wonder that many libraries define the very same `MaybeRef` or `MaybeReactive` types inside their codebase to improve their flexibility. To name a few: `vee-validate`, `villus` and `@vueuse/core` and many others. ## 2. Avoid repacking refs You may have already noticed the second source of "annoyance" that we want to address. Passing reactive refs is not straightforward especially if they are extracted from reactive or ref objects. This last statement might've confused you so let us go back to some composition API caveats: ```ts const obj = reactive({ id: 1, }); // βœ… Works! watch(obj, () => { // ... }); // ❌ Doesn't work watch(obj.id, () => { // ... }); ``` The last `watch` doesn't quite work because when you access `props.id` you get a non-reactive version of it, this is the raw prop value. So to keep it reactive you may resort to a few techniques: ```ts const obj = reactive({ id: 1, }); // converts all entries to refs const objRef = toRefs(obj); watch(objRef.id, () => { //... }); // You can also destruct it const { id: idRef } = toRefs(obj); watch(idRef, () => { //... }); // convert a single entry to reactive version of it const idRef = toRef(obj, 'id'); watch(idRef, () => { //... }); // just extract the value in a computed prop const idComp = computed(() => obj.id); watch(idComp, () => { //... }); ``` All of the previous statements give you a reactive `id` value out of a reactive object value, or any object ref as well. But that's the problem, for the reactive value to be passed to your function the user needs to pack it neatly in a reactive format, so they are forced to use any of those packing mechanisms. The main issue here is you are forcing the consumer to unpack their values and repack them again if they want to preserve reactivity. This often increases verbosity, I call this "repacking refs". This is more common than you think since the main sources of packed values are either props or route parameters. Usually, you will have this in your component: ```ts // reactive props object const props = defineProps<{ id: number; }>(); const route = useRoute(); // reactive route params route.params; ``` And it will have the same shortcomings when trying to pass some of their properties to your composable function. However there is a neat trick that you can use with `watch` and it has been around since Vue 2 days as well, using getters: ```ts const props = defineProps<{ id: number; }>(); // βœ… Works! watch( () => props.id, () => { // ... }, ); const route = useRoute(); // βœ… Works! watch( () => route.params.id, () => { // ... }, ); ``` The cool thing about this is you can easily put whatever you want in your getter function. You can extract props, combine props, or compute a value. After all this one core aspect of Vue's reactivity. Now how can this help us in our quest? First, let's improve the `MaybeRef` type by allowing getter functions. It's a couple of types, `LazyOrRef` and `MaybeLazyRef`. Feel free to find better names. ```ts // Raw value or a ref export type MaybeRef = Ref | T; // Can't be a raw value export type LazyOrRef = Ref | (() => T); // Can be a ref, a getter, or a raw value export type MaybeLazyRef = MaybeRef | (() => T); ``` Vue 3.3 added the `MaybeRefOrGetter` as a built-in type which is exactly the same as the `MaybeLazyRef` type we've defined above. Secondly, we need a way to extract the raw value from it. Since `unref` won't be helpful here, we should come up with our own function. I like to call it `unravel`, it is straightforward since we just need to detect if the passed value is a function. Vue 3.3 added the `toValue` helper which you can import in your code, it has the same functionality as the `unravel` function we've defined below. ```ts export function unravel(value: MaybeLazyRef): T { if (typeof value === 'function') { return value(); } return unref(value); } ``` I must mention that this might cause problems if you are passing reactive functions, so another layer of checks might be necessary in that case. Third, we need a way to detect if the passed value can be watched or not. Similar to `isRef` except it will also include that function type check. ```ts export function isWatchable( value: MaybeLazyRef, ): value is LazyOrRef { return isRef(value) || typeof value === 'function'; } ``` The `value is LazyOrRef` makes this function useful as it tells Typescript to reduce the possible types when this function is evaluated to true. Lastly, let's bake this all in into our `useProduct` function: ```ts async function fetchProduct(id: number) { // returns products from API return fetch(`/api/products/${id}`).then((res) => res.json()); } export function useProduct(id: MaybeLazyRef) { const product = ref(null); onMounted(async () => { product.value = await fetchProduct(unravel(id)); }); if (isWatchable(id)) { // Works because both a getter fn or a ref are watchable watch(id, async (newId) => { product.value = await fetchProduct(newId); }); } return product; } ``` Now check how a consumer of this function can easily pass reactive expressions in without extra unpacking and packing: ```ts const route = useRoute(); // Fetched only once on mount const product = useProduct(route.params.productId); // In sync whenever the param changes const product = useProduct(() => route.params.productId); ``` You can notice this pattern emerging with a lot of libraries, notably `villus` and `@vueuse/core` are using them for a lot of resumable APIs they expose. ## 3. Requiring reactivity and when to do so The previous tips approached how to improve certain aspects of using refs as arguments. But a quick thing you can also utilize is to expose intent through your argument types. Straying away from our previous product examples. Let's assume you want to build a `usePositionFollower` function that can only accept reactive position argument denoted in `x,y` coordinates. There is no point in receiving a non-reactive position because there won't be anything to follow. So you would like to let the consumer know to only give you reactive values. Or rather reactive expressions. So this means `MaybeLazyRef` won't work well for us, but remember that we also created `LazyOrRef` for that purpose to use with the `isWatchable` function. Here is what `usePositionFollower()` might look like: ```ts export function usePositionFollower( position: LazyOrRef<{ x: number; y: number }>, ) { const style = computed(() => { const { x, y } = unravel(position); return { position: 'fixed', top: 0, left: 0, transform: `translate3d(${x}px, ${y}px, 0)`, }; }); const Follower = defineComponent( (props, { slots }) => () => h('div', { ...props, style: style.value }, slots), ); return Follower; } ``` The previous example might be unusual since it returns a component but it is a very useful pattern to return a component in your composition functions. Now, this can be used with the famous `useMousePosition` composable that we can see in the [Vue docs](https://vuejs.org/guide/reusability/composables.html#mouse-tracker-example). ```ts const { x, y } = useMouse(); const Follower = usePositionFollower(() => ({ x: x.value, y: y.value, })); ``` Note how all our previous improvements made passing the position much easier. Here is this example in action: ## Conclusions I think those three tips may come in handy, especially since the Vue ecosystem seems to be using them more and more. These can make it so much easier to work with your composable APIs and reduce the verbosity that may come with it. Your consumers will appreciate these. If you liked this article feel free to let me know, I might end up making a series of articles on these small yet effective bits of the composition API. --- --- title: Building Pinia Stores from Composition API description: Combine composition API functions with Pinia stores tags: ['vuejs', 'pinia', 'frontend'] --- # Building Pinia Stores from Composition API
Since the introduction of the composition API, I have wondered if will it be the end of state stores like Vuex. But Pinia offers a win-win concept to try in your current application.
## Why did we need state stores? Sharing state and its mutations in a "props-down, events up" approach became inconvenient when a contextual state like the authenticated user needed to be passed around everywhere. Also when such a state was drilled deep into the component hierarchy. When Vuex was introduced, it was the solution for sharing these kinds of state contexts. You define a state store and interested components could read and mutate the state with little overhead. Here is an example of a current-user store: ```js [stores/auth.js] // initial state const state = () => ({ currentUser: null, }); // actions const actions = { async login({ commit }) { const response = await fetch('/login', { method: 'post', body: JSON.stringify({ email, password }), }).then((r) => r.json()); commit('setUser', response.user); }, }; // mutations const mutations = { setUser(state, user) { state.currentUser = user; }, }; export default { state, getters, actions, mutations, }; ``` ## Vue state landscape after the composition API The introduction of the composition API allowed the creation of reactive states. Components could share and mutate them in the same way as a state store. Here is how you could get the same thing as above: ```js const currentUser = ref(null); export function useCurrentUser() { async function login(email, password) { const response = await fetch('/login', { method: 'post', body: JSON.stringify({ email, password }), }).then((r) => r.json()); currentUser.value = response.user; } return { currentUser, login, }; } ``` On top of that, it offered a more complete reactive ecosystem with watchers, effects, and much better TypeScript support. However, we did not gain this without losing anything. Since we are no longer using a managed state store, it meant you lost a few advantages of state stores like: **Devtools and debugging** You can't track the state changes and what caused mutations in your states. Also, timelines and loading serialized state for debugging. **Hot module replacement** State is more likely to be cleared when you change something during development. **Mutation strictness** If you use plain `ref` or `reactive` then your state becomes mutable to all components. Meaning you no longer force components to mutate the state through something like mutations. However, you could get the same effect by using `readonly` composition to get your state locked down. ```js {1,11,12} const currentUser = ref(null); export function useCurrentUser() { async function login(email, password) { // ... } return { // only the login function can now mutate the user currentUser: readonly(currentUser), login, }; } ``` To be honest, I never needed those debugging features in my daily work, the most important thing for me was state sharing. A lot of developers and Vue community authors seem to have similar opinions. To re-iterate, I think the composition API did not necessarily eliminate the need for state management. More like it "significantly reduced" that need. However, I would say that **if** you make sure your composition functions are small and well organized, you probably won't need a state management solution. But that's a huge "if". You probably will keep adding more complex logic, and slowly you might abandon the restrictions you introduce like with `readonly` in the previous example. You could reach a point where you no longer can track what or why a state was changed and you might be on the lookout for new store management solutions. So back to square one. ## Pinia [Pinia](https://pinia.vuejs.org/) is the new recommended state management solution for Vue 3, it offers a similar API to Vuex but fixes most of its issues. The highlights for me are: - Simpler API with less stuff to learn - Vastly better TypeScript support - Both composition API and options API support Pinia has a very good read on how it differs from Vuex, so be sure to read it [here](https://pinia.vuejs.org/introduction.html#comparison-with-vuex). However, the same question arises. If you don't need the debugging experience of a state management solution and already using the composition API, then why bother, right? I even heard that if you use the options API, then you should use Pina. It's either you use the composition API and wouldn't need it or you use the options API and you would. It's one or the other, right? Pinia has an interesting feature up its sleeve that I have stumbled upon recently that voids the first half of that opinion. You don't have to compromise with the debugging experience if you are using the composition API with Pinia with this neat feature. ## Pinia and composition functions I think this is [only mentioned once]() on the Pinia documentation, but you can actually use a setup function to build your store. This means you could use `ref` to declare state, and regular functions to define actions. ```js export const useCounterStore = defineStore('counter', () => { const count = ref(0); function increment() { count.value++; } return { count, increment }; }); ``` In other words, you could pass your existing composition functions into Pinia's `defineStore` and get the best of both worlds. Here is how we would do it with the initial authenticated user example: ```js const currentUser = ref(null); function useCurrentUser() { // same thing... } export const useCurrentUserStore = defineStore( 'currentUser', userCurrentUser, ); ``` This store would work exactly in the same way in your components, and if you open your Vue dev tools, you get all those nice timelines and state debugging utilities. This isn't limited to your composition functions, it also extends to any 3rd party composition functions. This opens the door to so many things. Allow me to demonstrate with a few 3rd party libraries. ### Pinia + Villus (GraphQL) A shameless plug here, so `villus` is a small Vue GraphQL library built by yours truly. It is meant to be a minimal alternative to the apollo ecosystem libraries if you don't need all of their features. Anyways, since `villus` mainly offers composition API functions that you use to perform queries and mutations on your GraphQL API, it means you can use `villus` as a composition source for your Pinia stores. Here is a quick running example: The beauty of this is all of the reactive state that `villus` functions expose will all be tracked by Pinia. And any functions exposed will be treated as actions. It fits like a glove and truly shows the awesomeness of the composition API when embraced like this by a state store library like Pinia. This is one way to solve the long-standing problem of using GraphQL libraries inside your state stores. ### Pinia + VeeValidate I'm an advocate of not putting your form state into state stores because they are hardly shareable, with some exceptions of course. But an interesting use case here is you can do the same thing with composable form libraries like vee-validate. You can use some of the composition functions offered by vee-validate like `useForm` as a composition source for your Pinia stores. Here is a not-very-practical-example (still cool) of Pinia taking over vee-validate `useForm` API and re-exposing it as a state store. ## Conclusion This Pinia "composition-function-as-a-store" takes what we already like and love about the composition API, then spices it up with the DX features that it offers. Try it out in your application if you already use Pinia. --- --- title: Generically Typed Vue Components with Composition API description: Another take on generic type Vue.js components tags: ['vuejs', 'typescript', 'frontend'] --- # Generically Typed Vue Components with Composition API Vue 3.3 was released on May 11, 2023 with built-in support for [TS generics in SFC components](https://vuejs.org/api/sfc-script-setup.html#generics). This article is still relevant for older versions of Vue.js but you should use the official way instead.
Last year, I covered [how to create generic Vue.js components](/blog/generically-typed-vue-components). But as I have used it more, I found situations where it doesn't work as intended or outright broke especially with slots. In this article, I share another way that is more robust and flexible and it uses the composition API.
## What are generic-typed components? To refresh your memory, [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) enable your functional units (functions and classes) to operate on a dynamic type that is determined when it is used instead of a fixed one. This allows for greater flexibility and makes these generic units highly re-usable. So when we apply this concept to components you get the same outcome, a highly re-usable and very flexible component. These components existed for a while in the Vue.js ecosystem, but we never had a good way to properly type-check them when we are using them. A very good example of this is a select input component. Normally you would settle for selecting primitive values. Like strings or numbers, but with very rich datasets you may want the user to be able to choose an object. You can think of components like [VueMultiSelect](https://vue-multiselect.js.org/) as a good example of such components. What we want here, is [volar](https://github.com/johnsoncodehk/volar) to be able to deduce the props/slots/emit generic types for these components and allow for type-checking and autocompletion. But at the time of this writing, Vue.js doesn't have a way to [define such generic components](https://github.com/vuejs/core/pull/3682). But as you read through this article, it might be easier than you think without hacking too much. ## Defining components in Setup You can define components in your component's `setup` function and use it in the template. This is the first part you need to know before you can formulate this workaround. That means it is possible to **dynamically** define a component and hook it up for your template. It would be similar to this: ```vue ``` But this is hardly enough, you still cannot make generic components out of it. ## Generic props Let's start with a simple goal. We need to define a generic component that accepts a `value` prop that can be of any type the consumer requires. For that reason, we will need to wrap it within a function. Functions can be generic and they can relay that information to the dynamic component definition. We can call this "component factory" which I will refer to later. ```vue ``` This is a very cool way to create such components. However, our example here is lacking a template, and we know that writing render functions isn't exactly a fun experience. Ideally, we want to be able to use Vue's SFC to enable us to fully utilize the template syntax and the compiler's optimizations. ## Using SFC as a base component Volar has a setting called "takeover mode" which allows it to "hijack" the typing server for TypeScript which I recommend for many reasons. The reason that matters here, is it allows you to import Vue.js components from `.vue` SFC files into your regular `.ts` files giving you full type information on that imported component. So let's move our component code into an SFC: ```vue ``` We dropped the `as TValue` casting since we have no way to communicate that in an SFC component file which is part of the problem, but that doesn't matter as you will always need to treat the `value` prop as `unknown` or any constrained base type you choose. After all, when you design a generic function or class, you cannot make assumptions about the actual type the consumer is going to use. You could introduce restrictions with `extends` but that's beside the point. Now back in the "component factory" function, we can import the base component definition from the Vue file and it will be fully typed: ```ts function useGenericComponent() { // What now? } ``` Now that we got the generic component with all its template rendering glory imported, how can we use it in a way that makes our typescript tooling understand the `value` prop type correctly? This is an interesting problem with a few interesting solutions. In a nutshell, we need a way to pass down the generic type information and replace the `value` prop type with it. One way is to shadow the prop types of the imported component with a generic one. I prefer this approach since it is less intrusive and doesn't require doing far-fetched things to get it to work. All we have to do is wrap the original imported component with a very thin component layer that has the same props but they are generic instead. This brings us back to higher-order components as they will serve as that kind of layer. The thinnest of layers we can make in Vue.js is a functional component, and with Vue 3 we can easily just use a single setup function as an argument to `defineComponent` to build it. ```ts {5-11} function useGenericComponent() { const wrapper = defineComponent((props) => { // Returning functions in `setup` means this is the render function return () => h(BaseGenericComponent, props); }); return wrapper; } ``` This looks simple enough but we didn't type the `props` yet for this thin layer. You could try to re-build the same component types from `BaseGenericComponent`: ```ts {4-6,9} interface BaseProps { value: TValue; } function useGenericComponent() { const wrapper = defineComponent((props: BaseProps) => { // Returning functions in `setup` means this is the render function return () => h(BaseGenericComponent, props); }); return wrapper; } ``` This would work very well, assuming we have simple components like this one. But if you have a component with a lot of props, re-building the type yourself like this sounds like a chore and needless to say, you will have a maintenance burden to keep both of the prop definitions in `BaseGenericComponent.vue` to match the `BaseProps` interface which is not guaranteed and is almost likely won't hold for long. So the first question is, how can we get the prop types of an imported SFC? ## Extracting Prop Types from SFC components You need here a utility type that extracts the prop types from component definition value, I don't think there is such a utility type yet available. Luckily there is a way to do this with some `infer` keyword sorcery. ```ts export type ExtractComponentProps = TComponent extends new () => { $props: infer P; } ? P : never; ``` This `ExtractComponentProps` type accepts a component definition type, whose instance will have an internal `$props` Object of type `P`. So return that type `P`. Otherwise, it is not possible to infer the props type. By putting this together with what we had before, we now need to remove all generic properties from the original component prop types and then re-add it as the generic type. To do the removal bit, we can use the `Omit` utility type that is available in TypeScript like this: ```ts Omit< ExtractComponentProps, 'value' >; ``` To understand how it works, check the following diagram