Easily migrate Vue 2 component to Composition API (setup)

Converting your Vue Component to Composition API shouldn’t be hard if you want to keep that orderly grouped with data, computed and methods and without changing
this.

Also I want to point out the current explanation really doesn’t make Vue fun, and that I want to bring that back.

Install Composition API

npm i -S @vue/composition-api
package.json: "@vue/composition-api": "^1.0.0-beta.22"
One of the advantages could be from migrating to Composition API what I could think of is: Reusing a piece of code (in this case a composition) to a different theme of Page Components.

Quick Template Composition

With this template we can use similar logic to make a modular Composition.

import { computed } from '@vue/composition-api'

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})

export default toComputed
Quick Template toComputed: toComputed.js
import { reactive, toRefs } from '@vue/composition-api'
import toComputed from '../toComputed'

const setup = props => {
    // data: state
    const state = reactive({
    })
    // computed: compute
    const compute = {
    }
    // methods
    const methods = {
    }
    
    return {
        ...toRefs(state),
        ...toComputed(compute),
        ...methods
    }
}

export default setup
Quick Template Composition API: composition/setup-template.js
import setup from '../composition/setup-template.js'

export default {
    name: 'TemplateComponent',
    setup,
    // Life Cycle Hooks
    mount() {}
}
Quick Template Component: vue/TemplateComponent.vue

Things you should mind (for this example).

data

When you have data in your Component.
export default {
	data: () => ({
		list: [],
		filters: []
	})
}
Snippet: data of .vue

Change data as state to:

import { reactive, toRefs } from '@vue/composition-api'

const setup = props => {
    // data
    const state = reactive({
        list: [],
        filters: []
    })
    
    return {
        ...toRefs(state)
    }
}

export default setup
Snippet: data as state of .js
Easy access with by simply: state.list and state.list.push('addMe').

// I find it tedious to write like this:

import { ref } from '@vue/composition-api'

const setup = props => {
    // data
    const list = ref([]),
        filters = ref([])
    
    return {
        list,
        filters
    }
}

export default setup
Tedious...
I find it tedious to write it like this: const list = ref([]), and accessing values doing this list.value.push('something'). This takes fun out of coding zen using Vue.

computed

When you have computed in your Component.
export default {
    computed: {
        selected: {
            get() {
                return this.filters[0]
            },
            set(value) {
                // console.log({ value })
                if (value == undefined) {
                    this.filters = []
                    return
                }

                const { filters } = this
                const index = filters.indexOf(value)

                if (index > 0) {
                    filters.unshift(...filters.splice(index, 1))
                } else {
                    filters.splice(0, 1, value)
                }
            }
        },
        mappedOptionsFromUniqueTagsFromList() {
            const options = this.uniqueTagsFromList.map(item => ({
                value: item.id,
                text: item.name
            }))
            const label = {
                selected: true,
                // disabled: true,
                value: null,
                text: 'Choose Category'
            }

            return [label, ...options]
        }
    }
}
Snippet: computed of .vue

Change computed as compute to:

import { computed } from '@vue/composition-api'

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})

const setup = props => {
    // computed
    const compute = {
        selected: {
            get() {
                return this.filters[0]
            },
            set(value) {
                // console.log({ value })
                if (value == undefined) {
                    this.filters = []
                    return
                }

                const { filters } = this
                const index = filters.indexOf(value)

                if (index > 0) {
                    filters.unshift(...filters.splice(index, 1))
                } else {
                    filters.splice(0, 1, value)
                }
            }
        },
        mappedOptionsFromUniqueTagsFromList() {
            const options = this.uniqueTagsFromList.map(item => ({
                value: item.id,
                text: item.name
            }))
            const label = {
                selected: true,
                // disabled: true,
                value: null,
                text: 'Choose Category'
            }

            return [label, ...options]
        }
    }
    
    return {
        ...toComputed(compute)
    }
}

