React is not updating my state using useEffect and setState - javascript

I have the following piece of code: The first one is a modal that holds some inputs:
export const MessagesForm = (props) => {
const [filteredMessages, setFilteredMessages] = useState(props.subject.messages);
const [currentPage, setCurrentPage] = useState(1);
const [recordsPerPage] = useState(10);
const [messages, setMessages] = useState(props.subject.messages);
const indexOfLastRecord = currentPage * recordsPerPage;
const indexOfFirstRecord = indexOfLastRecord - recordsPerPage;
const nPages = Math.ceil(messages.length / recordsPerPage)
const { handleSubmit, reset, register, control, getValues } = useForm({
mode: 'onSubmit',
defaultValues: {
author: '',
message: '',
}
});
useEffect(() => {
const newMessages = messages.slice(indexOfFirstRecord, indexOfLastRecord);
setFilteredMessages(newMessages);
}, [currentPage])
const { remove } = useFieldArray({
control,
name: 'messages',
});
const handleDelete = (id) => {
subjectService.deleteMessage(props.subject['_id']['$oid'], id);
let filters = filteredMessages.filter((el) => el.id !== id);
setFilteredMessages(filters);
}
const handleEdit = (index) => {
const updatedMessage = getValues(`messages.${index}.message`);
const updatedAuthor = getValues(`messages.${index}.author`);
const messageId = getValues(`messages.${index}.id`);
const body = {
message: updatedMessage,
author: updatedAuthor,
id: messageId,
};
subjectService.updateMessage(props.subject['_id']['$oid'], body);
}
const onSubmit = async (formData) => {
const body = {
author: formData.author,
message: formData.message,
}
subjectService.createMessage(props.subject['_id']['$oid'], body)
.then(res => {
Swal.fire({
title: 'Success',
text: 'Message updated successfully',
icon: 'success',
confirmButtonText: 'Close',
confirmButtonColor: '#5CBDA5'
}).then(props.onHide)
})
}
return (
<Modal
{...props}
animation
size="lg"
centered
>
<Modal.Header>
<Modal.Title>
Mensajes
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Pagination
nPages={nPages}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
/>
{
filteredMessages.map((message, index) => {
return (
<div
key={index}
className="message-row"
>
<Form.Group
className='row tes'
>
<div className='col'>
<Form.Label>Author</Form.Label>
<Form.Control
name='id'
style={{ display: 'none' }}
{...register(`messages.${index}.id`)}
defaultValue={message.id}
/>
<Form.Control
type="text"
name='author'
{...register(`messages.${index}.author`)}
defaultValue={message.author}
/>
</div>
<div className='col'>
<Form.Label>Message</Form.Label>
<Form.Control
as="textarea"
name='msg'
{...register(`messages.${index}.message`)}
defaultValue={message.message}
/>
</div>
</Form.Group>
<div className='action-buttons'>
<button className='btn btn-info' type="button" onClick={() => {
Swal.fire({
title: '¿Estás seguro?',
text: "No podrás revertir esto",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Sí, guardarlo!'
}).then((result) => {
if (result.value) {
handleEdit(index);
}
})
}}><BiSave /></button>
<button className='btn btn-danger' type="button" onClick={() => {
Swal.fire({
title: '¿Estás seguro?',
text: "No podrás revertir esto!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Sí, borrarlo!'
}).then((result) => {
if (result.value) {
remove(index);
handleDelete(message.id);
}
})
}}><Trash /></button>
</div>
</div>
)
})
}
</Modal.Body>
<Modal.Footer>
<Button
type="submit"
variant="primary"
form="hook-form"
onClick={() => {
if (props.action !== 'NEW') props.onHide();
}}
>OK</Button>
</Modal.Footer>
</Modal>
)
}
And i'm using a functional component "Pagination" to paginate my modal:
import React from 'react'
const Pagination = ({ nPages, currentPage, setCurrentPage }) => {
const pageNumbers = [...Array(nPages + 1).keys()].slice(1)
const nextPage = () => {
if (currentPage !== nPages) setCurrentPage(currentPage + 1)
}
const prevPage = () => {
if (currentPage !== 1) setCurrentPage(currentPage - 1)
}
return (
<nav>
<ul className='pagination justify-content-center'>
<li className="page-item">
<a className="page-link"
onClick={prevPage}
href='#'>
Previous
</a>
</li>
{pageNumbers.map(pgNumber => (
<li key={pgNumber}
className={`page-item ${currentPage == pgNumber ? 'active' : ''} `} >
<a onClick={() => setCurrentPage(pgNumber)}
className='page-link'
href='#'>
{pgNumber}
</a>
</li>
))}
<li className="page-item">
<a className="page-link"
onClick={nextPage}
href='#'>
Next
</a>
</li>
</ul>
</nav>
)
}
export default Pagination
As my probably wrong understanding, by using the "useEffect" hook listening to the currentPage, it should update the state of my filteredMessages array, but it isn't
By using console log of the newMessages i'm actually obtaining the adequate output of data, but I don't quite know why is this happening only on the console...

