https://lafor.ge/feed.xml

🇬🇧 Vue 3 and composition API

2020-05-10

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:

<template>
    Component
</template>

<script>
export default {
    name: 'App'
}
</script>

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.

<script>
export default {
    name: 'App',
    setup() {
        console.log("setup")
    },
    beforeCreate() {
        console.log("beforeCreate")
    }
}
</script>

Will display

setup
beforeCreate

Let's start to play with the setup hook.

<template>
    {{name}}
</template>

<script>
export default {
    name: 'App',
    setup() {
        return {
            name: "test"
        }
    }
}
</script>

This will display

test

Ok it's a quite it feels like the data

<template>
    {{name}}
</template>

<script>
export default {
    name: 'App',
    data() {
        return {
            name: "test"
        }
    }
}
</script>

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.

<template>
    {{count}}
    <button @click="inc">Inc</button>
</template>

<script>
    export default {
        name: 'App',
        setup() {

            let count = 0;

            function inc() {
                console.log("inc:" + count)
                count++
            }

            return {
                count,
                inc
            }
        }
    }
</script>

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.

<template>
    {{state.count}}
    <button @click="inc">Inc</button>
</template>

<script>
    import {reactive} from "vue";

    export default {
        name: 'App',
        setup() {

            let state = reactive({
                count: 0
            });

            function inc() {
                console.log("inc: "+state.count)
                state.count++
            }

            return {
                state,
                inc
            }
        }
    }
</script>

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.

<template>
    {{count}}
    <button @click="inc">Inc</button>
</template>

<script>
    import {ref} from "vue";

    export default {
        name: 'App',
        setup() {

            let count = ref(0)

            function inc() {
                console.log("inc: "+count.value)
                count.value++
            }

            return {
                count,
                inc
            }
        }
    }
</script>
Computed

This allows to return an immutable reference on a reactive data. It takes a closure without argument and returns value modified or not.

<template>
    {{count}} + 1 = {{countPlusOne}}
    <button @click="inc">Inc</button>
</template>

<script>
    import {ref, computed} from "vue";

    export default {
        name: 'App',
        setup() {

            let count = ref(0);

            let countPlusOne = computed(() => count.value + 1)

            function inc() {
                console.log("inc: "+count.value)
                count.value++
            }

            return {
                count,
                inc,
                countPlusOne
            }
        }
    }
</script>

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.

<template>
    {{countRef}} + 1 = {{count}}
    <button @click="inc">Inc</button>
</template>

<script>
    import {ref, computed, unref} from "vue";

    export default {
        name: 'App',
        setup() {

            let countRef = ref(0);

            let count = computed({
                get: () => countRef.value,
                set: val => countRef.value = unref(val)
            })

            function inc() {
                count.value++
            }

            return {
                count,
                inc,
                countRef
            }
        }
    }
</script>

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.

<script>
    import {onMounted, onBeforeMount} from "vue";

    export default {
        name: 'App',
        setup() {
            onMounted(() => {
                console.log('setup mounted!')
            })
            onBeforeMount(() => {
                console.log('setup before mount')
            })
        },
        mounted() {
            console.log('mounted!')
        },
        beforeMount() {
            console.log('before mount')
        }
    }
</script>

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.

<template>
    {{count}} + 1 = {{countPlusOne}}
    <button @click="inc">Inc</button>
</template>

<script>
    import {ref, computed, watchEffect} from "vue";

    export default {
        name: 'App',
        setup() {

            let count = ref(0);

            let countPlusOne = computed(() => count.value + 1)

            function inc() {
                count.value++
            }

            watchEffect(() => {
                console.log("inc: "+count.value)
            })

            return {
                count,
                inc,
                countPlusOne
            }
        }
    }
</script>

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:

<template>
    count1: {{count1}} | count2: {{count2}}
    <button @click="inc1">Inc count 1</button>
    <button @click="inc2">Inc count 2</button>
</template>

