Pagination on API requests in Javascript/React? - javascript

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 = '&region=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.

Related

Only one item is added in state when adding multiple with multiple setState calls

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.

How to use a custom-hook that returns a value, inside another custom Hook?

I am using React-native and in it, I have a custom Hook called useUser that gets the user's information from AWS Amplify using the Auth.getUserInfro method, and then gets part of the returned object and sets a state variable with it. I also have another Hook called useData hook that fetches some data based on the userId and sets it to a state variable.
useUser custom-Hook:
import React, { useState, useEffect } from "react";
import { Auth } from "aws-amplify";
const getUserInfo = async () => {
try {
const userInfo = await Auth.currentUserInfo();
const userId = userInfo?.attributes?.sub;
return userId;
} catch (e) {
console.log("Failed to get the AuthUserId", e);
}
};
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
getUserInfo().then((userId) => {
setId(userId);
});
}, []);
return id;
};
export default useUserId;
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) = > { // code to fetch data from GraphQl}
const useData = () => {
const [data, setData] = useState();
useEffect(() => {
const userId = useUser();
fetchData(userId).then( // the rest of the code to set the state variable data.)
},[])
return data
}
When I try to do this I get an error telling me
*Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.*
I think the problem is that I am calling the Hook useUser inside of the use effect, but using it inside the function will cause the problem described here, and I can't use it outside the body of the fetchData since the useData itself is a hook, and it can be only used inside a functional component's or Hook's body. So I don't know how to find a way around this problem.
Correct, React hooks can only be called from React function components and other React hooks. The useEffect hook's callback isn't a React hook, it's a callback. According to the Rules of Hooks, don't call hooks inside loops, conditions, or nested functions.
I suggest refactoring the useData hook to consume the userId as an argument, to be used in the dependency array of the useEffect.
const fetchData = async (userId) => {
// code to fetch data from GraphQl
};
const useData = (userId) => {
const [data, setData] = useState();
useEffect(() => {
fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
});
}, [userId]);
return data;
};
Usage in Function component:
const userId = useUser();
const data = useData(userId);
If this is something that is commonly paired, abstract into a single hook:
const useGetUserData = () => {
const userId = useUser();
const data = useData(userId);
return data;
};
...
const data = useGetUserData();
Though you should probably just implement as a single hook as follows:
const useGetUserData = () => {
const [data, setData] = useState();
useEffect(() => {
getUserInfo()
.then(fetchData) // shortened (userId) => fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
setData(....);
});
}, []);
return data;
};
You can't call hook inside useEffect, Hook should be always inside componet body not inside inner function/hook body.
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) => {
// code to fetch data from GraphQl}
};
const useData = () => {
const [data, setData] = useState();
const userId = useUser();
useEffect(() => {
if (userId) {
fetchData(userId).then(setData);
}
}, [userId]);
return data;
};

React component not re-rendering on URL parameter change when using useEffect hook to fetch data

