React Hooks populate array - javascript

Scenario: I have an array of objects to populate a list. When the user clicks on one of the items, that item needs to be come "selected" visually and what I would think is pushed to a new array called "selectedList". If the user clicks on another unselected item that becomes selected and also maintaining the previous selection. If the user clicks on a selected item it needs to toggle the selected state to unselected and remove within the selectedList array.
My thinking on this is to have a selectedList array which consists of the default object added first, then as the user clicks on other objects, push them into the array. I have this so far.
How can I toggle the selection within the array and how can I check the selectedList array so I can tell the "FundCard" component to set it's selected state to true so it visually updates to a selected state?
Here's what I have so far:
import React, {useState, useEffect} from 'react';
import styled from 'styled-components';
import FundCard from '../FundCard'
const FundDLWrap = styled.ul`
margin: 0;
padding: 0;
list-style-type: none;
border-left: 1px solid #e6e6e6;
margin-top: -40px;
height: 1007px;
overflow-y: scroll;
li {
&:hover {
cursor: pointer;
}
}
`
const FundDetailList = ({data, fundID}) => {
const [selectedList, setSelectedList] = useState([fundID]);
const listSelection = (i) => {
setSelectedList([...selectedList, i]);
}
console.log("selected list", selectedList);
return (
<FundDLWrap>
{data.map((item, i) => {
// one item must always be selected - the item ID the user came from
const selected = fundID === i;
return (
<li key={i} onClick={() => listSelection(i)}>
<FundCard data={item} vertical={true} selected={selected} />
</li>
);
})}
</FundDLWrap>
)
}
export default FundDetailList;
here's what one of the data objects look like:
{
saved: false,
name: 'Title here',
dailyChange: "3.52",
inc: true,
price: '132.42',
priceDate: '11 Mar 2020',
volRating: 1,
},
please note the data is external and is not kept in state.

If the specified index i is in the selectedList then filter it out, otherwise, append it to a copy of the array.
Use the same selectedList.includes(i) check in the return to set the selected prop of FundCard.
const FundDetailList = ({data, fundID}) => {
// Don't populate index 0 with fundID, it'll get matched in the map function
const [selectedList, setSelectedList] = useState([]);
const listSelection = (i) => {
setSelectedList(
selectedList.includes(i)
? selectedList.filter(index => index !== i)
: [... selectedList, i]
);
}
return (
<FundDLWrap>
{data.map((item, i) => {
// one item must always be selected - the item ID the user came from
const selected = selectedList.includes(i) || fundID === i;
return (
<li key={i} onClick={() => listSelection(i)}>
<FundCard data={item} vertical selected={selected} />
</li>
);
})}
</FundDLWrap>
)
}

Related

how to add object into array with hook state in react js

