Searchbar cause input unchecked - reactjs

I have a ul that displays users with a checkbox input. When searching for a user by surname/first name, the previously selected input checkboxes are removed. How to prevent it?
function App() {
let copyList = useRef();
const [contacts, setContacts] = useState([]);
useEffect(() => {
fetch(api)
.then((res) => res.json())
.then((data) => {
copyList.current = data;
setContacts(copyList.current);
})
.catch((err) => console.log(err));
}, []);
contacts.sort((a, b) => (a.last_name > b.last_name ? 1 : -1));
const searchHandler = (value) => {
const contactsList = [...copyList.current].filter((x) => {
if (value.toLowerCase().includes(x.last_name.toLowerCase())) {
return x.last_name.toLowerCase().includes(value.toLowerCase());
} else if (value.toLowerCase().includes(x.first_name.toLowerCase())) {
return x.first_name.toLowerCase().includes(value.toLowerCase());
} else if (value === "") {
return x;
}
});
setContacts(contactsList);
};
return (
<div className="App">
<Header />
<SearchBar onSearch={(value) => searchHandler(value)} />
<ContactsList contacts={contacts} />
</div>
);
}
Input component is in ContactsList component.
function Input(props) {
const [isChecked, setIsChecked] = useState(false);
const [id] = useState(props.id);
const handleChange = () => {
setIsChecked(!isChecked);
console.log(id);
};
return (
<input
type="checkbox"
className={style.contact_input}
checked={isChecked}
onChange={handleChange}
value={id}
/>
);
}

When you filter the contacts and update the contacts state, the list in ContactList will be re-rendered as it is a new array which means you will have a new set of Input components with the default state. In order to persist the previously selected items you will also have to store the array of selected IDs in state. The Input component should receive the isChecked and onChange values from props so you can pass in a condition where you check if the current ID is in the list of selected IDs which means it is checked and when the user clicks on it, you can just simply update that array by adding the ID if it's not currently in the array or removing from it if it is already present (copying the array first since it's a state variable).

Related

The component dot't render when it must be rendered

https://github.com/patr4519/githubUserInfo.git
The essence of the application:
In input entering logins of github users separated by commas, the app shows the result consisting of photo, user name and data reg. After entering the user (users) into the input and pressing the Enter button, an array with users in the form of objects gets into the users state, which are then used to display cards.
The problem is that the Main component is rendered only after the Enter button is pressed and some other action is performed, for example, pressing the Clear button or additional input in the input field, despite the fact that the users state is updated after pressing the Enter button. And in theory, there should be a rerender, which does not happen when it is necessary.
What could be the problem? I attach a link to the repository.
function App() {
return (
<div className='wrapper'>
<Nav />
<Header />
<Main />
</div>
);
}
function Main() {
const [users, setUsers] = React.useState([]);
const [searchValue, setSearchValue] = React.useState('');
const onChangeSearchValue = (event) => {
setSearchValue(event.target.value);
}
const addUsers = () => {
let arrOfJson = []
for (let user of searchValue.split(', ')) {
fetch(`https://api.github.com/users/${user}`)
.then(us => us.json())
.then((json) => arrOfJson.push(json))
.catch((err) => {
console.log(err);
})
}
setUsers(arrOfJson);
}
const clearInput = () => {
setSearchValue('')
}
return (
<div className='main'>
<InputForm addUsers={addUsers} onChangeSearchValue={onChangeSearchValue} clearInput={clearInput} />
<Users users={users} />
</div>
);
}
export default App;
The fetch is asynchronous so when you have setUsers after a loop inside addUsers, it stores an empty array actually because data hasn't been populated yet:
const addUsers = () => {
let arrOfJson = []
for (let user of searchValue.split(', ')) {
fetch(`https://api.github.com/users/${user}`)
.then(us => us.json())
.then((json) => arrOfJson.push(json))
.catch((err) => {
console.log(err);
})
}
setUsers(arrOfJson); // sets an empty array
}
You can fix with something like this:
const addUsers = async () => {
for (let user of searchValue.split(", ")) {
let resultJson = await fetch(`https://api.github.com/users/${user}`);
let result = await resultJson.json();
setUsers((ps) => [...ps, result]);
}
};

How can I make syntax less repetitive (DRY)?