export default setup
Snippet: computed as compute of .js

methods

When you have methods in your Component
export default {
    methods: {
        foundByTags(queryElements, item) {
            let found = true

            queryElements.forEach(categoryId => {
                let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
                found *= foundIndex > -1
            })

            return found
        }
    }
}
Snippet: methods of .vue

Change methods to:

const setup = props => {
    // methods
    const methods = {
        foundByTags(queryElements, item) {
            let found = true

            queryElements.forEach(categoryId => {
                let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
                found *= foundIndex > -1
            })

            return found
        }
    }
    
    return {
        ...methods
    }
}

export default setup
Snippet: methods of .js

Combined result as a module

import { reactive, toRefs, computed } from '@vue/composition-api'

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})

const setup = props => {
    // data
    const state = reactive({
        list: [],
        filters: []
    })
    // computed
    const compute = {
        selected: {
            get() {
                return this.filters[0]
            },
            set(value) {
                // console.log({ value })
                if (value == undefined) {
                    this.filters = []
                    return
                }

                const { filters } = this
                const index = filters.indexOf(value)

                if (index > 0) {
                    filters.unshift(...filters.splice(index, 1))
                } else {
                    filters.splice(0, 1, value)
                }
            }
        },
        mappedOptionsFromUniqueTagsFromList() {
            const options = this.uniqueTagsFromList.map(item => ({
                value: item.id,
                text: item.name
            }))
            const label = {
                selected: true,
                // disabled: true,
                value: null,
                text: 'Choose Category'
            }

            return [label, ...options]
        }
    }
    // methods
    const methods = {
        foundByTags(queryElements, item) {
            let found = true

            queryElements.forEach(categoryId => {
                let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
                found *= foundIndex > -1
            })

            return found
        }
    }
    
    return {
        ...toRefs(state),
        ...toComputed(compute),
        ...methods
    }
}

export default setup
File: lib/compostion/setup-articles.js
import setup from '../../lib/composition/setup-articles.js'

export default {
    name: 'Articles'
    setup,
    // Life Cycle Hooks
    mount () {}
}
File: vue/components/articles.vue

Mirgrate from Vue Component to Composite API

Before

<template>
	<div class="article-overview">
		<div class="main__inner">
			<header class="article-overview__header">
				<h1 class="article-overview__title"><slot name="title" /></h1>
				<span class="u-flex u-flex-cell">
					<SelectOptions
						class="u-flex-cell article-overview__select"
						:options="mappedOptionsFromUniqueTagsFromList"
						v-model="selected"
						v-on:change="onChangeCategory"
					/>
					<object-pulldown class="u-flex-cell filter-options" title="Filter">
						<div class="u-flex-cell">
							<tag-group
								class="filter-type-item"
								:query-items="queryItemsFromFilters"
								:filtered-items="
									differenceQueryItemsWithUniqueTagsFromFilteredList
								"
								:on-remove-tag-from-filter="onRemoveFromFilters"
								:on-add-tag-to-filter="onAddToFilters"
							>
							</tag-group>
						</div>
					</object-pulldown>
				</span>
			</header>
			<div class="article-overview__count">{{ filteredList.length }}</div>
			<div class="post-feed">
				<Card
					class="post-card"
					v-for="(item, index) in filteredList"
					:key="item.id"
					:item="item"
					:class="{ 'card--masthead': !index }"
				/>
			</div>
		</div>
	</div>
</template>

<script>
const uniq = require('lodash/fp/uniq'),
	uniqBy = require('lodash/uniqBy'),
	sortBy = require('lodash/sortBy'),
	differenceBy = require('lodash/differenceBy'),
	dayjs = require('dayjs')

const urlQueryToParams = require('../lib/urlQueryToParams'),
	changeUrlPushStateFromFilters = require('../lib/changeUrlPushStateFromFilters'),
	milliseconds = require('../lib/milliseconds')