I'm trying to set the id property of a state object based on the id attribute of the button I clicked, and then append it into a state array and then store it in the localstorage. but there are weird things happening in my code:
when I clicked the 1st button, the console.log shows the id value of the 1st button (5) but the state object were still empty
then I clicked on the 2nd button, the console.log shows the id value of the 2nd button (12) but the state object now contains the value from the 1st button (5)
then I called the 3rd button, the console.log shows the id value of the 3rd button (100) but the state object contains the value of the 2nd button (12)
why are the Buttons still look like a regular/default button even though I already added the react-bootstrap dependency, imported the Button component, and applied the variant="link" in the Button?
here's my code (and the codesandbox)
import { useState, useEffect } from "react";
import { Button } from "react-bootstrap";
export default function App() {
const [cartList, setCartList] = useState([]);
const [cartItem, setCartItem] = useState({});
useEffect(() => {
let localCart = localStorage.getItem("cartList") || "[]";
console.log("localcart", localCart);
if (localCart) {
localCart = JSON.parse(localCart);
}
setCartList(localCart);
}, []);
const handleClick = (e, item) => {
e.preventDefault();
const arr = e.target.id.split("-");
const selectID = arr[1];
console.log("selectID", selectID);
setCartItem({ ...cartItem, id: selectID });
console.log("cartItem", cartItem);
let itemIndex = -1;
console.log("cartlist", cartList);
for (let i = 0; i < cartList.length; i++) {
if (cartItem && selectID === cartList[i].id) {
itemIndex = i;
}
}
if (itemIndex < 0) {
console.log("added to cartList on index", itemIndex, cartItem);
setCartList([...cartList, cartItem]);
localStorage.setItem("cartList", JSON.stringify(cartList));
}
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Button
variant="link"
id="item-5"
onClick={(e) => handleClick(e, cartItem)}
>
item no.5
</Button>
<Button
variant="link"
id="item-12"
onClick={(e) => handleClick(e, cartItem)}
>
item no.12
</Button>
<Button
variant="link"
id="item-100"
onClick={(e) => handleClick(e, cartItem)}
>
item no.100
</Button>
<div>
{cartList.length > 0 ? (
cartList.map((item, index) => <div key={index}>{item.id}</div>)
) : (
<div>there is no data here</div>
)}
</div>
</div>
);
}
The problems number 1, 2, 3 are all caused by the same reason: setCartItem works asynchronously so when you do this:
setCartItem({ ...cartItem, id: selectID });
console.log("cartItem", cartItem);
The value that you print in the console.log is still the old state because it doesn't wait for the setCartItem to complete the update. So, if you want to do a console.log of the new state each time it changes you can use instead a useEffect hook:
useEffect(() => {
console.log("cartItem", cartItem); // this will be executed each time the cartItem has been changed
}, [cartItem])
About the question number 4, make sure to import the bootstrap css file:
/* The following line can be included in your src/index.js or App.js file*/
import 'bootstrap/dist/css/bootstrap.min.css';
More infos here in the official site:
https://react-bootstrap.github.io/getting-started/introduction/#stylesheets

React usestate array length varies depending on item i click

