fetchMore causes page to re-render unexpectedly - reactjs

I have an infinite scroll list. I recently updated to the latest Apollo client and noticed infinite scroll no longer works.
Upon deeper investigation. I noticed when I call fetchmore with the incremented skip, it causes the entire page to re-render. Any ideas?
Query:
const {
data,
loading: queryLoading,
fetchMore,
error,
networkStatus
} = useQuery(SEARCH_PROFILES, {
variables: { ...searchParams, skip: skip.current },
fetchPolicy: "cache-first",
notifyOnNetworkStatusChange: true
});
FetchMore
const fetchData = () => {
ErrorHandler.setBreadcrumb("Fetch more profiles");
skip.current =
skip.current + parseInt(process.env.REACT_APP_SEARCHPROS_LIMIT);
const intLimit = parseInt(process.env.REACT_APP_SEARCHPROS_LIMIT);
if (hasMore.current) {
fetchMore({
variables: {
searchType,
long,
lat,
distance,
ageRange,
interestedIn,
skip: skip.current,
limit: intLimit,
isMobile: sessionStorage.getItem("isMobile")
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
hasMore.current = false;
return previousResult;
} else if (
fetchMoreResult.searchProfiles.profiles.length < intLimit
) {
hasMore.current = false;
}
const newData = produce(previousResult, (draftState) => {
if (draftState.searchProfiles.profiles) {
draftState.searchProfiles.profiles.push(
...fetchMoreResult.searchProfiles.profiles
);
} else {
draftState.searchProfiles.profiles =
fetchMoreResult.searchProfiles.profiles;
}
});
return newData;
}
});
}
};

Well, From your explanation, re-rendering is necessary since you're loading new content on while you scroll,
but for entire page not to be re-rendered is what we're concerned here... below tips might help.
extract the part of the page which needs to fetch data on scroll into a separate component, since it will be the only component which needs to be re-rendered
wrap your extracted component with React.memo() so it doesnt re-render when there is no change on data.
Make good use of life cycle hooks methods, they're the tools to manage on where or how to re-render

Related

Stop useEffect if a condition is met

I have a useEffect set up how I thought would only run once on initial render but it continues to rerun.
This breaks a function that is supposed to set a piece of state to true if a condition is truthy and show appropriate UI.
This sort of works but then the useEffect runs again flicks back to false immediately. I am also using a use effect to check on first render if the condition is truthy and show appropriate UI if so.
Basically when setIsPatched is true I don't want the useEffect to rerun because it flicks it back to false and breaks the UI
Here is the function:
const [isPatched, setIsPatched] = useState<boolean>(false);
useEffect(() => {
getApplied(x);
}, []);
const getApplied = (x: any) => {
console.log(x);
if (x.Log) {
setIsPatched(true);
return;
} else {
setIsPatched(false);
}
};
I also pass getApplied() to child component which passes a updated data to the function for use in this parent component:
const updatePatch = async (id: string) => {
//check if patch already in db
const content = await services.data.getContent(id);
const infoToUpdate = content?.data[0] as CN;
if (!infoToUpdate.applyLog && infoToUpdate.type == "1") {
// add applyLog property to indicate it is patched and put in DB
infoToUpdate.applyLog = [
{ clID: clID ?? "", usID: usID, appliedAt: Date.now() },
];
if (content) {
await services.data
.updateX(id, content, usId)
.then(() => {
if (mainRef.current) {
setDisabled(true);
}
});
}
getApplied(infoToUpdate);
} else {
console.log("retrying");
setTimeout(() => updatePatch(id), 1000); // retries after 1 second delay
}
};
updatePatch(id);
}

Redux Query Dependent Mutations With Rollback Using createApi()

