I'm building a Movie website to practice on VueJS. During app initialization, I get a list of movie genres from 3rd-party API. Since this list is needed in several components of the app, I manage and store it via Vuex, like so:
main.js:
new Vue({
router,
store,
vuetify,
render: h => h(App),
created () {
this.$store.dispatch('getGenreList')
}
}).$mount('#app')
Vuex's index.js:
export default new Vuex.Store({
state: {
genres: []
},
mutations: {
setGenreList (state, payload) {
state.genres = payload
}
},
actions: {
async getGenreList ({ commit }) {
try {
const response = await api.getGenreList() // axios call defined in api.js
commit('setGenreList', response)
} catch (error) {
console.log(error)
}
}
}
})
Now, in my Home view, I want to retrieve a list of movies for each genres, something like this:
Home.vue:
<script>
import { mapState } from 'vuex'
import api from '../api/api'
export default {
name: 'home',
data () {
return {
movies: null
}
},
computed: {
...mapState({
sections: state => state.genres
})
},
async mounted () {
const moviesArray = await Promise.all(
this.sections.map(section => {
return api.getMoviesByGenre(section.id)
})
)
this.movies = moviesArray
}
}
</script>
The issue here is that, on initial load, sections===[] since genres list hasn't been loaded yet. If I navigate to another view and come back, sections holds an array of genres objects as expected.
Question: How can I properly wait on sections to be loaded with genres? (since the getGenreList action isn't called from that component, I can't use this method)
I was thinking in implementing the movie list retrieval in a Watcher on sections instead of in mounted() but not sure if it's the right approach.
Yep, it is right approach, that's what watchers are for.
But if you only can... try to do actions like this one inside one component family. (parent passing props to children, controlling it);
You can read this article, about vuex - https://markus.oberlehner.net/blog/should-i-store-this-data-in-vuex/.
It will maybe clarify this idea. Just simply don't store in vuex everything, cause sometimes it' does not make sense
https://v2.vuejs.org/v2/api/#watch - for this one preferably you should use immedaite flag on watcher and delete mounted. Watcher with immedaite flag is kinda, watcher + created at once
Related
In my store module /store/template.js I have:
const templateConfig = {
branding: {
button: {
secondary: {
background_color: '#603314',
background_image: ''
}
}
}
}
export const state = () => ({
branding: {},
...
})
export const actions = {
initializeStore (state) {
state.branding = templateConfig.branding
}
}
(initializeStore() is called when app initially loads)
I want to retrieve the branding the branding object in my component:
computed: {
...mapState({
branding: state => state.template.branding
})
}
But when trying to console.log() branding I see this:
Why don't I simply see the branding object? (and what on earth is this?)
You need to always use a mutation to change state. You can call one from your action:
export const mutations = {
SET_BRANDING(state, payload) {
state.branding = payload;
}
}
export const actions = {
initializeStore ({ commit }) {
commit('SET_BRANDING', templateConfig.branding);
}
}
What you're seeing with the observer is normal, and indicates that the branding object has been successfully mapped and accessed.
What you see is Vue's observable object, which is how Vue implements reactivity. Without this, there would be no reactivity, and you will see such a wrapper on all top-level reactive objects. You can pretend it's not there.
Vue in fact applies this same "wrapper" to the data object internally to make it observable:
Internally, Vue uses this on the object returned by the data function.
You won't see it on other reactive properties, but if they're reactive, they belong to some parent observable object.
You need to import { mapState, mapActions } from 'vuex' (already done I guess).
And then, you can write this
...mapState(['branding']) // or ...mapState('#namespacedModule', ['branding'])
Still, why do you not simply put the state directly (with your background_color) rather than going through a Vuex action ?
If you want to keep it this way, do not forget to await this.initializeStore() in your component before trying to access the state.
First I'm making the GET request and creating the Vue instance:
main.js
import Vue from 'vue';
import App from './App.vue';
let getData = () => { // currently unused - need to get promise return value into components
let url = '';
return fetch(url)
.then(response => {
return response.json();
})
.then(data => {
return data;
}
}
new Vue({
render: h => h(App)
}).$mount('#app');
Then I'm creating the top-level component that attaches to my index.html:
App.Vue
<template>
<div id="app">
<Badge></Badge>
<Card></Card>
</div>
</template>
<script>
import Badge from './components/Badge.vue';
import Card from './components/Card.vue';
export default {
name: 'app',
//props: ['data'], // unused, possible solution?
components: {
Badge,
Card
}
}
</script>
And then Badge.vue and Card.vue are both components that (need to) display different data from the fetch in main.js
I've tried using props to pass data from main.js -> App.vue -> Card.vue but I wasn't able to figure out how to do that with this code in main.js:
new Vue({
render: h => h(App)
}).$mount('#app');
I suspect render may be my problem; I'm using it from an example I followed in a tutorial - I'm pretty sure it's for the live webserver I'm using when I run vue-cli-service serve so maybe this is as simple as doing things differently to send props to App.vue
However, it seems like passing data through props this way is a bad idea and that I should be doing things differently, I just don't know what that would be, so I'm hoping there may be a more elegant solution. I'm only making a single ajax call which hopefully simplifies things, but it seems like using props this way can get too messy if I start adding more components.
You need to move getData to the App.vue
App.vue:
<template>
<div id="app">
<Badge></Badge>
<Card></Card>
</div>
</template>
<script>
import Badge from './components/Badge.vue';
import Card from './components/Card.vue';
export default {
name: 'app',
data: () => ({ // To store data in App.vue
someData: []
}),
components: {
Badge,
Card
},
created: {
this.getData(); // Call getData function when the component is created
},
methods: {
getData() {
// Making reference to component instance since we can't access `this` inside arrow function
const self = this;
let url = "";
fetch(url)
.then(response => {
return response.json();
})
.then(data => {
self.someData = data;
}
}
}
}
</script>
If you need to share data between multiple components, then i highly recommend you to use Vuex instead.
I am trying to write a simple todoList using vue.js and I want to save those todos into cookies before the vue instance is destroyed. But I find it weird that though I wrote callback in beforeDestory hook, the hook is never called.
I checked Vue documents and could not find any hint.
when I tried to
save those todos into cookies by adding callback to window.onbeforeunload and window.onunload, it works.
my code is like
computed: {
todos() {
return this.$store.getters.todos
},
...
},
beforeDestroy() {
myStorage.setTodos(this.todos)
}
todos is a array defined in store.js, which has been imported in main.js, like
import myStorage from '#/utils/storage'
...
const store = new Vuex.Store({
state: {
todos: myStorage.getTodos()
...
},
getters: {
todos: state => state.todos
}
and myStorage is defined as:
import Cookies from 'js-cookie'
const todoKey = 'todo'
const setTodos = (todos) => {
Cookies.set(todoKey, JSON.stringify(todos))
}
const getTodos = () => {
const todoString = Cookies.get(todoKey)
let result = []
if (todoString) {
const todoParsed = JSON.parse(todoString)
if (todoParsed instanceof Array) {
result = todoParsed
}
}
return result
}
export default {
setTodos: setTodos,
getTodos: getTodos
}
I am using vue 2.6.10, and my project is constructed by vue-cli3.
I develop this todolist using Chrome on Window 10.
I expect that after I close the window or after I refresh the window, the todolist can still fetch todo written previously from cookies. But the fact is that the beforeDestory hook is never called.
When you refresh the window, the component's beforeDestroy() is not called, because you are not programmatically destroying the component, but ending the entire browser session.
A better solution would simply to call myStorage.setTodos whenever the todos object in the component is mutated. You can do that by setting up a watcher for the computed property:
computed: {
todos() {
return this.$store.getters.todos
},
},
watch: {
todos() {
myStorage.setTodos(this.todos)
}
}
Altertively, you let the VueX store handle the storage. It is unclear from your question if you are mutating the todos state: if you are mutating it, you can also do myStorage.setTodos in the store. The actual component can be dumb in that sense, so that all it needs to do is to update the store.
What I'm trying to accomplish
I'm trying to figure out a good architecture for the application I'm working on.
If you look at some screenshots of the current version of our application you'll see tables/lists of data in various formats. The table in the second screenshot has its own navigation. In some cases, table rows can be selected. In other screens, the list of items can be filtered on specified keywords, and so on.
The front-end is going to be rebuilt in VueJS. Instead of building one-off components for each specific screen, I'd like to have a more generic solution that I can use throughout the application.
What I need is a system of components where the root contains a set of data and the children provide (possibly multiple) representations of that data. The root component also contains – mostly user-specified – config, filters and selections which is used to control the representations.
What I'm trying to achieve is best illustrated with some pseudocode.
Questions
What is the best way to store the data? How should the child components access this data?
Passing the data-display's data down with props to each of its children seems cumbersome to me. Especially since the representations have implicit access to this data.
Should I use Vuex for this? How would that work with multiple instances of data-display? I read something about dynamic module registration, could that be useful?
Are there better ways? Any help is greatly appreciated!
I'm not sure I grasp all the details of your requirements, but understand data is common across multiple 'pages' in the SPA.
I would definitely recommend Vuex:
same data for all pages
access via component computed properties,
which are reactive to store changes but also cached to save
recomputing if no changes in dependencies
saves a lot of potentially complex passing of data in props (passing downwards) and events (passing upwards)
Looking at the pseudocode, you have three types of filters on the sidebar. Put that data in the store as well, then computed properties on views can apply filters to the data. Computation logic is then easily tested in isolation.
child.vue
<template>
<div>{{ myChildData.someProperty }}</div>
</template>
<script>
export default {
props: [...],
data: function () {
return {
selection: 'someValue' // set this by, e.g, click on template
}
},
computed: {
myChildData() {
return this.$store.getters.filteredData(this.selection)
}
}
}
</script>
store.js
const state = {
myData: {
...
},
filters: { // set by dispatch from sidebar component
keyword: ...,
toggle: ...,
range: ...
},
}
const getters = {
filteredData: state => {
return childSelection => { // this extra layer allows param to be passed
return state.myData
.filter(item => item.someProp === state.filters.keyword)
.filter(item => item.anotherProp === childSelection)
}
},
}
const mutations = {
'SET_DATA' (state, payload) {
...
},
'SET_FILTER' (state, payload) {
...
}
}
export default {
state,
getters,
mutations,
}
Generic children
There are probably many ways to tackle this. I've used composition, which means a thin component for each child each using a common component, but you only need this if the child has specific data or some unique markup which can be put into a slot.
child1.vue
<template>
<page-common :childtype="childType">
<button class="this-childs-button" slot="buttons"></button>
</page-common>
</template>
<script>
import PageCommon from './page-common.vue'
export default {
props: [...],
data: function () {
return {
childType: 'someType'
}
},
components: {
'page-common': PageCommon,
}
}
</script>
page-common.vue
<template>
...
<slot name="buttons"></slot>
</template>
<script>
export default {
props: [
'childtype'
],
data: function () {
return {
...
}
},
}
</script>
Multiple Instances of Data
Again, many variations - I used an object with properties as index, so store becomes
store.js
const state = {
myData: {
page1: ..., // be sure to explicitly name the pages here
page2: ..., // otherwise Vuex cannot make them reactive
page3: ...,
},
filters: { // set by dispatch from sidebar component
keyword: ...,
toggle: ...,
range: ...
},
}
const getters = {
filteredData: state => {
return page, childSelection => { // this extra layer allows param to be passed
return state.myData[page] // sub-select the data required
.filter(item => item.someProp === state.filters.keyword)
.filter(item => item.anotherProp === childSelection)
}
},
}
const mutations = {
'SET_DATA' (state, payload) {
state.myData[payload.page] = payload.myData
},
'SET_FILTER' (state, payload) {
...
}
}
export default {
state,
getters,
mutations,
}
Let's say I've got an app with two reducers - tables and footer combined using combineReducers().
When I click on some button two actions are being dispatched - one after another: "REFRESH_TABLES" and "REFRESH_FOOTER".
tables reducer is listening for the first action and it modifies the state of tables. The second action triggers footer reducer. The thing is it needs current state of tables in order to do it's thing.
My implementation looks something like below.
Button component:
import React from 'react';
const refreshButton = React.createClass({
refresh () {
this.props.refreshTables();
this.props.refreshFooter(this.props.tables);
},
render() {
return (
<button onClick={this.refresh}>Refresh</button>
)
}
});
export default refreshButton;
ActionCreators:
export function refreshTables() {
return {
type: REFRESH_TABLES
}
}
export function refreshFooter(tables) {
return {
type: REFRESH_FOOTER,
tables
}
}
The problem is that the props didn't update at this point so the state of tables that footer reducer gets is also not updated yet and it contains the data form before the tables reducer run.
So how do I get a fresh state to the reducer when multiple actions are dispatched one after another from the view?
Seems you need to handle the actions async so you can use a custom middleware like redux-thuk to do something like this:
actions.js
function refreshTables() {
return {
type: REFRESH_TABLES
}
}
function refreshFooter(tables) {
return {
type: REFRESH_FOOTER,
tables
}
}
export function refresh() {
return function (dispatch, getState) {
dispatch(refreshTables())
.then(() => dispatch(refreshFooter(getState().tables)))
}
}
component
const refreshButton = React.createClass({
refresh () {
this.props.refresh();
},
{/* ... */}
});
Although splitting it asynchronous may help, the issue may be in the fact that you are using combineReducers. You should not have to rely on the tables from props, you want to use the source of truth which is state.
You need to look at rewriting the root reducer so you have access to all of state. I have done so by writing it like this.
const rootReducer = (state, action) => ({
tables: tableReducer(state.tables, action, state),
footer: footerReducer(state.footer, action, state)
});
With that you now have access to full state in both reducers so you shouldn't have to pass it around from props.
Your reducer could then looks like this.
const footerReducer = (state, action, { tables }) => {
...
};
That way you are not actually pulling in all parts of state as it starts to grow and only access what you need.