<script>
    import {ref, watch, watchEffect} from "vue";

    export default {
        name: 'App',
        setup() {

            let count1 = ref(0);
            let count2 = ref(2);

            function inc1() {
                count1.value++
            }

            function inc2() {
                count2.value++
            }

            watchEffect(() => {
                console.log("watchEffects count1: "+count1.value)
                console.log("watchEffects count2: "+count2.value)
            })

            watch(count2, () => {
                console.log("watch count2: " +count2.value)
            })

            return {
                count1,
                count2,
                inc1,
                inc2,
            }
        }
    }
</script>

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
import { createApp } from "vue";
import App from "./App.vue";

createApp(App, {
     suffix: 'toto'
}).mount("#app");
<!-- App.vue -->
<template>
    {{name}}
</template>

<script>
    export default {
        name: 'App',
        props: {
          suffix: {
              type: String,
              default: ''
          }
        },
        setup(props) {
            return {
                name: "test_"+props.suffix
            }
        }
    }
</script>

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 -->
<script>
    export default {
        name: 'App',
        setup(props, context) {
            console.log(context)
        }
    }
</script>

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 -->
<template>
    <button @click="handleClick">Button</button>
</template>

<script>
    export default {
        name: "Emitter",
        setup(props, {emit}) {
            function handleClick() {
                emit('custom', "click on button")
            }
            return {handleClick}
        }
    };