I have two mutations that need to happen one after another if the first one succeeds. As bonus I would like to undo the first mutation if the second fails.
I have the first part working but it feels clumsy and I'm wondering if there is better way.
Here is how I bring in my two mutations:
const [updateSubscription, {
isLoading: isLoadingUpdateSubscription,
isSuccess: isSuccessUpdatedSubscription,
isError: isErrorUpdateSubscription,
error: errorUpdateSubscription,
reset: resetUpdateSubscription
}] = useUpdateSubscriptionMutation();
const [updateDeviceListing, {
isLoading: isLoadingUpdateDeviceListing,
isSuccess: isSuccessUpdatedDeviceListing,
isError: isErrorUpdateDeviceListing,
error: errorUpdateDeviceListing,
reset: resetUpdateDeviceListing
}] = useUpdateDeviceListingMutation();
As part of a button click I run the first mutation where a subscription is updated:
const handleListIt = () => {
if (deviceListing && subscriptions) {
const updatedAvailableSeats = subscriptions[0].availableSeats - deviceListing.count;
const updatedUsedSeats = subscriptions[0].usedSeats + deviceListing.count;
updateSubscription({...subscriptions[0], availableSeats: updatedAvailableSeats, usedSeats: updatedUsedSeats});
}
};
I then use useEffect() to check isSuccessUpdatedSubscription and run the second mutation:
useEffect(() => {
if (isSuccessUpdatedSubscription && deviceListing) {
updateDeviceListing({...deviceListing, status: 'open'})
}
if (isSuccessUpdatedDeviceListing) {
onClosed();
}
}, [isSuccessUpdatedDeviceListing, isSuccessUpdatedSubscription, deviceListing, onClosed, updateDeviceListing]);
The same useEffect() is also used to check if the second mutation worked, isSuccessUpdatedDeviceListing, at which point onClosed() is called and the user is shown some different UI.
You can just handle both in handleListIt, there is really no good reason for the re-render with the useEffect. And then you can also handle the rollback as you want.
const handleListIt = async () => {
if (deviceListing && subscriptions) {
const updatedAvailableSeats = subscriptions[0].availableSeats - deviceListing.count;
const updatedUsedSeats = subscriptions[0].usedSeats + deviceListing.count;
try {
await updateSubscription({...subscriptions[0], availableSeats: updatedAvailableSeats, usedSeats: updatedUsedSeats}).unwrap();
await updateDeviceListing({...deviceListing, status: 'open'}).unwrap()
} catch (error) {
// do your rollback
}
}
};

Firestore: calling collections.get() inside promise()

useEffect(() => {
if (!stop) {
// get current user profile
db.collection('events').get(eventId).then((doc) => {
doc.forEach((doc) => {
if (doc.exists) {
let temp = doc.data()
let tempDivisions = []
temp["id"] = doc.ref.id
doc.ref.collection('divisions').get().then((docs) => {
docs.forEach(doc => {
let temp = doc.data()
temp["ref"] = doc.ref.path
tempDivisions.push(temp)
});
})
temp['divisions'] = tempDivisions
setEvent(temp)
setStop(true)
// setLoading(false);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
<Redirect to="/page-not-found" />
}
})
})
}
}, [stop, eventId]);
I am curious if this is the properly way to extract nested data from Cloud Firestore.
Data model:
Collection(Events) -> Doc(A) -> Collection(Divisions) -> Docs(B, C, D, ...)
Pretty much I'm looking to get metadata from Doc(A), then get all the sub-collections which contain Docs(B, C, D, ...)
Current Problem: I am able to get meta data for Doc(A) and its subcollections(Divisions), but the front-end on renders metadata of Doc(A). Front-End doesn't RE-RENDER the sub-collections even though. However, react devtools show that subcollections(Divisions) are available in the state.
EDIT 2:
const [entries, setEntries] = useState([])
useEffect(() => {
let active = true
let temp = []
if (active) {
divisions.forEach((division) => {
let teams = []
let tempDivision = division
db.collection(`${division.ref}/teams`).get().then((docs) => {
docs.forEach((doc, index) => {
teams.push(doc.data())
})
tempDivision['teams'] = teams
})
setEntries(oldArray => [...oldArray, temp])
})
}
return () => {
active = false;
};
}, [divisions]);
is there any reason why this is not detecting new array and trigger a new state and render? From what I can see here, it should be updating and re-render.
Your inner query doc.ref.collection('divisions').get() doesn't do anything to force the current component to re-render. Simply pushing elements into an array isn't going to tell the component that it needs to render what's in that array.
You're going to have to use a state hook to tell the component to render again with new data, similar to what you're already doing with setEvent() and setStop().

