React js useState&useEffect array duplicates elements after a change - javascript

I am a beginner in react js programming. I'm trying to do the todo project, which is a classic project. When I delete or add an element from the list, the newly formed list appears on the screen by combining with the previous one, I will show it with a picture below. I did not understand the source of the eror so wanted to post it here to get some advices suggestions about why it is happening.Thank you.(I am getting and storing data in firebase firestore database)
Before Adding an element initial array state
After adding an element to the array.
I am using useState for array and using useEffect to get initial data
MainPage.js that contains form and the list components.
const MainPage = () => {
const [isLoading, setLoding] = useState(true);
const [array, setArray] = useState([]);
const sub = async (email) => {
var result = [];
await onSnapshot(doc(db, "users", email), (doc) => {
var data = doc.data().todos;
data.forEach((element) => {
Object.keys(element).map(() => {
result.push(element["title"]);
});
});
setArray(result);
setLoding(false);
});
};
useEffect(() => {
sub(auth.currentUser.email);
}, []);
const onAddToDo = (todoTitle) => {
setArray((prevAray) => {
return [...prevAray, todoTitle];
});
};
const onRemove = (title) => {
setArray((prevAray) => {
return [array.pop(array.indexOf(title))];
});
};
return (
<div>
{isLoading && <h1>Loading</h1>}
{!isLoading && (
<div>
<section>
<NavBar></NavBar>
<ToDoForm passData={onAddToDo} />
</section>
<section>
<CardList removeCards={onRemove} array={array} />
</section>
</div>
)}
</div>
);
};
export default MainPage;
Firebase.js that stores the firebase update methods
export const deleteItem = (title) => {
updateDoc(doc(db, "users", auth.currentUser.email), {
todos: arrayRemove({ title: title }),
});
};
export const addnewTodo = (title) => {
updateDoc(doc(db, "users", auth.currentUser.email), {
todos: arrayUnion({ title: title }),
});
};
TodoForm.js component
const ToDoForm = (props) => {
const [todoTitle, setTitle] = useState("");
const titleChangeHandler = (event) => {
setTitle(event.target.value);
};
const newTodoAdder = (event) => {
event.preventDefault();
addnewTodo(todoTitle);
props.passData(todoTitle);
};
return (
<div className="form_holder">
<div className="form_container">
<form onSubmit={newTodoAdder}>
<h3>Add Events</h3>
<label>Title</label>
<input
onChange={titleChangeHandler}
type="text"
placeholder="Title"
id="title"
></input>
<div className="holder">
<button type="sumbit">Add</button>
</div>
</form>
</div>
</div>
);
};
export default ToDoForm;
CardList.js component
const CardList = (props) => {
const array = props.array;
if (array.length === 0) {
return (
<div className="grid_container">
<h2>Found no todos</h2>
</div>
);
}
return (
<div className="grid_container">
{array.map((element, index) => {
return (
<Card
removeSelf={() => {
props.removeCards(element);
}}
key={index}
title={element}
/>
);
})}
</div>
);
};
export default CardList;
Card.js component
const Card = (props) => {
const handleRemove = (event) => {
event.preventDefault();
deleteItem(props.title);
props.removeSelf();
};
return (
<div className="card">
<h2 className="card__title">{props.title}</h2>
<button type="button" onClick={handleRemove}>
Delete
</button>
</div>
);
};
export default Card;
EDIT ;
Index.js file
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);

SOLUTION
I fixed the issue by changing the add and remove functions that were inside of MainPage.js file You can see the new versions bellow. Hope someday it will help somebody.
Use effect was called once all I had to do get the data again after a change...
New Remove and Add functions
const onAddToDo = (todoTitle) => {
console.log(todoTitle + " Added");
sub(auth.currentUser.email);
};
const onRemove = (title) => {
console.log(title + " Deleted");
sub(auth.currentUser.email);
};

Related

How can I prevent the array of identifier tags for each student disappearing after re-fetching from API?

