🔥 (#123) Lightweight State Management in Vue
Hey all!
I hope you're enjoying summer (or winter). I've got some more tips for you this week.
Over the past year I've been slowly converting parts of my website and blog over to Nuxt. But it's never really been a priority, so I kept pushing it off because my blog was working.
Until a few weeks ago, when I spent a few hours trying to get my website to build so I could publish a new article.
So I've been spending the last week or so finishing the job, as well as combining my course platform into the same website. Dealing with builds and project infrastructure is always tedious and slow going, but it'll be worth it in the end!
The blog is prerendered, while the course platform is using SSR and the nuxt-auth
package. I may investigate the beta version of nuxt-auth
since it supports static content, but for now I'm happy that things are working.
I was hoping to have Clean Components finished by the end of July, so this was an unwanted detour. But at least I now have paid down lots of my tech debt and don't need to worry about it.
— Michael
Vue Tips Collection
Maybe you just want to stay on top of the latest features, remind yourself of interesting things Vue can do, and get daily inspiration.
Vue Tips Collection is a beautiful book of 115 awesome tips, as well as a daily email to get your creative juices flowing.
🔥 Lightweight State Management
We can add computed and methods directly on to a reactive object:
const counter = reactive({ count: 0, increment() { this.count += 1; }, decrement() { this.count -= 1; }, });
This works because this
is set to the object that the method is accessed through, which happens to be the reactive object.
Vue's reactivity system uses Proxies to watch for when a property is accessed and updated. In this case, we have a small overhead from accessing the method as a property on the object, but it doesn't trigger any updates.
If we had a whole series of counters we can reuse this over and over:
const listOfCounters = []; for (const i = 0; i < 10; i++) { const counter = reactive({ id: i, count: 0, increment() { this.count += 1; }, decrement() { this.count -= 1; }, }) listOfCounters.push(counter); }
In our template we can use the counters individually:
<div v-for="counter in listOfCounters" :key="counter.id"> <button @click="counter.decrement()">-</button> {{ counter.count }} <button @click="counter.increment()">+</button> </div>
Instead of making the entire object reactive, we can use ref
to make only our state reactive:
const counter = { count: ref(0), increment() { this.count.value += 1; }, decrement() { this.count.value -= 1; }, };
This saves us a small and likely unnoticeable overhead. But it also feels somewhat better since we're being more thoughtful with our use of reactivity instead of spraying it everywhere.
Here's our example from before, but this time I'm going to add in a factory function to make it more readable:
const createCounter = (i) => ({ id: i, count: ref(0), increment() { this.count.value += 1; }, decrement() { this.count.value -= 1; }, }); const listOfCounters = []; for (const i = 0; i < 10; i++) { listOfCounters.push(createCounter(i)); }
Of course, we can use a factory method with the previous reactive method as well.
🔥 Default Content with Nested Slots
If you have multiple levels of nested slots, it's possible to have defaults at each level:
<!-- Parent.vue --> <template> <Child> <slot> We're in the Parent! </slot> </Child> </template>
<!-- Child.vue --> <template> <div> <slot> We're in the Child! </slot> </div> </template>
The slot content provided at the highest point in the hierarchy will override everything below it.
If we render Parent
, it will always display We're in the Parent
. But if we render just the Child
component, we get We're in the Child!
.
And if the component rendering the Parent
component provides slot content, that will take precedence over everything:
<!-- Grandparent.vue --> <template> <Parent> Haha this content rules them all! </Parent> </template>
🔥 Simpler testing with dependency injection
Jest makes it easy to mock or stub out functions, but you can also use dependency injection to make things easy to stub:
export default { props: { fetchData: { type: Function, required: true, }, }, methods: { setText() { this.text = this.fetchData(); }, }, };
it('should work', () => { const { getByText } = render(MyComponent, { props: { async fetchData() { return 'Test text'; }, }, }); getByText(/Test text/); });
(Example is simplified to illustrate the concept)
This is great for mocking API calls or anything tricky when testing.
If it's coming from outside of the component, it's pretty straightforward to stub it out or mock it how you need to get the test to do what you want.
You can do this in a variety of ways, depending on your use case:
- props
- provide/inject
- Vuex
- custom plugin
(There are probably many more)
📜 The Extract Conditional Pattern in Vue
An extremely common question I get asked all the time is, "how do you know when to split up a component?"
I want to share a simple pattern with you that is basically fool-proof, and can be applied to lots of components with almost no thought.
Check it out here: The Extract Conditional Pattern in Vue
💬 Repeated failure
"As a rule, software systems do not work well until they have been used, and have failed repeatedly, in real applications." — Dave Parnas
🧠 Spaced-repetition: Reactive Routes
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.
It took me way too long to figure this one out, but here it is:
// Doesn't change when route changes const route = useRoute(); // Changes when route changes const path = useRoute().path;
If we need the full route object in a reactive way, we can do this:
// Doesn't change when route changes const route = useRoute(); // Changes when route changes const route = useRouter().currentRoute.value;
Since Nuxt uses Vue Router internally, this works equally well in Nuxt and vanilla Vue apps.
Here's a demo to see this for yourself: Demo
p.s. I also have four courses: Vue Tips Collection, Mastering Nuxt 3, Reusable Components and Clean Components
评论
发表评论