useEffect and redux a cache trial not working

I have a rather complex setup and am new to React with Hooks and Redux.
My setup:
I have a component, which when first mounted, should fetch data. Later this data should be updated at a given interval but not too often.
I added useRef to avoid a cascade of calls when one store changes. Without useEffect is called for every possible change of the stores linked in its array.
The data is a list and rather complex to fetch, as I first have to map a name to an ID and
then fetch its value.
To avoid doing this over and over again for a given "cache time" I tried to implement a cache using redux.
The whole thing is wrapped inside a useEffect function.
I use different "stores" and "reducer" for different pieces.
Problem:
The cache is written but during the useEffect cycle, changes are not readable. Even processing the same ISIN once again the cache returns no HIT as it is empty.
Really complex implementation. It dawns on me, something is really messed up in my setup.
Research so far:
I know there are libs for caching redux, I do want to understand the system before using one.
Thunk and Saga seem to be something but - same as above plus - I do not get the concept and would love to have fewer dependencies.
Any help would be appreciated!
Component - useEffect
const calculateRef = useRef(true);
useEffect(() => {
if (calculateRef.current) {
calculateRef.current = false;
const fetchData = async (
dispatch: AppDispatch,
stocks: IStockArray,
transactions: ITransactionArray,
cache: ICache
): Promise<IDashboard> => {
const dashboardStock = aggregate(stocks, transactions);
// Fetch notation
const stockNotation = await Promise.all(
dashboardStock.stocks.map(async (stock) => {
const notationId = await getXETRANotation(
stock.isin,
cache,
dispatch
);
return {
isin: stock.isin,
notationId,
};
})
);
// Fetch quote
const stockQuote = await Promise.all(
stockNotation.map(async (stock) => {
const price = await getQuote(stock.notationId, cache, dispatch);
return {
isin: stock.isin,
notationId: stock.notationId,
price,
};
})
);
for (const s of dashboardStock.stocks) {
for (const q of stockQuote) {
if (s.isin === q.isin) {
s.notationId = q.notationId;
s.price = q.price;
// Calculate current price for stock
if (s.quantity !== undefined && s.price !== undefined) {
dashboardStock.totalCurrent += s.quantity * s.price;
}
}
}
}
dispatch({
type: DASHBOARD_PUT,
payload: dashboardStock,
});
return dashboardStock;
};
fetchData(dispatch, stocks, transactions, cache);
}
}, [dispatch, stocks, transactions, cache]);
Action - async fetch action with cache:
export const getXETRANotation = async (
isin: string,
cache: ICache,
dispatch: AppDispatch
): Promise<number> => {
Logger.debug(`getXETRANotation: ${isin}`);
if (CACHE_ENABLE) {
const cacheTimeExceeding = new Date().getTime() + CACHE_NOTATION;
if (
isin in cache.notation &&
cache.notation[isin].created < cacheTimeExceeding
) {
Logger.debug(
`getXETRANotation - CACHE HIT: ${isin} (${cache.notation[isin].created} < ${cacheTimeExceeding} current)`
);
return cache.notation[isin].notationId;
}
}
// FETCH FANCY AXIOS RESULT
const axiosRESULT = ...
if (CACHE_ENABLE) {
Logger.debug(`getXETRANotation - CACHE STORE: ${isin}: ${axiosRESULT}`);
dispatch({
type: CACHE_NOTATION_PUT,
payload: {
isin,
notationId: axiosRESULT,
created: new Date().getTime(),
},
});
}
//FANCY AXIOS RESULT
return axiosRESULT;
}

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

Resources