Wait for VueX value to load, before loading component - javascript

When a user tries to directly navigate load a component url, an http call is made in my vuex actions, which will define a value in my state once it resolves.
I don't want to load my component until the http call is resolved, and the state value is defined.
For Example, in my component
export default {
computed: {
...mapState({
// ** this value needs to load before component mounted() runs **
asyncListValues: state => state.asyncListValues
})
},
mounted () {
// ** I need asyncListValues to be defined before this runs **
this.asyncListValues.forEach((val) => {
// do stuff
});
}
}
How can I make my component wait for asyncListValues to load, before loading my component?

One way to do it is to store state values.
For example, if your store relies on single API, you would do something like this. However, for multiple APIs, it's a good idea to store each api load state individually, or using a dedicated object for each API.
There are usualy 4 states that you can have, which I prefer to have in a globally accessible module:
// enums.js
export default {
INIT: 0,
LOADING: 1,
ERROR: 2,
LOADED: 3
};
Then, you can have the variable stored in the vuex state, where the apiState is initialized with INIT. you can also initialize the array with [], but that shouldn't be necessary.
import ENUM from "#/enums";
// store.js
export default new Vuex.Store({
state: {
apiState: ENUM.INIT,
accounts: [],
// ...other state
},
mutations: {
updateAccounts (state, accounts) {
state.accounts = accounts;
state.apiState = ENUM.LOADED;
},
setApiState (state, apiState) {
state.apiState = apiState;
},
},
actions: {
loadAccounts ({commit) {
commit('setApiState', ENUM.LOADING);
someFetchInterface()
.then(data=>commit('updateAccounts', data))
.catch(err=>commit('setApiState', ENUM.ERROR))
}
}
});
Then, by adding some computed variables, you can toggle which component is shown. The benefit of using state is that you can easily identify the Error state, and show a loading animation when state is not ready.
<template>
<ChildComponent v-if="apiStateLoaded"/>
<Loader v-if="apiStateLoading"/>
<Error v-if="apiStateError"/>
</template>
<script>
import ENUM from "#/enums";
export default {
computed: {
...mapState({
apiState: state=> state.apiState
}),
apiStateLoaded() {
return this.apiState === ENUM.LOADED;
},
apiStateLoading() {
return this.apiState === ENUM.LOADING || this.apiState === ENUM.INIT;
},
apiStateError() {
return this.apiState === ENUM.ERROR;
},
})
}
</script>
aside... I use this pattern to manage my applications as a state machine. While this example utilizes vuex, it can be adapted to use in a component, using Vue.observable (vue2.6+) or ref (vue3).
Alternatively, if you just initialize your asyncListValues in the store with an empty array [], you can avoid errors that expect an array.

Since you mentioned vue-router in your question, you can use beforeRouteEnter which is made to defer the rendering of a component.
For example, if you have a route called "photo":
import Photo from "../page/Photo.vue";
new VueRouter({
mode: "history",
routes: [
{ name: "home", path: "/", component: Home },
{ name: "photo", path: "/photo", component: Photo }
]
});
You can use the beforeRouteEnter like this:
<template>
<div>
Photo rendered here
</div>
</template>
<script>
export default {
beforeRouteEnter: async function(to, from, next) {
try {
await this.$store.dispatch("longRuningHttpCall");
next();
} catch(exception) {
next(exception);
}
}
}
</script>
What it does is, waiting for the action to finish, updating your state like you want, and then the call to next() will tell the router to continue the process (rendering the component inside the <router-view></router-view>).
Tell me if you need an ES6-less example (if you do not use this syntax for example).
You can check the official documentation of beforeRouteEnter on this page, you will also discover you can also put it at the route level using beforeEnter.