Here's the issue:
I have a component that is meant to be the same structure for ≈ 25 different items/pages. So, I do what anyone would when trying to use React, and I am passing dynamic URL parameters into my API request (pictured below).
const [{ items, isLoading, isError }] = useDataApi(
`http://localhost:5000/api/teams/topspending/${params.team}`,
[],
params.team);
This is simply using a useEffect component that has been separated for simplicity (pictured below).
const useDataApi = (initialUrl, initialData, effect) => {
console.log("start/top Data API");
const [items, setItems] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
setUrl(initialUrl);
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
console.log("data loading");
try {
console.log(url, "this should be the new Url");
const result = await axios(url, opts);
setItems(result.data.data);
} catch (error) {
setIsError(true);
}
console.log("data loaded...");
setIsLoading(false);
};
fetchData();
return () => abortCtrl.abort();
}, [effect]);
return [{ items, isLoading, isError }];
};
export default useDataApi;
The task is pretty simple. Upon clicking on a simple navigation link in the navbar to change the URL from http://localhost:5000/api/teams/topspending/Team1 to http://localhost:5000/api/teams/topspending/Team2 I am wishing that the SAME component will re-render after fetching NEW data with the UPDATED URL.
I have tried many things... and I can get the component to update after every URL change, BUT the data fetched is still the OLD data!
(I am using React-Router to route the single component to this link)
Ok, I think there are 2 little issues in your code.
Inside the parent function
This is my main function that is going to use your custom hook. If you see, I don't use interpolation because it is not going to be detected by your custom hook. That is why your initialUrl variable (Your URL) in your custom hook never change.
const App = () => {
const [id, setId] = React.useState(1);
const response = useDataApi(
'https://jsonplaceholder.typicode.com/posts/' + id,
[],
id,
);
return (
<>
<div>My id {id}</div>
<button onClick={() => setId(id + 1)}>Click Me!</button>
</>
);
};
Inside the custom hook
It seems to me that you are misunderstanding the setState function provided by react.
Remember that every time you call the setState function is not synchronous. I mean, if you use setUrl(initialUrl), then in the next line of code your state variable url will not necessarily have the values already updated. To know more about it, you can read this: https://reactjs.org/docs/faq-state.html#when-is-setstate-asynchronous
I would suggest using another variable to call the correct URL and change the variable names of your custom hook. I added some comments to your code //Note:
export const useDataApi = (initialUrl, initialData, effect) => {
console.log("start/top Data API", effect, initialUrl);
const [items, setItems] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
// Note: This not sync function
setUrl(initialUrl);
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
console.log("data loading");
try {
console.log(url, "this should be the new Url");
// Note: I changed this url variable of your state, to use your initialUrl variable. (this initialUrl parameter should have your UPDATED URL)
const result = await axios(initialUrl, opts);
setItems(result.data);
} catch (error) {
setIsError(true);
}
console.log("data loaded...");
setIsLoading(false);
};
fetchData();
return () => abortCtrl.abort();
}, [effect]);
return [{ items, isLoading, isError }];
};
I Hope, this can help you!.
setState is asynchronous, so there's no guarantee as to when it will be affected before the next render. There's multiple ways to rewrite your code to work more predictably, but with the examples you've provided the easiest solution is to remove the url state altogether and just use initalUrl in your call to axios.
This isn't great.
So another option would be to keep your url state and add a second useEffect that watches url.
eg.
const useDataApi = (initialUrl, initialData, effect) => {
console.log("start/top Data API");
const [items, setItems] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
setUrl(initialUrl);
}, [effect]);
useEffect(() => {
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
console.log("data loading");
try {
console.log(url, "this should be the new Url");
const result = await axios(url, opts);
setItems(result.data.data);
} catch (error) {
setIsError(true);
}
console.log("data loaded...");
setIsLoading(false);
};
fetchData();
return () => abortCtrl.abort();
}, [url])
return [{ items, isLoading, isError }];
};
export default useDataApi;
Still not great, but does what you're trying to do?

How to re-render component after update hash correctly with useEffect?

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!

Using hooks for a simple fetch request and breaking the rules of hooks, unsure how?

I'm trying to create a simple fetch with hooks from an AWS database. At the moment it errors out and the only reason I can see is because it breaks the rules of hooks but I'm not sure how. It's at the top level of this functional component and it's not called inside an event handler.
The result of this call (an array of user data), needs to be exported as a function and called in another file.
If anyone can spot something I have missed and can highlighted how I'm breaking the rules of hooks I'd be grateful!
Thanks!
const FetchUsers = () => {
const [hasError, setErrors] = useState(false);
const [Users, setUsers] = useState({});
async function fetchData() {
const res = await fetch(
"USERSDATABASE"
);
res
.json()
.then(res => setUsers(res))
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
}, []);
return Users;
};
export { FetchUsers };
consumed here....
class UsersManager {
constructor() {
this.mapUserCountries = {};
}
init() {
const mappedUsers = FetchUsers();
mappedUsers.forEach(user => {
const c = user.country;
if (!this.mapUserCountries[c])
this.mapUserCountries[c] = { nbUsers: 0, users: [] };
this.mapUserCountries[c].nbUsers++;
this.mapUserCountries[c].users.push(user);
});
}
getUsersPerCountry(country) {
return this.mapUserCountries[country];
}
}
export default new UsersManager();
The problem is that you are calling the FetchUsers inside a Class component, and the FetchUsers is executing a React Hook. This is not allowed by React.
First - Hooks don't work inside class based components.
Second - All custom hooks should start with use, in your case useFetchUsers. By setting use as prefix, react will track your hook for deps and calling in correct order and so on.

Categories

Resources