Recommendation on how to update an array if the index changes - javascript

I want to update array elements and I´m using the index to reference the position. the problem is that the index change while searching for names (keyword) it basically sets the name to the wrong element in the users array (because is taking the indes from the filtered users array)
const [users, setUsers] = useState(["John", "Marty", "Mary", "Johanna"]);
const [keyword, setKeyword] = useState("")
const updateName = (index, name) => {
const newState = [...users];
newState[index] = name;
setNames(newState);
};
I have an input field to search for names
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="search"/>
and then I render a child component with each name and I pass a prop to update the name
users
.filter((user) =>
user.toLowerCase().includes(keyword.toLowerCase())
)
.map((user, index) => (
<User
user={user}
updateName={updateName}
index={index}
key={index}
/>
))
My User component
const User (props) => <button onClick={props.updateName(index, "some name")}>{props.user}</button>
this works perfectly fine. except when keyword changes. because the users.map will change and obviously will change the index. but the problem is that I´m using the index to update the array elements.
for example if I search for the "Ma" keyword. 2 names match so the index of the filtered users will change to 0, 1 but the users array is still the same.
How can I solve this problem? thank you.

If you want to keep your current data structure, you could just forgo the filter and do the filtering within your map function by only conditionally rendering the User component. This way, you don't lose accounting of your indexes.
users
.map((user, index) => (
user.toLowerCase().includes(keyword.toLowerCase()) && <User
user={user}
updateName={updateName}
index={index}
key={index}
/>
))

import React, { useState } from "react";
import "./styles.css";
export default function App() {
const names = ["John", "Marty", "Mary", "Johnna"];
// create a map of user objs to hold the position and name of each user
const mappedUsers = names.map((name, i) => {
return { position: i, name };
});
const [users, setUsers] = useState(mappedUsers);
const [keyword, setKeyword] = useState("");
const updateName = (position, name) => {
setUsers(prevUsers => {
prevUsers[position].name = name;
// return a new merged array of users
return [...prevUsers];
});
};
const User = ({ updateName, user }) => (
<div>
<button onClick={() => updateName(user.position, "someName")}>
{user.name}
</button>
</div>
);
const UserList = ({ users, keyword }) => {
return users
.filter(user => user.name.toLowerCase().includes(keyword.toLowerCase()))
.map(user => (
<User key={user.position} updateName={updateName} user={user} />
));
};
return (
<>
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="search"
/>
<UserList users={users} keyword={keyword} />
</>
);
}

I suggest you stop using index for keeping track of positions of items in an array as this will most likely cause issues when items get deleted or added. Instead, you should create a unique identifier for each item and keep track of them that way.
Here's an example of one way you could go about this:
function MyComponent() {
const initialUsers = ["John", "Marty", "Mary", "Johanna"].map(
(name, idx) => {
return { id: idx + 1, name };
}
);
const [users, setUsers] = React.useState(initialUsers);
const [keyword, setKeyword] = React.useState("");
const handleOnChange = event => {
const { value } = event.target;
const nextUsers = initialUsers.filter(user =>
user.name.toLowerCase().includes(value)
);
setUsers(nextUsers);
setKeyword(value);
};
return (
<div>
<p>Search for a name:</p>
<input
type="text"
onChange={handleOnChange}
value={keyword}
placeholder="Type..."
/>
<ul>
{users.map(user => (
<li>
[{user.id}] {user.name}
</li>
))}
</ul>
</div>
);
}
Here's a working example so that you can test it:
CodeSandbox
Hope this helps.

Related

How can i place values of mapped input element in a state array on the parent element