One approach would be to split your component into two different components. Your new parent component could handle fetching the data and rendering the child component once the data is ready.
ParentComponent.vue
<template>
<child-component v-if="asyncListValues && asyncListValues.length" :asyncListValues="asyncListValues"/>
<div v-else>Placeholder</div>
</template>
<script>
export default {
computed: {
...mapState({
asyncListValues: state => state.asyncListValues
})
}
}
</script>
ChildComponent.vue
export default {
props: ["asyncListValues"],
mounted () {
this.asyncListValues.forEach((val) => {
// do stuff
});
}
}

Simple way for me:
...
watch: {
vuexvalue(newVal) {
if (newVal == 'XXXX')
this.loadData()
}
}
},
computed: {
...mapGetters(['vuexvalue'])
}

Building on some of the other answers, if you're using Router, you can solve the problem by only calling RouterView when the state has been loaded.
Start with #daniel's approach of setting a stateLoaded flag when the state has been loaded. I'll just keep it simple here with a two-state flag, but you can elaborate as you like:
const store = createStore({
state () {
return {
mysettings: {}, // whatever state you need
stateLoaded: false,
}
},
mutations: {
set_state (state, new_settings) {
state.settings = new_settings;
state.stateLoaded = true;
},
}
}
Then, in app.vue you'll have something like this:
<div class="content">
<RouterView/>
</div>
Change this to:
<div class="content">
<RouterView v-if="this.$store.state.stateLoaded"/>
</div>
The v-if won't even attempt to do anything with RouterView until the (reactive) stateLoaded flag goes true. Therefore, anything you're rendering with the Router won't get called, and so there won't be any undefined state variables in it when it does get loaded.
You can of course build on this with a v-else to perhaps show a "Loading..." screen or something, just in case the state loading takes longer than expected. Using #daniel's multi-state flag, you could even report if there was a problem loading the state, and offer a Retry button or something.

Related

How to pass data between vue components

This is regarding Vuex and passing of data from one component to another. I want the Module variable within CoreMods.vue to be passed to the ExternalWebpage.vue. Or rather, be watched in the external webpage for changes.
My store.js looks like this:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.store({
state:{
CoreModule: ""
},
mutations:{
update: (state, n) => {
state.CoreModule = n;
}
},
getters:{
updated: state =>{
return state.CoreModule;
}
},
actions:{
async createChange({ commit }, n) {
commit("update", n);
}
}
});
I have a CoreMods.vue
methods:{
checkModule() {
if(!this.completed_cm.includes(this.Module)) {
if (this.core.includes(this.Module)) {
this.completed_cm.push(this.Module);
this.$store.dispatch('createChange',this.Module);
}
},
An ExternalWebpage.vue
watch:{
'$store.state.CoreModule': function(){
var cm = this.$store.getters.updated;
if(this.CompletedCore.indexOf(cm) == -1){
this.CompletedCore.push(cm);
}
}
}
I can't make use of props by importing one component in another. This is because:
1) I do not want the entire component placed within the parent component.
2) CoreMod is a component on the home page. Upon clicking a link on the home page, it leads to ExternalWebpage. (This has already been implemented using router)
Currently this code isn't working. Can someone help find a solution/alternative. Additionally, how should I add this part to the main.js?
Thanks!!!
possible solution is to return vuex store value from computed and watch the computed value.
computed: {
coreModule () {
return this.$store.state.CoreModule;
}
},
watch:{
'coreModule': function(){
var cm = this.$store.getters.updated;
if(this.CompletedCore.indexOf(cm) == -1){
this.CompletedCore.push(cm);
}
}
}

How to change the value of v-overlay if getters are available in component?

In the component of Vue.js application I take information from the Vuex storage. Inside that component, I want to show v-overlay (preloader) until the data from storage will not be available. How correctly to make it?
<template>
<v-navigation-drawer
v-model="open"
absolute
right>
<v-overlay
:absolute="absolute"
:opacity="opacity"
:value="overlay">
<v-progress-circular
indeterminate
size="64">
</v-progress-circular>
</v-overlay>
<v-checkbox
v-if="!overlay"
hide-details
v-model="selectedGenders"
v-for="gender in genders"
:label="gender"
:value="gender"
:key="gender">
</v-checkbox>
<v-checkbox
v-if="!overlay"
hide-details
v-model="selectedIncomeRanges"
v-for="incomeRange in incomeRanges"
:label="incomeRange"
:value="incomeRange"
:key="incomeRange">
</v-checkbox>
</v-navigation-drawer>
</template>
<script>
import {
mapGetters,
mapActions
} from 'vuex'
export default {
name: 'RightNavigationDrawer',
props: {
open: {
type: Boolean,
default: false
}
},
data () {
return {
absolute: true,
opacity: 0.8,
overlay: true,
selectedGenders: [],
selectedIncomeRanges: []
}
},
mounted () {
this.getGenders()
this.getIncomeRanges()
},
computed: mapGetters('customStore', [
'genders',
'incomeRanges'
]),
methods: mapActions('customStore', [
'getGenders',
'getIncomeRanges'
])
}
</script>
Ideally, you would track the loading status in vuex so that it's available in all of your components, like your other vuex state. In your store, create a loading boolean state. Next, create a loading action that will be called in the component like:
loadData({ commit, dispatch }) {
commit('SET_LOADING', true);
const loader1 = dispatch('getGenders')
const loader2 = dispatch('getIncomeRanges')
Promise.all([loader1, loader2]).then(() => {
commit('SET_LOADING', false);
})
}
Promise.all takes an array of promises from your loading actions and will not resolve until all of those promises have resolved. Just make sure that your getGenders and getIncomeRanges actions return promises as well. Now, in your component, map only loading and loadData:
...mapState('customStore', ['loading']),
...mapActions('customStore', ['loadData'])
Change mounted to call this action:
mounted() {
this.loadData()
}
Now you can check loading anywhere instead of overlay in all of your components. This is a superior pattern because now loading is stored only once in vuex with your other state, and is available in all of your components, rather than being managed and passed locally.
Here is a demo where I'm simulating AJAX calls with a timeout. (The example uses a single file to manage vuex + vue so it will look slightly different, but shouldn't be too hard to follow.)
Put a watch on overlay (getter from Vuex storage).Let say the value is this.overlay = true
watch: {
overlay (val) {
val && setTimeout(() => {
this.overlay = false
}, 3000)
},
},
Follow code pen

