My search bar is delaying updating the results - javascript

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);
}

Related

Rendering two React components in a sequence

In Summary: {
How can I merge those 2 arrays into one. As in, instead of having it like this:
[1st,new1st,...] [2nd,new2nd,...]
I want it to be like this:
[1st,2nd,new1st,new2nd,...]
}
I have this note app that I am creating. I am trying to render the two components so that each note rendered is kind of the last element of an array. So, in short, I want each component to be below the previous added note (think of it like a list where each added input is added after the previous list items).
So, this is how it looks before adding anything.
and this is how it looks after adding one note on each create area.
and this is what I am trying to avoid after adding the new notes from each note create area.
What I want is
-1st -2nd -new1st - new2nd
As in no matter which create area I use, it gets rendered after all the previous ones.
Here's my code
import React, { useState } from "react";
import Header from "./Header";
import Footer from "./Footer";
import Note from "./Note";
import CreateArea from "./CreateArea";
function App() {
const [notes, setNotes] = useState([]);
const [notes2, setNotes2] = useState([]);
function addNote(newNote) {
setNotes(prevNotes => {
return [...prevNotes, newNote];
});
}
function addNote2(newNote) {
setNotes2(prevNotes => {
return [...prevNotes, newNote];
});
}
function deleteNote(id) {
setNotes(prevNotes => {
return prevNotes.filter((noteItem, index) => {
return index !== id;
});
});
}
function deleteNote2(id) {
setNotes(prevNotes => {
return prevNotes.filter((noteItem, index) => {
return index !== id;
});
});
}
return (
<div>
<Header />
<CreateArea onAdd={addNote} />
<CreateArea onAdd={addNote2} />
{notes.map((noteItem, index1) => {
return (
<Note
key={index1}
id={index1}
title={noteItem.title}
content={noteItem.content}
onDelete={deleteNote}
/>
);
})}
{notes2.map((noteItem, index2) => {
return (
<Note
key={index2}
id={index2}
title={noteItem.title}
content={noteItem.content}
onDelete={deleteNote2}
/>
);
})}
<Footer />
</div>
);
}
export default App;
You can test the app by copying the above code instead of App.jsx at CodeSandbox.
I need to do something like that:
-item1
=nested item 1
=nested item 2
=nested item 3
-item 2
so I need the second create area to eventually be used for nested items (children). and the 1st create area to be for 'item1' or 'item2' or ... (parent). But with the way it functions from my code, it gets rendered like that:
-item1
-item2
=nested item 1
=nested item 2
=nested item 3
I don't understand a reason why you would want to do that. You need to either have one list or two. If for rendering, you want it to be one list, you can have that in a single state. Also if it's just about having two input fields to add note, both fields can push to same state. Here is how it could be:
import React, { useState } from "react";
import Header from "./Header";
import Footer from "./Footer";
import Note from "./Note";
import CreateArea from "./CreateArea";
function App() {
const [notes, setNotes] = useState([]);
function addNote(newNote) {
setNotes(prevNotes => {
return [...prevNotes, newNote];
});
}
function deleteNote(id) {
setNotes(prevNotes => {
return prevNotes.filter((noteItem, index) => {
return index !== id;
});
});
}
return (
<div>
<Header />
<CreateArea onAdd={addNote} />
<CreateArea onAdd={addNote} />
{notes.map((noteItem, index1) => {
return (
<Note
key={index1}
id={index1}
title={noteItem.title}
content={noteItem.content}
onDelete={deleteNote}
/>
);
})}
<Footer />
</div>
);
}
export default App;
Well, if you still want it :D then here is a thing you can do:
import React, { useState } from "react";
import Header from "./Header";
import Footer from "./Footer";
import Note from "./Note";
import CreateArea from "./CreateArea";
function App() {
const [notes, setNotes] = useState([]);
const [notes2, setNotes2] = useState([]);
const [combinedNotes, setCombinedNotes] = useState([]);
useEffect(() => {
const notesList = [...notes, ...notes2].sort((note1, note2) => note1.timestamp - note2.timestamp);
setCombinedNotes(notesList);
}, [notes, notes2]);
function addNote(newNote) {
setNotes(prevNotes => {
return [...prevNotes, { ...newNote, timestamp: new Date().getTime() }];
});
}
function addNote2(newNote) {
setNotes2(prevNotes => {
return [...prevNotes, { ...newNote, timestamp: new Date().getTime() }];
});
}
function deleteNote(id) {
const isFirstNote = notes.find((note) => note.timestamp === id);
if (isFirstNote) {
setNotes(prevNotes => {
return prevNotes.filter((noteItem) => {
return noteItem.timestamp !== id;
});
});
} else {
setNotes2(prevNotes => {
return prevNotes.filter((noteItem) => {
return noteItem.timestamp !== id;
});
});
}
}
return (
<div>
<Header />
<CreateArea onAdd={addNote} />
<CreateArea onAdd={addNote2} />
{combinedNotes((noteItem, index) => {
return (
<Note
key={index}
id={noteItem.timestamp}
title={noteItem.title}
content={noteItem.content}
onDelete={deleteNote}
/>
);
})}
<Footer />
</div>
);
}
export default App;

