Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Hoisting Data Up

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We just discussed that, because the categories in our app are static - we don't load them with Ajax and they never change - they don't really need to live as data on a component and it would be totally ok to fetch them directly - wherever we need them - from categories-service.

But... we're going to take the more complex path by pretending that our categories are still loading via Ajax... which means they do change during the lifecycle of our app... which means that they do need to live as data on a component.

Computed property for Category Name

But... let's ignore that for a moment and finish this component. We know that the title component will receive currentCategoryId and categories props. To find the current category name, we'll need to write some logic. And... hey! That's a perfect case for a computed property.

Add computed with one key inside, how about, categoryName(). For the logic, if this.currentCategoryId - to reference that prop - === null, then return "All Products".

... lines 1 - 8
<script>
export default {
... lines 11 - 21
computed: {
categoryName() {
if (this.currentCategoryId === null) {
return 'All Products';
}
... lines 27 - 30
},
},
};
</script>
... lines 35 - 43

If we are on a category page, find the correct one with const category = this.categories.find(). Pass this an arrow function with a cat argument. We want to find the category whose @id property - which is the IRI string - matches this.currentCategoryId.

... lines 1 - 21
computed: {
categoryName() {
... lines 24 - 27
const category = this.categories.find((cat) => (cat['@id'] === this.currentCategoryId));
... lines 29 - 30
},
},
... lines 33 - 43

The find() function effectively loops over all the categories, calls this function for each one, and returns the first that makes this expression true.

At the bottom, add return and use the ternary syntax: if a category was found, which... it should be unless the categories data is loading via Ajax and is empty at first - then return category.name. Else, use an empty string... or you could say "Loading...".

... lines 1 - 21
computed: {
categoryName() {
... lines 24 - 29
return category ? category.name : '';
},
},
... lines 33 - 43

Perfect! Up in the template, use this: {{ categoryName }}.

<template>
<div :class="$style.component">
<h1>
{{ categoryName }}
</h1>
</div>
</template>
... lines 8 - 43

Hoisting the categories Data

And... this component is done! Let's get to the interesting part. We need to pass currentCategoryId and categories into this component. Open the parent component - catalog.vue - and let's scroll down a little. It already has access to currentCategoryId - yay! - but it does not have access to categories.

Where does the categories data live? Head over to the Vue dev tools. The Catalog component is rendered by Products... but it doesn't have access to categories either. Ah yes, that's because categories currently live as data in Sidebar.

And that makes sense! Until this moment, the Sidebar component was the only one that needed the categories. But now that catalog also needs that info - so it can pass it to the Title component - we need to hoist - or "pull up" - the categories data to a higher component: we need to move it into the Products component so that we can pass it to both Sidebar and Catalog as props.

This is a fairly common situation: you start by putting your data in one component, then later you need to move it higher so that more parts of your app can use it. We did this once earlier: we moved the collapsed boolean data from Sidebar up to Products so that we could use it in more places.

So let's get to work! Open up products.vue and then sidebar.vue. Copy the categories data and then remove the data option entirely. In products, find data and paste categories.

... lines 1 - 22
<script>
... lines 24 - 28
export default {
... lines 30 - 34
data() {
return {
... line 37
categories: [],
};
},
... lines 41 - 61
};
</script>

The other thing we need move is the created() function. Copy that full function and delete it. In products.... it looks like we don't have a created() function yet, so we can paste this one.

... lines 1 - 28
export default {
... lines 30 - 51
async created() {
const response = await fetchCategories();
this.categories = response.data['hydra:member'];
},
... lines 57 - 61
};
... lines 63 - 64

When we did that, PhpStorm automatically added the import... but I do need to fix how that code looks.

... lines 1 - 22
<script>
... lines 24 - 26
import { fetchCategories } from '@/services/categories-service';
... lines 28 - 62
</script>

So far so good. Now in sidebar, because we don't have categories as data anymore, we will need it as a prop. Inside props, add categories, type: Array and required: true.

... lines 1 - 54
export default {
... lines 56 - 59
props: {
... lines 61 - 68
categories: {
type: Array,
required: true,
},
},
... lines 74 - 78
};
... lines 80 - 99

Finally, back over in products, pass this to sidebar: :categories="categories".

<template>
... lines 2 - 3
<aside :class="asideClass">
<sidebar
... lines 6 - 7
:categories="categories"
... line 9
/>
</aside>
... lines 12 - 20
</template>
... lines 22 - 64

Unless I tripped over my keyboard somewhere, that should make the sidebar happy again! Back at the browser... I'll refresh just to be sure and... it works! We have some errors due to a missing prop in TitleComponent - but that's ok: we're working on passing that!

Passing categories down to Title

Now that we have access to categories inside products, we can also pass that to catalog: :categories="categories".

<template>
... lines 2 - 12
<div :class="contentClass">
<catalog
... line 15
:categories="categories"
/>
</div>
... lines 19 - 20
</template>
... lines 22 - 64

To add that prop, let's steal the prop code from sidebar... and paste it into catalog.

... lines 1 - 28
export default {
... lines 30 - 35
props: {
... lines 37 - 40
categories: {
type: Array,
required: true,
},
},
... lines 46 - 68
};
... lines 70 - 71

Use this shiny new prop to pass the categories again to their final location. The TitleComponent needs two things actually: :currentCategoryId="currentCategoryId" and :categories="categories".

<template>
... lines 2 - 3
<div class="col-12">
<title-component
:current-category-id="currentCategoryId"
:categories="categories"
/>
</div>
... lines 10 - 20
</template>
... lines 22 - 71

Phew! I think we've got all the wires connected. When we move over... yes! It's working... and there are no errors. We can go to "all products" and that title works too.

But that was a lot of prop passing. We moved the categories data to products... so we could pass it to catalog... so we could pass it to title. This one of the common ugly parts of a traditional Vue or React app: a lot of prop passing. It's not the end of the world, but as I've mentioned a few times, it is something that can be solved by centralizing your data, which is possible in Vuex or in Vue3. I'm particularly excited about the possibilities in Vue 3.

Next: this product listing page is really looking good. But since we're going to have a ton of these... um... "useful" products in our store, let's add a search bar. This will be a perfect opportunity to talk about the last, super important directive: v-model.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.0
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.1.0
        "eslint-plugin-import": "^2.19.1", // 2.20.2
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.5
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.11
        "vue-loader": "^15.9.1", // 15.9.2
        "vue-template-compiler": "^2.6.11", // 2.6.11
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}
userVoice