import Card from './Card'
import SelectOptions from './SelectOptions'
import ObjectPulldown from './ObjectPulldown'
import TagGroup from './TagGroup'

export default {
	name: 'ArticleOverviewAlternative',
	components: {
		SelectOptions,
		ObjectPulldown,
		TagGroup,
		Card
	},
	props: {
		endpoint: String
	},
	data() {
		return {
			list: [],
			filters: []
		}
	},
	computed: {
		selected: {
			get() {
				return this.filters[0]
			},
			set(value) {
				if (value == undefined) {
					this.filters = []
					return
				}

				const { filters } = this
				const index = filters.indexOf(value)

				if (index > 0) {
					filters.unshift(...filters.splice(index, 1))
				} else {
					filters.splice(0, 1, value)
				}
			}
		},
		mappedOptionsFromUniqueTagsFromList() {
			const options = this.uniqueTagsFromList.map(item => ({
				value: item.id,
				text: item.name
			}))
			const label = {
				selected: true,
				// disabled: true,
				value: null,
				text: 'Choose Category'
			}

			return [label, ...options]
		},
		queryItemsFromFilters() {
			return this.filters.map(value => ({
				id: +value,
				name: (() => {
					const found = this.uniqueTagsFromList.find(({ id }) => id == value)
					return found ? found.name : value
				})()
			}))
		},
		uniqueTagsFromList() {
			const filters = this.list.reduce(
				(acc, item) => [...acc, ...item.categories],
				[]
			)

			return sortBy(uniqBy(filters, 'id'), 'id')
		},
		filteredList() {
			const {
				filters: queryElements,
				list,
				foundByTags,
				onlyShowPastPublishedAtDate,
				sortByPublishedAt
			} = this

			if (!queryElements.length)
				return onlyShowPastPublishedAtDate(sortByPublishedAt(list))
			const filtered = list.filter(item => foundByTags(queryElements, item))

			return onlyShowPastPublishedAtDate(sortByPublishedAt(filtered))
		},
		uniqueTagsFromFilteredList() {
			const filters = this.filteredList.reduce(
				(acc, item) => [...acc, ...item.categories],
				[]
			)

			return sortBy(uniqBy(filters, 'id'), 'id')
		},
		differenceQueryItemsWithUniqueTagsFromFilteredList() {
			const { uniqueTagsFromFilteredList, queryItemsFromFilters } = this
			return differenceBy(
				uniqueTagsFromFilteredList,
				queryItemsFromFilters,
				'id'
			)
		}
	},
	methods: {
		foundByTags(queryElements, item) {
			let found = true

			queryElements.forEach(categoryId => {
				let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
				found *= foundIndex > -1
			})

			return found
		},
		setFiltersFromQueryUrl() {
			const queryString = location.search
			const { filters } = urlQueryToParams(queryString)
			this.filters = Array.isArray(filters)
				? filters.map(i => +i)
				: filters === (null || undefined)
				? []
				: [+filters]
		},
		sortByPublishedAt(collection) {
			return collection.sort(
				(a, b) =>
					milliseconds(b['published_at']) - milliseconds(a['published_at'])
			)
		},
		onlyShowPastPublishedAtDate(collection) {
			return collection.filter(
				item => milliseconds(item['published_at']) < dayjs().valueOf()
			)
		},
		// Buttons Events
		onChangeCategory() {
			const value = this.selected
			this.filters = value ? [value] : []
			changeUrlPushStateFromFilters({ filters: value })
		},
		onAddToFilters(categoryId) {
			const { filters } = this
			const foundIndex = filters.indexOf(categoryId)
			if (!(foundIndex > -1)) {
				filters.push(categoryId)
				changeUrlPushStateFromFilters({
					filters
				})
			}
		},
		onRemoveFromFilters(categoryId) {
			console.log('onRemoveFromFilters:', { categoryId })
			const { filters } = this
			const foundIndex = filters.indexOf(+categoryId)
			console.log({ foundIndex })
			if (foundIndex > -1) {
				filters.splice(foundIndex, 1)
				changeUrlPushStateFromFilters({
					filters
				})
			}
		},
		onPopstate(event) {
			const { filters } = event.state
			this.filters = filters || []
		}
	},
	// Life Cycle Hooks
	async beforeMount() {
		const response = await (await fetch(this.endpoint)).json()
		this.list = response
	},
	mounted() {
		window.onpopstate = this.onPopstate
		if (this.filters.length) {
			changeUrlPushStateFromFilters({
				filters: this.filters
			})
		} else {
			this.setFiltersFromQueryUrl()
		}
	}
}
</script>
File: js/vue/ArticleOverview.vue