I have a list of students that will display onto the web browser depending on what you filter by name/tag. If those filter fields become empty, the page re-fetches all the students from an API and displays them.
The tags are stored in an array using useState for each Student object.
Example Problem: After adding a tag to a student, then somehow filtering the students, and then finally clearing the filter fields, all the students will be displayed again but WITHOUT their tags.
Expected Outcome: I need the student to keep their tags, at least for a current session on the website.
Question: How can I solve this? Should I use localStorage? or a Database such as MongoDB? or something else?
Students.jsx
import { useState } from 'react';
import styles from "../views/Home.module.css";
import { v4 as uuidv4 } from 'uuid';
import AddIcon from '#mui/icons-material/Add';
import RemoveIcon from '#mui/icons-material/Remove';
const Students = ({student}) => {
const [isShown, setIsShown] = useState(true);
const [tags, setTags] = useState([]);
const findAverageGrade = arr => {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += parseInt(arr[i]);
}
return sum / arr.length;
}
const addTag = (event) => {
if (event.key === 'Enter') {
setTags([...tags, event.target.value])
event.target.value = "";
}
}
return (
<div key={student.email} className={styles.studentItem}>
<img className={styles.studentImage} src={student.pic} />
<div className={styles.studentInfoContainer}>
<div className={styles.studentHeader}>
<p className={styles.studentName}>{student.firstName.toUpperCase()} {student.lastName.toUpperCase()}</p>
<button className={styles.expandBtn} onClick={() => {
setIsShown(!isShown);
}}>
{ isShown ? <AddIcon className={styles.expandBtn} /> : <RemoveIcon className={styles.expandBtn} /> }
</button>
</div>
<ul className={styles.studentDetail}>
<li>Email: {student.email}</li>
<li>Company: {student.company}</li>
<li>Skill: {student.skill}</li>
<li>Average: {findAverageGrade(student.grades)}%</li>
{!isShown ? <div>
<table className={styles.gradesTable}>
<tbody>
{student.grades.map((grade) => (
<tr key={uuidv4()}>
<td>Test</td>
<td>{grade}%</td>
</tr>
))}
</tbody>
</table>
</div>
: null }
<div className={styles.tagOutput}>
{tags.map(tag => (<p className={styles.tag}>{tag}</p>))}
</div>
<input id="tag-input" className={styles.addTagInput} type="text" placeholder="Add a tag" onKeyPress={(e) => addTag(e)}/>
</ul>
</div>
</div>
)
}
export default Students;
Home.jsx
import axios from 'axios';
import { useState, useEffect } from 'react';
import Students from '../components/Students';
import styles from "./Home.module.css";
const Home = () => {
const [students, setStudents] = useState([]);
const [nameFilteredStudents, setNameFilteredStudents] = useState([]);
const [tagFilteredStudents, setTagFilteredStudents] = useState([]);
const fetchStudents = async () => {
const response = await axios.get(`https://api.hatchways.io/assessment/students`);
setStudents(response.data.students);
setNameFilteredStudents(response.data.students);
console.log(response.data.students);
}
const filterStudentName = async (searchName) => {
const searchNameFiltered = searchName.toLowerCase();
console.log(searchNameFiltered);
if (searchNameFiltered === "") {
fetchStudents();
return;
}
var newArray = await students.filter((student) => {
return student.firstName.toLowerCase().includes(searchNameFiltered)
|| student.lastName.toLowerCase().includes(searchNameFiltered);
})
await setNameFilteredStudents(newArray);
}
const filterStudentTag = async (searchTag) => {
const searchTagFiltered = searchTag.toLowerCase();
console.log(searchTagFiltered)
console.log(students.filter((student) => {
console.log(student);
}))
// var newArray = await students.filter((student) => {
// return student.firstName.toLowerCase().includes(searchNameFiltered)
// || student.lastName.toLowerCase().includes(searchNameFiltered);
// })
}
useEffect(() => {
fetchStudents();
}, [])
return(
<>
<div>
<input className={styles.searchInput} type="text" placeholder="Search by name" onChange={(event) => filterStudentName(event.target.value) }/>
<input className={styles.searchInput} type="text" placeholder="Search by tag" onChange={(event) => filterStudentTag(event.target.value) }/>
{nameFilteredStudents.map((student) => (
<Students key={student.id} student={student} />
))}
</div>
</>
)
}
export default Home;
Since you are passing the students prop to the child component, any time the students change the component will be re-rendered. Also since the filter is in the parent component, the child component will re-render because you are calling fetchStudents() in the filter function. You can toy with changing how you filter the students.

how can i display an array with the highest price to the lowest

