I have two arrays,
const [imagesUrlArray, setURls] = useState([])
const [imagesArray, setImages] = useState([])
using handle change below; imagesUrlArray is used to display the images on the screen, and imagesArray is saved to later update those same images to the database
const handleChange = (e) => {
let selected = e.target.files[0]
var selectedImageSrc = URL.createObjectURL(selected);
addUrl(selectedImageSrc)
addImage(selected)
};
Though I now want to click the X(delete) button and remove the item at index of imagesUrlArray and imagesArray (say if the user no longer wants to use that image)
<div className="img-grid">
{ imagesUrlArray && imagesUrlArray.map((url,index) => {
return ( <div key={index}
className="img-wrap">
{/* <span class="close">×</span> */}
<button onClick={ () => handleRemove(index)} className="close">X</button>
<img src={url} alt="uploaded" />
</div>
)
}
)}
</div>
I have tried splice and slice etc but still cannot find a perfect solution,
here is the handleRemove Function
const handleRemove = (index) => {
const newImagesArray = imagesArray.splice(index);
const newImagesUrlArray = imagesUrlArray.splice(index);
setImages(newImagesArray);
setURls(newImagesUrlArray);
}
You can do something like this:
const handleRemove = (index) => {
setImages(imagesArray.filter((x,i) => i !== index));
setURls(imagesUrlArray.filter((x,i) => i !== index));
}
So, basically the idea is to skip the element at specific index and return the remaining items and set the state.
Related
I want to add and remove the components dynamically, so far just i can add, but when i tried to remove it remove too weird, lets say i dont want to remove just i would like to hide the components
import {
MinusCircleOutlined,
PlusOutlined,
} from '#ant-design/icons'
import { useState } from "react"
const MyInput = ({ index, removeInput }) => {
return (<div >
<Input placeholder="Email address" />
<MinusCircleOutlined className="icon-left" onClick={() => { removeInput(index) }} />
</div>
)
}
const MyComponent = ({ }) => {
const [form] = Form.useForm()
const [index, setIndex] = useState(0)
const [inputsFields, setInputsFields] = useState([])
const [hiddenFields, setHiddenFields] = useState([])
const AddInput = () => {
const newInviteField = <MyInput index={index} removeInput={removeInput} />
setInputsFields([...inputsFields, newInviteField])
const newIndex = index + 1
setIndex(newIndex)
}
const removeInput = (currentIndex) => {
let a = hiddenFields
a.push(currentIndex)
setHiddenFields([...a])
}
return (
<Card>
<Form form={form} layout="vertical">
<Form.Item className='form-item item-container'>
{inputsFields.map((item, index) => !hiddenFields.includes(index) && <div key={index}>{item}</div>)}
</Form.Item>
<Form.Item >
<a href="#" onClick={AddInput}>Add</a>
</Form.Item>
</Form>
</Card>)
}
i tried to filter by the index, just showing the indexes does not into the hidden array !hiddenFields.includes(index)
the problem is when i am deleting, sometimes it is not deleting, sometimes other component is deleting
You should never use an array method index as key, if you modify the array. It has to be unique. When you delete element with index 2, the index 3 becomes index 2. This should not happend. You should not change the key prop value
The solution:
keep information about the inputs in the state, not the inputs itself.
// keep necessarry information for each input here.
// Like id, name, hidden, maybe what to render. Whatever you want
const [inputsFields, setInputsFields] = useState([{
id: 'name',
hidden: false
}])
// and map them
inputsFields.map(element => !element.hidden && <Input key={element.id} />)
When each element has unique id, you will delete the element with that id, not with the array map index
If you do not need that much info. Just make array of numbers in that state,
const counter = useRef(1)
const [inputsFields, setInputsFields] = []
const AddInput = () => {
counter.current += 1
setInputsFields(oldInputs => [...oldInputs, counter.current])
}
// and render them:
inputsFields.map(element => <Input key={element} />)
function DetailContainer() {
const router = useRouter();
const { id } = router.query;
const numbering = Number(id);
const currentPostList = useRecoilValue(PostListState);
const [postObj] = currentPostList.filter((post) => post.id === numbering);
const title = postObj.title;
const content = postObj.content;
const date = postObj.date;
const postId = postObj.id;
const setPostList = useSetRecoilState(PostListState);
const onHandleDelete = (postId) => {
setPostList(currentPostList.filter((item) => item.id !== postId));
};
return (
<>
<div>{title}</div>
<div>{content}</div>
<div>{date}</div>
<Link href="/EditorPage">
<button>Edit</button>
</Link>
<button>Remove</button>
</>
);
}
When I do not add onHandleDelete to Remove button, it works fine. But when I add onHandleDelete, error 'cannot read properties of undefined' appears. Is there a way that I can fix this so that I can delete postObj?
=> I fixed error by replacing to 'postObj?.title' since postObj has been removed when button was clicked
What is postId? When you are filtering "currentPostList" it check for the all items whose ids are not equal to postId. Call onHandleDelete on Remove button and pass postId when calling.
Something like this:
<button onClick={() => handleDelete("paste postId here if you are getting it from props")>Remove</button>
it will work.
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.
I am trying to remove an input field with filter function but it's not working.
In the following code add operation works fine but remove operation is not working properly ,it is not removing the corresponding element.Another problem the values on the inputs fields not present when the component re-renders.so experts guide me how i can achieve removing the corresponding row when the remove button is clicked and the input values should not be reset when the component re-renders
So when I refresh the page and click to remove an input it will clear all other input data. How can I fix this problem ?
Update adding full component in question:
const Agreement = (props) => {
const { agreement, editable, teamData, teamId, fetchTeamData } = props;
const [editing, setEditing] = useState(false);
const [title, setTitle] = useState("");
const [showErrors, setShowErrors] = useState(false);
const [errorsArr, setErrorsArr] = useState();
const initialFormState = {
rule_0: teamData.rules.rule_0,
rule_1: teamData.rules.rule_1,
rule_2: teamData.rules.rule_2,
rule_3: teamData.rules.rule_3,
creator: teamData.User.public_user_id,
};
const [updateTeamData, setUpdateTeamData] = useState(initialFormState);
const [inputs, setInputs] = useState(
teamData.rules.map((el) => ({
...el,
guid: uuidV4(),
}))
);
const handleChange = (event) => {
const { name, value } = event.target;
// Update state
setUpdateTeamData((prevState) => ({
...prevState,
[name]: value,
}));
};
// Add more input
const addInputs = () => {
setInputs([...inputs, { name: `rule_${inputs.length + 1}` }]);
};
// handle click event of the Remove button
const removeInputs = (index) => {
const newList = inputs.filter((item, i) => index !== i); // <-- compare for matching index
setInputs(newList);
};
const clearInput = (dataName) => {
setUpdateTeamData((prevState) => {
delete prevState[dataName];
return {
...prevState,
};
});
};
const handleSubmit = async (event) => {
event.preventDefault();
setEditing(false);
// Send update request
const res = await axios.put(`/api/v1/teams/team/${teamId}`, updateTeamData);
// If no validation errors were found
// Validation errors don't throw errors, it returns an array to display.
if (res.data.validationErrors === undefined) {
// Clear any errors
setErrorsArr([]);
// Hide the errors component
setShowErrors(false);
// Call update profiles on parent
fetchTeamData();
} else {
// Set errors
setErrorsArr(res.data.validationErrors.errors);
// Show the errors component
setShowErrors(true);
}
};
const handleCancel = () => {
setEditing(false);
};
useEffect(() => {
if (agreement === "default") {
setTitle(defaultTitle);
// setInputs(teamData.rules);
} else {
setTitle(agreement.title ?? "");
}
}, [agreement, teamData]);
// console.log("teamData.rules", teamData);
console.log("inputs", inputs);
return (
<div className="team-agreement-container">
{!editing && (
<>
<h4 className="team-agreement-rules-title">{title}</h4>
{editable && (
<div className="team-agreement-rules">
<EditOutlined
className="team-agreement-rules-edit-icon"
onClick={() => setEditing(true)}
/>
</div>
)}
{teamData.rules.map((rule, index) => (
<div className="team-agreement-rule-item" key={`rule-${index}`}>
{rule ? (
<div>
<h4 className="team-agreement-rule-item-title">
{`Rule #${index + 1}`}
</h4>
<p className="team-agreement-rule-item-description">
- {rule}
</p>
</div>
) : (
""
)}
</div>
))}
</>
)}
{/* Edit rules form */}
{editing && (
<div className="team-agreement-form">
{showErrors && <ModalErrorHandler errorsArr={errorsArr} />}
<h1>Rules</h1>
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.guid}>
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
{inputs.length < 4 && (
<div className="team-agreement-add-rule">
<button type="submit" onClick={addInputs}>
<Add />
</button>
</div>
)}
<div className="div-button">
<button className="save-button" onClick={handleSubmit}>
Save
</button>
<button className="cancel-button" onClick={handleCancel}>
Cancel
</button>
</div>
</div>
)}
</div>
);
};
export default Agreement;
When i do console.log(inputs) this is the data that I got:
0: 0: "t" 1: "e" 2: "s" guid: "e18595a5-e30b-4b71-8fc2-0ad9c0e140b2"
proto: Object 1: 0: "d" 1: "a" 2: "s" 3: "d" 4: "a" 5: "s" guid: "537ca359-511b-4bc6-9583-553ea6ebf544" ...
Issue
The issue here is that you are using the array index as the React key. When you mutate the underlying data and reorder or add/remove elements in the middle of the array then the elements shift around but the React key previously used doesn't move with the elements.
When you remove an element then all posterior elements shift forward and the index, as key, remains the same so React bails on rerendering the elements. The array will be one element shorter in length and so you'll see the last item removed instead of the one you actually removed.
Solution
Use a React key that is intrinsic to the elements being mapped, unique properties like guids, ids, name, etc... any property of the element that guarantees sufficient uniqueness among the dataset (i.e. the siblings).
const [inputs, setInputs] = useState(teamData.rules);
const removeInputs = (index) => {
// compare for matching index
setInputs(inputs => inputs.filter((item, i) => index !== i));
};
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.id}> // <-- use a unique property
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
If your teamData.rules initial state value doesn't have any unique properties to use then you can map this to a new array and add a sufficient id property.
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: generateId()***,
})));
*** this is a function you need to define yourself, or import from a module like uuid***
import { v4 as uuidV4 } from 'uuid';
...
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: uuidV4(),
})));
// Add more input
const addInputs = () => {
setInputs(inputs => [
...inputs,
{
name: `rule_${inputs.length + 1}`,
guid: uuidV4();
},
]);
};
Then when mapping use the guid property.
<div className="agreement-form-grid" key={data.guid}>
The issue is because you are trying to compare index with array item in filter method. You should use the second argument in filter which denotes the array index of the current iterating item
const removeInputs = (index) => {
const newList = inputs.filter((item,i) => index !== i);
setInputs(newList);
};
That's your solution, you are trying with item but you are comparing it with index that's wrong. You should do it like this,
const newList = inputs.filter((item, key) => index !== key);
I'm working on random user api, the fetching of user name and pagination is working fine but not the search event. Please help.
I pushed my code on stackblitz, to help you guys to debug it easily.
here's the link: https://stackblitz.com/edit/search-and-pagination-in-react-by-react-hooks?file=src/App.js
below in image you can see that the name i mentioned in search box is present in api but its not comming on first place.
Working example in here.
const App = () => {
const [myApi, setMyApi] = useState([]);
const [data, setData] = useState([]); // add your data to here
const [currentPage, setCurrentPage] = useState(1);
const [postsPerPage] = useState(10);
const [searchUser, setSearchUser] = useState("");
useEffect(() => {
fetch("https://randomuser.me/api/?results=50")
.then(data => data.json())
.then(json_result => {
setData(json_result.results); // set your data to state
let myApi = renderData(json_result.results); // render your component
setMyApi(myApi); // set it to state
});
}, []);
const renderData = (data) => {
return data.map((item, idx) => {
return (
<div key={idx}>
<img src={item.picture.thumbnail} alt="" /> {item.name.first}
<hr />
</div>
);
});
}
// get current post
const indexOfLastPost = currentPage * postsPerPage; // 1 * 10 = 10
const indexOfFirstPost = indexOfLastPost - postsPerPage; // 10 - 10 = 0
const currentPosts = myApi?.slice(indexOfFirstPost, indexOfLastPost); // 0 to 10
// search users by user input
const handleSearchInput = event => {
setSearchUser(event.target.value);
const newData = renderData(data.filter(item => item.name.first.toLowerCase().includes(event.target.value))); // render filtered data
setMyApi(newData); // and set it to state
};
const paginate = pageNumber => setCurrentPage(pageNumber);
return (
<div>
<Search onChange={handleSearchInput} />
<Pagination
postsPerPage={postsPerPage}
totalPosts={myApi?.length}
paginate={paginate}
/>
{currentPosts}
</div>
);
};
const Search = ({ onChange }) => {
return (
<div>
<input
type="text"
autoFocus={true}
placeholder="search users"
onChange={onChange}
/>
</div>
);
};
Since you're useEffect has [] (empty array) as the dependency, you're user fetching logic will only be called once i.e. on the initial rendering. You can add searchUser as useEffect's dependency so you can fetch users whenever the searchUser text changes.