im trying to build a list you can add and delete components from.
Adding works but when i try to delete an item, every item that comes after that also gets deleted.
I found that the length of the use state array i use varies depending on which item i click delete on.
const Alerting = () => {
const [Alerts, setAlert] = useState([]);
const AddAlertingChoice = () => {
const addedAlerts = Alerts => [...Alerts, <AlertingCoinChoice coins={coins} id={new Date().getUTCMilliseconds()}];
setAlert(addedAlerts);
}
const DeleteAlertingChoice = id =>{
console.log("alerts "+Alerts.length) //This length always displays the item index-1?
const removedArr = [...Alerts].filter(alert => alert.props.id != id)
setAlert(removedArr)
}
return (
<>
<AlertingContainer>
<CoinChoiceContainer>
{Alerts.map((item, i) => (
item
))}
</CoinChoiceContainer>
<AddAlertButton onClick={AddAlertingChoice}>+</AddAlertButton>
</AlertingContainer>
</>
)
The items
const AlertingCoinChoice = ({coins, id, DeleteAlertingChoice}) => {
return (
<>
<AlertingCoin>
<CoinSelect id={'SelectCoin'}>
<OptionCoin value='' disabled selected>Select your option</OptionCoin>
</CoinSelect>
<ThresholdInput id={'LowerThresholdInput'} type='number'
pattern='^-?[0-9]\d*\.?\d*$'/>
<ThresholdInput id={'UpperThresholdInput'} type='number'
pattern='^-?[0-9]\d*\.?\d*$'/>
<SaveButton id={'AlertSaveAndEdit'} onClick={ClickSaveButton}>Save</SaveButton>
<DeleteAlertButton onClick={() => {DeleteAlertingChoice(id)}}>X</DeleteAlertButton>
</AlertingCoin>
</>
)
why cant it just delete the item i pass with the id parameter?
It sounds like you're only passing down the DeleteAlertingChoice when initially putting the new <AlertingCoinChoice into state, so when it gets called, the length is the length it was when that component was created, and not the length the current state is.
This also causes the problem that the DeleteAlertingChoice that a component closes over will only have the Alerts it closes over from the time when that one component was created - it won't have the data from further alerts.
These problems are all caused by one thing: the fact that you put the components into state. Don't put components into state, instead transform state into components only when rendering.
const Alerting = () => {
const [alerts, setAlerts] = useState([]);
const AddAlertingChoice = () => {
setAlerts([
...alerts,
{ coins, id: Date.now() }
]);
}
const DeleteAlertingChoice = id => {
console.log("alerts " + alerts.length);
setAlerts(alerts.filter(alert => alert.id !== id));
}
return (
<AlertingContainer>
<CoinChoiceContainer>
{Alerts.map((alertData) => (
<AlertingCoinChoice {...alertData} DeleteAlertingChoice={DeleteAlertingChoice} />
))}
</CoinChoiceContainer>
<AddAlertButton onClick={AddAlertingChoice}>+</AddAlertButton>
</AlertingContainer>
);
};
You also don't need fragments <> </> when you're already only rendering a single top-level JSX item.

Two way communication between two functional React JS components

I have two functional components built in React, one is an Item component - it holds some data about stuff, with optional graphics, some text data and price information. On the bottom there is a button, that allows you to select this particular item. It also keeps information in its props on ID of currently selected Item - that's how i planned to solve this problem.
My second component is a ItemList - it basically holds a list of aforemetioned Items - plus it sorts all the items and must keep information about which component is currently selected - the selected one basically looks different - some stuff like the border box and button's colour gets switched via CSS.
My logic to implement goes like this - when user clicks on a "Select" button of a particular Item, the Item should change its look (unless it's already selected, then do nothing), after that somehow propagate info up onto the ItemList, so that it can "disable" the previously selected component. There can be only one selected Item, and once user decide to select another one, the previously selected should change its state and go back to unselected standard graphic style.
I've ran across a solution with state in the ItemList component plus passing a function via props into Item, but that doesn't solve the second part - ItemList needs to get info about a change, so it can rerender all the components according to actual state. What part of React API should I dive into to solve this issue?
Here is code for my components:
Item
interface Props {
receivedObject: itemToDisplay;
selectedItemId: string;
onClick?: () => void;
}
export default function Item(props: Props) {
const {name, description, price} = props.receivedObject;
const imageUrl = props.receivedObject?.media?.mainImage?.small?.url;
const priceComponent = <Price price={price}/>;
const [isItemSelected, setSelection] = useState(props.selectedItemId == props.receivedObject.id);
const onClick = props.onClick || (() => {
setSelection(!isItemSelected)
});
return (
<>
<div className="theDataHolderContainer">
// displayed stuff goes here
<div className="pickButtonContainer">
// that's the button which should somehow send info "upwards" about the new selected item
<Button outline={isItemSelected} color="danger" onClick={onClick}>{isItemSelected ? "SELECTED" : "SELECT"}</Button>
</div>
</div>
</>)
};
ItemList
interface Props {
packageItems: Array<itemToDisplay>
}
export default function ItemList(props: Props) {
const itemsToDisplay = props.packageItems;
itemsToDisplay.sort((a, b) =>
a.price.finalPrice - b.price.finalPrice
);
let selectedItemId = itemsToDisplay[0].id;
const [currentlySelectedItem, changeCurrentlySelectedItem] = useState(selectedItemId);
const setSelectedItemFunc = () => {
/* this function should be passed down as a prop, however it can only
* have one `this` reference, meaning that `this` will refer to singular `Item`
* how do I make it change state in the `ItemList` component?
*/
console.log('function defined in list');
};
return(
<div className="packageNameList">
<Item
key={itemsToDisplay[0].id}
receivedObject={itemsToDisplay[0]}
onClick={setSelectedItemFunc}
/>
{itemsToDisplay.slice(1).map((item) => (
<Item
key={item.id}
receivedObject={item}
onClick={setSelectedItemFunc}
/>
))}
</div>
);
}
In React, the data flows down, so you'd better hold state data in a stateful component that renders presentation components.
function ListItem({ description, price, selected, select }) {
return (
<li className={"ListItem" + (selected ? " selected" : "")}>
<span>{description}</span>
<span>${price}</span>
<button onClick={select}>{selected ? "Selected" : "Select"}</button>
</li>
);
}
function List({ children }) {
return <ul className="List">{children}</ul>;
}
function Content({ items }) {
const [selectedId, setSelectedId] = React.useState("");
const createClickHandler = React.useCallback(
id => () => setSelectedId(id),
[]
);
return (
<List>
{items
.sort(({ price: a }, { price: b }) => a - b)
.map(item => (
<ListItem
key={item.id}
{...item}
selected={item.id === selectedId}
select={createClickHandler(item.id)}
/>
))}
</List>
);
}
function App() {
const items = [
{ id: 1, description: "#1 Description", price: 17 },
{ id: 2, description: "#2 Description", price: 13 },
{ id: 3, description: "#3 Description", price: 19 }
];
return (
<div className="App">
<Content items={items} />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
.App {
font-family: sans-serif;
}
.List > .ListItem {
margin: 5px;
}
.ListItem {
padding: 10px;
}
.ListItem > * {
margin: 0 5px;
}
.ListItem:hover {
background-color: lightgray;
}
.ListItem.selected {
background-color: darkgray;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

How target DOM with react useRef in map

I looking for a solution about get an array of DOM elements with react useRef() hook.
example:
const Component = () =>
{
// In `items`, I would like to get an array of DOM element
let items = useRef(null);
return <ul>
{['left', 'right'].map((el, i) =>
<li key={i} ref={items} children={el} />
)}
</ul>
}
How can I achieve this?
useRef is just partially similar to React's ref(just structure of object with only field of current).
useRef hook is aiming on storing some data between renders and changing that data does not trigger re-rendering(unlike useState does).
Also just gentle reminder: better avoid initialize hooks in loops or if. It's first rule of hooks.
Having this in mind we:
create array and keep it between renders by useRef
we initialize each array's element by createRef()
we can refer to list by using .current notation
const Component = () => {
let refs = useRef([React.createRef(), React.createRef()]);
useEffect(() => {
refs.current[0].current.focus()
}, []);
return (<ul>
{['left', 'right'].map((el, i) =>
<li key={i}><input ref={refs.current[i]} value={el} /></li>
)}
</ul>)
}
This way we can safely modify array(say by changing it's length). But don't forget that mutating data stored by useRef does not trigger re-render. So to make changing length to re-render we need to involve useState.
const Component = () => {
const [length, setLength] = useState(2);
const refs = useRef([React.createRef(), React.createRef()]);
function updateLength({ target: { value }}) {
setLength(value);
refs.current = refs.current.splice(0, value);
for(let i = 0; i< value; i++) {
refs.current[i] = refs.current[i] || React.createRef();
}
refs.current = refs.current.map((item) => item || React.createRef());
}
useEffect(() => {
refs.current[refs.current.length - 1].current.focus()
}, [length]);
return (<>
<ul>
{refs.current.map((el, i) =>
<li key={i}><input ref={refs.current[i]} value={i} /></li>
)}
</ul>
<input value={refs.current.length} type="number" onChange={updateLength} />
</>)
}
Also don't try to access refs.current[0].current at first rendering - it will raise an error.
Say
return (<ul>
{['left', 'right'].map((el, i) =>
<li key={i}>
<input ref={refs.current[i]} value={el} />
{refs.current[i].current.value}</li> // cannot read property `value` of undefined
)}
</ul>)
So you either guard it as
return (<ul>
{['left', 'right'].map((el, i) =>
<li key={i}>
<input ref={refs.current[i]} value={el} />
{refs.current[i].current && refs.current[i].current.value}</li> // cannot read property `value` of undefined
)}
</ul>)
or access it in useEffect hook. Reason: refs are bound after element is rendered so during rendering is running for the first time it is not initialized yet.
I'll expand on skyboyer's answer a bit. For performance optimization (and to avoid potential weird bugs), you might prefer to use useMemo instead of useRef. Because useMemo accepts a callback as an argument instead of a value, React.createRef will only be initialized once, after the first render. Inside the callback you can return an array of createRef values and use the array appropriately.
Initialization:
const refs= useMemo(
() => Array.from({ length: 3 }).map(() => createRef()),
[]
);
Empty array here (as a second argument) tells React to only initialize refs once. If ref count changes you may need to pass [x.length] as "a deps array" and create refs dynamically: Array.from({ length: x.length }).map(() => createRef()),
Usage:
refs[i+1 % 3].current.focus();
take the parent Reference and manipulate the childrens
const Component = () => {
const ulRef = useRef(null);
useEffect(() => {
ulRef.current.children[0].focus();
}, []);
return (
<ul ref={ulRef}>
{['left', 'right'].map((el, i) => (
<li key={i}>
<input value={el} />
</li>
))}
</ul>
);
};
I work this way and I think that's more simple than other proposed answers.
Instead of using array of refs or something like that, you can seperate each map item to component. When you seperate them, you can use useRefs independently:
const DATA = [
{ id: 0, name: "John" },
{ id: 1, name: "Doe" }
];
//using array of refs or something like that:
function Component() {
const items = useRef(Array(DATA.length).fill(createRef()));
return (
<ul>
{DATA.map((item, i) => (
<li key={item.id} ref={items[i]}>
{item.name}
</li>
))}
</ul>
);
}
//seperate each map item to component:
function Component() {
return (
<ul>
{DATA.map((item, i) => (
<MapItemComponent key={item.id} data={item}/>
))}
</ul>
);
}
function MapItemComponent({data}){
const itemRef = useRef();
return <li ref={itemRef}>
{data.name}
</li>
}
If you know the length of the array ahead of time, to which you do in your example you can simply create an array of refs and then assign each one by their index:
const Component = () => {
const items = Array.from({length: 2}, a => useRef(null));
return (
<ul>
{['left', 'right'].map((el, i)) => (
<li key={el} ref={items[i]}>{el}</li>
)}
</ul>
)
}
I had a problem like this and read 'Joer's answer and realised you can just loop through using the index setting the querySelector class dynamically and set only one ref to the overall parent. Apologies for the load of code but hope this helps someone:
import React, { useRef, useState } from 'react';
import { connectToDatabase } from "../util/mongodb";
export default function Top({ posts }) {
//const [count, setCount] = useState(1);
const wrapperRef = useRef(null);
const copyToClipboard = (index, areaNumber) => {
//
// HERE I AM USING A DYNAMIC CLASS FOR THE WRAPPER REF
// AND DYNAMIC QUERY SELECTOR, THEREBY ONLY NEEDING ONE REF ON THE TOP PARENT
const onePost = wrapperRef.current.querySelector(`.index_${index}`)
const oneLang = onePost.querySelectorAll('textarea')[areaNumber];
oneLang.select();
document.execCommand('copy');
};
var allPosts = posts.map((post, index) => {
var formattedDate = post.date.replace(/T/, ' \xa0\xa0\xa0').split(".")[0]
var englishHtml = post.en1 + post.en2 + post.en3 + post.en4 + post.en5;
var frenchHtml = post.fr1 + post.fr2 + post.fr3 + post.fr4 + post.fr5;
var germanHtml = post.de1 + post.de2 + post.de3 + post.de4 + post.de5;
return (
<div className={post.title} key={post._id}>
<h2>{formattedDate}</h2>
<h2>{index}</h2>
<div className={"wrapper index_" + index}>
<div className="one en">
<h3>Eng</h3>
<button onClick={() => {copyToClipboard(index, 0)}}>COPY</button>
<textarea value={englishHtml} readOnly></textarea>
</div>
<div className="one fr">
<h3>Fr</h3>
<button onClick={() => {copyToClipboard(index, 1)}}>COPY</button>
<textarea value={frenchHtml} readOnly></textarea>
</div>
<div className="one de">
<h3>De</h3>
<button onClick={() => {copyToClipboard(index, 2)}}>COPY</button>
<textarea value={germanHtml} readOnly></textarea>
</div>
</div>
</div>
)
})
return (
<div ref={wrapperRef}>
<h1>Latest delivery pages </h1>
<ul>
{allPosts}
</ul>
<style jsx global>{`
body{
margin: 0;
padding: 0;
}
h1{
padding-left: 40px;
color: grey;
font-family: system-ui;
font-variant: all-small-caps;
}
.one,.one textarea {
font-size: 5px;
height: 200px;
width: 300px;
max-width: 350px;
list-style-type:none;
padding-inline-start: 0px;
margin-right: 50px;
margin-bottom: 150px;
}
h2{
font-family: system-ui;
font-variant: all-small-caps;
}
.one h3 {
font-size: 25px;
margin-top: 0;
margin-bottom: 10px;
font-family: system-ui;
}
.one button{
width: 300px;
height: 40px;
margin-bottom: 10px;
}
#media screen and (min-width: 768px){
.wrapper{
display: flex;
flex-direction: row;
}
}
`}</style>
</div>
);
}
In this case you would need to create an array of empty refs and push the refs while they are generated inside the React component.
You would then use useEffect to handle the corresponding refs.
Here's an example:
const refs = []
useEffect(() => {
refs.map(ref => {
console.log(ref.current)
// DO SOMETHING WITH ref.current
})
}, [refs])
{postsData.map((post, i) => {
refs.push(React.createRef())
return (
<div ref={refs[i]} key={post.id}>{...}</div>
)}
}

How to display selected Listbox value in ReactJS from state

What I want to do is select an item from the ListBox and display it in the same WebPage. The ListBox renders correctly, but when I try to Output the selected value, it shows target undefined. Refer comments in code.
I tried using event.target.value to retrieve the value selected which has always worked for me so far, but it says event.target is undefined and the value is inaccessible.
Note : I tried using Grommet for this app for styling, "List", "Box", "ListItem" are all grommet components.
Any help will be highly appreciated.
import React, {Component} from "react";
import Box from "grommet/components/Box";
import List from "grommet/components/List";
import ListItem from "grommet/components/ListItem";
const lb = [];
class ListBox extends Component{
state = {
products: [ //Array of Products
"Product1",
"Product2",
"Product3"
],
selected: null //State Variable where selected item will be stored
};
contents () {
for (let i = 0; i < this.state.products.length; ++i){
lb[i] =
<ListItem justify = "between" key = {i}>
<span>
{this.state.products[i]}
</span>
</ListItem>
}
}
itemSelected (event) {
console.log(event.target); //SHOWS TARGET UNDEFINED in Console Window
let temp = {
selected: event.target
}
this.setState(temp);
}
render () {
this.contents();
return (
<Box>
<List
selectable={true}
onSelect = { //The function call on selecting an item
(event) => {
this.itemSelected(event);
}
} >
{lb}
</List>
<p>Selected Product : {this.state.selected}</p>
</Box>
);
}
}
export default ListBox;
grommet give you selected item index not event.
please check official document.
http://grommet.io/docs/list
itemSelected (index) {
console.log(index);
let temp = {
selected: this.state.products[index]
}
this.setState(temp);
}

Categories

Resources