I have connected to an api and have pulled some data into my project with the name of 'data'. This data is being rendered dynamically into a card component. I am now trying to arrange the order from highest price to lowest price on the click of a button with useState but cannot figure it out. Below is what i have so far:
import React, { useState } from "react";
import "./App.scss";
import { useQuery } from "#apollo/react-hooks";
import GET_PRODUCTS_IN_COLLECTION from "./gql/getCollection";
import ProductCard from "./components/ProductCard/ProductCard";
const App = (props) => {
const { data, loading, error } = useQuery(GET_PRODUCTS_IN_COLLECTION, {
variables: {
count: 10,
handle: "skateboard",
},
});
// console.log(data)
const [reversed, setReversed] = useState(false);
const [highLow, setHighLow] = useState(false);
const [lowHigh, setLowHigh] = useState(false);
const [remove, setRemove] = useState(false);
const reverseOrder = () => {
setReversed(!reversed);
};
const highToLow = () => {
setHighLow(!highLow);
};
const lowToHigh = () => {
setLowHigh(!lowHigh);
};
const removeLast = () => {
setRemove(!remove);
};
if (loading) {
// Data is still loading....
return <div className="App">Loading....</div>;
}
return (
<div className="App">
<header className="App-header"></header>
<main>
<div className="buttonGroup">
<button onClick={reverseOrder}>Reverse Product Order</button>
<button onClick={highToLow}>Price High to Low</button>
<button onClick={lowToHigh}>Price Low to High</button>
<button onClick={removeLast}>Display 9 products</button>
</div>
{/*
Your render components go here
*/}
<div className="ProductList">
{reversed
? data.collectionByHandle.products.edges
.slice()
.reverse()
.map((product) => {
return <ProductCard productData={product} />;
})
: highLow
? data.collectionByHandle.products.edges
.slice()
.sort((a,b) => (a.node.vendor - b.node.vendor))
.map((product) => {
return <ProductCard productData={product} />;
})
: lowHigh
? data.collectionByHandle.products.edges
.slice()
.map((product) => {
return <ProductCard productData={product} />;
})
.splice(1)
: remove
? data.collectionByHandle.products.edges
.slice()
.map((product) => {
return <ProductCard productData={product} />;
})
.splice(1)
: data.collectionByHandle.products.edges.map((product) => {
return <ProductCard productData={product} />;
})}
</div>
</main>
</div>
);
};
export default App;
image of array
You can change your code like the following example:
Some points to keep in mind :
Try to avoid if statment in JSX .
Put your events in seprated functions to make it easy for you to manage .
import React, { useState ,useEffect} from "react";
import "./App.scss";
import { useQuery } from "#apollo/react-hooks";
import GET_PRODUCTS_IN_COLLECTION from "./gql/getCollection";
import ProductCard from "./components/ProductCard/ProductCard";
const App = (props) => {
const [myData, setMyData] = useState(data);
useEffect (() => {
const { data, loading, error } = useQuery(GET_PRODUCTS_IN_COLLECTION,
{
variables: {
count: 10,
handle: "skateboard",
},
});
setMyData(data);
},[]);
const reverseOrder = () => {
let newData = myData.reverse();
setMyData([...newData]);
};
const highToLow = () => {
let newData = myData.sort((a, b) => b.node.vendor- a.node.vendor);
setMyData([...newData]);
};
const lowToHigh = () => {
let newData = myData.sort((a, b) => a.node.vendor- b.node.vendor);
setMyData([...newData]);
};
const removeLast = () => {
myData.splice(-1, 1);
setMyData([...myData]);
};
if (loading) {
// Data is still loading....
return <div className="App">Loading....</div>;
}
return (
<div className="App">
<header className="App-header"></header>
<main>
<div className="buttonGroup">
<button onClick={reverseOrder}>Reverse Product Order</button>
<button onClick={highToLow}>Price High to Low</button>
<button onClick={lowToHigh}>Price Low to High</button>
<button onClick={removeLast}>Display 9 products</button>
</div>
{
myData.map((product) => {
return <ProductCard productData={product} />;
});
}
</div>
</main>
</div>
);
};
export default App;
Assuming the values are alphanumerical javascript has built in function "sort" to do that. Even if they are not numerical there has to be a way to read their value that you can use!
Then its pretty straight forward (modified from w3schools):
const fruits = [2,1,"Banana", "Orange", "Apple", "Mango"];
fruits.sort();
will create array [1,2,Apple,Banana,Mango,Orange]
You should be able to do something along these lines in your program.
(just droping: if you want to reverse the order simply use reverse() method on array)
I don't know what your data looks like but this should work.
https://www.w3schools.com/js/js_array_sort.asp

