React Infinite scroll, how to not re-render previous items - javascript

I am new to React so I was wondering. I am creating this component which contains array of 200+ items, and I don't want to render those items immediately, that's why I am using Infinite Scroll feature, so that when user nears the end of list new items are rendered. But when that happens because of state changes previous items are also re-rendered, because new array of objects is created. Is there some way to optimize this so only new items are rendered and the previous one remain the same?
This is my Component which uses infinity scroll and displays items:
import styles from "./CountriesSection.module.css";
import { useEffect, useMemo, useRef, useState } from "react";
import { useInfinityScroll } from "../hooks/useInfinityScroll";
import CountriesList from "./CountriesList";
const data = new Array(240).fill({});
const CountriesSection = () => {
const [currentItemsDisplayed, setCurrentItemsDisplayed] = useState(40);
const componentsToShow = useMemo(() => {
return data.slice(0, currentItemsDisplayed);
}, [currentItemsDisplayed]);
const divRef = useRef(null);
let isVisible = useInfinityScroll(divRef);
useEffect(() => {
if (!isVisible) return;
setCurrentItemsDisplayed((prevVal) => prevVal + 20);
}, [isVisible]);
return (
<>
<div className={styles["section-countries"]}>
<CountriesList countries={componentsToShow} />
</div>
<div className={styles.infinity} ref={divRef} />
</>
);
};
export default CountriesSection;
Country Card:
import CountryCard from "./CountryCard";
import React from "react";
const CountriesList = ({ countries }) => {
const countryToCard = (country, id) => <CountryCard key={id} />;
return countries.map(countryToCard);
};
export default React.memo(CountriesList);
Codesandbox here: https://codesandbox.io/s/gifted-mclaren-qhn4po?file=/src/components/CountriesList.jsx:0-263

You should use React.memo for CountryCard component. codesandbox
If you want to optimize a component(CountryCard) and memoize the result, you wrap it(CountryCard) with React.memo.
Please take a look at React.memo documentation.

Related

How do you make sure no two items have the same ID in an Array?

I have an array that stores the ID and Placeholder title of a new component every time the button "Create a list" is clicked. I am trying to make sure that there are no duplicated ID's. Essentially I just want check that the ID doesn't already exist, and if it does; to create a new one.
My code is below:
ElementContext.js:
import React, { createContext, useState } from 'react';
import Todobox from './components/Todobox';
export const ElementContext = createContext();
export const ElementContextProvider = ({children}) => {
const [elements, setElements] = useState([]);
const [elementId, setElementId] = useState(1);
const newElementId = (elements) =>{
const newId = Math.floor(Math.random()*100).toString();
setElementId(newId)
}
const newElement = () =>{
newElementId();
setElements((prev) => [...prev, {title: 'Placeholder', id:elementId}])
console.log(elements)
};
const value = {
elements,
setElements,
newElement,
newElementId,
elementId
};
return(
<ElementContext.Provider value={value}>
{children}
</ElementContext.Provider>
)
};
HomePage.jsx:
import react from 'react';
import { useContext } from 'react';
import '../App.css';
import Todobox from './Todobox';
import { ElementContext } from '../ElementContext';
export default function HomePage(){
const { elements, setElements, newElement, elementsId } = useContext(ElementContext);
return(
<div className='page-container'>
<div className='header'>
<a className='header-title'>Trello Clone!</a>
<a className='header-button' onClick={newElement}>Create a list</a>
</div>
<div className='element-field'>
{elements.length !== 0 &&
elements.map((elements, newElementId) => <Todobox key={newElementId} />)}
</div>
</div>
)
}
Idea:
You need to implement a state that holds an object const [refDict, setRefDict] = useState({}); that stores the key as elementId and value as true. So when you want to insert a new element need to check whether the state already has this key or not. if the state has the key it means elementId is not unique so no need to push to setElements() and vice versa.
Declare a new state as:
const [refDict, setRefDict] = useState({});
and inside your newElement() add logics like this:
const newElement = () => {
newElementId();
if (!refDict[elementId]) { //so if nothing in "refDict" that means the elementId is unique
setElements(prev => [...prev, { title: 'Placeholder', id: elementId }]);
setRefDict((prev) => ({...prev, [elementId]: true}));
}
console.log(elements);
};
You can also play around with this sandbox
you need increments your array
setElementId(elementId + 1)
you only need this. more then this, is not necessary.
but, filter is one option.

Is there a way to populate option html tag with an array in react?

