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

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

Related

Prevent losing data when refreshing on a different route - react

I wanted to prevent losing state on page refresh while being on a different route path. Im curious why the first example does not work. From what i understand when app mounts first thing that gonna render is component itself and then useEffects run. Since i got 3 here, first fetches and saves the data to the invoiceList state and then next useEffect that run should fill localStorage key with invoiceList state data. The last one obviously retrieve the data.
The second one does fill the "invoiceData" localStorage key with an empty array. Why is this happening if the invoiceList state already have the data after the first useEffect?
The second example that i provided works. I removed second useEffect and set localStorage key in the first useEffect with response data that i get from fetch.
I also wonder if im doing everything correct here. Any feedback appreciated :)
First example (not working):
import { ReactElement, useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { Bar } from "../../components/Bar/Bar";
import { Invoice } from "./Root.utils";
type Props = {};
const Root = (props: Props): ReactElement => {
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
useEffect(() => {
const fetchData = async () => {
const response = await fetch("./data.json");
const data = await response.json();
setInvoiceList(data);
};
fetchData();
}, []);
useEffect(() => {
window.localStorage.setItem("invoiceData", JSON.stringify(invoiceList));
}, []);
useEffect(() => {
setInvoiceList(
JSON.parse(window.localStorage.getItem("invoiceData") || "[]")
);
}, []);
return (
<div>
<Bar />
<Outlet context={{ invoiceList }} />
</div>
);
};
export default Root;
Second example (working):
import { ReactElement, useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { Bar } from "../../components/Bar/Bar";
import { Invoice } from "./Root.utils";
type Props = {};
const Root = (props: Props): ReactElement => {
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
useEffect(() => {
const fetchData = async () => {
const response = await fetch("./data.json");
const data = await response.json();
window.localStorage.setItem("invoiceData", JSON.stringify(data));
setInvoiceList(data);
};
fetchData();
}, []);
useEffect(() => {
setInvoiceList(
JSON.parse(window.localStorage.getItem("invoiceData") || "[]")
);
}, []);
return (
<div>
<Bar />
<Outlet context={{ invoiceList }} />
</div>
);
};
export default Root;
The first example is never storing the data into the localStorage because the fetch is an asynchronous function that and you are writing basically always the empty array into your localStorage.
The order of execution in the first example will be:
fetchData called
window.localStorage.setItem("invoiceData", JSON.stringify(invoiceList)); <- still empty array
setInvoiceList(JSON.parse(window.localStorage.getItem("invoiceData") || "[]"));
response.json() called
setInvoiceList(data); called
I would also recommend to improve your code a little like that:
import React, { useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { Bar } from "../../components/Bar/Bar";
import { Invoice } from "./Root.utils";
const Root: React.FC = () => {
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
useEffect(() => {
setInvoiceList(
JSON.parse(window.localStorage.getItem("invoiceData") || "[]")
);
const fetchData = async () => {
const response = await fetch("./data.json");
const data = await response.json();
window.localStorage.setItem("invoiceData", JSON.stringify(data));
setInvoiceList(data);
};
fetchData();
}, []);
return (
<div>
<Bar />
<Outlet context={{ invoiceList }} />
</div>
);
};
export default Root;
You can use the Link component from react-router and specify to={} as an object where you specify pathname as the route to go to. Then add a variable e.g. data to hold the value you want to pass on. See the example below.
Using the <Link /> component:
<Link
to={{
pathname: "/page",
state: data // your data array of objects
}}
>
Using history.push()
this.props.history.push({
pathname: '/page',
state: data // your data array of objects
})
Using either of the above options you can now access data on the location object as per the below in your page component.
render() {
const { state } = this.props.location
return (
// render logic here
)
}

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

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.

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: Passing value to a functional component results in undefined value

I'm trying to pass values one by one to a functional component from array.map function. Unfortunately the component is not redering the value.
This is what I'm getting. There are room names stored in DB that should be printed here.
Homescreen.js
import React, { useState, useEffect } from "react";
import axios from "axios";
import Room from "../components/Room";
export default function Homescreen() {
const [rooms, setRooms] = useState([]);
const [loading, setLoading] = useState();
const [error, setError] = useState();
useEffect(async () => {
try {
setLoading(true);
const data = (await axios.get("/api/rooms/getallrooms")).data;
setRooms(data);
setLoading(false);
setError(false);
} catch (error) {
setLoading(false);
setError(true);
console.log(error);
}
}, []);
return (
<div>
{loading ? (
<h1>Loading...</h1>
) : error ? (
<h1>Error fetching details from API</h1>
) : (
rooms.map((room) => {
return (<div>
<Room key={room._id} room={room}></Room>
</div>
)
})
)}
</div>
);
}
Room.js (Funcitonal component that should print room names):
import React from "react";
function Room(room){
console.log(room.name)
return(
<div>
<h1>Room name: {room.name}.</h1>
</div>
)
}
export default Room;
The data is fetched correctly from db because if, instead of passing the value to component I print directly into my main screen, the values are printed.
In otherwords, in Homescreen.js, doing <p>{room.name}</p> instead of <Room key={room._id} room={room}></Room> print room names correctly.
So I reckon the problem is coming when I'm passing the values as props.
Any help is much appreciated. Thanks.
The parameter passed to a function component is the props object which contains the passed props, so you just need to grab props.room from there:
function Room(props){
console.log(props.room.name)
return(
<div>
<h1>Room name: {props.room.name}.</h1>
</div>
)
}
Or, with object destructuring:
function Room({ room }){
console.log(room.name)
return(
<div>
<h1>Room name: {room.name}.</h1>
</div>
)
}

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.

Categories

Resources