The object of this app is to allow input text and URLs to be saved to localStorage. It is working properly, however, there is a lot of repeat code.
For example, localStoredValues and URLStoredVAlues both getItem from localStorage. localStoredValues gets previous input values from localStorage whereas URLStoredVAlues gets previous URLs from localStorage.
updateLocalArray and updateURLArray use spread operator to iterate of previous values and store new values.
I would like to make the code more "DRY" and wanted suggestions.
/*global chrome*/
import {useState} from 'react';
import List from './components/List'
import { SaveBtn, DeleteBtn, DisplayBtn, TabBtn} from "./components/Buttons"
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
//these items are used for the state of localStorage
const [display, setDisplay] = useState(false);
const localStoredValues = JSON.parse(
localStorage.getItem("localValue") || "[]"
)
let updateLocalArray = [...localStoredValues, leadValue.inputVal]
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = JSON.parse(localStorage.getItem("URLValue") || "[]")
const tabBtn = () => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
const url = tabs[0].url;
setMyLeads((prev) => [...prev, url]);
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
localStorage.setItem("URLValue", JSON.stringify(updateURLArray));
});
setDisplay(false)
};
//handles change of input value
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
setDisplay(false);
// update state of localStorage
localStorage.setItem("localValue", JSON.stringify(updateLocalArray))
};
const displayBtn = () => {
setDisplay(true);
};
const deleteBtn = () => {
window.localStorage.clear();
setMyLeads([]);
};
const listItem = myLeads.map((led) => {
return <List key={led} val={led} />;
});
//interates through localStorage items returns each as undordered list item
const displayLocalItems = localStoredValues.map((item) => {
return <List key={item} val={item} />;
});
const displayTabUrls = URLStoredVAlues.map((url) => {
return <List key={url} val={url} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<TabBtn tabBtn={tabBtn} />
<DisplayBtn displayBtn={displayBtn} />
<DeleteBtn deleteBtn={deleteBtn} />
<ul>{listItem}</ul>
{/* displays === true show localstorage items in unordered list
else hide localstorage items */}
{display && (
<ul>
{displayLocalItems}
{displayTabUrls}
</ul>
)}
</main>
);
}
export default App
Those keys could be declared as const and reused, instead of passing strings around:
const LOCAL_VALUE = "localValue";
const URL_VALUE = "URLValue";
You could create a utility function that retrieves from local storage, returns the default array if missing, and parses the JSON:
function getLocalValue(key) {
return JSON.parse(localStorage.getItem(key) || "[]")
};
And then would use it instead of repeating the logic when retrieving "localValue" and "URLValue":
const localStoredValues = getLocalValue(LOCAL_VALUE)
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = getLocalValue(URL_VALUE)
Similarly, with the setter logic:
function setLocalValue(key, value) {
localStorage.setItem(key, JSON.stringify(value))
}
and then use it:
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
setLocalValue(URL_VALUE, updateURLArray);
// update state of localStorage
setLocalValue(LOCAL_VALUE, updateLocalArray)

Rendering Items Array in localStorage to Unordered List

When I click DisplayBtn() it should sets the display state to true and display myLeads Array from localStorage. localStorage contains MyLeads Array and I've used a map() in an attempt to fetch items and place them in an unordered list. I've done this before on arrays and it has worked but its not currently working.
Basically, I just want them items in localStorage to render in an unordered list. I've attempted several approaches to solve this issue my latest error message is 'Cannot read properties of null (reading 'map')'
import {useState} from 'react';
import List from './components/List'
import { SaveBtn } from './components/Buttons';
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: ""
})
const [display, setDisplay] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const [localItems, setLocalItems] = useState(
JSON.parse(localStorage.getItem("myLeads"))
);
const displayLocalItems = localItems.map((item) => {
return <List key={item} val={item}/>
})
const saveBtn = () => {
setMyLeads(prev => [...prev, leadValue.inputVal]);
localStorage.setItem("myLeads", JSON.stringify(myLeads))
setLocalItems((prevItems) => [...prevItems, leadValue.inputVal]);
setDisplay(false);
};
const displayBtn = () => {
setDisplay(true)
};
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<button onClick={displayBtn}>Display Leads</button>
{display && (
{displayLocalItems}
)
}
</main>
);
}
export default App;
You can do this:
const [localItems, setLocalItems] = useState(JSON.parse(localStorage.getItem("myLeads")) || []);
So if the local storage is empty you initialize your state to an empty array, which can be safely mapped.

How to Filter by States Within React Components?