You are using messages in your useEffect but you haven't included that in the dependency array.
useEffect(() => {
setFilteredMessages( messages.slice(indexOfFirstRecord, indexOfLastRecord));
}, [currentPage, messages]) // if you add it here it works, but read on
The problem with above code is it would now fire state updates anytime messages or currentPage changes. You could add some logic to look at the previous page but that just overcomplicates things.
My recommendation would be to not perform this type of logic in a useEffect. The best way to handle this scenario is to add this logic directly into nextPage and prevPage event handlers:
const updateFilteredMessages = (page) => {
const indexOfLastRecord = page * recordsPerPage;
const indexOfFirstRecord = indexOfLastRecord - recordsPerPage;
setFilteredMessages(messages.slice(indexOfFirstRecord,
indexOfLastRecord))
}
const nextPage = () => {
if (currentPage !== nPages) {
setCurrentPage(currentPage + 1)
updateFilteredMessages(currentPage + 1)
}
}
All of this logic is related to the nextPage user action, so it's also a lot clearer to encapsulate it within a callback versus splitting off message filtering in the useEffect
I would construct the nextPage & prevPage handlers in your MessageForm component and pass them into Pagination (since you need access to the MessageForm local state)

Related

Switching between product category cards

I have cards with categories, clicking on which opens hidden content. I need the card to close when switching between them and if a click occurs behind the content.
import React, { useRef } from "react";
import s from "./Shop.module.css";
import { useState } from "react";
export const Shop = () => {
const card = [
{
name: "Brands",
cat: ["Adidas", "Nike", "Reebok", "Puma", "Vans", "New Balance"],
show: false,
id: 0,
},
{
name: "Size",
cat: ["43", "43,5"],
show: false,
id: 1,
},
{
name: "Type",
cat: ["Sneakers ", "Slippers"],
show: false,
id: 2,
},
];
const [active, setActive] = useState({});
const handleActive = (id) => {
setActive({ ...active, [id]: !active[id] });
};
const handleDisable = (index) => {
setActive(index);
};
return (
<div className={s.container}>
<div className={s.brandInner}>
{card.map((i, index) => {
return (
<div className={s.brandCard} key={i.id}>
<button
className={`${s.brandBtn} `}
onClick={() => handleActive(i.id, index)}
onBlur={() => handleDisable(index)}
>
<p className={`${active[i.id] ? `${s.brandBtnActive}` : ``}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active[i.id] ? "" : `${s.dNone}`}`}
>
<ul className={s.brandList}>
{i.cat.map((elem) => {
return (
<li key={elem} className={s.brandItem}>
{elem}
</li>
);
})}
</ul>
<button className={s.brandOpenBtn}>Apply</button>
</div>
</div>
);
})}
</div>
</div>
);
};
I tried to do it through onBlur, but this way I can't interact with the content that appears when opening the card, please help
You could do this a few different ways, here are two.
Array version
You can do this by using a array to keep track of the ids that are active.
const [active, setActive] = useState([]);
For the event handler we will creata a new toggleActive function which replaces the others. This will check if the id is already in the array and if so remove it, else add it.
const toggleActive = (id) => {
setActive((prevActive) => {
if (prevActive.includes(id)) {
return prevActive.filter((activeId) => activeId !== id);
}
return [...prevActive, id];
});
};
Then in the return of the component we need to updated some logic as well. Then handlers only take in the id of the i. To check if the id is in the array with can use includes.
<button
className={s.brandBtn}
onClick={() => toggleActive(i.id)}
>
<p className={`${active.includes(i.id) ? s.brandBtnActive : ""}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active.includes(i.id) ? "" : s.dNone}`}
>
Object version
This version is to do it with a object.
const [active, setActive] = useState({});
The handler, this will toggle the value of the id starting with false if there is no value for the id yet.
const toggleActive = (id) => {
setActive((prevActive) => {
const prevValue = prevActive[id] ?? false;
return {
...prevActive,
[id]: !prevValue,
};
});
};
The elements
<button
className={s.brandBtn}
onClick={() => toggleActive(i.id)}
>
<p className={`${active[i.id] ? s.brandBtnActive : ""}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active[i.id] ? "" : s.dNone}`}
>
Edit: toggle with closing others
First we declare the state with a initial value of null
const [active, setActive] = useState(null)
We create the toggleActive function which checks if the id to toggle is the previous id, if so return null else return the new active id
const toggleActive = (id) => {
setActive((prevActive) => {
if (prevActive === id) return null;
return id;
});
};
For the rendering it is quite simple, add the toggleActive function to the button and check if the active is the same id
<button
className={s.brandBtn}
onClick={() => toggleActive(i.id)}
>
<p className={`${active === i.id ? s.brandBtnActive : ""}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active === i.id ? "" : s.dNone}`}
>

Toggle line-through in jsx

Trying to toggle a line-through in jsx when clicking on an item in an app I'm building. The line appears when clicked but I want to remove the line when clicked a second time. Here's my code so far
import { useState } from "react";
function ToDo() {
const [toDoInput, setToDoInput] = useState("");
const [toDoList, setToDoList] = useState([
{ text: "Read a Book", completed: false },
{ text: "Play Apex Legends", completed: false },
{ text: "Bake a Cake", completed: false },
]);
// Add a todo
const addToDo = () => {
if (!toDoInput) return;
const newList = [...toDoList];
newList.push({ text: toDoInput, completed: false });
setToDoList(newList);
setToDoInput("");
};
// delete a todo
const deleteToDo = (index) => {
const newArray = [...toDoList];
newArray.splice(index, 1);
setToDoList(newArray);
};
// toggle if a todo has been completed or not
const toggleToDo = (index) => {
const newArray = [...toDoList];
newArray[index].completed = !newArray[index.completed];
setToDoList(newArray);
};
return (
<div>
<h2>To Do App</h2>
<input
value={toDoInput}
onChange={(e) => setToDoInput(e.target.value)}
></input>
<button onClick={addToDo}>Add To Do</button>
<ul>
{toDoList.map((toDo, key) => {
return (
<li
key={key}
style={{ textDecoration: toDo.completed && "line-through" }}
>
<span onClick={() => toggleToDo(key)}>{toDo.text}</span>
<button onClick={() => deleteToDo(key)}>x</button>
</li>
);
})}
</ul>
</div>
);
}
export default ToDo;
I tried to write code that would allow me to toggle a line-through