I'm trying to make an option in jsx to be populated by the values in an array (currencyOptions). I used this approach but it is not working as the options still remain to be blank. The array is passed down to the component as a prop. I set the array using usestate and the data is gotten from an API. Please help.
import React from "react";
function Currencyrow(props) {
const {
currencyOptions,
selectedCurrency,
onChangeCurrency,
amount,
onChangeAmount,
} = props;
// console.log(currencyOptions);
return (
<>
<input
type="number"
className="input"
value={amount}
onChange={onChangeAmount}
></input>
<select value={selectedCurrency} onChange={onChangeCurrency}>
{currencyOptions.map((option) => {
<option key={option} value={option}>
{option}
</option>;
})}
</select>
</>
);
}
export default Currencyrow;
That is the component where I pass down currencyOptions as a prop from my main app.js
import "./App.css";
import React from "react";
import Currencyrow from "./Components/Currencyrow";
import { useEffect, useState } from "react";
const BASE_URL =
"http://api.exchangeratesapi.io/v1/latest?access_key=1fe1e64c5a8434974e17b04a023e9348";
function App() {
const [currencyOptions, setCurrencyOptions] = useState([]);
const [fromCurrency, setFromCurrency] = useState();
const [toCurrency, setToCurrency] = useState();
const [exchangeRate, setExchangeRate] = useState();
const [amount, setAmount] = useState(1);
const [amountInFromCurrency, setAmountInFromCurrency] = useState(true);
let toAmount, fromAmount;
if (amountInFromCurrency) {
fromAmount = amount;
toAmount = fromAmount * exchangeRate;
} else {
toAmount = amount;
fromAmount = amount / exchangeRate;
}
useEffect(() => {
fetch(BASE_URL)
.then((res) => res.json())
.then((data) => {
const firstCurrency = Object.keys(data.rates)[0];
setCurrencyOptions([Object.keys(data.rates)]);
setFromCurrency(data.base);
// console.log(currencyOptions);
setToCurrency(firstCurrency);
setExchangeRate(data.rates[firstCurrency]);
});
}, []);
function handleFromAmountChange() {
// setAmount(e.target.value);
setAmountInFromCurrency(true);
}
function handleToAmountChange() {
// setAmount(e.target.value);
setAmountInFromCurrency(false);
}
return (
<>
<h1>Convert</h1>
<Currencyrow
currencyOptions={currencyOptions}
selectedCurrency={fromCurrency}
onChangeCurrency={(e) => {
setFromCurrency(e.target.value);
}}
amount={fromAmount}
onChangeAmount={handleFromAmountChange}
/>
<div className="equals">=</div>
<Currencyrow
currencyOptions={currencyOptions}
selectedCurrency={toCurrency}
onChangeCurrency={(e) => {
setToCurrency(e.target.value);
}}
amount={toAmount}
onChangeAmount={handleToAmountChange}
/>
</>
);
}
export default App;
When I run the app the option element is still blank.
Is there a way to populate option html tag with an array in react?
This is possible. Just as a tip, you can always try hardcoding currencyOptions in your CurrencyRow and test it out.
Looking through your code, firstly it may be not what you want wrapping Object.keys() in an additional array in setCurrencyOptions([Object.keys(data.rates)]). Object.keys() already returns an array. You probably are not accessing the actual options in your currencyOptions.map((option) => ..). Try setting the keys array directly like this setCurrencyOptions(Object.keys(data.rates)).
Secondly, you should return the desired value inside map by either using it as an arrow function or adding the return keyword in front of the option JSX.
Other than that, is there any error displayed in the browser console? And it would certainly help you to log the mapped option to the console and see what you are actually getting from it.
Your map function should return a value.
<select>{numbers.map((m)=>{return(<option>{m}</option>)})}</select>

React hooks: Dynamically mapped component children and state independent from parent

I am gathering posts (called latestFeed) from my backend with an API call. These posts are all mapped to components and have comments. The comments need to be opened and closed independently of each other. I'm governing this mechanic by assigning a piece of state called showComment to each comment. showComment is generated at the parent level as dictated by the Rules of Hooks.
Here is the parent component.
import React, { useState, useEffect } from "react";
import { getLatestFeed } from "../services/axios";
import Child from "./Child";
const Parent= () => {
const [latestFeed, setLatestFeed] = useState("loading");
const [showComment, setShowComment] = useState(false);
useEffect(async () => {
const newLatestFeed = await getLatestFeed(page);
setLatestFeed(newLatestFeed);
}, []);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
return (
<div className="dashboardWrapper">
<Child posts={latestFeed} showComment={showComment} handleComment={handleComment} />
</div>
);
};
export default Parent;
latestFeed is constructed along with showComment. After latestFeed comes back with an array of posts in the useEffect hook, it is passed to the child show here:
import React, { useState } from "react";
const RenderText = ({ post, showComment, handleComment }) => {
return (
<div key={post._id} className="postWrapper">
<p>{post.title}</p>
<p>{post.body}</p>
<Comments id={post._id} showComment={showComment} handleComment={() => handleComment(post)} />
</div>
);
};
const Child = ({ posts, showComment, handleComment }) => {
return (
<div>
{posts.map((post) => {
<RenderPosts posts={posts} showComment={showComment} handleComment={handleComment} />;
})}
</div>
);
};
export default Child;
However, whenever I trigger handleComments, all comments open for all posts. I'd like them to be only the comment that was clicked.
Thanks!
You're attempting to use a single state where you claim you want multiple independent states. Define the state directly where you need it.
In order to do that, remove
const [showComment, setShowComment] = useState(false);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
from Parent, remove the showComment and handleComment props from Child and RenderText, then add
const [showComment, handleComment] = useReducer(state => !state, false);
to RenderText.