I am making a front-end UI returning student objects (grades, email, etc.) from an API call. I currently have a filter set up to return objects by names. I need to set up a second filter by tags, which can be added through an input element within each student component returned from .map() the API. I cannot figure out how to set up the filter as the tags are stored within each instance of the student Profile.js component. Can you please help me? Ultimately the UI should return search results from both filters (name && tags)
Snippet from App.js:
function App() {
const [students, setStudents] = useState([])
const [filteredStudents, setFilteredStudents] = useState([])
const [search, setSearch] = useState("")
// Get request and store response in the 'students' state //
useEffect(()=>{
axios.get('Link to the API')
.then(res => {
const result = res.data.students
setStudents(result)
})
},[])
// Filter students by name and store filtered result in 'filteredStudents' //
useEffect(() => {
const searchResult = []
students.map(student => {
const firstName = student.firstName.toLowerCase()
const lastName = student.lastName.toLowerCase()
const fullName = `${firstName}` + ` ${lastName}`
if (fullName.includes(search.toLowerCase())) {
searchResult.push(student)
}
return
})
setFilteredStudents(searchResult)
}, [search])
return (
<div>
<SearchBar
search={search}
onChange={e => setSearch(e.target.value)}
/>
//Second search bar by tag here//
{search.length == 0 &&
//unfiltered students object here
}
{search.length != 0 &&
<div>
{filteredStudents.map(student => (
<Profile
//Some props here//
/>
))}
</div>
}
</div>
)}
Snippet from Profile.js
//bunch of code before this line//
const [tags, setTags] = useState([])
const [tag, setTag] = useState("")
function handleKeyPress(e) {
if(e.key === 'Enter') {
tags.push(tag)
setTag("")
}
}
return(
<div>
//bunch of code before this line//
<Tag
onChange={e => setTag(e.target.value)}
onKeyPress={handleKeyPress}
tags={tags}
tag={tag}
/>
</div>
)
Snippet from Tag.js:
export default function Tag({tag, tags, onChange, onKeyPress}) {
return (
<div>
{tags.length > 0 &&
<div>
{tags.map(tag => (
<span>{tag}</span>
))}
</div>
}
<input
type='text'
value={tag}
placeholder="Add a tag"
key='tag-input'
onKeyPress={onKeyPress}
onChange={onChange}
/>
</div>
)
}
Edit
With your comment I think I now understand what you're trying to do, the images you provided really helped. You want to change the student object whenever a tag is added in the Profile component if I'm not mistaken (again, correct me if I am). That would mean the Profile component needs access to a handler so that whenever a tag is added, it also sets a new students state. It would look like this:
App.js
function App() {
const [students, setStudents] = useState([]);
const [filteredStudents, setFilteredStudents] = useState([]);
const [search, setSearch] = useState("");
const handleTagAdded = (tag, index) => {
setStudents((prevStudents) => {
// We copy object here as the student we're accessing
// is an object, and objects are always stored by reference.
// If we didn't do this, we would be directly mutating
// the student at the index, which is bad practice
const changedStudent = {...prevStudents[index]};
// Check if student has 'tags` and add it if it doesn't.
if (!("tags" in changedStudent)){
changedStudent.tags = [];
}
// Add new tag to array
changedStudent.tags.push(tag);
// Copy array so we can change it
const mutatableStudents = [...prevStudents];
mutatableStudents[index] = changedStudent;
// The state will be set to this array with the student
// at the index we were given changed
return mutatableStudents;
})
}
// Get request and store response in the 'students' state //
useEffect(() => {
axios.get("Link to the API").then((res) => {
const result = res.data.students;
setStudents(result);
});
}, []);
// Filter students by name and tag, then store filtered result in //'filteredStudents'
useEffect(() => {
// Array.filter() is perfect for this situation //
const filteredStudentsByNameAndTag = students.filter((student) => {
const firstName = student.firstName.toLowerCase();
const lastName = student.lastName.toLowerCase();
const fullName = firstName + lastName;
if ("tags" in student){
// You can now do whatever filtering you need to do based on tags
...
}
return fullName.includes(search.toLowerCase()) && yourTagComparison;
});
setFilteredStudents(filteredStudentsByNameAndTag);
}, [search]);
return (
<div>
<SearchBar search={search} onChange={(e) => setSearch(e.target.value)} />
//Second search bar by tag here //
{search.length === 0 &&
// unfiltered students //
}
{search.length !== 0 && (
<div>
{filteredStudents.map((student, index) => (
<Profile
// Some props here //
onTagAdded={handleTagAdded}
// We give the index so Profile adds to the right student
studentIndex={index}
/>
))}
</div>
)}
</div>
);
}
In handleTagAdded, I copy the object at prevStudents[index] because it is a reference. This may sound odd if you don't know what I'm referring to (pun intended). Here is a link to an article explaining it better than I will be able to.
Profile.js
function Profile({ onTagAdded, studentIndex }) {
// Other stuff //
const [tags, setTags] = useState([]);
const [tag, setTag] = useState("");
const handleTagKeyPress = (e) => {
if (e.key === "Enter") {
// Use this instead of tags.push, when changing state you always
// must use the `setState()` function. If the new value depends on the
// previous value, you can pass it a function which gets the
// previous value as an argument like below. It is also bad
// practice to change, or 'mutate' the argument you're given
// so we instead copy it and change that.
setTags((previousTags) => [...previousTags].push(tag));
setTag("");
onTagAdded(tag, studentIndex)
}
};
return (
<div>
// Other stuff
<Tag onChange={(e) => setTag(e.target.value)} onKeyPress={handleTagKeyPress} tags={tags} tag={tag} />
</div>
);
}
Now, each <Profile /> component has its own tags state, but through the use of handleTagAdded(), we can change the student within each profile component based on tags.
Apologies for the confusion in my first answer, I hope this solves your issue!
Old answer
There's a very important concept in React known as "Lifting State". What this means is that if a parent component needs to access the state of a child component, one solution is to 'lift' the state from the child to the parent.
You can read some more about it in the React documentation.
In this example, you need to lift the tag state up from the <Profile /> component to the <App /> component. That way, both search and tag are in the same place and can be compared.
I believe the code below is along the lines of what you want:
App.js
function App() {
const [students, setStudents] = useState([]);
const [filteredStudents, setFilteredStudents] = useState([]);
const [tags, setTags] = useState([]);
const [tag, setTag] = useState("");
const [search, setSearch] = useState("");
const handleTagChange = (e) => setTag(e.target.value);
const handleTagKeyPress = (e) => {
if (e.key === "Enter") {
// Use this instead of tags.push, when changing state you always
// must use the `setState()` function. If the new value depends on the
// previous value, you can pass it a function which gets the
// previous value as an argument like below.
setTags((previousTags) => previousTags.push(tag));
setTag("");
}
};
// Get request and store response in the 'students' state //
useEffect(() => {
axios.get("Link to the API").then((res) => {
const result = res.data.students;
setStudents(result);
});
}, []);
// Filter students by name and tag, then store filtered result in //'filteredStudents'
useEffect(() => {
// Array.filter() is perfect for this situation //
const filteredStudentsByNameAndTag = students.filter((student) => {
const firstName = student.firstName.toLowerCase();
const lastName = student.lastName.toLowerCase();
const fullName = firstName + lastName;
return fullName.includes(search.toLowerCase()) && student.tag === tag;
});
setFilteredStudents(filteredStudentsByNameAndTag);
}, [search]);
return (
<div>
<SearchBar search={search} onChange={(e) => setSearch(e.target.value)} />
//Second search bar by tag here //
{search.length == 0 &&
// unfiltered students //
}
{search.length != 0 && (
<div>
{filteredStudents.map((student) => (
<Profile
// Some props here //
onChange={handleTagChange}
onKeyPress={handleTagKeyPress}
tag={tag}
tags={tags}
/>
))}
</div>
)}
</div>
);
}
Profile.js
function Profile({ onChange, onKeyPress, tags, tag }) {
// Other stuff //
return (
<div>
// Other stuff
<Tag onChange={onChange} onKeyPress={onKeyPress} tags={tags} tag={tag} />
</div>
);
}
We've moved the tag state up to the <App /> component, so now when we filter we can use both the search and tag. I also changed students.map to students.filter as it is a better alternative for filtering an array.
I'm not clear on how you wanted to filter the tags, so I assumed the student object would have a tag attribute. Feel free to correct me about how the data is structured and I'll reformat it.
I hope this helped, let me know if you have any more problems.