Im creating a course marketplace using MERN stack but i get the React-Quill error when I try to open the page of Edit Course

Iam getting this error when I load the page for course edit
Unhandled Runtime Error Error: You are passing the delta object from
the onChange event back as value. You most probably want
editor.getContents() instead. See:
https://github.com/zenoamaro/react-quill#using-deltas
Below is the code of the edit form page CreateCourseForm.js which im importing to the other page [slug].js
import { Badge } from "antd";
import dynamic from "next/dynamic";
const QuillNoSSRWrapper = dynamic(import('react-quill'), {
ssr: false,
loading: () => <p>Loading ...</p>,
});
const CreateCourseForm = ({
handleSubmit,
handleCourseImageUpload,
handleChange,
values,
setValues,
categories,
previewImage,
imageUploadButtonText,
editCategories,
handleCourseImageRemove = (f) => f,
editPage = false,
}) => {
const handleQuillEdit = (value) => {
setValues((prev) => {
return {
...prev,
description: value
}
});
};
const modules = {
toolbar: [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block', 'image'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
[{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
// custom dropdown
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
[{ 'align': [] }],
['clean']
],
clipboard: {
// toggle to add extra line breaks when pasting HTML:
matchVisual: false,
},
};
return (
<>
{values &&
<form onSubmit={handleSubmit}>
<div className="form-group">
<input
type="text"
className="form-control"
placeholder="Course Name"
name="name"
value={values.name}
onChange={handleChange}
/>
</div>
<div className="form-group">
<QuillNoSSRWrapper theme="snow"
placeholder="Course Description"
modules={modules}
value={values.description || ''}
onChange={handleQuillEdit}/>
</div>
<div className="row form-group">
<label>Course Fee</label>
<div className="col-md-6">
<select
className="form-control"
value={values.paid}
onChange={(v) => setValues({...values, paid: !values.paid})}
>
<option value={false}>Free</option>
<option value={true}>Paid</option>
</select>
</div>
{values.paid &&
<div className="col-md-6">
<input
type="number"
className="form-control"
placeholder="Enter Price in $"
name="price"
value={values.price}
onChange={handleChange}
/>
</div>
}
</div>
<div className="row">
<div className="col-md-8">
<div className="form-group">
<label className="btn btn-outline-secondary btn-block text-left">
{imageUploadButtonText}
<input
type="file"
name="image"
onChange={handleCourseImageUpload}
accept="image/*"
hidden
/>
</label>
</div>
</div>
{previewImage && (
<div className="col-md-4" onClick={handleCourseImageRemove} style={{cursor:'pointer'}}>
<Badge count={"Delete"} style={{color: '#fe4a55'}}>
<img src={previewImage} width="50" alt="image" />
</Badge>
</div>
)}
{editPage && values.image && (
<div className="col-md-2">
<img src={values.image.Location} width="50" alt="image" />
</div>
)}
<p>** If you don't upload any course image then Progyan will update it with a default system image</p>
</div>
<div className="form-group">
<label>Category</label>
<select className="form-control" value={values.category} name="category" onChange={handleChange}>
<option value="none" disabled>Select an Option</option>
{!editPage && categories.map((c) => (
<option value={c.name}>{c.name}</option>
))}
{editPage && editCategories.map((c) => (
<option value={c.name}>{c.name}</option>
))}
</select>
</div>
<button
onClick={handleSubmit}
disabled={values.loading || values.uploading || !values.name || !values.description}>
{values.loading ? "Saving..." : "Save & Continue"}
</button>
</form>
}
</>
);
};
export default CreateCourseForm;
And here is the [slug].js page code
const CourseEdit = () => {
{/*States For Updating Course*/}
const [values, setValues] = useState({
name: "",
description: "",
price: "",
uploading: false,
paid: false,
category: "",
loading: false,
lessons : [],
assessments : [],
});
const [image,setImage] = useState({});
const [previewImage, setPreviewImage] = useState("");
const [imageUploadButtonText, setImageUploadButtonText] = useState('Upload Course Thumbnail');
{/*States For Updating Lesson*/}
const [lessonVisible,setLessonVisible] = useState(false);
const [assessmentVisible,setAssessmentVisible] = useState(false);
const [current, setCurrent] = useState({});
const [updateVideoButtonText, setUpdateVideoButtonText] = useState("Update Lesson Video");
const [progress, setProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const router = useRouter();
const {slug} = router.query;
useEffect(() => {loadCourse();}, [slug]);
const [editCategories, setEditCategories] = useState([]);
useEffect(() => {loadCategories(); }, []);
const loadCategories = async () => {
try {
let { data } = await axios.get("/api/categories");
setEditCategories(data);
} catch (err) {
toast.error(err.response.data);
}
};
{/*Function for load the data from MongoDB to the form*/}
const loadCourse = async () => {
const {data} = await axios.get(`/api/course/${slug}`);
if(data) setValues(data);
if(data && data.image) setImage(data.image);
}
{/*Function for saving all of the data from input form*/}
const handleChange = (e) => {
setValues({...values, [e.target.name]: e.target.value});
};
{/*Function for uploading the course image to AWS S3*/}
const handleCourseImageUpload = (e) => {
let file = e.target.files[0];
setPreviewImage(window.URL.createObjectURL(file));
setImageUploadButtonText(file.name);
setValues({...values, loading: true});
//Resize Image
Resizer.imageFileResizer(file, 750, 500, "JPEG", 90, 0, async (uri) => {
try{
let {data} = await axios.post(`/api/course/upload-image`,{
image: uri,
});
//set Image in the state
setImage(data);
setValues({...values, loading: false});
}catch(err){
setValues({...values, loading: false});
toast.error(err.response.data);
}
});
};
{/*Function for removing the course image from AWS S3*/}
const handleCourseImageRemove = async (e) => {
try{
setValues({...values, loading: true});
const res = await axios.post(`/api/course/remove-image`, {image});
setImage({});
setPreviewImage("");
setImageUploadButtonText('Upload Course Thumbnail');
setValues({...values, loading: false});
}catch(err){
setValues({...values, loading: false});
toast.error(err.response.data);
}
}
{/*Function for updating the course to MongoDB*/}
const handleSubmit = async (e) => {
e.preventDefault();
try{
if(!values.paid){ values.price=0; }
const {data} = await axios.put(`/api/course/${slug}`, {
...values,
image,
});
toast.success("Course Updated Successfully..!!");
}catch(err){
toast.error(err.response.data);
}
};
{/*Function for updating the lesson video to AWS S3*/}
const handleVideoUpdate = async (e) => {
//Remove Previous Video
if(current.video && current.video.Location){
const res = await axios.post(`/api/course/video-remove/${values.instructor._id}`, current.video);
toast.success(res);
}
//Upload
const file = e.target.files[0];
setUpdateVideoButtonText(file.name);
setUploading(true);
//Send Video as form data
const videoData = new FormData();
videoData.append('video',file);
videoData.append('courseId',values._id);
//Save progress bar and send video form data to backend
const {data} = await axios.post(
`/api/course/video-upload/${values.instructor._id}/${values.slug}`,
videoData,
{
onUploadProgress: (e) => setProgress(Math.round((100 * e.loaded) / e.total)),
}
);
setCurrent({...current, video: data});
setUploading(false);
};
{/*Function for dragging the lesson sequence*/}
const handleLessonDrag = (e, index) => {
e.dataTransfer.setData("itemIndex", index);
};
{/*Function for dropping the lesson sequence*/}
const handleLessonDrop = async (e, index) => {
const movingItemIndex = e.dataTransfer.getData("itemIndex");
const targetItemIndex = index;
let allLessons = values.lessons;
let movingItem = allLessons[movingItemIndex]; //Clicked or Dragged Item to Reorder
allLessons.splice(movingItemIndex, 1); //Remove 1 item from the given index
allLessons.splice(targetItemIndex, 0, movingItem); //Push item after target item index
setValues({...values, lessons: [...allLessons]});
//Save the new lessons order in MongoDB
const {data} = await axios.put(`/api/course/${slug}`,{...values, image});
toast.success("Lesson Rearrange Successfully");
};
{/*Function for deleting the lesson from the course & MongoDB*/}
const handleDeleteLesson = async (index) => {
Swal.fire({
title: 'Are you sure?',
text: "Do You Want To Delete This Lesson..??",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, Delete It.!'
}).then(async (result) => {
if (result.isConfirmed) {
let allLessons = values.lessons;
const removed = allLessons.splice(index, 1);
setValues({...values, lessons: allLessons});
const {data} = await axios.put(`/api/course/${slug}/${removed[0]._id}`);
toast.success("Lesson Deleted Successfully..!!!");
}
});
};
{/*Function for updating the lesson to MongoDB*/}
const handleUpdateLesson = async (e) => {
e.preventDefault();
const {data}= await axios.put(`/api/course/lesson/${slug}/${current._id}`, current);
setUpdateVideoButtonText("Update Lesson Video");
setLessonVisible(false);
//Update UI
if(data.ok){
let arr = values.lessons;
const index = arr.findIndex((el) => el._id === current._id);
arr[index] = current;
setValues({...values, lessons:arr});
toast.success("Lesson Updated Successfully..!");
}
};
{/*Function for dragging the assessment sequence*/}
const handleAssessmentDrag = (e, index) => {
e.dataTransfer.setData("itemIndex", index);
};
{/*Function for dropping the assessment sequence*/}
const handleAssessmentDrop = async (e, index) => {
const movingItemIndex = e.dataTransfer.getData("itemIndex");
const targetItemIndex = index;
let allAssessments = values.assessments;
let movingItem = allAssessments[movingItemIndex]; //Clicked or Dragged Item to Reorder
allAssessments.splice(movingItemIndex, 1); //Remove 1 item from the given index
allAssessments.splice(targetItemIndex, 0, movingItem); //Push item after target item index
setValues({...values, assessments: [...allAssessments]});
//Save the new assessment order in MongoDB
const {data} = await axios.put(`/api/course/${slug}`,{...values, image});
toast.success("Assessment Rearrange Successfully");
};
{/*Function for deleting the assessment from the course & MongoDB*/}
const handleDeleteAssessment = async (index) => {
Swal.fire({
title: 'Are you sure?',
text: "Do You Want To Delete This Assessment..??",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, Delete It.!'
}).then(async (result) => {
if (result.isConfirmed) {
let allAssessments = values.assessments;
const removed = allAssessments.splice(index, 1);
setValues({...values, assessments: allAssessments});
const {data} = await axios.put(`/api/course/assessment/${slug}/${removed[0]._id}`);
toast.success("Assessment Deleted Successfully..!!!");
}
});
};
{/*Function for updating the assessment to MongoDB*/}
const handleUpdateAssessment = async (e) => {
e.preventDefault();
const {data}= await axios.put(`/api/course/assessment/${slug}/${current._id}`, current);
setAssessmentVisible(false);
//Update UI
if(data.ok){
let arr = values.assessments;
const index = arr.findIndex((el) => el._id === current._id);
arr[index] = current;
setValues({...values, assessments:arr});
toast.success("Assessment Updated Successfully..!");
}
};
return (
<InstructorRoute>
<React.Fragment>
<PageBanner
pageTitle="Edit Course"
homePageUrl="/instructor"
homePageText="Dashboard"
activePageText="Edit Course"
/>
<div className="footer-area pt-100 pb-70">
<div className="container">
<div className="login-form">
<CreateCourseForm
handleSubmit={handleSubmit}
handleCourseImageUpload={handleCourseImageUpload}
handleChange={handleChange}
values={values}
setValues={setValues}
editCategories={editCategories}
previewImage={previewImage}
imageUploadButtonText={imageUploadButtonText}
handleCourseImageRemove={handleCourseImageRemove}
editPage={true}
/>
</div>
</div>
</div>
{values.lessons && values.lessons.length > 0 &&
<div className="about-area bg-fef8ef ptb-100">
<div className="container">
<div className="section-title">
<h5>Drag & Drop the Lessons to Change The Sequence</h5>
</div>
<List
onDragOver={(e) => e.preventDefault()}
dataSource={values && values.lessons}
renderItem={(item, index) => (
<Item
draggable
onDragStart={(e) => handleLessonDrag(e, index)}
onDrop={(e) => handleLessonDrop(e, index)}>
<div className="single-courses-item">
<div className="courses-content d-flex justify-content-between">
<h3>{index+1}) {item.title.length > 10 ? item.title.substring(0,10)+"...": item.title}</h3>
<div className="float-right">
<EditOutlined className="complementary-btn mx-5" onClick={() => {
setLessonVisible(true);
setCurrent(item);
}}/>
<DeleteOutlined
className="text-danger"
onClick={() => handleDeleteLesson(index)}/>
</div>
</div>
</div>
</Item>
)}>
</List>
</div>
<div className="shape1"><img src="/images/shape1.png" alt="image" /></div>
<div className="shape2"><img src="/images/shape2.png" alt="image" /></div>
<div className="shape3"><img src="/images/shape3.png" alt="image" /></div>
<div className="shape4"><img src="/images/shape4.png" alt="image" /></div>
</div>
}
{values.assessments && values.assessments.length > 0 &&
<div className="main-banner-wrapper ptb-100">
<div className="container">
<div className="section-title">
<h5>Drag & Drop the Assessment to Change The Sequence</h5>
</div>
<List
onDragOver={(e) => e.preventDefault()}
dataSource={values && values.assessments}
renderItem={(item, index) => (
<Item
draggable
onDragStart={(e) => handleAssessmentDrag(e, index)}
onDrop={(e) => handleAssessmentDrop(e, index)}>
<div className="single-courses-item">
<div className="courses-content d-flex justify-content-between">
<h3>{index+1}) {item.question.length > 10 ? item.question.substring(0,10)+"...": item.question}</h3>
<div className="float-right">
<EditOutlined className="complementary-btn mx-5" onClick={() => {
setAssessmentVisible(true);
setCurrent(item);
}}/>
<DeleteOutlined
className="text-danger"
onClick={() => handleDeleteAssessment(index)}/>
</div>
</div>
</div>
</Item>
)}>
</List>
</div>
<div class="banner-shape14"><img src="/images/banner-shape15.png" alt="image"/></div>
<div class="banner-shape15"><img src="/images/banner-shape16.png" alt="image"/></div>
<div class="banner-shape16"><img src="/images/banner-shape17.png" alt="image"/></div>
<div class="banner-shape17"><img src="/images/banner-shape18.png" alt="image"/></div>
<div class="banner-shape18"><img src="/images/banner-shape19.png" alt="image"/></div>
</div>
}
<div className="newsletter-modal" style={{display: lessonVisible?"block":"none"}}>
<div className="newsletter-modal-content">
<a className="close-btn" onClick={() => setLessonVisible(false)}>Close</a>
<UpdateLessonForm
current={current}
setCurrent={setCurrent}
handleVideoUpdate={handleVideoUpdate}
handleUpdateLesson={handleUpdateLesson}
updateVideoButtonText={updateVideoButtonText}
progress={progress}
uploading={uploading}
/>
</div>
</div>
<div className="newsletter-modal" style={{display: assessmentVisible?"block":"none"}}>
<div className="newsletter-modal-content">
<a className="close-btn" onClick={() => setAssessmentVisible(false)}>Close</a>
<UpdateAssessmentForm
current={current}
setCurrent={setCurrent}
handleUpdateAssessment={handleUpdateAssessment}
/>
</div>
</div>
<div className="lines">
<div className="line"></div>
<div className="line"></div>
<div className="line"></div>
</div>
</React.Fragment>
</InstructorRoute>
);
};
export default CourseEdit;

Action being callled but not hitting

I am using MERN stack and redux, i have created a findOneAndUpdate api and tested it on Postman. All works as it should but i can't get it working on my website. It never actually hits the updateSubject action. Am i passing in the data incorrectly? Anyone any idea what i am missing?
action
export const updateSubject = (id, rate, noOfVotes, rating) => (dispatch) => {
console.log("updateSubject hitting");
fetch(`/api/subjects/Subject/${id}/${rate}/${noOfVotes}/${rating}`)
.then((res) => res.json())
.then((subject) =>
dispatch({
type: UPDATE_SUBJECT,
subjects: subject,
})
);
};
api
// update subject
subjectRouter.put("/subject/:_id/:rate/:noOfVotes/:rating", (req, res) => {
Subject.findOneAndUpdate(
{ _id: req.params._id },
{
rating: Number(req.params.rating) + Number(req.params.rate),
noOfVotes: Number(req.params.noOfVotes) + 1,
},
{
new: true,
useFindAndModify: false,
}
)
.then((subjects) => res.json(subjects))
.catch((err) => console.log(err));
});
component
import React, { Component } from "react";
import PropTypes from "prop-types";
import GoogleSearch from "./GoogleSearch";
import { connect } from "react-redux";
import { fetchSubjects } from "../../actions/subject";
import { fetchComments } from "../../actions/comment";
import { updateSubject } from "../../actions/subject";
class Subject extends Component {
// on loading the subjects and comments
// are fetched from the database
componentDidMount() {
this.props.fetchSubjects();
this.props.fetchComments();
}
constructor(props) {
super(props);
this.state = {
// set inital state for subjects description
// and summary to invisible
viewDesription: -1,
viewSummary: -1,
comments: [],
};
}
componentWillReceiveProps(nextProps) {
// new subject and comments are added to the top
if (nextProps.newPost) {
this.props.subjects.unshift(nextProps.newPost);
}
if (nextProps.newPost) {
this.props.comments.unshift(nextProps.newPost);
}
}
clickHandler = (id) => {
// when a subject title is clicked pass in its id
// and make the desciption visible
const { viewDescription } = this.state;
this.setState({ viewDescription: viewDescription === id ? -1 : id });
// add relevant comments to the state
var i;
var temp = [];
for (i = 0; i < this.props.comments.length; i++) {
if (this.props.comments[i].subject === id) {
temp.unshift(this.props.comments[i]);
}
}
this.setState({
comments: temp,
});
// save the subject id to local storage
// this is done incase a new comment is added
// then the subject associated with it can be retrieved
// and added as a property of that comment
localStorage.setItem("passedSubject", id);
};
// hovering on and off subjects toggles the visibility of the summary
hoverHandler = (id) => {
this.setState({ viewSummary: id });
};
hoverOffHandler = () => {
this.setState({ viewSummary: -1 });
};
rateHandler = (id, rate) => {
var currRate;
var currVotes;
var i;
for (i = 0; i < this.props.subjects.length; i++) {
if (this.props.subjects[i]._id === id) {
currRate = this.props.subjects[i].rating;
currVotes = this.props.subjects[i].noOfVotes;
}
}
updateSubject(id, rate, currVotes, currRate);
console.log(id, rate, currVotes, currRate);
};
findAuthor(id) {
// search users for id return name
}
render() {
const subjectItems = this.props.subjects.map((subject) => {
// if the state equals the id set to visible if not set to invisible
var view = this.state.viewDescription === subject._id ? "" : "none";
var hover = this.state.viewSummary === subject._id ? "" : "none";
var comments = this.state.comments;
return (
<div key={subject._id}>
<div
className="subjectTitle"
onClick={() => this.clickHandler(subject._id)}
onMouseEnter={() => this.hoverHandler(subject._id)}
onMouseLeave={() => this.hoverOffHandler()}
>
<p className="title">{subject.title}</p>
<p className="rate">
Rate this subject:
<button onClick={() => this.rateHandler(subject._id, 1)}>
1
</button>
<button onClick={() => this.rateHandler(subject._id, 2)}>
2
</button>
<button onClick={() => this.rateHandler(subject._id, 3)}>
3
</button>
<button onClick={() => this.rateHandler(subject._id, 4)}>
4
</button>
<button onClick={() => this.rateHandler(subject._id, 5)}>
5
</button>
</p>
<p className="rating">
Rating: {(subject.rating / subject.noOfVotes).toFixed(1)}/5
</p>
<p className="summary" style={{ display: hover }}>
{subject.summary}
</p>
</div>
<div className="subjectBody " style={{ display: view }}>
<div className="subjectAuthor">
<p className="author" style={{ fontWeight: "bold" }}>
Subject created by: {subject.author} on {subject.date}
</p>
</div>
<div className="subjectDescription">
<p className="description">{subject.description}</p>
</div>
<div className="subjectLinks">Links:</div>
<div className="subjectComments">
<p style={{ fontWeight: "bold" }}>Comments:</p>
{comments.map((comment, i) => {
return (
<div key={i} className="singleComment">
<p>
{comment.title}
<br />
{comment.comment}
<br />
Comment by : {comment.author}
</p>
</div>
);
})}
<a href="/addcomment">
<div className="buttonAddComment">ADD COMMENT</div>
</a>
</div>
</div>
</div>
);
});
return (
<div id="Subject">
<GoogleSearch />
{subjectItems}
</div>
);
}
}
Subject.propTypes = {
fetchSubjects: PropTypes.func.isRequired,
fetchComments: PropTypes.func.isRequired,
subjects: PropTypes.array.isRequired,
comments: PropTypes.array.isRequired,
newPost: PropTypes.object,
};
const mapStateToProps = (state) => ({
subjects: state.subjects.items,
newSubject: state.subjects.item,
comments: state.comments.items,
newComment: state.comments.item,
});
// export default Subject;
export default connect(mapStateToProps, { fetchSubjects, fetchComments })(
Subject,
Comment
);
You are not attaching the updateSubject to the component itself, currently you only have fetchSubjects and fetchComments instead.
So, you would need to change your connect function like so:
export default connect(mapStateToProps, { fetchSubjects, fetchComments, updateSubject })(
Subject,
Comment
);
and then your calling function could be changed like so:
rateHandler = (id, rate) => {
const subject = this.props.subjects.find( subject => subject._id === id);
// when no subject was found, the updateSubject won't be called
subject && this.props.updateSubject( id, rate, subject.noOfVotes, subject.rating );
};
This would also mean that you should update your proptypes like:
Subject.propTypes = {
fetchSubjects: PropTypes.func.isRequired,
fetchComments: PropTypes.func.isRequired,
updateSubject: PropTypes.func.isRequired,
subjects: PropTypes.array.isRequired,
comments: PropTypes.array.isRequired,
newPost: PropTypes.object,
};
As you mentioned in the comments, you are using a put endpoint, so you should probably update your updateSubject method to the following
export const updateSubject = (id, rate, noOfVotes, rating) => (dispatch) => {
console.log("updateSubject hitting");
fetch(`/api/subjects/Subject/${id}/${rate}/${noOfVotes}/${rating}`, { method: 'PUT' })
.then((res) => res.json())
.then((subject) =>
dispatch({
type: UPDATE_SUBJECT,
subjects: subject,
})
);
};

Fetching multiple items in React/Redux causing infinite loop and no jsx on screen?

Goal:
I want to be able to fetch multiple profiles from an array and list them out on the screen. Something like:
John, Sandy, Drew
I am using react and trying to list out users from a friendRequest array. This array is filled with user id's and I want to map over them to get the user and show him/her on the screen.
What is happening is that in the console.log(pendingFriend), it is a infinite loop of in this case two profiles over and over again getting logged. Also no jsx is being displayed on the screen.
Here is the code.
Look in the render > return > where you see the currentUser.friendRequests being mapped over.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import swal from 'sweetalert';
import actions from '../../actions';
import { UpdateProfile } from '../view';
import { DateUtils } from '../../utils';
class Profile extends Component {
constructor() {
super();
this.state = {
profile: {
image:
'https://lh3.googleusercontent.com/EJf2u6azJe-TA6YeMWpDtMHAG6u3i1S1DhbiUXViaF5Pyg_CPEOCOEquKbX3U-drH29oYe98xKJiWqYP1ZxPGUQ545k',
bannerImage:
'https://lh3.googleusercontent.com/RAdfZt76XmM5p_rXwVsfQ3J8ca9aQUgONQaXSE1cC0bR0xETrKAoX8OEOzID-ro_3vFfgO8ZMQIqmjTiaCvuK4GtzI8',
firstName: 'First Name',
lastName: 'Last Name',
email: 'Contact Email',
bio: 'Bio will go here'
}
};
this.deleteProfile = this.deleteProfile.bind(this);
}
componentDidMount() {
const { id } = this.props.match.params;
if (this.props.profiles[id] != null) {
return;
}
this.props
.getProfile(id)
.then(() => {})
.catch(err => {
console.log(err);
});
}
createUpdatedProfile(params) {
const { id } = this.props.match.params;
const profile = this.props.profiles[id];
const { currentUser } = this.props.user;
if (currentUser.id !== profile.id) {
swal({
title: 'Oops...',
text: 'You do not own this profile',
icon: 'error'
});
return;
}
this.props
.updateProfile(currentUser, params)
.then(response => {
swal({
title: `${response.username} Updated!`,
text: 'Thank you for updating your profile',
icon: 'success'
});
})
.catch(err => {
console.log(err);
});
}
deleteProfile() {
const { id } = this.props.match.params;
const profile = this.props.profiles[id];
const { currentUser } = this.props.user;
if (currentUser.id !== profile.id) {
swal({
title: 'Oops...',
text: 'You do not own this profile',
icon: 'error'
});
return;
}
swal({
closeOnClickOutside: false,
closeOnEsc: false,
title: 'Are you sure?',
text:
'All data related to profile will be deleted as well with the profile! If you wish to delete your profile you must type DELETE',
icon: 'warning',
dangerMode: true,
buttons: true,
content: 'input'
}).then(value => {
if (value === 'DELETE') {
const userPosts = this.props.post.all.filter(p => p.profile.id === profile.id);
const userReplies = this.props.reply.all.filter(r => r.user.id === profile.id);
userPosts.map(post => {
this.props.deleteRecord(post);
});
userReplies.map(reply => {
this.props.deleteReply(reply);
});
this.props
.deleteProfile(profile)
.then(data => {
return this.props.logoutUser();
})
.then(data => {
this.props.history.push('/');
swal('Deleted!', 'Your Profile has been deleted.', 'success');
return null;
})
.catch(err => {
console.log(err);
});
}
swal({
title: 'Profile not deleted',
text: 'Make sure you type "DELETE" with caps',
icon: 'error'
});
});
}
addFriend() {
const { id } = this.props.match.params;
const profile = this.props.profiles[id];
const { currentUser } = this.props.user;
if (currentUser == null || profile == null) {
swal({
title: 'Oops...',
text: 'Must be logged in, and user must exist',
icon: 'error'
});
return;
}
const friendRequests = profile.friendRequests || [];
const params = {};
friendRequests.push(currentUser.id);
params.friendRequests = friendRequests;
this.props
.updateProfile(profile, params)
.then(() => {
swal({
title: 'Success',
text: 'Friend Request Sent',
icon: 'success'
});
})
.catch(err => {
console.log(err);
});
}
render() {
const { id } = this.props.match.params;
const profile = this.props.profiles[id];
const { currentUser } = this.props.user;
const defaultProfile = this.state.profile;
const bannerUrl =
profile == null
? defaultProfile.bannerImage
: profile.bannerImage || defaultProfile.bannerImage;
const bannerStyle = {
backgroundImage: `url(${bannerUrl})`,
backgroundSize: '100%',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center'
};
const nameStyle = {
background: 'rgba(255, 255, 255, 0.7)',
borderRadius: '8px'
};
const imageStyle = {
maxHeight: '150px',
margin: '20px auto'
};
return (
<div>
{profile == null ? (
<div>
<h1>Profile no longer exists</h1>
</div>
) : (
<div>
{currentUser == null ? null : currentUser.id !== profile.id ? null : (
<div className="list-group">
{currentUser.friendRequests
? currentUser.friendRequests.map(request => {
this.props
.getProfile(request)
.then(pendingFriend => {
console.log(pendingFriend);
return (
<div key={pendingFriend.id} className="list-group-item">
<p>{pendingFriend.username}</p>
</div>
);
})
.catch(err => {
console.log(err);
});
})
: null}
</div>
)}
<div className="jumbotron jumbotron-fluid" style={bannerStyle}>
<div className="container" style={nameStyle}>
<img
src={profile.image || defaultProfile.image}
style={imageStyle}
className="rounded img-fluid mx-auto d-block"
/>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<h1 className="display-3 text-center">{profile.username}</h1>
<p className="lead text-center">
{profile.firstName || defaultProfile.firstName}{' '}
{profile.lastName || defaultProfile.lastName}
</p>
<p className="lead text-center text-muted">
{profile.email || defaultProfile.email}
</p>
<p className="text-center text-muted">
Became a User: {DateUtils.relativeTime(profile.timestamp)}
</p>
<hr className="my-4" />
<p className="lead" style={{ border: '1px solid #e6e6e6', padding: '20px' }}>
{profile.bio || defaultProfile.bio}
</p>
</div>
</div>
{currentUser == null ? null : currentUser.id !== profile.id ? (
<div className="row justify-content-center" style={{ marginBottom: '100px' }}>
<div className="col-sm-6">
{profile.friendRequests ? (
profile.friendRequests.indexOf(currentUser.id) === -1 ? (
<button
className="btn btn-primary btn-lg btn-block"
onClick={this.addFriend.bind(this)}
>
Add Friend
</button>
) : (
<button className="btn btn-success btn-lg btn-block">
Pending Friend Request
</button>
)
) : (
<button
className="btn btn-primary btn-lg btn-block"
onClick={this.addFriend.bind(this)}
>
Add Friend
</button>
)}
</div>
</div>
) : (
<div>
<UpdateProfile
currentProfile={profile}
onCreate={this.createUpdatedProfile.bind(this)}
/>
<div className="row justify-content-center" style={{ marginBottom: '100px' }}>
<div className="col-sm-6">
<button
className="btn btn-danger btn-lg btn-block"
onClick={this.deleteProfile}
>
DELETE Profile
</button>
</div>
</div>
</div>
)}
</div>
)}
</div>
);
}
}
const stateToProps = state => {
return {
profiles: state.profile,
user: state.user,
post: state.post,
reply: state.reply
};
};
const dispatchToProps = dispatch => {
return {
getProfile: id => dispatch(actions.getProfile(id)),
updateProfile: (entity, params) => dispatch(actions.updateProfile(entity, params)),
deleteProfile: entity => dispatch(actions.deleteProfile(entity)),
deleteRecord: entity => dispatch(actions.deleteRecord(entity)),
deleteReply: entity => dispatch(actions.deleteReply(entity)),
logoutUser: () => dispatch(actions.logoutUser())
};
};
const loadData = store => {
return store.dispatch(actions.getProfile(this.props.match.params.id));
};
export default {
loadData: loadData,
component: connect(stateToProps, dispatchToProps)(Profile)
};
Your code is breaking the pattern in so many ways, i'm not sure where to start :)
First of all as for your question about the infinite loop, you
probably want to start with this line in your render method:
this.props.getProfile(request)
.then(pendingFriend => {
console.log(pendingFriend);
You should never ever update the state or dispatch actions.
These are the two main ways to re-render a component, state change
and new props. When you dispatch an action you actually causing a
new render call as a new prop will be received to your connected
component. With that said, do not do async calls inside the
render method, render method should be pure with no side effects.
Async calls and data fetching should be triggered in
componentDidMount.
Another thing not related to your problem directly, most of your
handlers are not bind the this object to the class, the only
handler you did bind is deleteProfile. bind them all or use
arrow functions which will use the this in a lexical context.
Again, not related directly to your problem, always pass props when
using the constructor and super:
constructor(props) {
super(props);
Edit
As a followup to your comment:
is it okay if I directly bind the functions like
this.deleteProfile.bind(this) instead of doing this.deleteProfile =
this.deleteProfile.bind(this) in the constructor just do
this.deleteProfile.bind(this) inside the render method
This won't change anything as bind isn't mutating the function, instead it returns a new instance with the this object is attached to it (it uses call behind the scenes) you can see the implementation.
So you must override your handlers with the new instance.
By the way, inside your render function, doing binding or any other operation that will create a new instance of a function or object is less preferable as you will create new instance on each render call of course. if you can "lift" it up to a method like the constructor (which is called only once in a component's life time) is much more performant.

Categories

Resources