Vue.js Object in array undefined in data from store - arrays

Sorry if this is really obvious but I’m new to Vue and could use some help.
I’m grabbing an array of data (posts) from my store and trying to console log just one of the objects in the array, but it’s showing undefined every time. If I console log the whole array it returns fine.
I’m guessing this is something to do with the data not loading before the console.log in the created hook? I’ve tried everything I can and it’s driving me nuts. Any help appreciated (simplified code below).
<script>
export default {
components: {},
computed: {
posts() {
return this.$store.state.posts;
}
},
created() {
this.$store.dispatch("getPosts");
console.log(this.posts[0])
},
};
</script>
//Store code Below
export const state = () => ({
posts: [],
})
export const mutations = {
updatePosts: (state, posts) => {
state.posts = posts
}
}
export const actions = {
async getPosts({
state,
commit,
dispatch
}) {
if (state.posts.length) return
try {
let posts = await fetch(
`${siteURL}/wp-json/wp/v2/video`
).then(res => res.json())
posts = posts
.filter(el => el.status === "publish")
.map(({
acf,
id,
slug,
video_embed,
title,
date,
content
}) => ({
acf,
id,
slug,
video_embed,
title,
date,
content
}))
commit("updatePosts", posts)
} catch (err) {
console.log(err)
}
}
}

Console behavior explained
When you log an object or array to the console, then click to expand/view the properties, the console shows you the properties as they are now, at the time of the click, not the time of the log. So if they've changed after logging, you'll see the new values.
How? Since objects and arrays are reference variables, the console stores the reference, and then updates itself when you click. This is only true of objects and arrays.
Conversely, When you log a primitive to the console, you see it only as it was at the time of the log.
In Chrome, you will also see a little blue square next to the object in the console. When you mouse over, it tells you, "Value below was evaluated just now."
This is why you never saw the value when logging one item, because it was an item that didn't exist yet. But posts always has a reference, since it's initialized to an empty array. So when logging the posts reference, by the time you click, the data has arrived.
Here is a demo that tries to make that very clear.

you get an undefined because the asynchronous functions have not yet filled the state.
With asynchronous data you should always use getters.
getter's result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.
Vuex Getters
// Store
export const getters = {
get_posts(state) {
return state.posts;
}
}
-
// component
computed: {
posts() {
return this.$store.getters['get_posts'];
}
};

You need to use promises as per the docs https://vuex.vuejs.org/guide/actions.html#composing-actions. Your action was not returning its promise to get data, nor were you waiting for the promise to resolve before you console.logged the result. Promises are a very important concept to master. Here's an example that basically matches your code, though I used setTimeout instead of a real fetch call.
const store = new Vuex.Store({
state: {
posts: []
},
getters: {
itemList: (state) => {
return state.items;
}
},
mutations: {
updatePosts: (state, posts) => {
state.posts = posts
}
},
actions: (actions = {
async getPosts({ state, commit, dispatch }) {
if (state.posts.length) {
return Promise.resolve();
}
try {
return new Promise(resolve => setTimeout(() => {
const posts = [{ id: 42 }];
commit("updatePosts", posts);
resolve();
}, 1000));
} catch (err) {
console.log(err);
}
}
})
});
const app = new Vue({
el: "#app",
computed: {
posts() {
return this.$store.state.posts;
}
},
created() {
this.$store.dispatch("getPosts").then(() => console.log(this.posts[0]));
},
store
});
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<script src="https://unpkg.com/vuex#2.5.0/dist/vuex.js"></script>
<div id="app">
</div>

If you want to get the correct data on created() hook, the action should be executed before displaying the data.
In created() hook, you can dispatch the action asynchronously.
async created() {
await this.$store.dispatch("getPosts");
console.log(this.posts[0])
},
You can use JavaScript Promises.
async created() {
this.$store.dispatch("getPosts").then(()=> {
console.log(this.posts[0]);
});
},

Related

Multiple useLazyQuery hooks (Apollo Client) in React Function Component

