React map doesn't re-render as state changes - javascript

Could you help me with this please guys?
I'm mapping this emails state but when I remove an email from the array in handleDelete the map doesn't re-render. When I console.log(emails) it's right, the email is removed correctly.
import { TextField, Chip, Avatar } from "#material-ui/core";
import React, { useState } from "react";
import "./Email.css";
export default function Email() {
const [value, setValue] = useState<string>();
const [emails, setEmails] = useState<string[]>([]);
function onTextChange(e: any) {
setValue(e.target.value.replace(" ", ""));
if (value?.includes(",")) {
let separated = e.target.value.split(",");
const re = /^(([^<>()[\]\\.,;:\s#"]+(\.[^<>()[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const newEmails = separated.filter((val: any) => re.test(val));
if (newEmails[0] !== undefined) {
emails.push(newEmails[0]);
setEmails(emails);
console.log(emails);
}
setValue("");
}
}
function handleDelete(email: string) {
const index = emails.indexOf(email);
if (index > -1) {
setEmails(emails.splice(index, 1));
console.log(email, "removed");
console.log('new list', emails)
}
}
return (
<div>
<TextField
value={value || ""}
variant='outlined'
onChange={onTextChange}
id='textInput'
InputProps={{
startAdornment: (
<div style={{ top: "50%" }}>
{emails.map((email: any) => (
<Chip
label={email}
key={email}
avatar={<Avatar>{email[0].toUpperCase()}</Avatar>}
onDelete={() => handleDelete(email)}
style={{ display: "-webkit-inline-box" }}
/>
))}
</div>
),
}}
/>
</div>
);
}

The issue is this line setEmails(emails.splice(index, 1)); Splice just changes the array in place so the memory location doesn't change for the array, so react doesnt see a change.
You want something like this
setEmail(prevEmails => prevEmails.filter(e => e !== email))

Yes, I have met a similar problem before.
First, it's because the "emails" variable is not changed. So I mean you call emails.splice function but the email variable not changed.
You should create new array variable and set the changed value into it. Only that React state change listener can understand "emails" value changed so "I should render function again..".
function handleDelete(email: string) {
const newEmails = [...emails];
const index = newEmails.indexOf(email);
if (index > -1) {
setEmails(newEmails.splice(index, 1));
}
}

Related

My search bar is delaying updating the results

I was making a search bar component that modifies an array and then a mapping function that displays the resulted array it as the page results, the problem is that the page is delaying to update, in other words when I type a character in the search bar nothing changes but when I add another character the results are being updated with the first character input only and the.
I was using a hook state to hold the value of the search input and then using a filter function to update the array, finally I used a mapping function to display the modified array data as card components. As I said the problem is the delay that the website takes to update the array and it seams that the problem is with the state hook I uses but I couldn't solve that problem.
I also reuse the filtered array to display search suggetions
Here is app.js
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import Card from "./components/Card";
import resourcesData from "./resourcesData";
import { type } from "#testing-library/user-event/dist/type";
function App() {
const [filteredList, setFilteredList] = useState(resourcesData);
const [searchTerm, setSearchTerm] = useState("");
const changeInput = (event) => {
setSearchTerm(event);
};
function handleSearchTerm(event) {
setSearchTerm(event.target.value);
var updatedList = [...resourcesData];
updatedList = updatedList.filter((val) => {
if (searchTerm === "") return val;
else if (
val.title.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase())
) {
return val;
} else if (
val.thematicArea
.toLocaleLowerCase()
.includes(searchTerm.toLocaleLowerCase())
) {
return val;
}
});
setFilteredList(updatedList);
}
return (
<div className="App">
<input
type="text"
value={searchTerm}
onChange={handleSearchTerm}
className="input"
></input>
<div className="dropdown">
{filteredList.slice(0, 10).map((item) => (
<div
onClick={() => changeInput(item.title)}
className="dropdown-row"
key={item.title}
>
{item.title}
</div>
))}
</div>
<div className="cards">
{filteredList.map((value, index) => (
<Card
resourceURL={value.link}
thumbnailURL=""
title={value.title}
subtitle=""
date={value.date}
description=""
cost=""
cardkeywords={
value.cost === "free"
? [
value.level,
value.language,
value.type,
value.thematicArea,
value.cost,
]
: [
value.level,
value.language,
value.type,
...value.thematicArea.split(","),
]
}
key={index}
/>
))}
</div>
</div>
);
}
export default App;
In the function handleSearchTerm you use setSearchTerm(event.target.value); and after you are using searchTerm which updates asynchronously.
Use in this function event.target.value.
function handleSearchTerm(event) {
const newValue = event.target.value;
setSearchTerm(newValue);
var updatedList = [...resourcesData];
updatedList = updatedList.filter((val) => {
if (newValue === "") return val;
else if (
val.title.toLocaleLowerCase().includes(newValue.toLocaleLowerCase())
) {
return val;
} else if (
val.thematicArea
.toLocaleLowerCase()
.includes(newValue.toLocaleLowerCase())
) {
return val;
}
});
setFilteredList(updatedList);
}

MUI Select with Array of Objects as values. How to unselect predefined state?

I have an array of object that looks like this:
[
{
_id: "6311c197ec3dc8c083d6b632",
name: "Safety"
},
........
];
I load this array as potential Menu Items for my Select:
{categoryData &&
categoryData.map((cat: any) => (
<MenuItem key={cat._id} value={cat}>
<Checkbox
checked={categories.some((el: any) => el._id === cat._id)}
/>
<ListItemText primary={cat.name} />
</MenuItem>
))}
In my Select I have predefined value for it:
const [categories, setCategories] = useState([
{
name: "Safety",
_id: "6311c197ec3dc8c083d6b632"
}
]);
.......
<Select
labelId="demo-multiple-checkbox-label"
id="demo-multiple-checkbox"
multiple
value={categories}
onChange={(event: any) => {
const {
target: { value }
} = event;
console.log(value);
setCategories(value);
}}
input={<OutlinedInput label="Tag" />}
renderValue={(selected) => selected.map((cat) => cat.name).join(", ")}
>
The problem is I am unable to unselect(de-select) the predefined value. In stead of removing it from array of categories I got it once again in it.
Here is the sandbox example:
https://codesandbox.io/s/recursing-river-1i5jw8?file=/src/Select.tsx:632-757
I understand that values has to be exactly equal to be removed but how I can do that? What is wrong with this kind of handling?
Also I found this case as reference but still couldn't do it as in the case they use formik:
Unselect MUI Multi select with initial value
You can't directly save the Object as the value. You must use a unique string or stringify the entire object and store it as the value. And based on that value calculate the selected value rendered text. Here is something that will work for you.
Changes: use _id as the value instead of the entire object. And added a new selected value renderer.
import {
FormControl,
Select,
MenuItem,
InputLabel,
Checkbox,
ListItemText,
OutlinedInput
} from "#mui/material";
import React, { useState, useMemo } from "react";
const categoryData = [
{
_id: "6311c197ec3dc8c083d6b632",
name: "Safety"
},
{
_id: "6311c8e6ec3dc8c083d6b63b",
name: "Environment"
},
];
const SelectForm = () => {
const [categories, setCategories] = useState(["6311c197ec3dc8c083d6b632"]);
const selectedCategories = useMemo(() => {
let value = "";
categoryData.forEach((cat) => {
if (categories.some((catId: any) => catId === cat._id)) {
if (value) {
value += ", " + cat.name;
} else {
value = cat.name;
}
}
});
return value;
}, [categories]);
return (
<FormControl fullWidth>
<InputLabel id="demo-multiple-checkbox-label">Category</InputLabel>
<Select
labelId="demo-multiple-checkbox-label"
id="demo-multiple-checkbox"
multiple
value={categories}
onChange={(event: any) => {
const {
target: { value }
} = event;
console.log(value);
setCategories(value);
}}
input={<OutlinedInput label="Tag" />}
renderValue={() => selectedCategories}
>
{categoryData &&
categoryData.map((cat: any) => (
<MenuItem key={cat._id} value={cat._id}>
<Checkbox
checked={categories.some((catId: any) => catId === cat._id)}
/>
<ListItemText primary={cat.name} />
</MenuItem>
))}
</Select>
</FormControl>
);
};
export default SelectForm;
Initially, you have to pass an empty array while setting the state. This will solve your problem.
Code changes will look like this -
const [categories, setCategories] = useState([]);

TextInput gets unfocused after typing each character

I'm using React to build a form and I'm trying to filter a list with the SearchInput (which works the same as TextInput) located in the child component Header. But everytime I type a character the SearchInput gets unfocused
function index() {
const list = [//data\\]
const [search, setSearch] = useState("");
const [filteredResults, setFilteredResults] = useState([]);
const searchItems = (searchValue) => {
setSearch(searchValue);
if (search !== "") {
const filteredData = partners.filter((item) => {
return Object.values(item)
.join("")
.toLowerCase()
.includes(search.toLowerCase());
});
setFilteredResults(filteredData);
} else {
setFilteredResults(partners);
}
};
const Header = () => (
<Box>
<SearchInput
placeholder="Search"
value={search}
onChange={(e) => searchItems(e.target.value)}
/>
</Box>
);
return (
<Parent
headerContent={<Header />}
>
<Box>
<Table data={search.length > 1 ? filteredResults : list} />
</Box>
</Parent>
);
}
export default index;
Oh, I think I can see the problem now - it's the way you're rendering the <SearchInput /> component. You're inadvertantly creating a new functional component on every render. Either inline the Header directly into the Parent control's headerContent property, or create an entirely separate component:
const Header = ({ search, onSearchChange }) => {
const handleChange = (e) => onSearchChange(e.target.value);
return (
<Box>
<SearchInput
placeholder="Search"
value={search}
onChange={handleChange}
/>
</Box>
);
}
function index() {
// ----- 8< -----
return (
<Parent
headerContent={<Header search={search} onSearchChange={searchItems} />}
>
{/* ... */}
</Parent>
);
}
While you're there, you have a subtle bug with your comparison - it looks like you're searching your partners effectively as a list of strings; but, since you're joining them, if you had partners with the names:
'one'
'two'
You're creating a search string as 'onetwo' - so searching for 'et' would match, even though you don't actually have a partner matching that. You can fix that by just checking each partner individually... something like:
const searchItems = (searchValue) => {
setSearch(searchValue);
if (search !== "") {
const searchValueLower = searchValue.toLowerCase();
const filteredData = partners.filter((item) => {
return Object.values(item)
.some(item => item.toLowerCase().includes(searchValueLower);
});
setFilteredResults(filteredData);
} else {
setFilteredResults(partners);
}
};

filtering object of array by id - REACT

I'm having a big struggle with filtering an object of an array of objects by its ID in React. Let me explain:
The App is a Notes app, that stores every note you create with its Title, Text(name) and created date. The key is the ID.
Now I'm trying to create a popup modal every time I click on a note, which I managed to do ok, except for one thing: when the modal appears, it doesn't show only the selected note but all the notes list. I've tried with different array methods to filter the note I need, but didn't succeed.
This is the App.js file:
import React, { useState } from 'react';
import './App.css';
import Form from './components/Form';
import List from './components/List';
import { v4 as uuidv4 } from 'uuid';
import Modal from 'react-modal';
import ModalList from './components/ModalList';
Modal.setAppElement('#root');
function App() {
/*HOOKS */
const [list, setList] = useState([]);
const [modalList, setModalList] = useState([]);
//for modal:
let subtitle;
const [modalIsOpen, setIsOpen] = React.useState(false);
/*FUNCTIONS */
//add new notes
function handleAdd(title, name) {
if (name) {
const newList = list.concat({ title: title, name: name, id: uuidv4(), date: getCurrentDate() });
setList(newList);
console.log(newList);
const newModalList = list.concat({ title: title, name: name, id: uuidv4(), date: getCurrentDate() });
setModalList(newModalList);
}
else { alert("You should complete the notes field") }
}
//get the date for adding the note
function getCurrentDate() {
let newDate = new Date()
let date = newDate.getDate();
let month = newDate.getMonth() + 1;
let year = newDate.getFullYear();
let hours = newDate.getHours();
let minutes = newDate.getMinutes();
return `${month < 10 ? `0${month}` : `${month}`}/${date}/${year}, ${hours}:${minutes < 10 ? `0${minutes}` : `${minutes}`} hs.`
}
//deleting a note
function del(x) {
if (window.confirm("Do you really want to delete this item? The action is permanent.")) {
const newList = list.filter((item) => item.id !== x);
setList(newList);
}
}
//opening a modal
function openModal() {
setIsOpen(true);
}
//after opening a modal
function afterOpenModal() {
// references are now sync'd and can be accessed.
subtitle.style.color = '#f00';
}
//closing a modal
function closeModal() {
setIsOpen(false);
}
/*APP */
return (
<>
<div>
{/* MODAL */}
<Modal
isOpen={modalIsOpen}
onAfterOpen={afterOpenModal}
onRequestClose={closeModal}
style={customStyles}
contentLabel="Example Modal"
>
{modalList.map((item) => { return <ModalList key={item.id} item={item} quit={closeModal} /> })}
</Modal>
</div>
{/* FORM */}
<div className='App'>
<Form handleNew={handleAdd} />
</div>
{/* NOTES LIST */}
<div className='notes-list'>
{list.map((item) => { return <List key={item.id} item={item} quit={del} addModal={openModal} /> })}
</div>
</>
);
}
export default App;
And this is the ModalList.jsx file:
const ModalList = (props) => {
const { item, quit} = props;
/*LIST */
return (
<li ><button className='delete' onClick={()=>quit(item.id)}>x</button><p className='note-title'>{item.title}</p><p>{item.date}</p><p className='note-name'>{item.name}</p> </li>
);
}
export default ModalList;
I know I have to someway filter the object by its ID so that only appears what I clicked and not all the existing elements in the list, but I'm not finding the way.
Thanks a lot!
You are using Array.map here which is doing what it's supposed to do (listing the items), instead you should be using Array.filter which would return the ModalItem you need
{list.map((item) => { return <List key={item.id} item={item} quit={del} addModal={openModal} /> })}
openModal needs pass the item you clicked as a parameter and pass it to the callback.
Something like:
{list.map((item) => { return <List key={item.id} item={item} quit={del} addModal={() => openModal(item)} /> })}
Then openModal function needs to pass that parameter to the Modal component. To achieve that you can store it in your modalList for instance via setModalList([item])

How can I display my reversed array using the onClick event in React?

I pulled data from an API and mapped it but I am wanting to reverse the order when the user clicks MARKET CAP. I want the user to click the
<div>MARKET CAP</div>
Down in the code I am wanting to replace this array I am mapping:
{props.coins.filter(searchingFor(search)).map...
with the reversed one I made:
const [reverseCoin, setReverseCoin] = useState(
[...coin].filter(searchingFor(search)).reverse()
);
I have no idea how to replace the original data with the reversed one so any suggestions would be great. I'm using React, styled components and hooks. Here's my code:
import Button from "../../UI/Forms/Button/Button";
import styled from "styled-components";
import Icon from "../../assets/images/sort-solid.svg";
import SearchIcon from "../../assets/images/search-solid.svg";
import * as Styles from "../../components/Table/Tables.styles";
import { Link } from "react-router-dom";
import "./PriceList.scss";
function searchingFor(search) {
return function(x) {
return x.name.toLowerCase().includes(search.toLowerCase()) || false;
};
}
//MY FUCTIONAL COMPONENT*************
//one prop has been passed to this which I called it "coins"
const PriceList = props => {
console.log(props.coins);
const [coin, setCoin] = useState([]);
const [color, setColor] = useState("");
const [MarketCapLow, setMarketCapLow] = useState(false);
const [search, setSearch] = useState("");
/// this is the variable that holds the reversed array
const [reverseCoin, setReverseCoin] = useState(
[...coin].filter(searchingFor(search)).reverse()
);
const timeIntervels = ["1H", "24H", "1W", "1M", "1Y"];
useEffect(() => {
setCoin(props.coins);
}, [props.coins]);
const updateSearch = e => {
setSearch(e.target.value);
};
const handleClick = name => {
setColor(name);
};
//creating a table for my data********
return (
<TableContainer>
<SearchBarMainContainer>
<SearchBarContainer>
<SearchInputContainer>
<img
src={SearchIcon}
width="20"
height="20"
style={{ marginRight: "16px" }}
/>
<SearchBarInput
type="text"
value={search}
onChange={updateSearch}
placeholder="Search coins..."
/>
</SearchInputContainer>
<SearchPriceChange>
{timeIntervels.map(d => (
<SearchPriceChangeItems
id={d}
onClick={() => {
handleClick(d);
}}
className={color === d ? "purple" : "black"}
>
{d}
</SearchPriceChangeItems>
))}
</SearchPriceChange>
</SearchBarContainer>
</SearchBarMainContainer>
<Styles.Tablestyles>
<tbody>
<Styles.TableRowStyles bg>
<Styles.TabelHeadingStyles bg>#</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>NAME</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>PRICE</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>CHANGE</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>
<Styles.MarketCap
onClick={() => {
setMarketCapLow(!MarketCapLow);
}}
>
<div>MARKET CAP</div>
<CoinIcon width height src={Icon} />
</Styles.MarketCap>
</Styles.TabelHeadingStyles>
<Styles.TabelHeadingStyles bg>TRADE</Styles.TabelHeadingStyles>
</Styles.TableRowStyles>
{props.coins.filter(searchingFor(search)).map(coin => {
const {
rank,
logo_url,
name,
["1d"]: { price_change_pct },
currency,
price,
market_cap
} = coin;
const newMarketPct = (price_change_pct * 100).toFixed(2);
const newPrice = Math.floor(price * 100) / 100;
const newMarketCap =
Math.abs(market_cap) > 999999999
? Math.sign(market_cap) *
(Math.abs(market_cap) / 1000000000).toFixed(1) +
"B"
: Math.sign(market_cap) * Math.abs(market_cap);
return (
<Styles.TableRowStyles key={rank}>
<Styles.TabelDataStyles>{rank}</Styles.TabelDataStyles>
<Styles.TabelDataStyles grey flex>
<CoinIcon style={{ marginRight: "12px" }} src={logo_url} />
{name} ({currency})
</Styles.TabelDataStyles>
<Styles.TabelDataStyles>${newPrice}</Styles.TabelDataStyles>
<Styles.TabelDataStyles
style={
price_change_pct.charAt(0) === "-"
? { color: "#ff2734" }
: { color: "#23cc9a" }
}
>
{newMarketPct}%
</Styles.TabelDataStyles>
<Styles.TabelDataStyles>${newMarketCap}</Styles.TabelDataStyles>
<Styles.TabelDataStyles>
<Link to={`/prices/${coin.currency}`}>
<Button padding style={{ width: "60%" }}>
Trade
</Button>
</Link>
</Styles.TabelDataStyles>
</Styles.TableRowStyles>
);
})}
</tbody>
</Styles.Tablestyles>
</TableContainer>
);
}
I'd recommend just using a flag stored in your state to toggle how the array is displayed. This allows you to avoid storing both arrays in your state when they're essentially the same data. You'd of course need to make a click handler to change the value of the reversed flag and place it wherever you want the click to occur, but I don't see where that is in your code.
[reversed, setReversed] = useState(false);
...
// declare this variable somewhere inside your component outside the return statement.
// You have create a copy to prevent reverse() from mutating the prop in place.
const displayedCoins = reversed ? Array.from(props.coins).reverse() : props.coins;
...
{displayedCoins.filter(searchingFor(search)).map((coin) => { ... });

Categories

Resources