🔥 (#130) Options to Composition: Making the Transition
Hey all!
Last week was a crazy one for me.
The pre-release of Clean Components Toolkit went great, but I also ended up at the hospital.
It was a very long, painful and exhausting week, but I'm well on the road to being recovered, so I'm grateful!
Now I'm working on the tools for passing state between components, as well as incorporating some of the feedback I've already been getting from you.
I do have one favour I'd like to ask.
I've been thinking over the idea of recording a series of refactoring videos for Clean Components Toolkit where I go through a whole project, refactoring it using the different tools available.
I'm not yet sure if/when/how I'll do this, but if you're interested in having your project refactored, reply with a link and a short explanation of what problems you're facing with it. If I do choose your project, you'll get the videos even if you haven't bought Clean Components Toolkit.
Have a great week!
— Michael
🔥 Nesting slots
As you start creating more abstractions with Vue, you may need to begin nesting your slots:
<!-- Parent.vue --> <template> <Child> <!-- We take the content from the grand-parent slot and render it inside the child's slot --> <slot /> </Child> </template>
This works similarly to how you would catch an error and then re-throw it using a try...catch
block:
try { // Catch the error reallyRiskyOperation(); } (e) { // Then re-throw as something else for the next // layer to catch and handle throw new ThisDidntWorkError('Heh, sorry'); }
Normally when using a slot we just render the content that's provided to us:
<template> <div> <!-- Nothing to see here, just a regular slot --> <slot /> </div> </template>
But if we don't want to render it in this component and instead pass it down again, we render the slot content inside of another slot:
<template> <Child> <!-- This is the same as the previous code example, but instead of a `div` we render into a component. --> <slot /> </Child> </template>
🔥 From Options to Composition — The Easy Way
You can use reactive
to make the switch from the Options API a little easier:
// Options API export default { data() { username: 'Michael', access: 'superuser', favouriteColour: 'blue', }, methods: { updateUsername(username) { this.username = username; }, } };
We can get this working using the Composition API by copying and pasting everything over using reactive
:
// Composition API setup() { // Copy from data() const state = reactive({ username: 'Michael', access: 'superuser', favouriteColour: 'blue', }); // Copy from methods updateUsername(username) { state.username = username; } // Use toRefs so we can access values directly return { updateUsername, ...toRefs(state), } }
We also need to make sure we change this
→ state
when accessing reactive values, and remove it entirely if we need to access updateUsername
.
Now that it's working, it's much easier to continue refactoring using ref
if you want to — or just stick with reactive
.
🔥 Nesting slots
As you start creating more abstractions with Vue, you may need to begin nesting your slots:
<!-- Parent.vue --> <template> <Child> <!-- We take the content from the grand-parent slot and render it inside the child's slot --> <slot /> </Child> </template>
This works similarly to how you would catch an error and then re-throw it using a try...catch
block:
try { // Catch the error reallyRiskyOperation(); } (e) { // Then re-throw as something else for the next // layer to catch and handle throw new ThisDidntWorkError('Heh, sorry'); }
Normally when using a slot we just render the content that's provided to us:
<template> <div> <!-- Nothing to see here, just a regular slot --> <slot /> </div> </template>
But if we don't want to render it in this component and instead pass it down again, we render the slot content inside of another slot:
<template> <Child> <!-- This is the same as the previous code example, but instead of a `div` we render into a component. --> <slot /> </Child> </template>
📜 Better Navigation with NuxtLink
The NuxtLink component may seem simple at first glance, but there's a lot going on beneath the surface.
It's one of the easiest Nuxt 3 components to use, while giving our apps a big performance boost.
In this article we see some things about NuxtLink you may not have known.
Check it out here: Better Navigation with NuxtLink
💬 Don't Change Anything
"A Fallacy of Software: If it works, and we don't change anything, it will keep working." — Jessica Kerr
🧠 Spaced-repetition: Too Many Props/Options is a Smell
The best way to commit something to long-term memory is to periodically review it, gradually increasing the time between reviews 👨🔬
Actually remembering these tips is much more useful than just a quick distraction, so here's a tip from a couple weeks ago to jog your memory.
Although the total number of options (and required params) isn't itself a problem, it is an indication that the design isn't quite as good as it could be.
Chances are that your composable (or component) is trying to do more than one thing, and should instead be separated into several composables. The point of composables is that they each do one specific thing really well, and can be composed together to produce more complex functionality.
Imagine that we have a useEvent
composable that looks like this:
import { ref, onMounted, onBeforeUnmount } from 'vue'; export function useEvent(event, handler, interval, options) => { // Default to targeting the window const { target = window, ...listenerOptions } = options; const startInterval = () => { setInterval(handler, interval); }; onMounted(() => { target.addEventListener(event, startInterval, listenerOptions); }); onBeforeUnmount(() => { target.removeEventListener(event, startInterval, listenerOptions); }); };
We'd use it like this. As soon as the button is clicked, we'll log to the console every second:
import useEvent from '~/composables/useEvent.js'; useEvent( 'click', () => console.log('Logging every second'), 1000, { target: buttonElement, } );
We can see that it's doing two separate things:
- Listening for events
- Setting up an interval
Instead of including the interval functionality in our useEvent
composable, it makes more sense to break it out into a second composable:
export function useInterval(callback, options) { const { interval = 1000 } = options; const intervalId = setInterval(callback, interval); return () => clearInterval(intervalId); };
Our useEvent
composable becomes simpler:
import { onMounted, onBeforeUnmount } from 'vue'; export function useEvent(event, handler, options) { // Default to targeting the window const { target = window, ...listenerOptions } = options; onMounted(() => { target.addEventListener(event, handler, listenerOptions); }); onBeforeUnmount(() => { target.removeEventListener(event, handler, listenerOptions); }); };
And now we can compose the two together to get the desired effect:
import useEvent from '~/composables/useEvent.js'; import useInterval from '~/composables/useInterval.js'; useEvent( 'click', () => useInterval( () => console.log('Logging every second') ), { target: buttonElement, } );
When we click on the buttonElement
, we call useInterval
to set up the interval that will log to the console every second.
p.s. I also have four products: Clean Components Toolkit, Vue Tips Collection, Mastering Nuxt 3 and Reusable Components
评论
发表评论