Using React Hooks in in an IIFE breaks rules of hooks

Background
I'm working on a sidebar component that can be opened from many buttons in the UI. I want to 1) only render it once, 2) grant access to update its isVisible state to these buttons without drilling props down through a common ancestor.
Expectations vs Reality
I'd expect that I could create a context with its own api method to update an internal state. In my code sample I'm attempting to do this with an IIFE.
Questions
How does this break the rules of hooks?
How else can I provide an update function to this context?
export const SidebarContext = createContext((() => {
const [isVisible, setIsVisible] = useState(false)
return {
isVisible,
toggleVisibility: () => setIsVisible(!isVisible)
}
})())
createContext() receives the default value. So you're defining a function which is invoked immediately and the result of it will be used as default value for the context. That's where the useState breaks this rule:
Call Hooks from React function components.
In order to accomplish what you want you can do this:
import React, { createContext, useContext, useState } from "react";
const SidebarContext = createContext();
function Provider({ children }) {
let [isVisible, setIsVisible] = useState(false);
let toggle = useCallback(() => setIsVisible(s => !s), [setIsVisible])
// Pass the `state` and `functions` to the context value
return (
<SidebarContext.Provider value={{ isVisible, toggle }}>
{children}
</SidebarContext.Provider>
);
}
function YourButton() {
let { isVisible, toggle } = useContext(SidebarContext);
return (
<div>
<div>Sidebar is {isVisible : 'open': 'close'}</div>
<button onClick={toggle}>
Toggle
</button>
</div>
);
}
function App() {
return (
<Provider>
<YourButton />
</Provider>
);
}

How to Pass Prop to a Component That is being Iterated?

Currently, in Portfolio component, counter prop is not getting displayed, but stock prop is getting displayed fine. Portfolio component is getting mapped by stockInfo to receive props, but I added another separate prop called counter, but it's not working out. What would be the correct way to pass down counter prop to Portfolio component, when Portfolio component is being iterated by another prop?
function App() {
const [stockInfo, setStockInfo] = useState([{ quote: "SPY", cost:320, currentHolding: true }]);
const [counter, setCounter] = useState(1);
let showChart = true;
const addStockSymbol = (quote, cost) => {
const newStockInfo = [...stockInfo, { quote: quote, cost: Number(cost), currentHolding: true }];
setStockInfo(newStockInfo);
setCounter(prevCounter => prevCounter + 1);
};
return (
<div>
<PortfolioForm addStockSymbol={addStockSymbol} />
{stockInfo.map((stock, index) => (
<div>
<Portfolio
key = {index}
index = {index}
stock={stock}
counter={counter}
/>
</div>
))}
</div>
)
}
export default App;
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import './Portfolio.css';
const Portfolio = ({stock}, {counter}) => {
const [stockData, setStockData] = useState([]);
useEffect(() => {
(async () => {
const data = await axios(
`https://finnhub.io/api/v1/quote?symbol=${stock.quote}&token=${process.env.REACT_APP_API_KEY}`
);
setStockData(data.data);
})();
},[]);
console.log(counter);
return (
<ul className="table-headings">
<li>{counter}</li>
<li>{stock.quote}</li>
<li>${stockData.pc}</li>
<li>${stock.cost}</li>
<li>320 days</li>
<li>36.78%</li>
</ul>
)
}
export default Portfolio;
Function components get props as argument, then you can destruct the props object to get only specific properties.
What you're doing right now in the Portfolio component is destructing stock from the props object (which is fine), but for counter you're destructing the second argument (which is also an object that represents forwardRef, but in this case there is not ref so its an empty object)
So, to fix the problem, just replace the Portfolio parameters from ({stock}, {counter}) to ({stock, counter}) which destructs these two properties from props
You can learn more about destructuring assignment in here

Categories

Resources