What can I do to apply a prop to only one item in a map/array on the render method of a React component?

I am trying to apply a change to only one item in an array depending on a prop.
I have this list:
interface FieldProps {
fileLimitWarning?: // I STILL NEED TO FIGURE THIS OUT
}
const DataField = (props: FieldProps) => {
const { fileLimitWarning, handleFileSelection } = props;
return (
<>
{fileLimitWarning && <p>This file exceeds the max size of 250mb</p>}
<input onChange={e => handleFileSelection(e.target.files)} />
</>
}
const ContainerOfDataField = () => {
const [fileLimitWarning, setFileLimitWarning] = useState(false);
const handleFileSelection = (files) => {
setFileLimitWarning(false);
const [currentFile] = files;
if(currentFile?.size <= 250000000) {
setFileLimitWarning(true);
}
}
return (
<ul>
{
fields?.map((promoField: Field, index: number) => {
return (
<DataField
key={index}
fileLimitWarning={fileLimitWarning}
handleFileSelection={handleFileSelection}
/>
);
})
}
</ul>
)
}
In this case what I want is to only return null in the DataField component when it corresponds. Right now whenever that fileLimitWarning === true on the ContainerOfDataField component, it hides/removes/deletes all of the Typography nodes from the DataField component. So what I need is to hide only the index that matches where the problem is coming from.
Is it clear?
I think ideally you would define fileLimitWarning in each iteration of your map, since (I assume) it is a property of the current item, rather than a global property:
return (
<ul>
{
fields?.map((promoField: Field, index: number) => {
// { currentFile } = promoField???
return (
<DataField
key={index}
fileLimitWarning={currentFile?.size <= 250000000}
handleFileSelection={handleFileSelection}
/>
);
})
}
</ul>
)
}

How to search innerHTML in react

