How to use dispatch dynamically in React? - javascript

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]);

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.

why useEffect is called infinitely

In useEffect in my react component I get data and I update a state, but I don't know why the useEffect is always executed:
const Comp1 = () => {
const [studies, setStudies]= useState([]);
React.useEffect( async()=>{
await axios.get('/api/expert/',
)
.then((response) => {
setStudies(response.data.studies);
}, (error) => {
console.log(error );
})
console.log("called+ "+ studies);
},[studies]);
return(
<Comp2 studies={studies}/>
)
}
Here is my second Component used in the first component...
const Comp2 = (props) => {
const [studies, setStudies]= useState([]);
React.useEffect( ()=>{
setStudies(props.studies)
},[props.studies, studies]);
return(
studies.map((study)=>{console.log(study)})
}
EDIT
const Comp2 = (props) => {
// for some brief time props.studies will be an empty array, []
// you need to decide what to do while props.studies is empty.
// you can show some loading message, show some loading status,
// show an empty list, do whatever you want to indicate
// progress, dont anxious out your users
return (
props.studies.map((study)=>{console.log(study)}
)
}
You useEffect hook depends on the updates that the state studies receive. Inside this useEffect hook you update studies. Can you see that the useEffect triggers itself?
A updates B. A runs whenever B is updated. (goes on forever)
How I'd do it?
const Comp1 = () => {
const [studies, setStudies]= useState([]);
React.useEffect(() => {
const asyncCall = async () => {
await axios.get('/api/expert/',
)
.then((response) => {
setStudies(response.data.studies);
}, (error) => {
console.log(error );
})
console.log("called+ "+ studies);
}
asyncCall();
}, []);
return(
<Comp2 studies={studies}/>
)
}
useEffect() has dependency array which causes it to execute if any value within it updates. Here, setStudies updates studies which is provided as dependency array and causes it to run again and so on. To prevent this, remove studies from the dependency array.
Refer: How the useEffect Hook Works (with Examples)

useEffect not triggering when clicking on dependency

I got a button that triggers data fetching from fetchDataHandler, that works with useEffect, it fetch data on page load also. But then when I add addMovieHandler as dependency to useEffect to auto-fetch data when I add another movie, it just doesn't work. So I figured out I may be using it wrong, but cant figure it out.
const App = () => {
const [movies, setMovies] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const fetchDataHandler = useCallback(async () => {
setIsLoading(true);
const respone = await fetch(
"https://react-http-3af47-default-rtdb.firebaseio.com/movies.json"
);
if (!respone.ok) {
setIsLoading(false);
throw new Error("Something went wrong!");
}
const data = await respone.json();
const movieList = [];
for (const key in data) {
movieList.push({
id: key,
title: data[key].title,
text: data[key].text,
});
}
setMovies(movieList);
setIsLoading(false);
}, []);
const addMovieHandler = useCallback(async (movie) => {
const respone = await fetch(
"https://react-http-3af47-default-rtdb.firebaseio.com/movies.json",
{
method: "POST",
body: JSON.stringify(movie),
headers: {
"Content-Type": "application/json",
},
}
);
const data = await respone.json();
console.log(data);
}, []);
useEffect(() => {
fetchDataHandler();
}, [fetchDataHandler, addMovieHandler]);
let content = <p>no movies found</p>;
if (movies.length > 0) {
content = <MovieList movies={movies} />;
}
if (isLoading) {
content = <p>loading...</p>;
}
return (
<React.Fragment>
<UserInput onAddMovie={addMovieHandler}></UserInput>
<Card>
<button onClick={fetchDataHandler}>Fetch movies</button>
</Card>
{content}
</React.Fragment>
);
};
export default App;
Try this instead:
Remove addMovieHandler from useEffect. When you hit addMovieHandler it is fetching the data anyway. So what you could to is set the movies with the latest response in the handler itself like so:
setMovies(...data)
This will replace the movie list with the newly fetched movie list. If you want to add on the old list you could do this:
setMovies([...movies, data])
This is concatenate the new fetched list to the already existing movie list and update the variable and re-render the movie list section as well.
Your useEffect is not triggering on addMovieHandler reference change because you're using useCallback with no dependencies ([] second parameter) to memoize it. This means the addMovieHandler reference won't change at all between rerenders but you're basing your useEffect calls on it doing so. In simple words - useEffect won't rerun if the provided dependencies have the same values (which is the case in your solution).
Same goes for fetchDataHandler.
In any case, this is not a good solution to the problem. You should be better-off updating your movies array on successful movie addition in the addMovieHandler itself. Same goes for other operations like delete and update.

How to fix infinite scroll logic after changing components?

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();
}, []);

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!

Categories

Resources