I am trying to rewrite a small app from vanilla js to react, and in one element I encountered a problem with passing on values in the inputs. What this element does, is after selecting a number it generates that many inputs to fill, and after filling send its id and value further (value can also be empty)
In Vanilla Js I did it with id and querySelector, but in React I have a trouble to change it correct
React code:
import React, { useState, useEffect } from "react";
import "./style.css";
import Values from "./Values";
export default function App() {
const [numberValue, setNumberValue] = useState("");
const [inputValues, setInputValues] = useState([]);
const [sendValues, setSendValues] = useState(false);
const [inputs, setInputs] = useState([]);
let numbers = [4, 6, 8];
//reset teamsName on change teamsValue
useEffect(() => {
for (let i = 1; i <= numberValue; i++) {
setInputValues(prev => [
...prev,
{
id: i,
value: ""
}
]);
}
}, [numberValue]);
const showButtons = numbers.map((number, i) => (
<button
className={`${numberValue === number ? "button active" : "button"}`}
onClick={() => {
setNumberValue(number);
setInputValues([]);
setInputs([]);
showInputs();
}}
>
{number}
</button>
));
//let inputs = [];
const showInputs = () => {
for (let i = 1; i <= numberValue; i++) {
setInputs(prev => [
...prev,
<input
type="text"
className="input"
placeholder={`Input ${i}`}
//value={inputValues.find(input => input.id === i && input.value)}
onChange={e =>
inputValues.filter(
input =>
input.id === i &&
setInputValues([
...inputValues,
{ id: i, value: e.target.value }
])
)
}
/>
]);
}
};
return (
<>
<div className="button-group">{showButtons}</div>
{numberValue && (
<>
<h3 className="title">Your inputs</h3>
<div className="input-group">{inputs}</div>
</>
)}
<button onClick={() => setSendValues(true)}>SEND</button>
{sendValues && <Values inputValues={inputValues} />}
</>
);
}
JS:
const buttonGroup = document.querySelector(".button-group");
const inputGroup = document.querySelector(".input-group");
const inputValues = document.querySelector(".input-values");
let n;
const showInputs = number => {
n = number;
inputGroup.innerHTML = ''
for (let i = 1; i <= number; i++) {
inputGroup.innerHTML += `
<input type="text" name="name" id="input-${i}" class="input" placeholder="team name"> <br>
`;
}
};
let values = []
const showValues = () => {
//clear
inputValues.innerHTML = '';
values = [];
//show new
for (let i = 1; i <= n; i++) {
const input_val = document.querySelector(`#input-${i}`).value;
values.push({
id: i,
value: input_val
});
}
for(let i = 0; i<=n; i++){
inputValues.innerHTML += `
<p>id: ${values[i].id} value:${values[i].value}
</p>
`
}
};
Links to code:
React -> https://stackblitz.com/edit/react-uw9dzc?file=src/App.js
JS -> https://codepen.io/Arex/pen/qBqLVBq?editors=1111
I took the liberty to simplify your code a bit. Basically I assigned value as it's own variable const value = e.target.value; as it is a synthetic event and tends to get lost if you pass it further down, so this preserves the value. Also, I changed inputValues to an object to make it easier to update:
// App.js
export default function App() {
const [numberValue, setNumberValue] = useState("");
const [inputValues, setInputValues] = useState({});
const [sendValues, setSendValues] = useState(false);
let numbers = [4, 6, 8];
const showButtons = numbers.map((number, i) => (
<button
className={`${numberValue === number ? "button active" : "button"}`}
onClick={async () => {
await setNumberValue(number);
await setInputValues({});
}}
>
{number}
</button>
));
return (
<>
<div className="button-group">{showButtons}</div>
{numberValue && (
<>
<h3 className="title">Your inputs</h3>
<div className="input-group">
{[...new Array(numberValue)].map((_value, id) => (
<input
type="text"
className="input"
placeholder={`Input ${id}`}
onChange={e => {
const value = e.target.value;
setInputValues(prev => {
prev[id] = value;
return prev;
});
}}
/>
))}
</div>
</>
)}
<button onClick={() => setSendValues(true)}>SEND</button>
{sendValues && <Values inputValues={inputValues} />}
</>
);
}
// Values.js
const Values = ({ inputValues }) => {
const showValues = Object.keys(inputValues).map(input => (
<div>
{input} : {inputValues[input]}
</div>
));
return <div>{showValues}</div>;
};
export default Values;
There are multiple issues in the shared code as follows:
It is not recommended to store components in state, in the shared code you are storing <input/> component in state. For more details check this
Unnecessary states are being used, always try to keep a minimum number of states as more number of states as more states are needed to be managed, making things unnecesarily complicated.
using previous state syntax to generate new state where it is not needed.
I am adding a working code with minimum changes for you reference.
App.js
import React, { useState, useEffect } from "react";
import "./style.css";
import Values from "./Values";
export default function App() {
const [numberValue, setNumberValue] = useState('');
const [inputValues, setInputValues] = useState([]);
const [sendValues, setSendValues] = useState(false);
let numbers = [4, 6, 8];
//reset teamsName on change teamsValue
useEffect(() => {
setInputValues(
Array(numberValue).fill("")
);
setSendValues(false)
}, [numberValue]);
const showButtons = numbers.map((number, i) => (
<button
className={`${numberValue === number ? "button active" : "button"}`}
onClick={() => {
setNumberValue(number);
}}
>
{number}
</button>
));
return (
<>
<div className="button-group">{showButtons}</div>
{numberValue && (
<>
<h3 className="title">Your inputs</h3>
<div className="input-group">
{inputValues.map((val, i) => (
<input
key={`input${i}`}
type="text"
className="input"
placeholder={`Input ${i+1}`}
value={val}
onChange={(e) => {let newValues = inputValues.slice(); newValues[i]=e.target.value; setInputValues(newValues)}
}
/>
))}
</div>
</>
)}
<button onClick={() => setSendValues(true)}>SEND</button>
{sendValues && <Values inputValues={inputValues} />}
</>
);
}
Values.js
import React from "react";
const Values = ({ inputValues }) => {
const showValues = inputValues.map((input, i) => (
<div key={'output'+i}>
{i+1} : {input}
</div>
));
return <div>{showValues}</div>;
};
export default Values;
I am also sharing a updated stackblitz code reference you shared for better understanding Updated snippet with fixes for reference.
Related
For example:
I'm displaying multiple Student objects to the website that's coming from an API. Each Student object contains an array of their test Grades along with other student information. Now, I want to display each Student's information onto the screen but instead of displaying the student's Grades, I want each Student to have a "+" Button which will be to toggle the display of the student's Grades.
I'm using the useState hook from React. My problem is that when I click the "+" Button, it toggles ALL student's grades on/off. What I want is to toggle the Grades of only the student whose button I clicked.
Here's my code:
import axios from 'axios';
import { useState, useEffect } from 'react';
import styles from "./Home.module.css";
import { v4 as uuidv4 } from 'uuid';
const Home = () => {
const [students, setStudents] = useState([]);
const [filteredStudents, setFilteredStudents] = useState([]);
const [isShown, setIsShown] = useState(true);
const fetchStudents = async () => {
const response = await axios.get(`https://api.hatchways.io/assessment/students`);
setStudents(response.data.students);
setFilteredStudents(response.data.students);
console.log(response.data.students);
}
const findAverageGrade = arr => {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += parseInt(arr[i]);
}
return sum / arr.length;
}
const searchStudentName = 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 setFilteredStudents(newArray);
}
const toggleGrades = () => {
console.log("toggle");
}
useEffect(() => {
fetchStudents();
}, [])
return(
<>
<div>
<input type="text" placeholder="Search by name" onChange={(event) => searchStudentName(event.target.value) }/>
{filteredStudents.map((student) => (
<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} {student.lastName}</p>
</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>
<button onClick={() => {
setIsShown(!isShown);
}}>
+
</button>
{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 }
</ul>
</div>
</div>
)
)}
</div>
</>
)
}
export default Home;
Just remove the mapping part to another component and import it in map part. In this way you will have still same structure and it will work without any extra logic
NEW COMPONENT
const Students = ({student}) => {
const [isShown, setIsShown] = useState(true);
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} {student.lastName}</p>
</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>
<button onClick={() => {
setIsShown(!isShown);
}}>
+
</button>
{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 }
</ul>
</div>
</div>
)
}
HOME COMPONENT
import axios from 'axios';
import { useState, useEffect } from 'react';
import styles from "./Home.module.css";
import { v4 as uuidv4 } from 'uuid';
const Home = () => {
const [students, setStudents] = useState([]);
const [filteredStudents, setFilteredStudents] = useState([]);
const fetchStudents = async () => {
const response = await axios.get(`https://api.hatchways.io/assessment/students`);
setStudents(response.data.students);
setFilteredStudents(response.data.students);
console.log(response.data.students);
}
const findAverageGrade = arr => {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += parseInt(arr[i]);
}
return sum / arr.length;
}
const searchStudentName = 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 setFilteredStudents(newArray);
}
const toggleGrades = () => {
console.log("toggle");
}
useEffect(() => {
fetchStudents();
}, [])
return(
<>
<div>
<input type="text" placeholder="Search by name" onChange={(event) => searchStudentName(event.target.value) }/>
{filteredStudents.map((student) => (
<Student student={student} />
)
)}
</div>
</>
)
}
export default Home;
Note: I did not include export import parts of component and css but it's the easy part
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.
I'm working on a React Notes Application and my App.js contains all the necessary functions props which are passed down to several components.
As a result I'm doing prop drilling a lot where I'm passing down around 10-20 props/functions in the components where it isn't needed.
I tried using useContext Hook but I guess it doesn't work with callback functions in the value parameter.
App.js
const App = () => {
const [ notes, setNotes ] = useState([]);
const [ category, setCategory ] = useState(['Notes']);
const [ searchText, setSearchText ] = useState('');
const [ alert, setAlert ] = useState({
show:false,
msg:'',
type:''
});
const [isEditing, setIsEditing] = useState(false);
const [editId, setEditId] = useState(null);
useEffect(()=>{
keepTheme();
})
// retreive saved notes
useEffect(()=>{
const savedNotes = JSON.parse(localStorage.getItem('react-notes-data'));
if (savedNotes) {
setNotes(savedNotes)
}
}, []);
// save notes to local storage
useEffect(() => {
localStorage.setItem('react-notes-data', JSON.stringify(notes))
setNotesCopy([...notes]);
}, [notes]);
// save button will add new note
const addNote = (text) => {
const date = new Date();
const newNote = {
id: nanoid(),
text: text,
date: date.toLocaleDateString(),
category: category,
}
const newNotes = [...notes, newNote]
setNotes(newNotes);
}
const deleteNote = (id) => {
showAlert(true, 'Note deleted', 'warning');
const newNotes = notes.filter(note => note.id !== id);
setNotes(newNotes);
}
// hardcoded values for categories
const allCategories = ['Notes', 'Misc', 'Todo', 'Lecture Notes', 'Recipe'];
// copy notes for filtering through
const [notesCopy, setNotesCopy] = useState([...notes]);
const handleSidebar = (category) => {
setNotesCopy(category==='Notes'?[...notes]:
notes.filter(note=>note.category===category));
}
// function to call alert
const showAlert = (show=false, msg='', type='') => {
setAlert({show, msg, type});
}
return (
<div>
<div className="container">
<Sidebar
allCategories={allCategories}
handleSidebar={handleSidebar}
notesCopy={notesCopy}
key={notes.id}
/>
<Header notes={notes} alert={alert} removeAlert={showAlert} />
<Search handleSearchNote={setSearchText} />
<NotesList
notesCopy={notesCopy.filter(note=>
note.text.toLowerCase().includes(searchText) ||
note.category.toString().toLowerCase().includes(searchText)
)}
handleAddNote={addNote}
deleteNote={deleteNote}
category={category}
setCategory={setCategory}
allCategories={allCategories}
showAlert={showAlert}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
isEditing={isEditing}
setIsEditing={setIsEditing}
/>
</div>
</div>
)
}
NotesList.js
const NotesList = (
{ notesCopy, handleAddNote, deleteNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, notes, setNotes, editId, setEditId }
) => {
const [ noteText, setNoteText ] = useState('');
const textareaRef = useRef();
// function to set edit notes
const editItem = (id) => {
const specificItem = notes.find(note=>note.id === id);
setNoteText(specificItem.text);
setIsEditing(true);
setEditId(id);
textareaRef.current.focus();
}
return (
<div key={allCategories} className="notes-list">
{notesCopy.map(note => {
return (
<Note
key={note.id}
{...note}
deleteNote={deleteNote}
category={note.category}
isEditing={isEditing}
editId={editId}
editItem={editItem}
/>)
})}
<AddNote
handleAddNote={handleAddNote}
category={category}
setCategory={setCategory}
showHideClassName={showHideClassName}
allCategories={allCategories}
showAlert={showAlert}
isEditing={isEditing}
setIsEditing={setIsEditing}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
noteText={noteText}
setNoteText={setNoteText}
textareaRef={textareaRef}
/>
</div>
)
}
AddNote.js
const AddNote = ({ notes, setNotes, handleAddNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, editId, setEditId, noteText, setNoteText, textareaRef }) => {
const [ show, setShow ] = useState(false);
const [ modalText, setModalText ] = useState('');
const charCount = 200;
const handleChange = (event) => {
if (charCount - event.target.value.length >= 0) {
setNoteText(event.target.value);
}
}
const handleSaveClick = () => {
if (noteText.trim().length === 0) {
setModalText('Text cannot be blank!');
setShow(true);
}
if (category === '') {
setModalText('Please select a label');
setShow(true);
}
if (noteText.trim().length > 0 && category!=='') {
showAlert(true, 'Note added', 'success');
handleAddNote(noteText);
setNoteText('');
setShow(false);
}
if (noteText.trim().length > 0 && category!=='' && isEditing) {
setNotes(notes.map(note=>{
if (note.id === editId) {
return ({...note, text:noteText, category:category})
}
return note
}));
setEditId(null);
setIsEditing(false);
showAlert(true, 'Note Changed', 'success');
}
}
const handleCategory = ( event ) => {
let { value } = event.target;
setCategory(value);
}
showHideClassName = show ? "modal display-block" : "modal display-none";
return (
<div className="note new">
<textarea
cols="10"
rows="8"
className='placeholder-dark'
placeholder="Type to add a note.."
onChange={handleChange}
value={noteText}
autoFocus
ref={textareaRef}
>
</textarea>
<div className="note-footer">
<small
className='remaining'
style={{color:(charCount - noteText.length == 0) && '#c60000'}}>
{charCount - noteText.length} remaining</small>
<div className='select'>
<select
name={category}
className="select"
onChange={(e)=>handleCategory(e)}
required
title='Select a label for your note'
defaultValue="Notes"
>
<option value="Notes" disabled selected>Categories</option>
{allCategories.map(item => {
return <option key={item} value={item}>{item}</option>
})}
</select>
</div>
<button className='save' onClick={handleSaveClick} title='Save note'>
<h4>{isEditing ? 'Edit':'Save'}</h4>
</button>
</div>
{/* Modal */}
<main>
<div className={showHideClassName}>
<section className="modal-main">
<p className='modal-text'>{modalText}</p>
<button type="button" className='modal-close-btn'
onClick={()=>setShow(false)}><p>Close</p>
</button>
</section>
</div>
</main>
</div>
)
}
I want the functions passed from App.js to NotesList.js to be in AddNote.js without them being passed in NotesList.js basically minimizing the props destructuring in NotesList.js
Context API does work with function. What you need to do is pass your function to Provider inside value :
<MyContext.Provider value={{notes: notesData, handler: myFunction}} >
For example:
// notes-context.js
import React, { useContext, createContext } from 'react';
const Context = createContext({});
export const NotesProvider = ({children}) => {
const [notes, setNote] = useState([]);
const addNote = setNote(...); // your logic
const removeNote = setNote(...); // your logic
return (
<Context.Provider value={{notes, addNote, removeNote}}>
{children}
</Context.Provider>
)
}
export const useNotes = () => useContext(Context);
Add Provider to your App.js like so:
// App.js
import NoteProvider from './notes-context';
export default App = () => {
return (
<NoteProvider>
<div>... Your App</div>
</NoteProvider>
)
}
Then call UseNote in your NoteList.js to use the function:
// NoteList.js
import {useNotes} from './note-context.js';
export const NoteList = () => {
const {notes, addNotes, removeNotes} = useNotes();
// do your stuff. You can now use functions addNotes and removeNotes without passing them down the props
}
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.
I have an input field that takes in a number.(between 1 and 30) I want to display an array of items depending on what number is placed in that text field. how can this been done with React hooks. I have something basic for a start like this, but this might not even be the best way to start this.
export default function App() {
const [state, setState] = React.useState({ value: "" });
const [myArray, updateMyArray] = React.useState([]);
const onSubmit = () => {
updateMyArray((arr) => [...arr, `${state.value}`]);
};
const handleChange = (event) => {
let { value, min, max } = event.target;
value = Math.max(Number(min), Math.min(Number(max), Number(value)));
setState({ value });
};
return (
<>
<input
type="number"
onChange={handleChange}
value={state.value}
min={""}
max={100}
/>
<button onClick={onSubmit}>Confirm</button>
{state.value && (
<>
<div>
{myArray?.map((e) => (
<div>{e}</div>
))}
</div>
</>
)}
</>
);
}
You can do it like this
updateMyArray(new Array(state.value).fill(""));
This will create a new array with the length of state.value and asign it to myArray
Maybe this example will be helpful for you.
function App() {
const [amount, setAmount] = useState(0);
const [submittedAmount, setSubmittedAmount] = useState(0);
// optionally
const onSubmit = () => {
setSubmittedAmount(amount);
};
const handleChange = (event) => {
let { value, min, max } = event.target;
value = Math.max(Number(min), Math.min(Number(max), Number(value)));
setAmount(value);
};
return (
<>
<input
type="number"
onChange={handleChange}
value={amount}
min={0}
max={100}/>
<button onClick={onSubmit}>Confirm</button>
{ /* you can use amount instead of submitted amount if you want */
{submittedAmount > 0 && Array.from({ length: submittedAmount }, (_, index) => <div key={index}>{index}</div> )}
</>
);
}
In my opinion if you can skip submitting and use only amount state. Thanks to this your UI will change automatically after input value change without submitting.
If you know the value of value, you can loop till that number, before the render, like:
const items = [];
for (let i; i < state.value; i++) {
items.push(<div>{i}</div>);
}
return (
<div>
{items}
</div>
)