I have a list of users on the page.
Each student has an input filed where user can add tags to their profile. There's a search bar on top of the all the students, searchStudentByTags. I am trying to implement this function, but have not been able to solve it yet. Any help would be appreciated.
This is the StudentContainer component where has the searchStudnetByTags function I write so far but not working
import React, { useState, useMemo } from "react";
import Student from "./Student";
import Input from "./Input";
import "../stylesheets/StudentsContainer.scss";
const StudentsContainer = ({ students }) => {
const [searchByName, setSearchByName] = useState("");
const [searchByTags, setSearchByTags] = useState("");
const filteredStudents = useMemo(
() =>
students.filter(
({ firstName, lastName }) =>
searchByName.length < 2 ||
(firstName + " " + lastName)
.toLowerCase()
.includes(searchByName.toLowerCase())
),
[students, searchByName]
);
const renderStudentsByTagSearch = ({ target }) => {
setSearchByTags(target.value);
const studentsContainer = document.querySelector(".students-container");
const allStudents = studentsContainer.getElementsByClassName("student");
const nameTags = document.querySelectorAll(".tag");
for (let i = 0; i < allStudents.length; i++) {
const student = allStudents[i];
const tag = nameTags[i];
if (
searchByTags.length > 1 &&
student.contains(tag) &&
tag.innerHTML.includes(searchByTags)
) {
student.style.display = "";
} else if (
searchByTags.length > 1 &&
student.contains(tag) &&
!tag.innerHTML.includes(searchByTags)
) {
student.style.display = "none";
} else if (searchByTags.length > 1 && !student.contains(tag)) {
student.style.display = "none";
} else if (searchByTags.length === 0 || !student.contains(tag)) {
student.style.display = "";
}
}
};
return (
<section className="students-container">
<Input
value={searchByName}
placeholder="Search by name"
onChange={({ target }) => setSearchByName(target.value)}
/>
<Input
className="tag-input"
value={searchByTags}
placeholder="Search by tag"
onChange={renderStudentsByTagSearch}
/>
{filteredStudents.map((student) => (
<Student
key={student.id}
student={student}
/>
))}
</section>
);
};
export default StudentsContainer;
This is the Student component
import React, { useState } from "react";
import "../stylesheets/Student.scss";
import AddTag from "./AddTag";
const Student = ({ student, addTagClick }) => {
const averageGrade =
student.grades.reduce((acc, grade) => {
return parseInt(acc) + parseInt(grade);
}) / student.grades.length;
const [isViewScores, setIsViewScores] = useState(false);
const viewScoreClick = () => {
setIsViewScores((prev) => !prev);
};
return (
<article className="student">
<figure>
<img src={student.pic} alt="student" />
</figure>
<aside>
<h2>
{student.firstName} {student.lastName}
</h2>
<ul>
<li>Email: {student.email}</li>
<li>Company: {student.company}</li>
<li>Skill: {student.skill}</li>
<li>
Average: {averageGrade}%
{isViewScores && (
<ul className="scores">
{student.grades.map((grade, index) => {
return (
<li key={index}>
Test {index + 1}: {grade}%
</li>
);
})}
</ul>
)}
</li>
</ul>
<AddTag studentId={student.id} addTagClick={addTagClick}/>
</aside>
<button onClick={viewScoreClick} className="view-scores-btn">
{isViewScores ? "-" : "+"}
</button>
</article>
);
};
export default Student;
This is the AddTag component
import React, { useState } from "react";
import { generateId } from "../helper";
import Input from "./Input";
const AddTag = ({ studentId }) => {
const [tag, setTag] = useState("");
const [tags, setTags] = useState([]);
const handleInputChange = ({ target }) => {
setTag(target.value);
};
const onSubmitClick = (e) => {
e.preventDefault();
const newTag = {
tag: tag,
id: generateId(),
studentId: studentId,
};
setTags((prev) => {
if (tag) {
return [newTag, ...prev];
} else {
return [...prev];
}
});
setTag("");
};
return (
<>
<div className="tags-container">
{tags.map((tag) => (
<button className="tag" key={tag.id}>
{tag.tag}
</button>
))}
</div>
<form onSubmit={onSubmitClick}>
<Input
className="add-tag-input"
placeholder="Add a tag"
type="text"
value={tag}
onChange={handleInputChange}
/>
</form>
</>
);
};
export default AddTag;
You need to approach this differently.. where the array of tags are available at the top level component - rather than doing DOM manipulation. Move
const [tags, setTags] = useState([]);
Into the StudentsContainer, and pass it down through Students and Add Tag as props, then refactor your search to use tags.
I've added a code sandbox here, with a basic gist of how I'd approach it.
https://codesandbox.io/s/frosty-ishizaka-hui8j
Theres quite a bit going in this question so we should focus on simplifying the problem by removing everything that is of no concern.
So how do we only render those students who have the tag that we currently are searching for? By using Array.prototype.filter() before we map over students and return a <Student /> for each array item.
import React, { useState } from "react";
const data = [
{id:1,firstName:"Mickey",lastName:"Mouse",tags:[{id:1,label:"mouse"}]},
{id:2,firstName:"Donald",lastName:"Duck",tags:[{id:1,label:"duck"}]},
{id:3,firstName:"Minnie",lastName:"Mouse",tags:[{id:1,label:"mouse"},{id:2,label:"cool"}]}
];
const StudentsContainer = ({ students = data }) => {
const [searchByTagsValue, setSearchByTagsValue] = useState("");
return (
<>
<input
value={searchByTagsValue}
placeholder="Search by tag"
onChange={(e) => setSearchByTagsValue(e.target.value)}
/>
{students.length &&
students
.filter((student) => shouldStudentDisplay(student.tags, searchByTagsValue))
.map((student) => <Student key={student.id} student={student} />)}
</>
);
};
const Student = ({ student, style }) => (
<div style={style}>
<h5>
{student.firstName} {student.lastName}
</h5>
<Tags tags={student.tags} />
<hr />
</div>
);
const Tags = ({ tags }) => (
<ul>
{tags.map((tag) => (
<li key={tag.id}>{tag.label}</li>
))}
</ul>
);
const shouldStudentDisplay = (tags, searchByTagsValue) => {
if (!searchByTagsValue) {
return true;
}
return tags.findIndex(({ label }) => label === searchByTagsValue) !== -1;
};
export default StudentsContainer;
Once you can filter your data in place like above, you need an updater function in StudentsContainer that will take a student id, and a new tag name, and update (a localised version of) the students data.
Pass this updater function all the way from StudentsContainer down to Tags so it can update the data in the ancestor component (commonly referred to as prop drilling).
const [localStudents, setLocalStudents] = useState(students);
const onSubmitTag = (label, id) => {
const index = localStudents.findIndex((student) => student.id === id);
if (index !== -1) {
const newStudents = [...localStudents];
newStudents[index] = {
...newStudents[index],
tags: [...newStudents[index].tags, { id: Date.now(), label }]
};
setLocalStudents(newStudents);
}
};
As you can see, we aren't really searching through the HTML to hide and show things in an imperative way.
In react, we are encouraged to update the source data, and allow the rendered UI to react in a declarative way.
React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes.
Declarative views make your code more predictable and easier to debug.

