I sometimes get a race condition when trying to use onSubscription hook from #apollo/react-hooks package in the following way.
let { data, loading, error } = useSubscription(MY_SUBSCRIPTION)
if (loading) return 'Loading...';
if (error) return 'Error...';
...
When I load the page, most of the time data gets filled perfectly and eventually loading will turn false, but every ~5th try there's some kind of race condition where loading stays true forever and data is undefined.
GraphQL query:
export const EXERCISE_SUBSCRIPTION = gql`
subscription {
exercises {
id
title
tasks {
id
title
start_time
end_time
}
}
}
`;
Package version is the (currently) latest:
#apollo/react-hooks": "^3.1.0-beta.0", but I have also tried with previous versions.
Has anyone experienced something similar and know how to solve it?
If you run into this issue, I found a workaround hack. You can see that when I add the callback option onSubscriptionData, the data IS present in there, but somehow does not end up in the data object outside.
// <HACK>
// sometimes data object is empty, but onSubscriptionData is filled.
// in that case use data from onSubscriptionData method.
const [dataFromCb, setDataFromCb ] = useState(null)
let { data, loading } = useSubscription(INJECT_SUBSCRIPTION, {
onSubscriptionData: (res) => {
setDataFromCb(res.subscriptionData.data)
},
});
if (loading && !dataFromCb) return 'Loading...';
data = (data === undefined) ? dataFromCb : data;
// </HACK>
Ok, I do believe I found the answer to this one, but to verify, you may need to double check and/or post your query code. Apparently Apollo is trying to marry up the data as it arrives, and it uses the id fields to do that by default. I had a query that was missing those ids in some nested layers of my structure, and when I put them in, this error has disappeared. It wasn't until I ran into this error that I found the resources that pointed me in the right direction.
For reference: https://github.com/apollographql/react-apollo/issues/1003
I had the same issue with useSubscription loading stuck. Afterwards noticed that I kept this hook in the child component, so moved it on the same level (parent component) with the useQuery which triggered subscriptions on the server. So in my case it was React rendering issue which affected such a behaviour.
Related
I'm trying to use a search bar component to dynamically filter the content of a table that's being populated by API requests, however when I use this implementation the component re-renders infinitely and repeatedly sends the same API requests.
The useEffect() Hook:
React.useEffect(() => {
const filteredRows = rows.filter((row) => {
return row.name.toLowerCase().includes(search.toLowerCase());
});
if (filteredRows !== rows){
setRows(filteredRows);
}
}, [rows, search]);
Is there something I've missed in this implementation that would cause this to re-render infinitely?
Edit 1:
For further context, adding in relevant segments of code from that reference this component which might cause the same behaviour.
Function inside the parent component that renders the table which calls my API through a webHelpers library I wrote to ease API request use.
function fetchUsers() {
webHelpers.get('/api/workers', environment, "api", token, (data: any) => {
if (data == undefined || data == null || data.status != undefined) {
console.log('bad fetch call');
}
else {
setLoaded(true);
setUsers(data);
console.log(users);
}
});
}
fetchUsers();
Edit 2:
Steps taken so far to attempt to fix this issue, edited the hook according to comments:
React.useEffect(() => {
setRows((oldRows) => oldRows.filter((row) => {
return row.name.toLowerCase().includes(search.toLowerCase());
}));
}, [search]);
Edit 3:
Solution found, I've marked the answer by #Dharmik pointing out how Effect calls are managed as this caused me to investigate the parent components and find out what was causing the component to re-render repeatedly. As it turns out, there was a useEffect hook running repeatedly by a parent element which re-rendered the page and caused a loop of renders and API calls. My solution was to remove this hook and the sub-components continued rendering as they should without loops.
It is happening because you've added rows to useEffect dependency array and when someone enters something into search bar, The rows get filtered and rows are constantly updating.
And because of that useEffect is getting called again and again. Remove rows from the useEffect dependency array and it should work fine.
I would like to complement Dharmik answer. Dependencies should stay exhaustive (React team recomendation). I think a mistake is that filteredRows !== rows uses reference equality. But rows.filter(...) returns a new reference. So you can use some kind of deep equality check or in my opinion better somethink like:
React.useEffect(() => {
setRows((oldRows) => oldRows.filter((row) => {
return row.name.toLowerCase().includes(search.toLowerCase());
}));
}, [search]);
I have a situation where on one screen (AScreen), I load JSON data from a .json file, I also have a Settings screen where the user can set 'settingA' as true or false.
AScreen (basically if settingA is set to true, load two JSON files, if settingA is false, load only one data file):
import React, { useContext } from 'react';
import { SettingsContext } from '../context/settingsContext';
const AScreen = () => {
const { settingA } = useContext(SettingsContext);
const loadedData = (() => {
if (settingA === true) {
console.log('True');
return Object.assign(
require('../assets/data/jsonFileA.json'),
require('../assets/data/jsonFileB.json')
)
}
console.log('False');
return require('../assets/data/jsonFileA.json')
})();
console.log(settingA):
console.log(loadedData);
return (
...
);
};
The issue here is that if I load the app and head to AScreen, I get the correct data loaded into loadedData. However, if I then head to the settings page and change the setting then go back to AScreen, the console.log(settingA) shows the updated setting and the correct 'True' or 'False' is printed but the loadedData still contains the data from the first time I visited AScreen.
This tells me that the require() function doesn't actually get called until the screen is fully re-rendered.
Is there a better way to load JSON data than require()? Or how can I make sure require() is called every time the screen is displayed even if the screen isn't re-rendered.
I know one option would be to load all the JSON data then simply filter it based on the user's settings but in the actual app there are a lot more data files so it seems that would be a waste of resources to load all the files at once.
Delete the require cache
delete require.cache[require.resolve('../assets/data/jsonFileA.json')];
...
Regardless of how many ways I tried setting loadedData to {}, running the delete require.cache[] command, using let instead of const, etc. I always ended up the same with the loadedData not updating properly.
It turns out that the issue was from the Object.assign() function. Instead of using:
return Object.assign(
require('../assets/data/jsonFileA.json'),
require('../assets/data/jsonFileB.json')
)
I used:
return Object.assign(
{},
require('../assets/data/jsonFileA.json'),
require('../assets/data/jsonFileB.json')
)
where the {} basically unsets the target, which is 'loadedData' and resets it with the new data. Without {}, the require statements only updated the location in memory where the require() function originally placed the data, therefore I wasn't getting the changes I was looking for.
Warning for anyone needing this in the future: The Object.assign() method creates a shallow copy so if the source changes, then the target also will change. Other methods will be necessary if you want a completely new object in memory.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
I have a mat-table that has over 150 rows, which doesn't seem like much but it completely freezes the page (especially if I have the developer console open).
To set the table data I use an input getter and setter (the reason being that it then allows me to make changes in the parent component and the child component will listen.)
#Input()
get data() {
return this.dataSource;
}
set data(tableData: any) {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.dataSource.data = tableData;
// moment dates cant be sorted by mat-table: this is their
// recommendation to convert it to timestamp to be sorted
// (still displays as moment date tho :D)
this.dataSource.sortingDataAccessor = (item, property) => {
switch (property) {
case 'momentDueDate': return new Date(item.momentDueDate);
default: return item[property];
}
};
}
The data itself loads relatively quick, however, as soon as I click anything on the page, the page freezes. This even includes trying to change page using mat-pagination on the table.
In my parent component, my data is created by an observable using combineLAtest like so:
this.combinedPaymentsAndfilters$ = combineLatest(
[this.combinedPaymentDetails$,
this.dateFilter$,
this.dateToFrom.asObservable(),
this.typeFilter$,
this.sortFilter$,
this.isAdmin$]).pipe(
tap(([payments, dateFilter, dateToFrom, type, sort, isAdmin]) => {
this._router.navigate(['.'], {
queryParams: {
date: dateFilter,
dateFrom: dateToFrom.date_from,
dateTo: dateToFrom.date_to,
type: type,
order: sort
},
relativeTo: this._route,
replaceUrl: true
});
}),
map(([payments, dateFilter, dateToFrom, type, sort, isAdmin]) => {
let combined = this.sortPayments(sort, payments);
if (dateFilter !== 'all') {
combined = this.filterByDate(payments, dateFilter, dateToFrom);
}
if (type !== 'all') {
combined = combined.filter(
(payment: any) => payment.paymentType === type);
}
this.paymentTitle = this.getViewValue(this.paymentTypeFilter, type);
this.dateTitle = this.getViewValue(this.dateFilterType, dateFilter);
return {
combined,
isAdmin
};
})
);
I also get the following chrome violations whilst in this component:
Other things I have tried:
So I have looked at a few similar things online, and people have recommended loading the data after the pagination etc (which you can see above I have).
Others have also recommended using AfterViewInit, I have also tried this, but it still makes no difference.
As a way of testing whether there was something else erroring in my code, I have also limited my firestore query to only return 5 items. Once I have done this, it works absolutely fine, which tells me the issue is definitely with the amount of data I am trying to display.
Any recommendations on improving this performance as currently, its unusable for production.
EDIT - The issue only really seems to happen when the chrome console is open
Complementing the answer of satanTime, if the combineLatest persists to be spammy even if distinctUntilChanged, you can use the operator debounceTime to force some milliseconds of silence and then get the last result.
Another angular list optimization is to use trackBy.
In this case I would say that too many emits are happening in combineLatest.
Perhaps with the same data in case if its filter wasn't changed.
every emit can cause changes of pointers even data is the same and it can cause a new render of the table.
I would add distinctUntilChanged as the first pipe operator to ensure that we really need to emit new value.
It's the implementation with JSON.encode.
.pipe(
distinctUntilChanged((prev, curr) => JSON.encode(prev) !== JSON.encode(curr))
...
)
Then regardless of emits with the same values your pipe won't react until there's a real change.
For what it's worth - I ran into the same symptoms and it turned out that Chrome had an update pending in the background. So, if you also experience "The issue only really seems to happen when the chrome console is open" -- try to reboot Chrome to let the update complete. It worked for me.
i'm new to react please forgive me if i'm asking a dumb question.
The idea is to access the tweets array from context, find the matching tweet and then set it in the component's state to access the data.
However, the tweets array results empty even though i'm sure it's populated with tweets
const { tweets } = useContext(TweeetterContext)
const [tweet, setTweet] = useState({})
useEffect(() => {
loadData(match.params.id, tweets)
}, [])
const loadData = (id, tweets) => {
return tweets.filter(tweet => tweet.id == id)
}
return (stuff)
}
You are accessing context perfectly fine, and it would be good if you could share a code where you set tweets.
Independent of that, potential problem I might spot here is related to the useEffect function. You are using variables from external context (match.params.id and tweets), but you are not setting them as dependencies. Because of that your useEffect would be run only once at the initial creation of component.
The actual problem might be that tweets are set after this initial creation (there is some delay for setting correct value to the tweets, for example because of the network request).
Try using it like this, and see if it fixes the issue:
useEffect(() => {
loadData(match.params.id, tweets)
}, [match.params.id, tweets])
Also, not sure what your useEffect is actually doing, as it's not assigning the result anywhere, but I'm going to assume it's just removed for code snippet clarity.
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!")
})
},