I have a component that displays list of recipes in my firestore database, I am using react-infinite-scroll component package.
Everything works, however once I click on one of the items it takes me to another component and when I go back with react router, useEffect starts calling for items on scroll again, even though all data is displayed already.
How Can I build a logic that once I return to the component it remembers where I left off?
const [mealSearchResults, setMealSearchResults] = useContext(
mealSearchResultsContext
);
const [latestMealDoc, setLatestMealDoc] = useContext(latestMealDocContext);
const getNextMeals = async () => {
const ref = db
.collection("meals")
.orderBy("timestamp")
.limit(6)
.startAfter(latestMealDoc || 0);
const data = await ref.get();
data.docs.forEach((doc) => {
const meal = doc.data();
setMealSearchResults((prev: any) => [...prev, meal]);
});
setLatestMealDoc((prev: any) => data.docs[data.docs.length - 1]);
};
useEffect(() => {
getNextMeals();
}, []);
Related
For learning purposes, I'm creating an e-shop, but I got stuck with localStorage, useEffect, and React context. Basically, I have a product catalog with a button for every item there that should add a product to the cart.
It also creates an object in localStorage with that item's id and amount, which you select when adding the product to the cart.
My context file:
import * as React from 'react';
const CartContext = React.createContext();
export const CartProvider = ({ children }) => {
const [cartProducts, setCartProducts] = React.useState([]);
const handleAddtoCart = React.useCallback((product) => {
setCartProducts([...cartProducts, product]);
localStorage.setItem('cartProductsObj', JSON.stringify([...cartProducts, product]));
}, [cartProducts]);
const cartContextValue = React.useMemo(() => ({
cartProducts,
addToCart: handleAddtoCart, // addToCart is added to the button which adds the product to the cart
}), [cartProducts, handleAddtoCart]);
return (
<CartContext.Provider value={cartContextValue}>{children}</CartContext.Provider>
);
};
export default CartContext;
When multiple products are added, then they're correctly displayed in localStorage. I tried to log the cartProducts in the console after adding multiple, but then only the most recent one is logged, even though there are multiple in localStorage.
My component where I'm facing the issue:
const CartProduct = () => {
const { cartProducts: cartProductsData } = React.useContext(CartContext);
const [cartProducts, setCartProducts] = React.useState([]);
React.useEffect(() => {
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
}
}
)();
}, []);
console.log('cartProducts', cartProducts);
return (
<>
<pre>
{JSON.stringify(cartProductsData, null, 4)}
</pre>
</>
);
};
export default CartProduct;
My service file with fetchProductById function:
const domain = 'http://localhost:8000';
const databaseCollection = 'api/products';
const relationsParams = 'joinBy=categoryId&joinBy=typeId';
const fetchProductById = async (id) => {
const response = await fetch(`${domain}/${databaseCollection}/${id}?${relationsParams}`);
const product = await response.json();
return product;
};
const ProductService = {
fetchProductById,
};
export default ProductService;
As of now I just want to see all the products that I added to the cart in the console, but I can only see the most recent one. Can anyone see my mistake? Or maybe there's something that I missed?
This looks bad:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
You run a loop, but cartProducts has the same value in every iteration
Either do this:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...cartProducts, fetchedProduct]);
});
Or this:
const values = Promise.all(Object.values(retrievedProducts).map(x => ProductService.fetchProductById(x.id)));
setCartProducts(values)
The last is better because it makes less state updates
Print the cartProducts inside useEffect to see if you see all the data
useEffect(() => {
console.log('cartProducts', cartProducts);
}, [cartProducts]);
if this line its returning corrects values
const productsObj = localStorage.getItem('cartProductsObj');
then the wrong will be in the if conditional: replace with
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetched = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...fetched, fetchedProduct]);
});
}
}
Issue
When you call a state setter multiple times in a loop for example like in your case, React uses what's called Automatic Batching, and hence only the last call of a given state setter called multiple times apply.
Solution
In your useEffect in CartProduct component, call setCartProducts giving it a function updater, like so:
setCartProducts(prevCartProducts => [...prevCartProducts, fetchedProduct]);
The function updater gets always the recent state even though React has not re-rendered. React documentation says:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
I am trying to implement "infinite scroll" in my react app in which I fetch all data at once and as user scrolls down the page it displays more and more data. For that I use Intersection Observer in my custom hook with threshold of 1 to detect when user scrolls to end of "section" element so that I then can display more data. The problem is that after initial data is rendered my Intersection observer doesn't fire anymore as if it's disconnected but it's not.
Here is my custom hook:
import {useCallback, useEffect, useState} from "react";
export function useInfinityScrollObserver(ref) {
let [isVisible, setIsVisible] = useState(false);
const handleIntersection = useCallback(([entry]) => {
if (entry.isIntersecting){
setIsVisible(true)
}else if (!entry.isIntersecting){
setIsVisible(false)
}
}, [])
useEffect(() => {
const options = {
threshold: 1
}
// Create the observer, passing in the callback
const observer = new IntersectionObserver(handleIntersection, options);
// If we have a ref value, start observing it
if (ref.current) {
observer.observe(ref.current);
}
// If unmounting, disconnect the observer
return () => {
observer.unobserve(ref.current)
observer.disconnect();
}
}, [handleIntersection]);
return isVisible;
}
And here is component where I fetched data and I wanna display more data when user scrolls to the end of the "section" element:
const CountriesSection = () => {
let [data ,setData] = useState(null)
let [loadedCountries, setLoadedCountries] = useState([])
let [loadedCountriesNum, setLoadedCountriesNum] = useState(0)
const ref = useRef(null);
const isVisible = useInfinityScrollObserver(ref) // set hook to watch "section" ref
// Fetch all data at once
useEffect(() => {
async function fetchData() {
const response = await fetch(`https://disease.sh/v3/covid-19/countries?sort=cases`)
const countries = await response.json()
setData(countries)
}
fetchData()
}, [])
// When initial data is fetched or when number of Countries we wanna display changes fire this useEffect
useEffect(() => {
if (data){
const nextCountriesToShow = data.slice(loadedCountriesNum, loadedCountriesNum + 20)
.map(country => <CountryCard key={country.countryInfo._id}
country={country.country}
flag={country.countryInfo.flag}
continent={country.continent}
infected={country.cases}
recovered={country.recovered}
deaths={country.deaths} />)
setLoadedCountries(previousCountries => {
return [...previousCountries, ...nextCountriesToShow]
})
}
}, [data, loadedCountriesNum])
// When Intersection Observer in custom hook changes its state fire this effect. Which means when user scrolls down to the end of "section" element
useEffect(() => {
if (isVisible){
setLoadedCountriesNum(previousNum => {
return previousNum + 20
})
}
}, [isVisible])
return (
<section ref={ref} className={styles['section-countries']}>
{loadedCountries}
</section>
)
}
export default CountriesSection;
One more thing I noticed is when I change threshold inside of my custom hook from 1 to 0, then additional data is rendered each time when section enters my viewport.
I have a codes below, my poblem is the dispatch is fetching the previous userId paramater.
The flow is I go first to the users-list, and then go to user-info (displays right), but when I go back to users-list then go back to user-info (it does not display the right userId, instead the previous one).
import { fetchUserInfo } from '../../redux/users/slice';
const UserInfo = () => {
const usersId = useParams().id;
useEffect(() => {
console.log('->->userId', userId); // it logs exact id
dispatch(fetchUserInfo(usersId)).then((res) => { // it fetch previous id
// some codes here
console.log('fetchUser', res.data);
});
}, []);
}
Updated: I just figured out that it fetch correctly, its just the Content component is not updating.
In my Content.js component, I use useSelector to display slice state.
import { useSelector } from 'react-redux';
const Content = () => {
const { userDetails } = useSelector((state) => state.users);
return (
<div className="bg-basic-400 m-px-10 p-px-16">
<p>{userDetails.title}</p>
//more codes here
</div>
)
}
You need to add any relevant variable as a dependency in useEffect. If you use an empty [] it only runs once. you need to save userId in state.
const [usersId, setUsersId] = useState(useParams().id);
useEffect(() => {
console.log('->->usersId', userId); // it logs exact id
dispatch(fetchUserInfo(usersId)).then((res) => { // it fetch previous id
// some codes here
});
}, [usersId, fetchUserInfo]);
I have a component that uses action and repository to request data from rails server and then render it in component, it returns promise, that I've handle and render:
const {loadNews} = props;
const [news, setNews] = useState([]);
const [newsState, setNewsState] = useState('active');
const [pageNum, setPageNum] = useState(1);
where {loadNews} - function, that imports from parent component; Others are state, where:
news - array with news, that map's in different component in render;
newsState- news field state (e.g. 'active', or 'past');
pageNum- pagination state for adding more news in component;
I'm use he next code for update state:
const locationHashChanged = () => {
if (window.location.hash === "#state-active") {
setNewsState('active');
setPageNum(pageNum);
}
else if (window.location.hash == "#state-played") {
setNewsState('played');
setPageNum(pageNum);
}
}
window.onhashchange = locationHashChanged;
const changeHash = () => {
setNews([]);
setNewsState('active');
setPageNum(1)
loadNews({pageNum, newsState});
};
const incrementPage = () => {
setPageNum(pageNum + 1);
loadNews({pageNum, newsState});
};
useEffect(() => {
locationHashChanged();
loadNews({pageNum, newsState})
.then((response) => {
const {data} = response;
setNews(news.concat(data));
})
.catch((response) => {
console.log(response);
});
}, [pageNum, newsState]);
If I just use incrementPage function - it works fine - it just add more news with previous array of news and update state. But, I see that state is updated, but array is not.
Expected behaviour - when I click on link in Header component (external component), that changes hash in this component ('active' or 'past' fields) and these news should reload with correct fields(active or past). But now I see no changes. How can I fix it? Thanks!
I have an app that fetches data from a movie API. It returns 20 items from page 1.
How would I go about adding the ability for pagination and allowing user to click a button that increases the page number value and returns the items from that page?
Here's my API call:
export const API_KEY = process.env.REACT_APP_MOVIES_API;
export const baseURL = 'https://api.themoviedb.org/3/movie/';
export const language = '&language=en';
export const region = '®ion=gb';
export const currentPage = 1;
export const fetchTopRatedMovies = async () => {
try {
const response = await fetch(
`${baseURL}top_rated?api_key=${API_KEY}${language}&page=${currentPage}${region}`
);
const data = await response.json();
console.log('TOP RATED', data);
return data;
} catch (err) {
console.log(err);
}
};
I'm thinking I need to add 1 to currentPage on request however I'm unsure how to set this up.
The function is called using useEffect in React in a functional component.
const [apiData, setApiData] = useState([]);
const [isLoading, setLoading] = useState(false);
const { results = [] } = apiData;
useEffect(() => {
setLoading(true);
fetchTopRatedMovies().then((data) => setApiData(data));
setLoading(false);
}, []);
You need to make currentPage a param of fetchTopRatedMovies, so that you can pass a dynamic value from your functional component.
You should control the current viewed page in the state of your functional component like this:
const [currentPage, setCurrentPage] = useState(1);
Then you can add a button to the rendering of the functional component that in the onClick handler sets the new value of currentPage and triggers the API call. Approximately like this:
<MyButton onClick={() => {
setCurrentPage(currentPage + 1);
fetchTopRatedMovies(currentPage).then(data => setApiData(data));
}}>
I say approximately because instead of doing immediately the call to fetchTopRatedMovies you could leverage useEffect to re-run the API request on each state / prop change. Or even better, trigger the API request using useEffect only when there's a meaningful state / prop change.
The fetchTopRatedMovies method should be improved like this:
export const fetchTopRatedMovies = async (pageNumber) => {
try {
const response = await fetch(
`${baseURL}top_rated?api_key=${API_KEY}${language}&page=${pageNumber}${region}`
);
const data = await response.json();
console.log('TOP RATED', data);
return data;
} catch (err) {
console.log(err);
}
};
This approach can be extended to all the other params of your API call.
Hope this helps!
Usually it's made by adding currentPage to the state, like
const [currentPage, setCurrentPage ] = useState(1);
So when you want to change it, via click or scroll, use setCurrentPage and in your api call it'll still use currentPage but now it'll reference the one in the state.