I have two components, the parent(name Parent) and child(name Child), on the parent, i map an array and render the child, so the child appears like 4 times(number of times the child is displayed based on the mapping), i have an input field on the Child component (which will be 1 input field for the rendered child component), i am basically trying to get the values of the input field from all the rendered Child component (4 been rendered based on the mapping) and then send it to my parent component (store it in a state on the parent component).
mock code
parent component
const Items = [1,2,3,4]
export const Parent= () => {
return (<div>
{Items.map((Item, index) => {
return (
<div key={index}>
<Child />
</div>
);
})}
</div>
)
}
child component
export const Child = () => {
const [Amount, setAmount] = useState();
return (
<input
value={Amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
)
}
sorry for the bad code formatting.
This is a mock of what it looks like
this should give a somewhat clear understanding or image of the issue, i basically want to get all the Amount on the 4 render children, place it in an array and send it to the Parent component (so i can call a function that uses all the amount in an array as an argument)
i tried to set the values of the Child component to a state on context (it was wrong, it kept on pushing the latest field values that was edited, i am new to react so i didnt understand some of the things that were said about state lifting
Congratulations, you've discovered the need for the Lifting State Up React pattern. Lift the "amount" state from the child component up to the parent component. The parent component holds all the state and provides it and a callback function down to children components via props.
Example:
import { useState } from "react";
import { nanoid } from "nanoid";
const initialState = Array.from({ length: 4 }, (_, i) => ({
id: nanoid(),
value: i + 1
}));
const Child = ({ amount, setAmount }) => {
return (
<input
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
);
};
const Parent = () => {
const [items, setItems] = useState(initialState);
const setAmount = (id) => (amount) =>
setItems((items) =>
items.map((item) =>
item.id === id
? {
...item,
value: amount
}
: item
)
);
return (
<div>
{items.map((item) => (
<Child
key={items.id}
amount={item.value}
setAmount={setAmount(item.id)}
/>
))}
</div>
);
};
try following method:
const Items = [1, 2, 3, 4];
export default function Parent() {
return <Child items={Items} />;
}
export function Child(props) {
const [childItems, setChildItems] = useState(props.items);
const handleChange = (e, index) => {
let temp = childItems;
temp[index] = e.target.value;
setChildItems(temp);
};
return (
<div>
{childItems.map((item, index) => {
return (
<div key={index}>
<input
value={item}
onChange={(e) => handleChange(e, index)}
placeholder="Amount"
/>
</div>
);
})}
</div>
);
}```

Why the filter does not return the list on the initial render?

What I have is a list that was fetched from an api. This list will be filtered based on the input. But at the first render it will render nothing, unless I press space or add anything to the input. Another solution is set the fetched data to the filteredList. But I don't know if it is the right thing to set the fetched data to two arrays.
import React, { useState, useEffect } from "react";
const PersonDetail = ({ person }) => {
return (
<div>
Id: {person.id} <br />
Name: {person.name} <br />
Phone: {person.phone}
</div>
);
};
const App = () => {
const [personsList, setPersonsList] = useState([]);
const [personObj, setPersonObj] = useState({});
const [showPersonDetail, setShowPersonDetail] = useState(false);
const [newPerson, setNewPerson] = useState("");
const [filter, setFilter] = useState("");
const [filteredList, setFilteredList] = useState(personsList);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
setPersonsList(data);
//setFilteredList(data) <-- I have to add this to work
console.log(data);
});
}, []);
const handleClick = ({ person }) => {
setPersonObj(person);
if (!showPersonDetail) {
setShowPersonDetail(!showPersonDetail);
}
};
const handleChange = (event) => {
setNewPerson(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
const tempPersonObj = {
name: newPerson,
phone: "123-456-7890",
id: personsList.length + 1,
};
setPersonsList((personsList) => [...personsList, tempPersonObj]);
//setFilteredList(personsList) <-- to render the list again when add new person
setNewPerson(" ");
};
const handleFilter = (event) => {
setFilter(event.target.value);
const filteredList =
event.target.value.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(event.target.value.toLowerCase())
)
: personsList;
setFilteredList(filteredList);
};
return (
<div>
<h2>List:</h2>
Filter{" "}
<input value={filter} onChange={handleFilter} placeholder="Enter" />
<ul>
{filteredList.map((person) => {
return (
<li key={person.id}>
{person.name} {""}
<button onClick={() => handleClick({ person })}>View</button>
</li>
);
})}
</ul>
<form onSubmit={handleSubmit}>
<input
placeholder="Add Person"
value={newPerson}
onChange={handleChange}
/>
<button type="submit">Add</button>
</form>
{showPersonDetail && <PersonDetail person={personObj} />}
</div>
);
};
export default App;
Your filtered list is actually something derived from the full persons list.
To express this, you should not create two apparently independent states in this situation.
When your asynchronous fetch completes, the filter is probably already set and you are just setting personsList which is not the list you are rendering. You are rendering filteredList which is still empty and you are not updating it anywhere, except when the filter gets changed.
To avoid all of this, you could create the filtered list on each rendering and — if you think this is not efficient enough — memoize the result.
const filteredList = useMemo(() =>
filter.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(filter.toLowerCase())
)
: personsList,
[filter, personsList]
);
When the filter input gets changed, you should just call setFilter(event.target.value).
This way, you will always have a filtered list, independent of when your asynchronous person list fetching completes or when filters get updated.
Side note: Writing const [filteredList, setFilteredList] = useState(personsList); looks nice but is the same as const [filteredList, setFilteredList] = useState([]); because the initial value will be written to the state only once, at that's when the component gets initialized. At that time personsList is just an empty array.

Dynamically Delete Elements

I'm having trouble deleting elements. Instead of deleting a specific element, it only deletes the last newly created element. I'm not sure where I'm going wrong here. I referenced this tutorial that shows what I kinda want to do. (I'm new to React)
import React,{useState, useRef} from "react";
const Body = () => {
const [list, setList] = useState([]);
const AddInput = () => {
setList([...list, {placeholder:"Class Name"}]);
};
const DeleteInput = (index) => {
const l = [...list];
l.splice(index,1);
setList(l);
};
const InputChangeHandler = (event, index) => {
const l = [...list];
(l[index]).value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key)=>
<div key={key}>
<input type={"text"} id={key} placeholder={item.placeholder} onChange={e=>InputChangeHandler(e, key)}/>
<button id={key} onClick={() => DeleteInput(key)}>Delete</button>
</div>
)}
</div>
);
}
export default Body;
Element (input fields + button):
Deletes Last Created:
I think the main problem is the key of items that you set as react doc says:
When you don’t have stable IDs for rendered items, you may use the item index as a key as a last resort:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a
key. If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
As in this Article says:
Reordering a list, or adding and removing items from a list can cause issues with the component state, when indexes are used as keys. If the key is an index, reordering an item changes it. Hence, the component state can get mixed up and may use the old key for a different component instance.
What are some exceptions where it is safe to use index as key?
-If your list is static and will not change.
-The list will never be re-ordered.
-The list will not be filtered (adding/removing items from the list).
-There are no ids for the items in the list.
If you set an reliable key in your items with some counter or id generator your problem would solve.
something like this:
export default function App() {
const [list, setList] = useState([]);
const id = useRef({ counter: 0 });
const AddInput = () => {
console.log(id);
setList([...list, { placeholder: "Class Name", id: id.current.counter++ }]);
};
const DeleteInput = (id) => {
setList(list.filter((item, i) => item.id !== id));
};
const InputChangeHandler = (event, index) => {
const l = [...list];
l[index].value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key) => (
<div key={item.id}>
<input
type={"text"}
id={key}
placeholder={item.placeholder}
onChange={(e) => InputChangeHandler(e, key)}
/>
<button id={item.id} onClick={() => DeleteInput(item.id)}>
Delete
</button>
</div>
))}
</div>
);
}
Use filter.
const DeleteInput = (index) => {
const l = list.filter((_, i) => i !== index);
setList(l);
};
Pass id to your DeleteInput function and for remove just filter the item list with id
const DeleteInput = (id) => {const filterItemList = list.filter((item) => item.id!== id);setList(filterItemList ); };

filter items based on multiple properties

I am trying to filter an array of objects based on both title and description.
It is working fine for a single item, how can I filter items based on both title and description.
Here is my code.
import * as React from "react"
const items = [
{
title: "React",
description:
"React (also known as React.js or ReactJS) is a JavaScript library for building user interfaces. It is maintained by Facebook and a community of individual developers and companies."
},
];
export default function App () {
const [searchTerm, setSearchTerm] = React.useState("");
const inputRef = React.useRef();
React.useEffect(() => {
inputRef.current.focus();
}, []);
const handleChange = event => {
setSearchTerm(event.target.value);
};
const filteredNames = items
.filter((entry) =>
entry.title.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<>
<header>
<input
type="text"
placeholder="Type to filter..."
value={searchTerm}
onChange={handleChange}
ref={inputRef}
/>
</header>
<ul>
{filteredNames.map(({title, description}, i) => (
<li key={i} className="box">
<h1>{title}</h1>
<p>{description}</p>
</li>
))}
</ul>
</>
)
}
I tried with filtered chaining
const filteredNames = items
.filter((entry) =>
entry.title.toLowerCase().includes(searchTerm.toLowerCase())
).filter((entry) =>
entry.description.toLowerCase().includes(searchTerm.toLowerCase())
)
However, this not working.
Your approach is the same as using an AND conditional. The first filter returns only title matches and the second returns title matches that also have description matches. You want OR instead
You can use Array#some() as an OR conditional
const filteredNames = items.filter(({title,description}) =>
const term = searchTerm.toLowerCase();
return [title, description].some(str => str.toLowerCase().includes(term));
})
Filter function expects a boolean query, and will run through the iterable list, and return every item that will return true for that boolean query.
const filteredNames = items.filter(item => {
return item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.description.toLowerCase().includes(searchTerm.toLowerCase())
})

React/ Redux select filtering and sorting using selectors

I have a list of items that I would like the user to be able to sort and filter by attribute. I figure using selectors is the most efficient way to do this, though I am not sure exactly how? Ideally, I would like the user to select an attribute from a dropdown above the list, then enter the actual value of the attribute into a text field which would trigger a filtering selector using both of those params. For example in a list of cars, the user wants to filter by "make" so they choose "make" from a list of other attributes like "make", "model", "year" etc. and then next to that they could type in "Nissan" and then they get a list of just cars made by Nissan.
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchItems } from "../../actions/items";
const ItemList = ({ match }) => {
const items = useSelector((state) => state.items);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchItems());
}, [dispatch]);
const renderedList =
Object.values(items).length > 0
? Object.values(items)
.map((item) => {
return (
<ListItem key={item.id}>
<ItemCard item={item} handleClick={handleClick} />
</ListItem>
);
})
: null;
return (
<Grid container direction="row" className={classes.cardGrid}>
<Grid item lg={4} className={classes.list}>
<Typography variant="h4" className={classes.title}>
Items
</Typography>
<List dense>{renderedList}</List>
</Grid>
</Grid>
);
};
export default ItemList;
Keep the filter attribute and value in Redux state, and then apply the filtering in a selector.
// selectors.js
const getFilterAttribute = state => state.filterAttribute;
const getFilterValue = state => state.filterValue;
const getFilteredItems = state => {
const filterAttribute = getFilterAttribute(state);
const filterValue = getFilterValue(state);
const items = getItems(state);
if (!filterAttribute || !filterValue) {
return items;
}
// apply your filter the way you need it
return Object.values(items).filter(...)
}
This helps separate the state-selection logic from the presentation logic. Now your component just has to call the selector:
// ItemList.js
const ItemList = (props) => {
const items = useSelector(getFilteredItems);
const renderedList = items.map(item => ...)
return (
...
)
}
EDIT:
The filter component might look like:
const FilterControl = () => {
const dispatch = useDispatch();
const [attribute, setAttribute] = useState('');
const [value, setValue] = useState('');
const onSubmit = () => {
dispatch({ type: 'SET_FILTER', attribute, value });
}
return (
<div>
<label>Attribute</label>
<input value={attribute} onChange={e => setAttribute(e.target.value)} />
<label>Value</label>
<input value={value} onChange={e => setValue(e.target.value)} />
<button onClick={onSubmit}>Filter</button>
</div>
)
}

Categories

Resources