I am trying to include two Apollo-Client useLazyQuery hooks in my function component. Either works fine alone with the other one commented out, but as soon as I include both, the second one does nothing. Any ideas?
export default function MainScreen(props) {
useEffect(() => {
validateWhenMounting();
}, []);
const [validateWhenMounting, { loading, error, data }] = useLazyQuery(
validateSessionToken,
{
onCompleted: (data) => console.log('data', data),
},
);
const [validate, { loading: loading2, error: error2, data: data2 }] =
useLazyQuery(validateSessionTokenWhenSending, {
onCompleted: (data2) => console.log('data2', data2),
});
const handleSendFirstMessage = (selectedCategory, title, messageText) => {
console.log(selectedCategory, title, messageText);
validate();
};
Figured it out: Adding the key-value pair fetchPolicy: 'network-only', after onCompleted does the trick. It seems that otherwise, no query is being conducted due to caching...
This is the pattern that I was talking about and mentioned in the comments:
const dummyComponent = () => {
const [lazyQuery] = useLazyQuery(DUMMY_QUERY, {variables: dummyVariable,
onCompleted: data => // -> some code here, you can also accept an state dispatch function here for manipulating some state outside
onError: error => // -> you can accept state dispatch function here to manipulate state from outside
});
return null;
}
this is also a pattern that you are going to need sometimes

Should manually updating the cache always be the preferred option after mutations as long as I get proper data from server?

I am writing a CRUD app with React Query and I created some custom hooks as described here: https://react-query.tanstack.com/examples/custom-hooks
In the docs I see that there are basically two ways to update the cache after a mutation:
Query invalidation (https://react-query.tanstack.com/guides/query-invalidation)
onSuccess: () => {
queryClient.invalidateQueries("posts");
}
Updating the cache manually (https://react-query.tanstack.com/guides/invalidations-from-mutations)
// Update post example
// I get the updated post data for onSuccess
onSuccess: (data) => {
queryClient.setQueryData("posts", (oldData) => {
const index = oldData.findIndex((post) => post.id === data.id);
if (index > -1) {
return [
...oldData.slice(0, index),
data,
...oldData.slice(index + 1),
];
}
});
},
I understand that manual update has the advantage of not doing an extra call for fetching the 'posts', but I wonder if there is any advantage of invalidating cache over the manual update. For example:
import { useMutation, useQueryClient } from "react-query";
const { API_URL } = process.env;
const createPost = async (payload) => {
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
};
if (API_URL) {
try {
const response = await fetch(API_URL, options);
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
} catch (error) {
throw new Error(error);
}
} else {
throw new Error("No api url is set");
}
};
export default function useCreatePost() {
const queryClient = useQueryClient();
return useMutation((payload) => createPost(payload), {
// DOES INVALIDATING HAVE ANY ADVANTAGE OVER MANUAL UPDATE IN THIS CASE?
// onSuccess: () => {
// queryClient.invalidateQueries("posts");
// },
onSuccess: (data) => {
queryClient.setQueryData("posts", (oldData) => {
return [...oldData, data];
});
},
});
}
Thanks for your time!
As you state it yourself, the only advantage is that you don't waste another network call to update data we already have.
Here we have a create and delete example.
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
// createPost(post: PostT) {
// const { data } = await http.post<{ post: PostT >('/posts', { post });
// return data.post;
// }
const mutation = useMutation(createPost, {
onSuccess: (post) => {
queryClient.setQueryData<PostT[]>(['posts'], (oldData || []) => [ ...oldData, post])
},
})
// deletePost(id: string) {
// await http.delete(`/posts/${id}`);
// }
const mutation = useMutation(deletePost, {
onSuccess: (_, id) => {
queryClient.setQueryData<PostT[]>(['posts'], (oldData || []) => oldData.filter((post) => id !== post.id)
},
})
Invalidating the query can also be an option is some cases. The query will be invalidated and the data will be marked as stale. This will trigger a refetching in the background. So you know for a fact that the data will be as fresh as possible.
This can be handy if you got:
multiple queries to update with data from a mutation
have a (difficult) nested data structure to update
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
const mutation = useMutation(createPost, {
onSuccess: () => {
queryClient.invalidateQueries('posts')
queryClient.invalidateQueries('meta')
queryClient.invalidateQueries('headers')
},
})
But it really is up to you.
The main advantage of using manual updates comes from the fact that you can do it before the request is sent to the server; so if you manually update after the request is successful, then there's not much of an advantage if the data that you get from the server doesn't need to be immediately present to the user & in those cases (which I have found to be the majority) you better off invalidating. when you use optimistic updates, you assume the request is successful before you send it to server & then if the request fails you just roll back your update. this way your action happens instantly which is a better UX than doing the action, showing a loading spinner or something & then showing the updated state. so I have found it more useful for giving instantaneous feedback to the user than saving an extra request to the server. in most cases (as in yours) you still need to invalidate the query after, because your manually added post doesn't have an id, so you should sync it with the list of posts from the server. so be very careful about that because if you reading from that id somewhere else in that page, it would be undefined & would throw an error. so at the end of the day your mutation is not a great candidate for optimistic update & you should be careful to handle all the problems that can come up with your posts value having a post with no id in it (as opposed to something like a follow action which is just changing a boolean value in your database & you can confidently mutate the cache & undo it if request was not successful). so if we assume that you can handle that problem your useMutation hook would be something like this:
return useMutation(
(payload) => {
queryClient.setQueryData("posts", (oldData) => {
return [...oldData, payload];
});
return createPost(payload);
},
{
onSettled: () => {
queryClient.invalidateQueries("posts");
},
}
);

How do I load firebase data into react-redux asynchronously?

I am currently trying to load my product data into redux, but so far I cant seem to pass the product information returned from firestore into the reducer.
Index.js -> load first 10 products from firestore soon after store was created.
store.dispatch(getAllProducts)
action/index.js
import shop from '../api/shop'
const receiveProducts = products => ({
type: types.RECEIVE_PRODUCTS
products
})
const getAllProducts = () => dispatch => {
shop.getProducts(products => {
dispatch(receiveProducts)
})
}
shop.js
import fetchProducts from './firebase/fetchProducts'
export default {
getProducts: (cb) => cb(fetchProducts())
}
fetchProducts.js
const fetchProducts = async() => {
const ProductList = await firebase_product.firestore()
.collection('store_products').limit(10)
ProductList.get().then((querySnapshot) => {
const tempDoc = querySnapshot.docs.map((doc) => {
return { id: doc.id, ...doc.data() }
})
}).catch(function (error) {
console.log('Error getting Documents: ', error)
})
}
In product reducers
const byId = (state={}, action) => {
case RECEIVE_PRODUCTS:
console.log(action); <- this should be products, but it is now promise due to aysnc function return?
}
I can get the documents with no issues (tempDocs gets the first 10 documents without any issue.) but I am not able to pass the data back into my redux. If I were creating normal react app, I would add a loading state when retrieving the documents from firestore, do I need to do something similar in redux as well ?
Sorry if the code seems messy at the moment.
fetchProducts is an async function so you need to wait for its result before calling dispatch. There are a few ways you could do this, you could give fetchProducts access to dispatch via a hook or passing dispatch to fetchProducts directly.
I don't quite understand the purpose of shop.js but you also could await fetchProducts and then pass the result of that into dispatch.
A generalized routine I use to accomplish exactly this:
const ListenGenerator = (sliceName, tableName, filterArray) => {
return () => {
//returns a listener function
try {
const unsubscribe = ListenCollectionGroupQuery(
tableName,
filterArray,
(listenResults) => {
store.dispatch(
genericReduxAction(sliceName, tableName, listenResults)
);
},
(err) => {
console.log(
err + ` ListenGenerator listener ${sliceName} ${tableName} err`
);
}
);
//The unsubscribe function to be returned includes clearing
// Redux entry
const unsubscriber = () => {
//effectively a closure
unsubscribe();
store.dispatch(genericReduxAction(sliceName, tableName, null));
};
return unsubscriber;
} catch (err) {
console.log(
`failed:ListenGenerator ${sliceName} ${tableName} err: ${err}`
);
}
};
};
The ListenCollectionGroupQuery does what it sounds like; it takes a tableName, an array of filter/.where() conditions, and data/err callbacks.
The genericReduxAction pretty much just concatenates the sliceName and TableName to create an action type (my reducers de-construct action types similarly). The point is you can put the dispatch into the datacallback.
Beyond this, you simply treat Redux as Redux - subscribe, get, etc just as if the data were completely local.

Issue with displaying data returned from REST API using React

I am trying out some stuff using the react-chatbot-kit in the front end and getting data from a REST API. Console.log shows the data inside .then, however I am getting the error "Uncaught TypeError: Cannot read property 'map' of undefined" when trying to output the data on the console inside the calling function. I need help to display the returned data in console.log in the function handleApiList(). Thanks in advance.
PS: I am a newbie of course in React :) since I am not clear on how to handle REST API calls that are done asynchronously. Look forward to getting this resolved. Any help and tips on resolving this will be greatly appreciated
Following is the code:
// ActionProvider starter code
class ActionProvider {
constructor(createChatBotMessage, setStateFunc) {
this.createChatBotMessage = createChatBotMessage;
this.setState = setStateFunc;
this.state = {
error: null,
users: []
}
}
greet() {
const greetingMessage = this.createChatBotMessage("Hi! Greeting!")
this.updateChatbotState(greetingMessage)
}
// This is being called when the user types in 'api' in chat window
handleApiList()
{
const { error, users } = this.state;
this.getData();
if(error) {
console.log("Error: ", error.message)
}
else {
let myarray=[]
users.map(function(user)
{
myarray += `${ user.name }\n`;
return `${ user.name }`;
})
console.log(myarray)
}
}
getData()
{
console.log("in now")
fetch("https://jsonplaceholder.typicode.com/users")
.then(res => res.json())
.then(
(result) => {
this.setState({
users: result
});
},
(error) => {
this.setState({ error });
}
)
}
handleJobList = () => {
const message = this.createChatBotMessage(
"Fantastic, I've got the following jobs available for you",
{
widget: "jobLinks",
}
);
this.updateChatbotState(message);
};
updateChatbotState(message) {
// NOTE: This function is set in the constructor, and is passed in
// from the top level Chatbot component. The setState function here
// actually manipulates the top level state of the Chatbot, so it's
// important that we make sure that we preserve the previous state.
this.setState(prevState => ({
...prevState, messages: [...prevState.messages, message]
}))
}
}
export default ActionProvider;
You are fetching in getData and it's an async function. The data is not ready. It's better to just return the data than to setting state.
simplified version of your code.
handleApiList()
{
const { error, users } = this.state;
const data = await this.getData();
//data is ready, do what u want with the data here.
}
}
const getData = async() => {
return fetch("https://jsonplaceholder.typicode.com/users")
.then(res => res.json())
)
}
.map returns an array, if you want to push u need to use forEach.
Example
let myarray=[]
data.forEach((user) =>
{
myarray.push(user.name });
})
console.log(myarray)
Issue description:
const { error, users } = this.state; // gets state values
this.getData(); // updates state values
if(error) {
console.log("Error: ", error.message)
}
else {
let myarray=[]
users.map(function(user) // users is value before state update
I would suggest returning from getData() a promise with result of api call. After that you can execute code in handleApiList() in .then().
Proposal:
getData()
{
console.log("in now")
return fetch("https://jsonplaceholder.typicode.com/users")
.then(res => res.json())
.then(
(result) => {
this.setState({
users: result
});
return result;
}
)
}
I would also move error handling to .catch().
Also have a look on this. Working using async/await instead of pure Promises is easier and cleaner ;)

Redux State count return

I'm trying to build out a feature in my React application showing num of comments for a specific post. Since I don't have this information from backend ill try to make a .lengthon the returned state.
However, it seems like I have built out the reducer in a faulty way but I'm not sure whats wrong with it. Right now I'm just receiving undefined.
Built up as following
Action
export function getPostComments(id) {
const request = API.fetchPostComments(id)
return (dispatch) => {
request.then(({data}) => {
dispatch({type: COMMENTS_GET_POSTCOMMENTS, payload: data})
});
};
}
Reducer
export default function(state = {}, action) {
switch (action.type){
case COMMENTS_GET_POSTCOMMENTS:
return {...state, ...action.payload}
Component
componentWillMount() {
this.props.getPostComments(this.props.id);
}
....
<span>{`comments ${this.props.comments.length}`}</span>
....
function mapStateToProps(state) {
return {
comments: state.comments,
}
}
export default connect(mapStateToProps, {postPostVote, getPostComments})(PostComponent);
EDIT
I am retrieving information from the server if I change my reducer to be return action.payloadI will first receive a comment number of 2 but then this gets wiped replacing it with a 0 since the last post in the list doesn't have any comments. So I'm overwriting here? And that most be wrong aswell
Repo : https://github.com/petterostergren/readable
Thanks for now!
export function getAllCategories() {
return (dispatch) => {
API.fetchAllCategories()
.then(data => {
dispatch({type: CATEGORIES_GET_ALL_CATEGORIES, payload: data})
});
};
}
The call to your API fetchAllCategories is asynchronous, what you were doing before was that you were calling your API but not waiting for it's response. That is why you were getting undefined passed in payload.
So what you needed to do was Chain that fetch call with the another promise.
I am using redux-thunk in my app, and this is how I am using it. See the code below.
export function loadPayments () {
return dispatch => PaymentsAPI.getCustomerPaymentMethods()
.then((paymentMethods) => {
dispatch({
type: actionTypes.LOAD_PAYMENTS_SUCCESS,
payments: paymentMethods
})
})
.catch((error) => {
console.log('Error', error);
})
}
For API Calls I am using Fetch & Axios. You can use any you want. Both are good.
To update your reducer, so that it adds the previous value do the following
case actionTypes.LOAD_SAVED_CARDS_SUCCESS: {
return {
...state,
payments: [ ...state.payments, ...action.payments],
totalpayments: state.payments.length + action.payments.length
};
}
What the reducers will do here is that, it will append all your suppose payments methods i,e previous methods + new methods along with the count update as well.

Resources