How can I create an instance of an Object in React..?

I am very new to react and javascript, but I am trying to build a simple ToDo App. It wasn't complicated until I wanted to read data from a file and to display that data on the screen. The problem is that I don't know how to create a new Todo object to pass it as parameter for addTodo function.. Thaaank you all and hope you can help me!!
I will let the code here (please see the -loadFromFile- function, there is the problematic place:
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import Todo from './Todo';
import data from './data/data.json'
function TodoList() {
const [todos, setTodos] = useState([]);
const loadFromFile = data.map( ( data) => {
const newTodo = addTodo(new Todo(data.id,data.text));
return ( {newTodo} )});
const addTodo = todo => {
if (!todo.text || /^\s*$/.test(todo.text)) {
return;
}
const newTodos = [todo, ...todos];
setTodos(newTodos);
console.log(...todos);
};
const updateTodo = (todoId, newValue) => {
if (!newValue.text || /^\s*$/.test(newValue.text)) {
return;
}
setTodos(prev => prev.map(item => (item.id === todoId ? newValue : item)));
};
const removeTodo = id => {
const removedArr = [...todos].filter(todo => todo.id !== id);
setTodos(removedArr);
};
const completeTodo = id => {
let updatedTodos = todos.map(todo => {
if (todo.id === id) {
todo.isComplete = !todo.isComplete;
}
return todo;
});
setTodos(updatedTodos);
};
return (
<>
<TodoForm onSubmit={addTodo} />
{loadFromFile}
<Todo
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
updateTodo={updateTodo}
/>
</>
);
}
export default TodoList;
I want to create new instance of Todo object. I tried many times, many different forms, but still doesn't work. I have an id and a text from the data.json file. I want to create that instance of Todo object with these two values. But how?
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import EditIcon from '#material-ui/icons/Edit';
import DeleteIcon from '#material-ui/icons/Delete';
const Todo = ({ todos, completeTodo, removeTodo, updateTodo }) => {
const [edit, setEdit] = useState({
id: null,
value: ''
});
const submitUpdate = value => {
updateTodo(edit.id, value);
setEdit({
id: null,
value: ''
});
};
if (edit.id) {
return <TodoForm edit={edit} onSubmit={submitUpdate} />;
}
return todos.map((todo, index) => (
<div
className={todo.isComplete ? 'todo-row complete' : 'todo-row'}
key={index}
>
<p> <div key={todo.id} onClick={() => completeTodo(todo.id)}>
{todo.text}
</div>
</p>
<div className='icons'>
<DeleteIcon fontSize="small"
onClick={() => removeTodo(todo.id)}
className='delete-icon'
/>
<EditIcon
onClick={() => setEdit({ id: todo.id, value: todo.text })}
className='edit-icon'
/>
</div>
</div>
));
};
export default Todo;
import React, { useState, useEffect, useRef } from 'react';
import { Fab, IconButton } from "#material-ui/core";
import AddIcon from '#material-ui/icons/Add';
function TodoForm(props) {
const [input, setInput] = useState(props.edit ? props.edit.value : '');
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
});
const handleChange = e => {
setInput(e.target.value);
};
const handleSubmit = e => {
e.preventDefault();
props.onSubmit({
id: Math.floor(Math.random() * 10000),
text: input
});
setInput('');
};
return (
<form onSubmit={handleSubmit} className='todo-form'>
{props.edit ? (
<>
<textarea cols="10"
placeholder='Update item'
value={input}
onChange={handleChange}
name='text'
ref={inputRef}
className='todo-input edit'
/>
<button onClick={handleSubmit} className='todo-button edit'>
Save
</button>
</>
) : (
<>
<input
placeholder='Add item'
value={input}
onChange={handleChange}
name='text'
className='todo-input'
ref={inputRef}
/>
<Fab color="primary" aria-label="add">
< AddIcon onClick={handleSubmit} fontSize="small" />
</Fab>
</>
)}
</form>
);
}
export default TodoForm;
Issue
Ah, I see what you are getting at now, you are wanting to load some list of todos from an external file. The main issue I see in your code is that you are attempting to call/construct a Todo React component manually and this simply isn't how React works. You render data/state/props into JSX and pass this to React and React handles instantiating the components and computing the rendered DOM.
const loadFromFile = data.map((data) => {
const newTodo = addTodo(new Todo(data.id, data.text));
return ({newTodo});
});
Todo shouldn't be invoked directly, React handles this.
Solution
Since it appears the data is already an array of objects with the id and text properties, it conveniently matches what you store in state. You can simply pass data as the initial todos state value.
const [todos, setTodos] = useState(data);
If the data wasn't readily consumable you could create an initialization function to take the data and transform/map it to the object shape your code needs.
const initializeState = () => data.map(item => ({
id: item.itemId,
text: item.dataPayload,
}));
const [todos, setTodos]= useState(initializeState);
Running Example:
import data from "./data.json";
function TodoList() {
const [todos, setTodos] = useState(data); // <-- initial state
const addTodo = (text) => {
if (!text || /^\s*$/.test(text)) {
return;
}
setTodos((todos) => [todo, ...todos]);
};
const updateTodo = (id, newTodo) => {
if (!newTodo.text || /^\s*$/.test(newTodo.text)) {
return;
}
setTodos((todos) => todos.map((todo) => (todo.id === id ? newTodo : todo)));
};
const removeTodo = (id) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
};
const completeTodo = (id) => {
setTodos((todos) =>
todos.map((todo) =>
todo.id === id
? {
...todo,
isComplete: !todo.isComplete
}
: todo
)
);
};
return (
<>
<TodoForm onSubmit={addTodo} />
<Todo
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
updateTodo={updateTodo}
/>
</>
);
}