vue js 2 : how to communicate between store and a component

I am a beginner with vue js.
i m trying to handle errors from component and display it on other component.
Apparently handling work becase i can see data in my store
With a props to my component (error.vue), it handle it in the data variable.
But after that it can t display it on my vue .
Why ?
Here is my code :
My store is :
var store = new Vuex.Store(
{
state: {
errors:{}
},
mutations: {
setErrors(state, error) {
for(var err in error) {
state.errors[err] = error[err]
}
}
}
})
my vue where i put my error component:
<template>
<div id="wrapper">
<div class="container">
<error_handling></error_handling>
<router-view></router-view>
</div>
</div>
</template>
<script>
import Error from './components/Error.vue'
import store from './store';
export default {
components: {
'error_handling': Error
},
data() {
return {
erreurs: store.state.errors
}
}
}
</script>
my error vue :
<template>
<div>
<ul>
{{errors_handling}}
<!--<li v-for="error in errors_handling">{{ error }}</li>-->
</ul>
</div>
</template>
<script>
export default {
props: ['errors_hand'],
data() {
return {
errors_handling: this.errors_hand
}
}
}
</script>
Based on your provided code.
You are getting a state of "errors"
You are not committing a mutation
By not committing a mutation, you are not changing a state
Docs: Vuex Mutations
Store.js
var store = new Vuex.Store(
{
state: {
errors:{}
},
mutations: { // Change the state object
setErrors(state, error) {
for(var err in error) {
state.errors[err] = error[err]
}
}
},
getters:{
// getters gets the current object of state
GetErrors(state) //by default getters get state as first paramater
{
return state.errors;
}
},
})
Error Component
<script>
export default {
computed:{
errors_handling(){
return this.$store.getters.GetErrors;
}
},
methods:{
//commit your mutation or dispatch when using action
ChangeState(error){
this.$store.commiit('setErrors',error);
}
}
}
</script>
But you must use actions to run it asyncronously
I would use a bus to pass errors from wherever they occur to the error component. This way the error component need not interact with your store or any other component directly, and can manage its own internal state easily. You also would not need to include the error component in any other component.
This example assumes that you are wanting only a single Error Component instance in your UI. I would put the error component instance in your main App template and have it show or hide itself based on whether it has any non-handled errors.
To declare a simple bus...
in file errorBus.js
import Vue from 'vue'
const errorBus = new Vue();
export default {
errorBus
}
Wherever an error occurs that you want to pass to the error component, use...
import errorBus from './errorBus.js'
errorBus.errorBus.$emit("notifyError", { msg: 'An error has occurred'});
In the error component...
import errorBus from './errorBus.js'
and within the component definition...
created: function() {
errorBus.errorBus.$on("notifyError", function(error) {this.addError(error)};
},
data () {
return {
errors: []
};
},
methods: {
addError: function(error) {
this.errors.push(error);
}
}
With this mechanism in place, you could easily handle different errors in different ways by passing additional information in the error object - for example, you could add {handling: "toast", priority: 0} which would cause the error component to immediately toast the error.
If you use this to toast, consider having the errors remain for later viewing after the error is toasted - I have always wanted something like an error drawer that I could open at my leisure instead of having to handle a toast immediately before it disappears.

How to design a store in Vuex to handle clicks in nested, custom components?

I'm trying to design a store to manage the events of my Vuex application. This far, I have the following.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = { dataRows: [], activeDataRow: {} };
const mutations = {
UPDATE_DATA(state, data) { state.dataRows = data; state.activeDataRow = {}; },
};
export default new Vuex.Store({ state, mutations });
I'm going to have a number of list items that are supposed to change the value of the data in the store when clicked. The design of the root component App and the menu bar Navigation is as follows (there will be a bunch of actions in the end so I've collected them in the file actions.js).
<template>
<div id="app">
<navigation></navigation>
</div>
</template>
<script>
import navigation from "./navigation.vue"
export default { components: { navigation } }
</script>
<template>
<div id="nav-bar">
<ul>
<li onclick="console.log('Clickaroo... ');">Plain JS</li>
<li #click="updateData">Action Vuex</li>
</ul>
</div>
</template>
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
}
}
</script>
Clicking on the first list item shows the output in the console. However, when clicking on the second one, there's nothing happening, so I'm pretty sure that the event isn't dispatched at all. I also see following error when the page's being rendered:
Property or method "updateData" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I'm very new to Vuex so I'm only speculating. Do I need to put in reference to the updateData action in the store, alongside with state and mutations? How do I do that? What/where's the "data option" that the error message talks about? Isn't it my components state and it's properties?
Why the error
You are getting the error, because when you have <li #click="updateData"> in the template, it looks for a method updateData in the vue component which it does not find, so it throws the error. To resolve this, you need to add corresponding methods in the vue component like following:
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
What this.$store.dispatch("updateData") is doing is calling your vuex actions as documented here.
What/where's the "data option"
You don't have any data properties defined, data properties for a vue component can be used, if you want to use that only in that component. If you have data which needs to be accessed across multiple components, you can use vuex state as I believe you are doing.
Following is the way to have data properties for a vue component:
<script>
import { updateData } from "../vuex_app/actions";
export default {
date: {
return {
data1 : 'data 1',
data2 : {
nesteddata: 'data 2'
}
}
}
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
You can use these data properties in the views, have computed properies based on it, or create watchers on it and many more.

How to show loading UI when calling getComponent in react-router?

I'm really new to React and I can't figure out how to render a "loading..." screen when a route is being loaded with getComponent. The getComponent call works fine and displays the component, but there's no indication on the UI that anything is happening between the request and the response. That's what I'm trying to figure out.
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
getComponent: function(path, cb) {
require.ensure([], (require) => {
cb(null, require("./pages/about/About.jsx"));
});
}
}
]
};
export default Routes;
After trying to unsuccessfully force a "loading" component to display using onEnter or within the getComponent function, I thought maybe I should try using Redux to set a loading state to true/false and getting my main view component to display a loading screen:
import React from 'react';
import {connect} from 'react-redux';
import NavBar from '../components/Navigation/NavBar.jsx';
import Footer from '../components/Footer.jsx';
import Loading from './Loading.jsx';
import navItems from '../config/navItems.jsx';
import setLoading from '../actions/Loading.jsx';
var Main = React.createClass({
renderPage: function() {
if (this.props.loading) {
return (
<Loading/>
);
} else {
return this.props.children;
}
},
render: function() {
return (
<div>
<header id="main-header">
<NavBar navigation={navItems}/>
</header>
<section id="main-section">
{this.renderPage()}
</section>
<Footer id="main-footer" />
</div>
);
}
});
function mapStateToProps(state) {
return {
loading: state.loading
}
}
export default connect(mapStateToProps)(Main);
This seems to work if I manually set the loading state using an action, which is what I was looking to do. But (and I feel this is going to be a real noob question) I can't figure out how to access the store/dispatcher from within the router.
I'm not sure if I'm using the wrong search terms or whatever, but I'm completely out of ideas and every react-router/redux tutorial seems to skip over what I feel like has to be a common problem.
Can anyone point me in the right direction (and also let me know if what I'm doing is best practice?)?
EDIT: I'll try and clarify this a bit more. In the first code block, you can see that if I click a <Link to="/about"> element then the getComponent function will fire, which will lazy-load the About.jsx component. The problem I am having is I can't figure out how to show some sort of loading indicator/spinner that would appear immediately after clicking the link and then have it get replaced once the component loads.
MORE EDITING: I've tried creating a wrapper component for loading async routes and it seems to work, however it feels really hacky and I'm sure it isn't the right way to go about doing this. Routes code now looks like this:
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import AsyncRoute from './pages/AsyncRoute.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
component: AsyncRoute("about")
}
]
};
export default Routes;
The AsyncRoute.jsx page looks like this:
import React from 'react';
function getRoute(route, component) {
switch(route) {
// add each route in here
case "about":
require.ensure([], (require) => {
component.Page = require("./about/About.jsx");
component.setState({loading: false});
});
break;
}
}
var AsyncRoute = function(route) {
return React.createClass({
getInitialState: function() {
return {
loading: true
}
},
componentWillMount: function() {
getRoute(route, this);
},
render: function() {
if (this.state.loading) {
return (
<div>Loading...</div>
);
} else {
return (
<this.Page/>
);
}
}
});
};
export default AsyncRoute;
If anyone has a better idea, please let me know.
I think I have this figured out. It may or may not be the correct way to go about things, but it seems to work. Also I don't know why I didn't think of this earlier.
First up, move my createStore code to its own file (store.jsx) so I can import it into the main entry point as well as into my Routes.jsx file:
import {createStore} from 'redux';
import rootReducer from '../reducers/Root.jsx';
var store = createStore(rootReducer);
export default store;
Root.jsx looks like this (it's an ugly mess, but I'm just trying to get something that works on a basic level and then I'll clean it up):
import {combineReducers} from 'redux';
import user from './User.jsx';
import test from './Test.jsx';
var loading = function(state = false, action) {
switch (action.type) {
case "load":
return true;
case "stop":
return false;
default:
return state;
}
};
export default combineReducers({
user,
test,
loading
});
I've made a basic component that shows Loading/Loaded depending on the Redux store's value of "loading":
import React from 'react';
import {connect} from 'react-redux';
var Loading = React.createClass({
render: function() {
if (this.props.loading) {
return (
<h1>Loading</h1>
);
} else {
return (
<h1>Loaded</h1>
);
}
}
});
export default connect(state => state)(Loading);
And now my Routes.jsx file looks like this (note I've imported the Redux store):
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import store from './config/store.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
getComponent: function(path, cb) {
store.dispatch({type: "load"})
require.ensure([], (require) => {
store.dispatch({type: "stop"});
cb(null, require("./pages/about/About.jsx"));
});
}
}
]
};
export default Routes;
This seems to work. As soon as a <Link/> is clicked to go to the /about route, an action is dispatched to set the "loading" state to true in the main store. That causes the <Loading/> component to update itself (I envision it would eventually render a spinner in the corner of the window or something like that). That weird require.ensure([]) function is run to get webpack to do its fancy code splitting, and once the component is loaded then another action is dispatched to set the loading state to false, and the component is rendered.
I'm still really new to React and while this seems to work, I'm not sure if it's the right way to do it. If anyone has a better way, please chime in!
Following the same approach as #David M I implemented a loading reducer and a function to wrap the dispatches.
Excluding the store creation and manage, they are basically as follows:
loadingReducer:
// ------------------------------------
// Constants
// ------------------------------------
export const LOADING = 'LOADING'
// ------------------------------------
// Actions
// ------------------------------------
const loadQueue = []
export const loading = loading => {
if (loading) {
loadQueue.push(true)
} else {
loadQueue.pop()
}
return {
type: LOADING,
payload: loadQueue.length > 0
}
}
export const actions = {
loading
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[LOADING]: (state, action) => (action.payload)
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = false
export default function reducer (state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}
Notice how loadingQueue keeps the loading message active while there are remaining modules to fetch, for nested routes.
withLoader function:
import { loading } from 'loadingReducer'
const withLoader = (fn, store) => {
return (nextState, cb) => {
store.dispatch(loading(true))
fn(nextState, (err, cmp) => {
store.dispatch(loading(false))
cb(err, cmp)
})
}
}
export default withLoader
Now when defining new routes we can dispatch the loading action implicitly using withLoader:
someRoute:
import withLoader from 'withLoader'
import store from 'store'
const route = {
path: 'mypath',
getComponent: withLoader((nextState, cb) => {
require.ensure([], require => {
cb(null, require('something').default)
}, 'NamedBundle')
}, store)
}
export default route
OK, let's see if I can shed some light on this here:
I can't figure out how to access the store/dispatcher from within the router
There is no need to do that AFAIK. You can specify all routes, listing the components that should answer each route (like you did above), and then connect each of the components to the redux store. For connecting, your mapStateToProps function can be written in a much simpler fashion, like this:
export default connect(state => state)(Main);
Regarding the loading state: I think it is a step in the wrong direction to have a slow-loading component and to display a waiting indicator while it is loading. I would rather have a fast-loading component that loads all of its data asynchronously from the backend, and while the data is not yet available, the component renders a waiting indicator. Once the data is available, it can be displayed. That is basically what you sketched in your second edit.
It would be even better if you could drive this off of your actual data, i.e. no data present -> show the loading screen / data present -> show the real screen. This way, you avoid issues in case your loading flag gets out of sync. (More technically speaking: Avoid redundancy.)
So, instead of making the wrapper generic, I would rather create a standalone component for the loading screen and display that whenever each individual component feels the need for it. (These needs are different, so it seems to be difficult to handle this in a generic way.) Something like this:
var Page = function(route) {
return React.createClass({
getInitialState: function() {
// kick off async loading here
},
render: function() {
if (!this.props.myRequiredData) {
return (
<Loading />
);
} else {
return (
// display this.props.myRequiredData
);
}
}
});
};
dynamic load async routers are using require.ensure, which use jsonp to download scripts from network.
because of slow networking, sometime, UI blocks, the screen is still showing the previews react component.
#Nicole , the really slow is not the data loading inside component, but is the component self, because of jsonp

Categories

Resources