React.js: How to create loadMore feature onClick? - javascript

I am trying to implement a small app with loadMore button.
By default, it should output 3 data, and show 3 more data each time the user clicks the load more button.
Trying multiple ways I came up with this solution suggested by this article.
Here is the codesandox link
App.js
let arrayForHoldingStories = [];
const [topStoryIds] = useStories();
const [storiesToShow, setStoriesToShow] = useState([]);
const [next, setNext] = useState(3);
const loopWithSlice = (start, end) => {
const slicedStories = topStoryIds.slice(start, end);
arrayForHoldingStories = [...arrayForHoldingStories, ...slicedStories];
setStoriesToShow([...arrayForHoldingStories, ...slicedStories]);
};
useEffect(() => {
loopWithSlice(0, storiesPerPage);
}, [topStoryIds]);
const handleShowMorePosts = () => {
let loadedMore = next + storiesPerPage;
loopWithSlice(next, loadedMore);
setNext(next + storiesPerPage);
};
But the problem is that merging the initial array and the new array, they have same elements and it throws an error
Warning: Encountered two children with the same key, `26991887`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
I tried to remove duplicates from both arrays via forEach or set functions before merging but didn't work either.
My try
const loopWithSlice = (start, end) => {
const slicedStories = topStoryIds.slice(start, end);
arrayForHoldingStories = [...arrayForHoldingStories, ...slicedStories];
setStoriesToShow([...arrayForHoldingStories, ...slicedStories]); //it throws mentioned error
/* slicedStories.forEach((uniqueId) => { //this will also throw same error
arrayForHoldingStories.forEach((addedId) => {
if (uniqueId !== addedId) {
setStoriesToShow([...slicedStories, ...arrayForHoldingStories]);
}
});
}); */
/* setStoriesToShow([ //this will also throw same error
...new Set([...arrayForHoldingStories, ...slicedStories])
]); */
};
Any help will be appreciated.

Per your description, you don't need to have an end or start arguments to your loop with slice function. You can use the length of the storiesToShow state
import React, { useState, useEffect } from "react";
import Stories from "./Stories";
import useStories from "./hooks/useStories";
function App() {
const storiesPerPage = 3;
let arrayForHoldingStories = [];
const [topStoryIds] = useStories();
const [storiesToShow, setStoriesToShow] = useState([]);
const [next, setNext] = useState(3);
const loopWithSlice = () => {
const toShow = topStoryIds.slice(
storiesToShow.length,
storiesToShow.length + storiesPerPage
);
setStoriesToShow([...storiesToShow, ...toShow]);
};
useEffect(() => {
if (topStoryIds) {
loopWithSlice();
}
}, [topStoryIds]);
const handleShowMorePosts = () => {
let loadedMore = next + storiesPerPage;
loopWithSlice(next, loadedMore);
setNext(next + storiesPerPage);
};
console.log("next", next);
console.log("storiesPerPage", storiesPerPage);
console.log("loopWithSlice", loopWithSlice);
console.log("arrayForHoldingStories", arrayForHoldingStories);
console.log("storiesToShow", storiesToShow);
return (
<>
<div>
{storiesToShow.length > 0 &&
storiesToShow.map((storyId) => (
<Stories storyId={storyId} key={storyId} />
))}
</div>
<button onClick={handleShowMorePosts}>load more</button>
</>
);
}
export default App;
You can check the sanbox here for the full code.

Related

React pushing to array duplicates it instead of adding

