I have the following Vue components structure:
Wrapper
-- QuestionsList
---- Question
-- QuestionDetail
And the following routes:
/ (goes to Questionslist)
/:id (goes to QuestionDetail)
When the user visits /, an HTTP GET request is done (via Vuex) and content is populated in an array of objects. Then, if he clicks on a "show more" link, he sees the QuestionDetail page (but without having to re-fetch content - it's taken directly from the State)
My problem is that every time we "route" in a page, we're not aware of the state of our Store.
For example, when the user navigates (deep-linked) to /:id, we need to see if the State is populated and therefore avoid making a new HTTP GET:
created() {
if (!this.$store.state.questions.length) {
this.$store.dispatch('requestItems');
}
},
computed: {
question() {
return this.$store.state.questions[this.$route.params.id] || {};
},
},
On a different scenario, since I always concat the new items fetched with the current state,I often see that data is duplicated:
[types.FETCHED_ADS_SUCCESS](state, payload) {
state.items = [...state.items, ...payload]; // when I go back in history, a new action is initiated and concats a copy of the same data
},
What is your store structure? I have found it useful to keep a boolean indicator in the store to indicate whether the content is loaded or not.
const store = new Vuex.store({
state: {
questions: {
isLoaded: false,
items: []
}
}
})
Then in the component where you're fetching the questions check for this and update once loaded.
Related
In my Vue app, I have a set of filters that build a query string upon selection and append it to the url. I want to have the filters applied and the data to be refreshed while also preserving state so that the user can keep adding filters without the modal being closed. I thought the only property would achieve this but it does not, the page still loses state. Here is what I've tested so far:
This achieves what I want (modal is not closed) but the data is not updated.
this.$inertia.visit(url, { preserveState: true })
This updates the data correctly, but still loses state as if the preserveState property is overidden (modal is closed)
...
props: ['data'],
...
this.$inertia.visit(url, {
preserverState: true,
only: ['data']
})
Is this possible with another method of hitting the url or custom option?
After some messing with configuration options, I found a way to update data while preserving state. Simply use the onSuccess option while preserving state:
...
props: ['data'],
...
this.$inertia.visit(url, {
preserveState: true,
onSuccess: page => {
this.data = page.props.data;
}
})
I have a view which shows all posts. There's a filter above those posts and user can filter posts depending on what options he chooses. After he chooses the options, filtered posts get returned.
Let's say user filtered posts and after that clicked on one of the posts, it means that parent component which was showing posts will be destroyed. If now, user(who is on the specific post page) clicks back button, it will take him to all posts page, but filters won't be persisted since parent component got destroyed and then created.
One solution to persist filters and filtered posts after clicking back button from specific page is to use vuex. when user chooses filters, we store the object in vuex. when user clicks back button, store would already have the filters. The problem is following this way causes some problems for me and takes much more time.
Any other way you can think of ? I can't use keep-alive since it seems i can only use it for dynamic components and not any other way.
I see 2 options here:
Vuex - it's used for state management, best to use when you need to communicate between 2+ components. You can will need a set of methods that will update the filter values in your store, e.g.:
const store = {
category: null,
tag: null,
date: null
}
const actions = {
updateFilter({ commit }, payload) {
commit('updateFilter', payload); // example payload structure: { filterName: 'category', filterValue: 'reviews' }
}
}
const mutations = {
updateFilter(state, payload) {
state[payload.filterName] = payload.filterValue;
}
}
export default {
namespaced: true,
store,
actions,
mutations
}
And you need to bind these actions to via #click events on your website. Then you need to bind the values from the store with your filters method (probably also you'll want to execute filtering method when your posts list changes, so you can use watcher for example)
If you're using Vue router and history mode, you can store your filters via query params:
router.push({ path: 'blog', query: { category: 'reviews' }})
So your url will become blog?category=reviews - and when you change your url to clicked article and then click back, you'll go first to the url with latest query params set you had (but of course you need to create a method that will filter out on component create the post list based on provided filters)
The additional win for the 2nd option is that you'll be able to share the link with other people (so they will gonna see the filtered posts in the same way as you do).
I'm creating a webshop for a hobby project in Nuxt 2.5. In the Vuex store I have a module with a state "currentCart". In here I store an object with an ID and an array of products. I get the cart from the backend with an ID, which is stored in a cookie (with js-cookie).
I use nuxtServerInit to get the cart from the backend. Then I store it in the state. Then in the component, I try to get the state and display the number of articles in the cart, if the cart is null, I display "0". This gives weird results. Half of the time it says correctly how many products there are, but the Vuex dev tools tells me the cart is null. The other half of the time it displays "0".
At first I had a middleware which fired an action in the store which set the cart. This didn't work consistently at all. Then I tried to set the store with nuxtServerInit, which actually worked right. Apparently I changed something, because today it gives the descibed problem. I can't find out why it produces this problem.
The nuxtServerInit:
nuxtServerInit ({ commit }, { req }) {
let cartCookie;
// Check if there's a cookie available
if(req.headers.cookie) {
cartCookie = req.headers.cookie
.split(";")
.find(c => c.trim().startsWith("Cart="));
// Check if there's a cookie for the cart
if(cartCookie)
cartCookie = cartCookie.split("=");
else
cartCookie = null;
}
// Check if the cart cookie is set
if(cartCookie) {
// Check if the cart cookie isn't empty
if(cartCookie[1] != 'undefined') {
let cartId = cartCookie[1];
// Get the cart from the backend
this.$axios.get(`${api}/${cartId}`)
.then((response) => {
let cart = response.data;
// Set the cart in the state
commit("cart/setCart", cart);
});
}
}
else {
// Clear the cart in the state
commit("cart/clearCart");
}
},
The mutation:
setCart(state, cart) {
state.currentCart = cart;
}
The getter:
currentCart(state) {
return state.currentCart;
}
In cart.vue:
if(this.$store.getters['cart/currentCart'])
return this.$store.getters['cart/currentCart'].products.length;
else
return 0;
The state object:
const state = () => ({
currentCart: null,
});
I put console.logs everywhere, to check where it goes wrong. The nuxtServerInit works, the commit "cart/setCart" fires and has the right content. In the getter, most of the time I get a null. If I reload the page quickly after another reload, I get the right cart in the getter and the component got the right count. The Vue dev tool says the currentCart state is null, even if the component displays the data I expect.
I changed the state object to "currentCart: {}" and now it works most of the time, but every 3/4 reloads it returns an empty object. So apparently the getter fires before the state is set, while the state is set by nuxtServerInit. Is that right? If so, why is that and how do I change it?
What is it I fail to understand? I'm totally confused.
So, you know that moment you typed out the problem to ask on Stackoverflow and after submitting you got some new ideas to try out? This was one of them.
I edited the question to tell when I changed the state object to an empty object, it sometimes returned an empty object. Then it hit me, the getter is sometimes firing before the nuxtServerInit. In the documentation it states:
Note: Asynchronous nuxtServerInit actions must return a Promise or leverage async/await to allow the nuxt server to wait on them.
I changed nuxtServerInit to this:
async nuxtServerInit ({ commit }, { req }) {
...
await this.$axios.get(`${api}/${cartId}`)
.then((response) => {
...
}
await commit("cart/clearCart");
So now Nuxt can wait for the results. The Dev Tools still show an empty state, but I think that is a bug, since I can use the store state perfectly fine in the rest of the app.
Make the server wait for results
Above is the answer boiled down to a statement.
I had this same problem as #Maurits but slightly different parameters. I'm not using nuxtServerInit(), but Nuxt's fetch hook. In any case, the idea is essentially: You need to make the server wait for the data grab to finish.
Here's code for my context; I think it's helpful for those using the Nuxt fetch hook. For fun, I added computed and mounted to help illustrate the fetch hook does not go in methods.
FAILS:
(I got blank pages on browser refresh)
computed: {
/* some stuff */
},
async fetch() {
this.myDataGrab()
.then( () => {
console.log("Got the data!")
})
},
mounted() {
/* some stuff */
}
WORKS:
I forgot to add await in front of the func call! Now the server will wait for this before completing and sending the page.
async fetch() {
await this.myDataGrab()
.then( () => {
console.log("Got the messages!")
})
},
I have a store with a simplified state tree:
{
routerReducer: {
state: {
url: '/blog'
},
queryParams: {
category: 'home'
}
params: { }
},
blog: {
posts: {
entities: { ... }
loading: false,
loaded: false,
pagination: {
sort: 'date',
filters: {
category: 'home',
tag: 'testTag'
},
page: 1
}
}
}
}
Basically, I'd like to pass down my router state into my blog state for pagination purposes, but only if the current URL belongs to that module if that makes sense? The pagination part of my blog -> posts state will be based on the URL parameters, already composed in my state. Perhaps this is not the correct method as I will no longer have a single source of truth? But I'd like to use this pagination state to essentially describe the set of entities I have in my store. That means, if I move pages or change filters, I plan on clearing all entities and refreshing with paginated content (performed server-side) from my API.
I supposed my flow will look like this:
Router navigation event e.g. /blog/2 (page 2 via queryParam)
Router action is dispatched and handled by router reducer to update
that part of my state tree
Side effect triggered on router navigation event, and if URL matches
my blog module e.g. "/blog/*" (could also contain URL parameters e.g.
?category=home) compose our local pagination state inside my blog state tree, then dispatch a loadPosts action which will be based off that piece of state
How does this flow sound? Is this the correct way of doing this?
1) It sounds feasable.
2) No. Whatever gets the job done.
What I would do
I'd create blog postPagination state where I would keep pagination data separate from entities. And a BlogPaginate action to alter it's state in reducer function.
{
{
sort: 'date',
filters: {
category: 'home',
tag: 'testTag'
}
},
page: 1
}
I'd make an effect that listens on router actions and maps the matching ones (url /blog/*) with appropriate search filters to BlogPaginate action which in turn would trigger a service call.
If you'd like to cache those entities
Making moving back to pages you've seen previously would be smoother than before. Depending on your content change rate I'd choose to either dispatch an action or just use the value in the cache if it exists.
Then I would add to postPagination state:
{
pageContents: {
// map of page to entity ids
1: [1,8,222]
}
{
sort: 'date',
filters: {
category: 'home',
tag: 'testTag'
}
},
currentPage: 1,
totalPages: 10
}
When pagination filters / sort changes in BlogPaginate reducer I would clear pageContents.
When pagination response's totalPages changes in BlogPaginateSuccess reducer I would clear other pageContents pages.
In BlogPaginateSuccess reducer I'd add/update new entities in blog posts and map their id's in as pageContents. Remember reducers can react to what ever action.
I would also create a selector that maps postPagination.currentPage, postPagination.pageContents and post.entities into an array of blog post entities.
Let me explain the problem that I've faced recently.
I have React.js + Flux powered application:
There is a list view of articles (NOTE: there are multiple of of different lists in the app) and article details view inside it.
But there is only one API endpoint per each list which returns array of articles.
In order to display the details I need to find article by id in array. That works pretty fine. I trigger action which makes request to server and propagates store with data, when I go to details screen then I just get the necessary article from that array in store.
When user lands on article details view before list (stores are empty) then I need to make a request.
Flow looks like: User loads details view -> component did mount -> stores are empty -> rendered empty -> fetchArticles action is triggered -> request response is 200 -> stores now have list of articles -> component did update -> rendered with data successfully
Component could look as follows:
let DetailsComponent = React.createClass({
_getStateFromStores() {
let { articleId } = this.getParams();
return {
article: ArticleStore.getArticle(articleId)
};
},
componentDidMount() {
// fire only if user wasn't on the list before
// stores are empty
if (!this.state.article) {
ArticleActions.fetchArticles('listType');
}
},
render() {
return <ArticleDetails article={this.state.article} />;
}
});
The interesting part comes next:
Now I need to make another request to server but request options depend on the article details. That's why I need to make second request after the first one on the details view.
I've tried several approaches but all of them look ugly. I don't like calling actions from stores that makes stores too complicated. Calling action inside action in this case doesn't work well because I will need to find article from store inside that action.
Solution (?!)
What I've came up with is to use callback in action inside component and it feels much more cleaner:
let DetailsComponent = React.createClass({
_getStateFromStores() {
let { articleId } = this.getParams();
return {
article: ArticleStore.getArticle(articleId)
};
},
componentDidMount() {
if (!this.state.article) {
ArticleActions.fetchArticles('listType', () => {
this._requestAdditionalData();
});
}
this._requestAdditionalData();
},
_requestAdditionalData() {
if (this.state.article) {
ArticleActions.fetchAdditional(this.state.article.property);
}
},
render() {
return <ArticleDetails article={this.state.article} />;
}
});
What's your input?
Consider move the second call to get a detail article to the ArticleDetails component componentDidMount() life cycle method.
So if the article is not set, do not render the ArticleDetails component at all by return null / false.