Need help filtering an autocomplete array with react v17

Most tutorials I found on this subject were outdated. So here I am.
What I expect to do with this app is, input text into the field and filter the results based on what you input. Currently I'm stuck and I've been through so many array methods such as filter, indexOf etc. I'm sure I am overthinking the issue so I need help. Here's the code I have currently:
import React, { useState, useEffect } from "react";
import axios from "axios";
const ITEMS_API_URL = "https://restcountries.eu/rest/v2/all";
function Autocomplete() {
const [countryArr, setCountryArr] = useState([]);
useEffect(() => {
axios.get(ITEMS_API_URL).then((res) => {
setCountryArr(() => {
let arr = res.data;
arr.splice(10, arr.length);
return arr;
});
console.log(countryArr);
});
}, []);
const onChange = e => {
const inputValue = e.target.value
const filteredSuggestions = countryArr.find(arr => arr.name == inputValue)
setCountryArr(filteredSuggestions)
}
return (
<div className="wrapper">
<div className="control">
<input type="text" className="input" onChange={onChange} />
</div>
<div className="list is-hoverable" />
{countryArr.map((country) => {
return (
<ul key={country.numericCode}>
{country.name}
</ul>
)
})}
</div>
);
}
export default Autocomplete;
You should not change to actual data source (countryArr) otherwise it reset and store last filtered on that. so I create state variable filteredCountryArr for filter and setting up filtered valued on that.
import React, { useState, useEffect } from "react";
import axios from "axios";
const ITEMS_API_URL = "https://restcountries.eu/rest/v2/all";
function Autocomplete() {
const [countryArr, setCountryArr] = useState([]);
const [filteredCountryArr, setFilteredCountryArr] = useState([]);
useEffect(() => {
axios.get(ITEMS_API_URL).then(res => {
setCountryArr(() => {
let arr = res.data;
arr.splice(10, arr.length);
return arr;
});
console.log(countryArr);
});
}, []);
const onChange = e => {
const inputValue = e.target.value;
const filteredSuggestions = countryArr.find(arr => arr.name == inputValue);
setFilteredCountryArr(filteredSuggestions);
};
return (
<div className="wrapper">
<div className="control">
<input type="text" className="input" onChange={onChange} />
</div>
<div className="list is-hoverable" />
{filteredCountryArr && filteredCountryArr.length > 0 && filteredCountryArr.map(country => {
return <ul key={country.numericCode}>{country.name}</ul>;
})}
</div>
);
}
export default Autocomplete;

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.

Categories

Resources