Text field should only change for one value and not over the entire list

I have a list and this list has several elements and I iterate over the list. For each list I display two buttons and an input field.
Now I have the following problem: as soon as I write something in a text field, the same value is also entered in the other text fields. However, I only want to change a value in one text field, so the others should not receive this value.
How can I make it so that one text field is for one element and when I write something in this text field, it is not for all the other elements as well?
import React, { useState, useEffect } from 'react'
import axios from 'axios'
function Training({ teamid }) {
const [isTrainingExisting, setIsTrainingExisting] = useState(false);
const [trainingData, setTrainingData] = useState([]);
const [addTraining, setAddTraining] = useState(false);
const [day, setDay] = useState('');
const [from, setFrom] = useState('');
const [until, setUntil] = useState('');
const getTrainingData = () => {
axios
.get(`${process.env.REACT_APP_API_URL}/team/team_training-${teamid}`,
)
.then((res) => {
if (res.status === 200) {
if (typeof res.data !== 'undefined' && res.data.length > 0) {
// the array is defined and has at least one element
setIsTrainingExisting(true)
setTrainingData(res.data)
}
else {
setIsTrainingExisting(false)
}
}
})
.catch((error) => {
//console.log(error);
});
}
useEffect(() => {
getTrainingData();
}, []);
const deleteTraining = (id) => {
axios
.delete(`${process.env.REACT_APP_API_URL}/team/delete/team_training-${teamid}`,
{ data: { trainingsid: `${id}` } })
.then((res) => {
if (res.status === 200) {
var myArray = trainingData.filter(function (obj) {
return obj.trainingsid !== id;
});
//console.log(myArray)
setTrainingData(() => [...myArray]);
}
})
.catch((error) => {
console.log(error);
});
}
const addNewTraining = () => {
setAddTraining(true);
}
const addTrainingNew = () => {
axios
.post(`${process.env.REACT_APP_API_URL}/team/add/team_training-${teamid}`,
{ von: `${from}`, bis: `${until}`, tag: `${day}` })
.then((res) => {
if (res.status === 200) {
setAddTraining(false)
const newTraining = {
trainingsid: res.data,
mannschaftsid: teamid,
von: `${from}`,
bis: `${until}`,
tag: `${day}`
}
setTrainingData(() => [...trainingData, newTraining]);
//console.log(trainingData)
}
})
.catch((error) => {
console.log(error);
});
}
const [editing, setEditing] = useState(null);
const editingTraining = (id) => {
//console.log(id)
setEditing(id);
};
const updateTraining = (trainingsid) => {
}
return (
<div>
{trainingData.map((d, i) => (
<div key={i}>
Trainingszeiten
<input class="input is-normal" type="text" key={ d.trainingsid } value={day} placeholder="Wochentag" onChange={event => setDay(event.target.value)} readOnly={false}></input>
{d.tag} - {d.von} bis {d.bis} Uhr
<button className="button is-danger" onClick={() => deleteTraining(d.trainingsid)}>Löschen</button>
{editing === d.trainingsid ? (
<button className="button is-success" onClick={() => { editingTraining(null); updateTraining(d.trainingsid); }}>Save</button>
) : (
<button className="button is-info" onClick={() => editingTraining(d.trainingsid)}>Edit</button>
)}
<br />
</div>
))}
)
}
export default Training
The reason you see all fields changing is because when you build the input elements while using .map you are probably assigning the same onChange event and using the same state value to provide the value for the input element.
You should correctly manage this information and isolate the elements from their handlers. There are several ways to efficiently manage this with help of either useReducer or some other paradigm of your choice. I will provide a simple example showing the issue vs no issue with a controlled approach,
This is what I suspect you are doing, and this will show the issue. AS you can see, here I use the val to set the value of <input/> and that happens repeatedly for both the items for which we are building the elements,
const dataSource = [{id: '1', value: 'val1'}, {id: '2', value: 'val2'}]
export default function App() {
const [val, setVal]= useState('');
const onTextChange = (event) => {
setVal(event.target.value);
}
return (
<div className="App">
{dataSource.map(x => {
return (
<div key={x.id}>
<input type="text" value={val} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
This is how you would go about it.
export default function App() {
const [data, setData]= useState(dataSource);
const onTextChange = (event) => {
const id = String(event.target.dataset.id);
const val = String(event.target.value);
const match = data.find(x => x.id === id);
const updatedItem = {...match, value: val};
if(match && val){
const updatedArrayData = [...data.filter(x => x.id !== id), updatedItem];
const sortedData = updatedArrayData.sort((a, b) => Number(a.id) - Number(b.id));
console.log(sortedData);
setData(sortedData); // sorting to retain order of elements or else they will jump around
}
}
return (
<div className="App">
{data.map(x => {
return (
<div key={x.id}>
<input data-id={x.id} type="text" value={x.value} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
What im doing here is, finding a way to map an element to its own with the help of an identifier. I have used the data-id attribute for it. I use this value again in the callback to identify the match, update it correctly and update the state again so the re render shows correct values.

Resources