How to toggle between boolean based on another value in react

If a user is selected from the list, the boolean is true, when the selected user is unselected from the list, the boolean is false. The boolean will remain true if another user is selected from the list.
Here's my code:
import React, { useEffect, useState } from "react";
import "./styles.css";
import Users from "./Users";
export default function UserApp() {
const [ selectUser, setSelectUser ] = useState(null);
const [ isUserSelected, setIsUserSelected ] = useState(false);
const handleUserClick = (user) => {
setSelectUser((prev) => (
user !== prev ? user : null
))
if(selectUser === null) {
setIsUserSelected(!isUserSelected)
} else {
setIsUserSelected(isUserSelected)
}
}
console.log(selectUser);
console.log(isUserSelected);
return (
<div className="App">
<Users selectedUser={handleUserClick} />
</div>
);
}
Update your if statement:
if(selectUser !== null) {
setIsUserSelected(true)
} else {
setIsUserSelected(false)
}
OR:
if(selectUser === null) {
setIsUserSelected(false)
} else {
setIsUserSelected(true)
}
You can check whether user is selected or not without using hooks. All you need is one state hook.
const User = props => {
const [selectedUser, setSelectedUser] = useState(null);
const handleUserSelect = user => setSelectedUser(user.id === selectedUser.id ? null : user);
return (
<>
<Users onSelectUser={handleUserSelect} />
<Checkbox checked={Boolean(selectedUser)} />
</>
)
}
If selectedUser is null Boolean(selectedUser) will return false otherwise it will return true. You don't need extra hook for this.
You do not need to store the 'isUserSelect' information since it will be redundant with the 'selectUser === null' condition.
Here is a code proposal
export default function UserApp() {
const [ selectedUser, setSelectedUser ] = useState(null);
const handleUserClick = (user) => {
if(selectedUser === user) {
setSelectedUser(null)
} else {
setSelectedUser(user)
}
}
return (
<div className="App">
<Users onUserClick={handleUserClick} />
</div>
)
}
export default function Users({ onUserClick }) {
return (
<ul>
{USERS_DATA.map((user, index) => (
<li
key={index}
className="users"
onClick={() => onUserClick(user)}
>
{user.user_name}
</li>
))}
</ul>
)
}
note: code not tested, might contain some typos

React map doesn't re-render as state changes

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));
}
}

Categories

Resources