After

import { computed } from '@vue/composition-api'

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})
Snippet: js/lib/composition/setup-articles.js
A snippet to easy convert your computed object to Vue Computed ones.
import { reactive, computed, toRefs } from '@vue/composition-api'

const uniq = require('lodash/fp/uniq'),
    uniqBy = require('lodash/uniqBy'),
    sortBy = require('lodash/sortBy'),
    differenceBy = require('lodash/differenceBy'),
    dayjs = require('dayjs')

const urlQueryToParams = require('../urlQueryToParams'),
    changeUrlPushStateFromFilters = require('../changeUrlPushStateFromFilters'),
    milliseconds = require('../milliseconds')

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})

const setup = props => {
    // data
    const state = reactive({
        list: [],
        filters: []
    })

    // computed
    const compute = {
        selected: {
            get() {
                return state.filters[0]
            },
            set(value) {
                // console.log({ value })
                if (value == undefined) {
                    state.filters = []
                    return
                }

                const { filters } = state
                const index = filters.indexOf(value)

                if (index > 0) {
                    filters.unshift(...filters.splice(index, 1))
                } else {
                    filters.splice(0, 1, value)
                }
            }
        },
        mappedOptionsFromUniqueTagsFromList() {
            const options = this.uniqueTagsFromList.map(item => ({
                value: item.id,
                text: item.name
            }))
            const label = {
                selected: true,
                // disabled: true,
                value: null,
                text: 'Choose Category'
            }

            return [label, ...options]
        },
        queryItemsFromFilters() {
            return state.filters.map(value => ({
                id: +value,
                name: (() => {
                    const found = this.uniqueTagsFromList.find(({ id }) => id == value)
                    return found ? found.name : value
                })()
            }))
        },
        uniqueTagsFromList() {
            const filters = state.list.reduce(
                (acc, item) => [...acc, ...item.categories],
                []
            )

            return sortBy(uniqBy(filters, 'id'), 'id')
        },
        filteredList() {
            const {
                filters: queryElements,
                list
            } = state
            const {
                foundByTags,
                onlyShowPastPublishedAtDate,
                sortByPublishedAt
            } = methods


            if (!queryElements.length)
                return onlyShowPastPublishedAtDate(sortByPublishedAt(list))
            const filtered = list.filter(item => foundByTags(queryElements, item))

            return onlyShowPastPublishedAtDate(sortByPublishedAt(filtered))
        },
        uniqueTagsFromFilteredList() {
            const filters = this.filteredList.reduce(
                (acc, item) => [...acc, ...item.categories],
                []
            )

            return sortBy(uniqBy(filters, 'id'), 'id')
        },
        differenceQueryItemsWithUniqueTagsFromFilteredList() {
            const { uniqueTagsFromFilteredList, queryItemsFromFilters } = this
            return differenceBy(
                uniqueTagsFromFilteredList,
                queryItemsFromFilters,
                'id'
            )
        }
    }

    // methods
    const methods = {
        foundByTags(queryElements, item) {
            let found = true

            queryElements.forEach(categoryId => {
                let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
                found *= foundIndex > -1
            })

            return found
        },
        setFiltersFromQueryUrl() {
            const queryString = location.search
            const { filters } = urlQueryToParams(queryString)
            state.filters = Array.isArray(filters)
                ? uniq(filters).map(i => +i)
                : filters === (null || undefined)
                    ? []
                    : [+filters]
        },
        sortByPublishedAt(collection) {
            return collection.sort(
                (a, b) =>
                    milliseconds(b['published_at']) - milliseconds(a['published_at'])
            )
        },
        onlyShowPastPublishedAtDate(collection) {
            return collection.filter(
                item => milliseconds(item['published_at']) < dayjs().valueOf()
            )
        },
        // Buttons Events
        onChangeCategory() {
            const { filters } = state
            changeUrlPushStateFromFilters({ filters })
        },
        onAddToFilters(categoryId) {
            const { filters } = state
            const foundIndex = filters.indexOf(categoryId)
            if (!(foundIndex > -1)) {
                filters.push(categoryId)
                changeUrlPushStateFromFilters({
                    filters
                })
            }
        },
        onRemoveFromFilters(categoryId) {
            console.log('onRemoveFromFilters:', { categoryId })
            const { filters } = state
            const foundIndex = filters.indexOf(+categoryId)
            console.log({ foundIndex })
            if (foundIndex > -1) {
                filters.splice(foundIndex, 1)
                changeUrlPushStateFromFilters({
                    filters
                })
            }
        },
        onPopstate(event) {
            const { filters } = event.state
            state.filters = filters || []
        }
    }

    // Spread Operator
    return {
        ...toRefs(state),
        ...toComputed(compute),
        ...methods
    }
}