Can anybody help me with this issue? I have a state called "filteredPokemon" which fetches a list of pokemon based on some things, then I pass it to this function called PokemonDisplayArea where I proceed to display the list of pokemon. However, when I change offset I expect the behavior to ADD onto the previous state of "Cards" which are the element rendering the HTML, however instead, it grabs the newest filteredpokemon and appends it twice. Any help would be appreciated!
Here is a video
https://clipchamp.com/watch/EcMJbOMjOaL
The code:
function PokemonDisplayArea({pokemons}) {
const [filteredPokemon, setFilteredPokemon] = useState([]);
const [offset, setOffset] = useState(20);
const [cards, setCards] = useState([]);
useEffect(() => {
let cardsSkele = [];
if (filteredPokemon.length > 0) {
for (let i = 0; i < filteredPokemon.length; i++) {
if (undefined !== filteredPokemon[i].name) {
cardsSkele.push(<PokemonCard key={filteredPokemon[i].id} name={filteredPokemon[i].name}></PokemonCard>);
cardsSkele.sort((a, b) => a.key - b.key)
}
}
setCards(prevArray => [...prevArray, ...cardsSkele]);
}
}, [filteredPokemon])
// SEARCH POKEMON RESULTS
// FILTER POKEMON RESULTS
return (
<div className="pokemon__display-area">
<GetFilteredPokemon pokemons={pokemons} amount={20} offset={offset} filteredPokemon={filteredPokemon} setFilteredPokemon={setFilteredPokemon}></GetFilteredPokemon>
{cards}
</div>
)
}
Any help would be appreciated, I am a beginner, thanks!
I tried passing different states into useEffect, and tried console logging the data but the data seems to change fine, and the list just duplicates.
For starters, I would just store data in the state rather than components itself as it easy to reason about and transform.
Iterate over the cards and update the data for existing items or add the new item. Something along these lines.
function getUpdatedCardsData(prevCards, filteredPokemon) {
// get list of all the ids for the filtered pokemons
const filteredPokemonIds = filteredPokemon.map(fp => fp.id);
// This will combine the data for existing cards that are part of
// filteredPokemon and maintain a map of ids that are new and need to be added to cards
const updatedCards = prevCards.reduce((memo, card) => {
const cardId = card.id;
const isNewCard = !filteredPokemonIds.includes(cardId);
// add it to the map if new
if(isNewCard) {
memo.newCardIds.push(cardId);
} else {
// find the card that is in filtered pokemon
const found = filteredPokemon.find(fp => fp.id === cardId);
if(found) {
const updatedCard = {
...card,
...found
};
memo.updatedCards.push(updatedCard);
}
}
return memo;
}, {
updatedCards: [],
newCardIds: []
});
}
function getNewCards(newCardIds, filteredPokemon) {
// List of the new cards
const newCards = newCardIds.reduce((memo, newCardId) => {
const found = filteredPokemon.find(fp => fp.id === newCardId);
if(found) {
memo.push(found);
}
return memo;
}, []);
}
function PokemonDisplayArea({pokemons}) {
const [filteredPokemon, setFilteredPokemon] = useState([]);
const [offset, setOffset] = useState(20);
const [cards, setCards] = useState([]);
useEffect(() => {
if(filteredPokemon) {
setCards(prevCards => {
const {
updatedCards,
newCardIds
} = getUpdatedCardsData(prevCards, filteredPokemon);
const newCards = getNewCards(newCardIds, filteredPokemon);
// add updated and new and sort them
const sortedCards = [...updatedCards, ...newCards].sort((a, b) => a.id - b.id);
return sortedCards;
});
}
}, [filteredPokemon])
// SEARCH POKEMON RESULTS
// FILTER POKEMON RESULTS
return (
<div className="pokemon__display-area">
<GetFilteredPokemon
pokemons={pokemons}
amount={20}
offset={offset}
filteredPokemon={filteredPokemon}
setFilteredPokemon={setFilteredPokemon}
/>
{cards.map((card, i) => {
const pokemon = card[i];
return (
<PokemonCard
key={pokemon.id}
name={pokemon.name}
/>
})}
</div>
)
}

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.

Wait Until Data is Fetched from MongoDB React JS

I'm creating a Quiz App. This is the Quiz Page Code. Quiz DB Document contains a QuizQuestions Array which has question ids. Then I fetch specific question from MCQ/Question DB. Fetching MCQ takes time and when I console.log fetched data. First and second time data in undefined then its viewable. Due to this I'm unable to display it as it cause TypeError: Cannot read properties of undefined mcqOptions How can I fix this?
`
import React, { useEffect, useState } from "react";
import shuffleMcq from "../components/shuffleMcq";
const QuizTakePage = ({ match }) => {
const reqUrl = match.params.path; //Getting Quiz ID From URL
const [quizInfo, setQuizInfo] = useState({
"q_title":"",
"q_seo_description":"",
"q_questions":[],
"q_tags":[],
"m_subject":"", })
//const fetchQuizData = fetch();
useEffect(() => {
const fetchQuizData = async () => {
const reqApiUrl = '/quiz/api/qid/'+reqUrl;
const fetchedApiResult = await fetch(reqApiUrl);
const resultJson = await fetchedApiResult.json();
setQuizInfo(resultJson);
}
fetchQuizData(); }, []);
// Quiz Data START
const quizTitle = quizInfo.q_title;
const quizDesc = quizInfo.q_seo_description;
const quizQuestions = quizInfo.q_questions;
// Quiz Data END
// MCQ Data START
const [currentQuestion, setCurrentQuestion] = useState(0);
const requestedQuestion = quizQuestions[currentQuestion];
const [mcqInfo, setMcqInfo] = useState({
"m_title":"",
"m_question":"",
"m_alternatives":[],
"m_language":"", })
useEffect(() => {
const fetchApiResponse = async () => {
const reqApiUrl = '/mcq/api/mid/'+requestedQuestion;
const fetchedApiResult = await fetch(reqApiUrl);
const resultJson = await fetchedApiResult.json();
setMcqInfo(resultJson);
}
fetchApiResponse(); }, [requestedQuestion]);
//const mcqLanguage = mcqInfo.m_language;
const mcqQuestion = mcqInfo.m_question;
const mcqOptions = mcqInfo.m_alternatives;
console.log(mcqOptions);
return (
<>
<h1>{quizTitle}</h1>
<p>{quizDesc}</p>
</>
);
};
export default QuizTakePage;
The reason you're getting undefined for the first and second time is due to the fact that useEffect would've not been executed by then. useEffect runs when the component is rendered and mounted for the first time, and then subsequent executions are made when there is a change in dependency array (If there are any dependencies).
You could get rid of the error by rendering the dynamic content conditionally, i.e, displaying it when the data has been fetched.
return (
<>
<h1>{quizTitle.length>0 ? quizTitle : "Loading Question"}</h1>
<p>{quizDesc.length>0 ? quizDesc: "Loading Description"}</p>
<ul>
{mcqOptions && mcqOptions.length>0 && mcqOptions.map(option=>{
return(<li key={Math.random()}>{option}</li>) //Using Math.random() for key to ensure all the mapped items have an unique key
}
}
</ul>
</>
);
Alternatively, if your mcqOptions is an array of objects you can map it accordingly, for instance, something like this,
<ul>
{mcqOptions && mcqOptions.length>0 && mcqOptions.map(mcqOption=>{
return(<li key={mcqOption.id}>{mcqOption.text}</li>) //Use the properties accordingly, this is an example only.
}
}
</ul>

How to trigger a function automatically when the data in an array changes?

i just started React js and im trying to create a simple recipe web with API.
I am trying to create a page that will display the data of favorite recipes from an array in local storage using map() like below.
const FavRecipes = () => {
const [recipeArray, setRecipeArray] = useState([]);
const refreshData = () => {
const existedFavRecipe = localStorage.getItem("FavRecipes");
const data = existedFavRecipe !== null ? JSON.parse(existedFavRecipe) : [];
setRecipeArray(data);
}
return (
<FavRecipesContainer>
{recipeArray.map( e => (
<>
<FavRecipeImage src ={e.image} />
<FavRecipeTitle>{e.title}</FavRecipeTitle>
</>
))}
</FavRecipesContainer>
)
}
The problem is I want the function of refreshData to get triggered automatically everytime the data in the array changes because i will create a delete button that can delete the favorite recipes. I am thinking of using useEffect() but I dont know how to do it. Is there any suggestion to solve this? Would appreciate it!
I want the function of refreshData to get triggered automatically everytime the data in the array changes because i will create a delete button that can delete the favorite recipes..
The problem is that there is no event that fires when local storage is changed by other code in the same window (the storage event only fires when storage is changed in other windows).
There are dodgy solutions like these, but really just make sure that your deletion code calls refreshData as part of its logic. Or actually, you don't even need refreshData, you could maintain the array locally in the component and just echo it to local storage:
const FavRecipes = () => {
const [recipeArray, setRecipeArray] = useState([]);
// Initial data load
useEffect(() => {
const existedFavRecipe = localStorage.getItem("FavRecipes");
const data = existedFavRecipe !== null ? JSON.parse(existedFavRecipe) : [];
setRecipeArray(data);
}, []);
// Deletion
const deleteRecipe = (recipe) => {
setRecipeArray(recipes => {
recipes = recipes.filter(r => r !== recipe);
localStorage.setItem("FavRecipes", JSON.stringify(recipes));
return recipes;
});
};
return (
<FavRecipesContainer>
{recipeArray.map( recipe => (
<>
<FavRecipeImage src ={recipe.image} />
<FavRecipeTitle>{recipe.title}</FavRecipeTitle>
<button onClick={() => deleteRecipe(recipe)}>X</button>
</>
))}
</FavRecipesContainer>
);
};
If you also want to listen for changes in other windows (users do that):
const FavRecipesKey = "FavRecipes";
const FavRecipes = () => {
const [recipeArray, setRecipeArray] = useState([]);
// Initial data load and watch for changes in other windows
useEffect(() => {
function refreshData() {
const existedFavRecipe = localStorage.getItem(FavRecipesKey);
const data = existedFavRecipe !== null ? JSON.parse(existedFavRecipe) : [];
setRecipeArray(data);
}
function storageEventHandler({key}) {
if (key === FavRecipesKey) {
refreshData();
}
}
refreshData();
window.addEventListener("storage", storageEventHandler);
return () => {
window.removeEventListener("storage", storageEventHandler);
};
}, []);
// Deletion
const deleteRecipe = (recipe) => {
setRecipeArray(recipes => {
recipes = recipes.filter(r => r !== recipe);
localStorage.setItem(FavRecipesKey, JSON.stringify(recipes));
return recipes;
});
};
return (
<FavRecipesContainer>
{recipeArray.map( recipe => (
<>
<FavRecipeImage src ={recipe.image} />
<FavRecipeTitle>{recipe.title}</FavRecipeTitle>
<button onClick={() => deleteRecipe(recipe)}>X</button>
</>
))}
</FavRecipesContainer>
);
};
This is a tough one. The closest one in React camp is useMutableSource. https://github.com/reactjs/rfcs/blob/main/text/0147-use-mutable-source.md
However useMutableSource is a bit too advanced. So maybe we should think of the problem in another way. For instance, if you can know the time or component who invokes localStorage.setItem, then you can turn it in a context.
Define a context
Create a file that you can share to other components.
const RecipeContext = React.createContext()
export default RecipeContext
Import it to set it
When you want to set the content, import the context and write it via current.
import RecipeContext from './RecipeContext'
const AComponent = () => {
const recipe = React.useContext(RecipeContext)
recipe.current = recipeArray
}
Import it to read it
When you want to read out the current value, import the context and read it via current.
import RecipeContext from './RecipeContext'
const BComponent = () => {
const recipe = React.useContext(RecipeContext)
const onClick = () => {
console.log(recipe.current)
}
}
You should be able to use RecipeContext as a "global" variable similar to localStorage. Even better if you have any default value, you can set it at the time you create it.
const RecipeContext = React.createContext(defaultRecipeArray)
You don't even need a provider <RecipeContext.Provider />, because you are using it as a "global" context, a very special usage.

React Hooks setState of an element in an array

How can I update a single element in a state array? Here is the code that I am currently using:
const Cars = props => {
const [cars, setCars] = React.useState(["Honda","Toyota","Dodge"])
const handleClick1 = () => { setCars[0]("Jeep") }
const handleClick2 = () => { setCars[1]("Jeep") }
const handleClick3 = () => { setCars[2]("Jeep") }
return (
<div>
<button onClick={handleClick1}>{cars[0]}</button>
<button onClick={handleClick2}>{cars[1]}</button>
<button onClick={handleClick3}>{cars[2]}</button>
</div>
)
};
When I click one of the rendered buttons, I get Uncaught TypeError: setCars[0] is not a function at handleClick1.
I know how to do this in a React Class, but how can I do this with React Hooks?
I suggest you map through your cars in order to render them - this is just overall a million times easier. From there you can apply an onClick handler to each button..
Furthermore, you should not mutate state like you are - always make a copy of state first, update the copy, then set your new state with the updated copy.
Edit: one thing that slipped my mind before was adding a key to each item when you are mapping over an array. This should be standard practice.
const { useState } = React;
const { render } = ReactDOM;
const Cars = props => {
const [cars, setCars] = useState(["Honda", "Toyota", "Dodge"]);
const updateCars = (value, index) => () => {
let carsCopy = [...cars];
carsCopy[index] = value;
setCars(carsCopy);
};
return (
<div>
{cars && cars.map((c, i) =>
<button key={`${i}_${c}`} onClick={updateCars("Jeep", i)}>{c}</button>
)}
<pre>{cars && JSON.stringify(cars, null, 2)}</pre>
</div>
);
};
render(<Cars />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>
I think you should correct these lines to spot the source of error
const handleClick1 = () => { setCars[0]("Jeep") }
into
const handleClick1 = () => { cars[0]="Jeep"; setCars(cars); }

Categories

Resources