</script>`
<!-- App.vue -->
<template>
    <Emitter @custom="handleEmit"></Emitter>
</template>

<script>
    import Emitter from "./Emitter";
    export default {
        name: 'App',
        components: {Emitter},
        setup() {
            function handleEmit(ev) {
                console.log(ev)
            }

            return {handleEmit}
        }
    };
</script>

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
import {ref} from "vue";

export default function buildName() {
    let name = ref("anonyme");

    return {
        name
    }
} 

Then use this method into the setup hook

<!-- App.vue -->
<template>
    {{name}}
</template>

<script>

    import buildName from "./logic";

    export default {
        name: 'App',
        setup() {
            return buildName()
        }
    }
</script>

"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
import {ref, computed, unref} from "vue";

export default function buildName() {
    let nameRef = ref("anonyme");
    let name = computed({
        get: () => nameRef.value,
        set: val => nameRef.value = unref(val)
    })

    return {
        name
    }
}
<!-- App.vue -->
<template>
    <input v-model="name">
    <p>I'm {{name}}</p>
</template>

<script>

    import buildName from "./logic";

    export default {
        name: 'App',
        setup() {
            return buildName()
        }
    }
</script>

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
const compose = (...fns) => x => fns.reduce((r, f) => f(r), x);

const matchRegexp = regex => ({result, value}) => {
    if (result !== null && result !== false) {
        result = regex.exec(value) !== null
    }
    return {result, value}
}

function hasAtLeastOneLetter(data) {
    const regex = /[a-zA-Z]+/gm
    return matchRegexp(regex)(data)
}

function hasAtLeastOneNumber(data) {
    const regex = /\d+/gm
    return matchRegexp(regex)(data)
}

const hasToken = token => data => {
    const regex = new RegExp(`.*${token}.*`)
    return matchRegexp(regex)(data)
}

const numberOfCharacter = count => ({result, value}) => {
    if (result !== null && result !== false) {
        result = value.length >= count
    }
    return {result, value}
}

export function checkPassword(password) {
    let pipe = compose(
        numberOfCharacter(5),
        hasAtLeastOneLetter,
        hasAtLeastOneNumber,
        hasToken("ff")
    );
    let {result} = pipe({value: password})
    return result;
}

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

export function buildName() {
    [...]
}

const RED = '#ff0000';
const GREEN = "#4caf50";

export function buildPassword() {
    let passwordRef = ref("");
    let style = ref({
        padding: "5px",
        borderWidth: "3px",
        borderStyle: "solid",
        borderColor: RED
    })

    let password = computed({
        get: () => passwordRef.value,
        set: val => passwordRef.value = unref(val)
    })

    watch(passwordRef, () => {
        style.value.borderColor = checkPassword(password.value) ? GREEN : RED;
    })

    return {
        password,
        style
    }
}

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 -->
<template>
    <input v-model="name">
    <p>I'm {{name}}</p>
    <input
            type="password"
            :style="passwordBehavior.style"
            v-model="passwordBehavior.password">
</template>

<script>

    import {buildName, buildPassword} from "./logic";

    export default {
        name: 'App',
        setup() {
            let {name} = buildName();
            let passwordBehavior = buildPassword();

            return {
                name,
                passwordBehavior
            }
        }
    };
</script>

<style>
    *:focus {
        outline: none;
    }
</style>

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 -->
<template>
    <button @click="getJoke">Update Joke</button>
    <div class="jokes">
        <Joke :text="joke"></Joke>
        <Joke :text="joke"></Joke>
        <Joke :text="joke"></Joke>
    </div>
</template>

<script>
    import {onBeforeMount, ref} from "vue";
    import Joke from "./Joke";

    export default {
        name: "JokeProvider",
        components: {Joke},
        setup() {
            let joke = ref("");

            onBeforeMount(() => {
                getJoke();
            });

            function getJoke() {
                fetch("http://api.icndb.com/jokes/random")
                .then(response => response.json())
                .then(result => {
                    joke.value = result.value.joke
                })
            }

            return {joke, getJoke}
        }
    };
</script>

<style scoped>
    .jokes {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
    }

    button {
        width: 100%;
        margin-bottom: 10px;
    }

</style>

The joke value comes from the props. The component has also an input text who allow to set up the replacement text.

<!-- Joke.vue -->
<template>
    <div class="container">
        <input type="text" v-model="replacement">
        <div>{{text}}</div>
    </div>
</template>

<script>
    import {computed, ref, unref} from "vue";

    export default {
        name: "Joke",
        props: {
            text: {
                type: String,
                default: ''
            }
        },
        setup(props) {
            let replacementRef = ref("Chuck Norris");
            let replacement = computed({
                get: () => replacementRef.value,
                set: val => replacementRef.value = unref(val)
            });

            let text = computed(() => props.text.replace("Chuck Norris", replacementRef.value))
            return {text, replacement}
        }
    };
</script>

<style scoped>
.container {
    margin-left: 10px;
}
</style>

Then we use the JokeProvider in App component

<template>
    <JokeProvider></JokeProvider>
</template>

<script>
    import JokeProvider from "./JokeProvider";
    export default {
        name: 'App',
        components: {JokeProvider},
    };
</script>

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 -->
<template>
    <button @click="getJoke">Update Joke</button>
</template>

<script>
    import {onMounted} from "vue";
    import Joke from "./Joke";

    export default {
        name: "JokeProvider",
        components: {Joke},
        setup(props, {emit}) {

            onMounted(() => {
                getJoke();
            });

            function getJoke() {
                fetch("http://api.icndb.com/jokes/random")
                .then(response => response.json())
                .then(result => {
                    emit('input', result.value.joke)
                })
            }

            return {getJoke}
        }
    };
</script>

<style scoped>

    button {
        width: 100%;
        margin-bottom: 10px;
    }

</style>

The App component gets another role, to store the joke data.

<template>
    <JokeProvider @input="handleJoke"></JokeProvider>
    <div class="jokes">
        <Joke :text="joke"></Joke>
        <Joke :text="joke"></Joke>
        <Joke :text="joke"></Joke>
    </div>
</template>

<script>
    import JokeProvider from "./JokeProvider";
    import Joke from "./Joke";
    import {ref} from "vue";
    export default {
        name: 'App',
        components: {Joke, JokeProvider},
        setup() {
            let joke = ref("")
            function handleJoke(jokeText) {
                joke.value = jokeText
            }

            return {joke, handleJoke}
        }
    };
</script>

<style>
    .jokes {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
    }
</style>

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
import {ref, provide} from "vue";

export const JokeSymbol = Symbol();

export function buildProvider() {
    let joke = ref("")
    provide(JokeSymbol, joke)
}

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:

let joke = inject(JokeSymbol, "");

or

let joke = ref("");

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
import {onMounted, inject} from "vue";
import {JokeSymbol} from "./logic";

export function buildJokeProvider() {

    let joke = inject(JokeSymbol, "");

    onMounted(() => {
        getJoke();
    });

    function getJoke() {
        fetch("http://api.icndb.com/jokes/random")
            .then(response => response.json())
            .then(result => {
                joke.value = result.value.joke
            })
    }

    return {getJoke}
}

Then use it into the JokeProvider component.

<!-- JokeProvider.vue -->
<template>
    <button @click="getJoke">Update Joke</button>
</template>

<script>

    import Joke from "./Joke";
    import {buildJokeProvider} from "./jokeProviderLogic";

    export default {
        name: "JokeProvider",
        components: {Joke},
        setup() {
            return buildJokeProvider();
        }
    };
</script>

<style scoped>

    button {
        width: 100%;
        margin-bottom: 10px;
    }

</style>

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 -->
<template>
    <div class="container">
        <input type="text" v-model="replacement">
        <div>{{text}}</div>
    </div>
</template>

<script>
    import {computed, ref, unref, inject} from "vue";
    import {JokeSymbol} from "./jokeProviderLogic";
    import {watch} from "../web_modules/vue";

    export default {
        name: "Joke",
        setup() {
            let replacementRef = ref("Chuck Norris");
            let replacement = computed({
                get: () => replacementRef.value,
                set: val => replacementRef.value = unref(val)
            });
            let textInjected = inject(JokeSymbol, "")

            let text = computed(() => {
                return textInjected.value.replace("Chuck Norris", replacementRef.value)
            })
            return {text, replacement}
        }
    };
</script>

<style scoped>
.container {
    margin-left: 10px;
}
</style>

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 -->
<template>
    <JokeProvider></JokeProvider>
    <div class="jokes">
        <Joke></Joke>
        <Joke></Joke>
        <Joke></Joke>
    </div>
</template>

<script>
    import JokeProvider from "./JokeProvider";
    import Joke from "./Joke";
    import {buildProvider} from "./logic";

    export default {
        name: 'App',
        components: {Joke, JokeProvider},
        setup() {
            buildProvider()
        }
    };
</script>

<style>
    .jokes {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
    }
</style>

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
import {reactive} from "vue";

let state = {
    joke : "",
    jokeDisplays: []
}

const mutations = {

    updateJoke(joke) {
        store.state.joke = joke
    },
    removeJokeDisplay(index) {
        store.state.jokeDisplays.splice(index, 1)
    },
    addJokeDisplay() {
        store.state.jokeDisplays.push(Symbol())
    }
}

export const StoreSymbol = Symbol();

export const store = reactive({
    state,
    mutations
})

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 -->
<template>
    <slot></slot>
</template>

<script>
    import {store, StoreSymbol} from "../store";
    import {provide} from "vue";

    export default {
        name: "Store",
        setup() {
            provide(StoreSymbol, store)
        }
    };
</script>

Wrap our App with a Store

<!-- App.vue -->
<template>
  <store>
    <Jokes></Jokes>
  </store>
</template>

<script>
import Jokes from "./components/Jokes";
import Store from "./components/Store";
export default {
  name: 'App',
  components: {Store, Jokes}
};
</script>

Doing this allow to use our store in all child components.

// jokeProviderLogic.js
import {inject, onMounted} from "vue";
import {StoreSymbol} from "../store";

export default function buildJokeProvider() {

    let store = inject(StoreSymbol, null);
    if (!store) return;

    onMounted(() => {
        getJoke();
    });

    function getJoke() {
        fetch("http://api.icndb.com/jokes/random")
            .then(response => response.json())
            .then(result => {
                store.mutations.updateJoke(result.value.joke)
            })
    }

    return {
        getJoke,
        addJoke: store.mutations.addJokeDisplay
    }
}

Or in a nested Component

<template>
    <button @click="remove">Delete</button>
</template>

<script>
    import {inject} from "vue";
    import {StoreSymbol} from "../store";

    export default {
        name: "DeleteButton",
        props: {
          index: Number
        },
        setup(props) {
            const store = inject(StoreSymbol, null);
            if (!store) return;

            function remove() {
                store.mutations.removeJokeDisplay(props.index)
            }

            return {
                remove
            }
        }
    };
</script>

<style scoped>
    button {
        background-color: lightcoral;
        margin-left: 10px;
    }
</style>

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 ❤️.

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.