export default setup
File: js/lib/composition/setup-articles.js
- Taking out data as state, computed as compute and methods and local dependencies.
- Correct reference this to state or methods or leave as this.
<template>
	<div class="article-overview">
		<div class="main__inner">
			<header class="article-overview__header">
				<h1 class="article-overview__title"><slot name="title" /></h1>
				<span class="u-flex u-flex-cell">
					<SelectOptions
						class="u-flex-cell article-overview__select"
						:options="mappedOptionsFromUniqueTagsFromList"
						v-model="selected"
						v-on:change="onChangeCategory"
					/>
					<object-pulldown class="u-flex-cell filter-options" title="Filter">
						<div class="u-flex-cell">
							<tag-group
								class="filter-type-item"
								:query-items="queryItemsFromFilters"
								:filtered-items="
									differenceQueryItemsWithUniqueTagsFromFilteredList
								"
								:on-remove-tag-from-filter="onRemoveFromFilters"
								:on-add-tag-to-filter="onAddToFilters"
							>
							</tag-group>
						</div>
					</object-pulldown>
				</span>
			</header>
			<div class="article-overview__count">{{ filteredList.length }}</div>
			<div class="post-feed">
				<Card
					class="post-card"
					v-for="(item, index) in filteredList"
					:key="item.id"
					:item="item"
					:class="{ 'card--masthead': !index }"
				/>
			</div>
		</div>
	</div>
</template>

<script>
import Card from './Card'
import SelectOptions from './SelectOptions'
import ObjectPulldown from './ObjectPulldown'
import TagGroup from './TagGroup'

import setup from '../lib/composition/setup-articles'

export default {
	name: 'ArticleOverviewAlternative',
	components: {
		SelectOptions,
		ObjectPulldown,
		TagGroup,
		Card
	},
	props: {
		endpoint: String
	},
	setup,
	// Life Cycle Hooks
	async beforeMount() {
		const response = await (await fetch(this.endpoint)).json()
		this.list = response
	},
	mounted() {
		window.onpopstate = this.onPopstate
		if (this.filters.length) {
			changeUrlPushStateFromFilters({
				filters: this.filters
			})
		} else {
			this.setFiltersFromQueryUrl()
		}
	}
}
</script>
File: js/vue/ArticleOverview.vue
- Importing setup from library setup-articles.
- Removing libraries and refactor.