🇬🇧 Vue 3 and composition API
Hello there!
I loved Vue JS at the first sight. It for me the cleanest way to build a complex component based application. I tried React but I wasn't fond of the jsx approch, I tried native Custom Element but the boilerplaiting part is really to heavy to be acceptable. So I tried framework like Svelte or Lit Element all are interesting, but I always come back to Vue 💕.
Why ? Because of is SFC ( Single File Component) where you can mix the template, the logic and the style.
So when I heard that Vue will be redesign, I was both excited and worried. Worried to loose some of the features I love, but excited to see what will come next.
Installation
You can install Vue 3 using npm, the beta package is available. To create a project we will use Snowpack but you are free to use webpack or another tool if you want. To create a project with snowpack see my article on the subject. Personaly I'll create the project with:
npx create-snowpack-app composition --template @snowpack/app-template-vue --use-yarn
I assume your project is up and running.
Please verify that you're using the 3rd version of Vue 😉.
Composition API
The flagship feature of Vue 3 is its composition API. Let's check out does it mean.
First wipe out the App.vue and replace its content by:
Component
After hot-reload you should have Component written of your screen.
Setup method
Most of Vue3 changes comes with the new setup
property, its a method like is mounted
and created
that performs action just after props
are defined.
A quick test can explain us the lifecycle of this new setup
hook.
Will display
setup
beforeCreate
Let's start to play with the setup
hook.
{{name}}
This will display
test
Ok it's a quite it feels like the data
{{name}}
But bear with me, there is a logic to do things like this.
Methods
The setup
allows to do a lot of things including defining methods.
{{count}}
Inc
The method inc
is well defined because pushing the button writes into the console the inc 0
, inc 1
and so on.
But the counter never increments on screen, it lacks something to tell to the view to redraw itself with the correct count
value.
Reactivity
It's time to introduce a new concept: the reactivity. Vue 3 composition API provides two ways to implement this.
Local state
The API gives us the reactive
method which wraps the object given into a Proxy that will add some behavior to it. Like tell to the view that something has changed.
Be carreful! The parameter passed to reactive
method has to be an object or extends from it.
{{state.count}}
Inc
Now the view is updating! 🎉
Weak reference pointer
Another way is to use ref
, it's quite the same thing but with two differences.
You can pass anything to ref
method not only an object.
But the drawback is that you can't access to the the inner value wrapped directly. You have to use the property value
to achieve this.
{{count}}
Inc
Computed
This allows to return an immutable reference on a reactive data. It takes a closure without argument and returns value modified or not.
{{count}} + 1 = {{countPlusOne}}
Inc
If you tried to do :
countPlusOne++
You'll break the data
To do so, you have to provide an object like this:
{
get: () => {}
set: val => {}
}
This allows to transform our value to a writable reference.
{{countRef}} + 1 = {{count}}
Inc
Yes this example is useless, but the next will clear your mind about this.
The unref
is just here to explicitely unref the value. In typescript it's mandatory, in classic javascript you can skip this step. But it's a good practice.
Lifecycle
The setup
hook has is own lifecycle system. Thanks to it, you can define behaviors.
Result to this in the console:
setup before mount
before mount
setup mounted!
mounted!
As we can see the setup hook is called right before the associated component hook.
It be can used for example to start something when the component is mounted.
Watching reactive properties
You can watch change on your properties. Vue 3 gives you two methods
WatchEffects
Listens all reactive properties and run the provided callback at each change.
{{count}} + 1 = {{countPlusOne}}
Inc
The method provides much more use cases.
Watch
Same that watchEffects
but allow to lazily watch the property. If you have a bunch of properties with the watchEffects
all properties are listened. But sometimes you only need to watch one property. Example:
count1: {{count1}} | count2: {{count2}}
Inc count 1
Inc count 2
There is two buttons one increments the reactive property count1
the other increments count2
.
If you push the Inc count 2
button, you see this.
watchEffects count1: 0
watchEffects count2: 2
watchEffects count1: 0
watchEffects count2: 3
watch count2: 3 <-- Only count2
But if you push Inc count 1
button
watchEffects count1: 0
watchEffects count2: 2
watchEffects count1: 1
watchEffects count2: 2
The count2 watch
isn't triggered. Therefore you can precisely target the right property. Here don't trigger something on count1 changes.
Props and context
Props
You can access to props passed to the component from the first parameter of the setup
hook.
// index.js
;
;
App, "#app";
<!-- App.vue -->
{{name}}
Will display test_toto
.
Be carreful! Don't try to destructure the props
parameter it will lose is reactivity. Don't do setup({suffix}) {...}
.
Context
The second parameter of the setup
hook provides a context.
Let's log it to see what it involves.
<!-- App.vue -->
This give us an object:
{
emit : [...],
attrs: [...],
slots: [...]
}
No doubt it the same thing as:
For the science ! Let's try the emit
it's the simple one to test.
We need two components. One emitter and one receiver.
<!-- Emitter.vue -->
Button
`
<!-- App.vue -->
If you click on the button the console writes click on button
, it works 🙂.
Split the code
Ok good but it's just a more complicate way to do what you we can already realise with Vue 2.
What is the idea behind all of this?
Simple example
First let's create a new file called logic.js
.
// logic.js
;
Then use this method into the setup
hook
<!-- App.vue -->
{{name}}
"anonyme" will be displayed. Congrats ! You just extract everything out of the compoment. This implies that you can reuse this buildName
method in any components you want.
Let's complexify a little bit:
// logic.js
;
<!-- App.vue -->
I'm {{name}}
Good we've created a simple input text system, if you type something the text under will change.
Compose behaviors
Why not adding a password input but a little bit more complex:
- a password must have more than 4 characters
- at least one letter
- at least on number
- two "f" consecutive character
If all rules are fulfilled, the border must be green.
Otherwise the border must be red.
First the password validation rules!
// checkPassword.js
;
Above a possible implementation using functional programming and composition ^^ ( I know it's not really optimized 😛 )
Then we add a new function in logic.js
// logic.js
;
;
This setup method will return both the reactive property handling the password value and the style of the password input border. This property is reactive following the password value.
That we can use it into our component.
<!-- App.vue -->
I'm {{name}}
We just add a quite complex behavior but our component logic remains clean and simple. And the best is that you can use this buildPassword
and its password validation in any component you want.
That's the heart of the composition API, you compose behaviors to create more complex one.
This design pattern allow us to "inherit" of more than one behavior. If you know the mixins
all of this must seem very familiar.
The interesting part of composition API against mixins is that there is no merge conflict strategy to define. In mixins all properties and methods must have an unique name otherwise they are overwrite.
With composition you do what you want and you overwrite properties in the way you want.
Share the state
One question I asked myself while I was learning the composition API is how can I share the state between components?
Let's say we have a Chuck Norris joke provider. And some components that can transform the "Chuck Norris" term in something else. But the rest of sentence must remains the same in all of the components.
There is a lot of ways to achieve this.
Child component
The JokeProvider has as job to get a joke from an API and dispatch it to his children.
<!-- JokeProvider.vue -->
Update Joke
The joke value comes from the props. The component has also an input text who allow to set up the replacement text.
<!-- Joke.vue -->
{{text}}
Then we use the JokeProvider in App component
Sibling using parent as source of truth
This time the data still coming from provider but is stored in the App component.
The Joke.vue
component stays the same.
The JokeProvider has changed, instead of keeping the joke, it emits a signal with as payload the joke.
<!-- JokeProvider.vue -->
Update Joke
The App component gets another role, to store the joke data.
Dependency injection
The idea is to don't need the App component part but keep the JokeProvider sibling to Joke components.
To use the pattern "dependency injection" Vue provides two methods.
Provide
This provide
method allow us to register something. It takes two arguments, the first one is the unique identifier, it can be a string but it's prefered to use a Symbol
, see them as an immutable value warranted to be unique.
We register a reference to joke value. With the identifier JokeSymbol
.
// logic.js
;
;
Inject
The second hand is to use the data previously registered, it's done through the inject
method. It takes two arguments, the identifier of the data that must be injected and a default value if the identifier match with non value registered.
We get the reactive joke value out of the logic.js
file through the injection with the JokeSymbol
.
Doing:
;
or
;
Is the same thing for the calling method. Both will give a reactive value that can be modified.
We extract the JokeProvider logic into a file called jokeProviderLogic.js
.
// jokeProviderLogic.js
;
;
Then use it into the JokeProvider
component.
<!-- JokeProvider.vue -->
Update Joke
In the same way we get a reference of joke
variable into the Joke
component.
As this reference is the same as in the JokeProvider
when this one will modified joke
, the joke value will also been modified for Joke
component.
We have created a share reference across components.
<!-- Joke.vue -->
{{text}}
The only restriction is that provide
must be called in a Component parent of Component calling the inject
method.
This implies to call buildProvider()
in App
component as it's parent to both Joke
and JokeProvider
components.
<!-- App.vue -->
Emulate a Store
We've gone far into the rabbit hole. Let's finish with a Store emulation.
The goal is to use one source of truth for all components as nested as we want.
I've written a little project to test this.
I'll explain the complex parts.
Store
// store.js
;
;
The store feels like a Vuex store, there is a state and mutations applied on in it.
Technical Store Component
This renders nothing, it's job is just to provide the Store to its children.
<!-- Store.vue -->
Wrap our App with a Store
<!-- App.vue -->
Doing this allow to use our store in all child components.
// jokeProviderLogic.js
;
;
Or in a nested Component
Delete
You can add a "joke display" and remove it. But all the store is handle outside. So it's a success 🤩.
Conclusion
It was a long journey through the composition API, but I totally love it. I can't wait to use it in production!
Phiew, there was a lot of concept to cover and there many like the typescript support ( maybe in a next post ).
I hope I managed to keep the things understandable. See you soon for more articles on JS or something else ❤️.
Ce travail est sous licence CC BY-NC-SA 4.0.