MERN Application - Problem with notifications and mapping array - javascript

I'm on a MERN stack to create a chat application.
And when I'm trying to display notifications when there is a new message in another chatroom or a new PM I'm having this issue :
Sidebar.js:99 Uncaught TypeError: Cannot read properties of undefined (reading 'Tech')
at Sidebar.js:99:1
at Array.map (<anonymous>)
Tech is a room in an array, server side.
And problem is on front side :
<ul className='chatroom'>
{rooms.map((room, idx) => (
<li
key={idx}
onClick={() => joinRoom(room)}
className={room === currentRoom ? 'actif' : ''}
>
{room}
{currentRoom !== room &&
<span className='notifs'>
{user.newMessages[room]}
</span>
}
</li>
))}
</ul>
on this part particulary :
{currentRoom !== room &&
<span className='notifs'>
{user.newMessages[room]}
</span>
I tried a lot of things, starting by testing if the state is here with a ternary or a conditional. But nothing more. And it works because the first part with {room} is working great, all rooms displays on a list.
I'm on it since three days, I'm losing all my hairs 🤣 so if you have any idea...
I'm putting parts of the code if it can help
Sidebar.js
import React, { useContext, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { AppContext } from '../../context/appContext';
import { Div } from './Sidebar.elements'
import { addNotifications, resetNotifications } from '../../features/userSlice'
import { FaCircle } from 'react-icons/fa'
function Sidebar() {
const user = useSelector((state) => state.user);
const dispatch = useDispatch();
const {
socket,
setMembers,
members,
setCurrentRoom,
setRooms,
privateMemberMsg,
rooms,
setPrivateMemberMsg,
currentRoom
} = useContext(AppContext)
function joinRoom(room, isPublic = true){
if(!user){
return alert("Veuillez vous identifier");
}
socket.emit("join-room", room);
setCurrentRoom(room);
if (isPublic){
setPrivateMemberMsg(null);
}
// dispatch for notifications
dispatch(resetNotifications(room));
}
socket.off("notifications").on("notifications", (room) => {
if(currentRoom !== room) dispatch(addNotifications(room));
console.log(room);
});
function getRooms(){
fetch("http://localhost:5001/rooms")
.then((res) => res.json())
.then((data) => setRooms(data));
}
useEffect(()=>{
if(user){
setCurrentRoom('General');
getRooms();
socket.emit('join-room', 'General');
socket.emit('new-user');
}
},[])
socket.off('new-user').on('new-user', (payload) => {
setMembers(payload);
})
function orderIds(id1, id2) {
if(id1 > id2) {
return id1 + "-" + id2;
} else {
return id2 + "-" + id1;
}
}
function handlePrivateMemberMsg(member){
setPrivateMemberMsg(member);
const roomId = orderIds(user._id, member._id);
joinRoom(roomId, false);
}
if(!user){
return <></>;
}
return (
<>
<Div>
<h2>Chatrooms disponibles</h2>
<ul className='chatroom'>
{rooms.map((room, idx) => (
<li
key={idx}
onClick={() => joinRoom(room)}
className={room === currentRoom ? 'actif' : ''}
>
{room}
{currentRoom !== room &&
<span className='notifs'>
{user.newMessages[room]}
</span>
}
</li>
))}
</ul>
<h2 className='chat'>Chaters</h2>
<ul className='membre'>
{members.map((member) => (
<li
key={member.id}
className={`
${privateMemberMsg?._id === member?._id ? 'actif' : ''}
${member._id === user._id ? 'disabling' : ''}
`}
onClick={() => handlePrivateMemberMsg(member)}
>
<div className="row">
<div className="col-2">
<img src={member.picture} className='member-pic'/>
{member.status == "En ligne" ? <FaCircle className='online'/> : <FaCircle className='offline'/>}
</div>
<div className="col-9">
{member.name}
{member._id == user?._id && " (Vous)"}
{member.status == "Hors Ligne" && <span className="offtxt">(Offline)</span> }
</div>
<div className="col-1">
<span className="notifs">
{/* {user.newMessages[orderIds(member._id, user._id)]} */}
</span>
</div>
</div>
</li>
))}
</ul>
</Div>
</>
)
}
export default Sidebar
appApi.js
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
// define a service user and base URL
const appApi = createApi({
reducerPath: 'appApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:5001'
}),
endpoints: (builder) => ({
// creating the user
signupUser: builder.mutation({
query: (user) => ({
url: '/users',
method: "POST",
body: user,
}),
}),
// login
loginUser: builder.mutation({
query: (user) => ({
url:'/users/login',
method: "POST",
body: user,
}),
}),
//logout
logoutUser: builder.mutation({
query: (payload) => ({
url: '/logout',
method: "DELETE",
body: payload,
}),
}),
}),
});
export const { useSignupUserMutation, useLoginUserMutation, useLogoutUserMutation } = appApi;
export default appApi;
appContext.js
import { io } from 'socket.io-client'
import React from 'react'
const SOCKET_URL = "http://localhost:5001";
export const socket = io(SOCKET_URL);
//app context
export const AppContext = React.createContext()
userSlice.js
import { createSlice } from "#reduxjs/toolkit";
import appApi from "../services/appApi";
export const userSlice = createSlice({
name: "user",
initialState: null,
reducers: {
addNotifications: (state, { payload }) => {
if (state.newMessages[payload]) {
state.newMessages[payload] = state.newMessages[payload] + 1;
} else {
state.newMessages[payload] = 1;
}
},
resetNotifications: (state, { payload }) => {
delete state.newMessages[payload];
},
},
extraReducers: (builder) => {
// save user after signup
builder.addMatcher(appApi.endpoints.signupUser.matchFulfilled, (state, { payload }) => payload);
// save user after login
builder.addMatcher(appApi.endpoints.loginUser.matchFulfilled, (state, { payload }) => payload);
// logout : destroy user session
builder.addMatcher(appApi.endpoints.logoutUser.matchFulfilled, () => null);
},
})
export const { addNotifications, resetNotifications } = userSlice.actions;
export default userSlice.reducer;
server.js on back side
const express = require('express');
const app = express();
const userRoutes = require('./routes/userRoutes');
const User = require('./models/User');
const Message = require('./models/Message');
const rooms = [
'General',
'Tech',
'Crypto',
'Gaming'
];
const cors = require('cors');
app.use(express.urlencoded({extended: true}));
app.use(express.json());
app.use(cors());
app.use('/users', userRoutes)
require('./connection')
const server = require('http').createServer(app);
const PORT = 5001;
const io = require('socket.io')(server, {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST']
}
})
app.get('/rooms', (req, res)=> {
res.json(rooms)
})
async function getLastMessagesFromRoom(room){
let roomMessages = await Message.aggregate([
{$match: {to: room}},
{$group: {_id: '$date', messagesByDate: {$push: '$$ROOT'}}}
])
return roomMessages;
}
function sortRoomMessagesByDate(messages){
return messages.sort(function(a, b){
let date1 = a._id.split('/');
let date2 = b._id.split('/');
date1 = date1[2] + date1[0] + date1[1];
date2 = date2[2] + date2[0] + date2[1];
return date1 < date2 ? -1 : 1
})
}
// socket connection
io.on('connection', (socket)=> {
socket.on('new-user', async ()=>{
const members = await User.find();
io.emit('new-user', members);
})
socket.on('join-room', async(room)=> {
socket.join(room);
let roomMessages = await getLastMessagesFromRoom(room);
roomMessages = sortRoomMessagesByDate(roomMessages);
socket.emit('room-messages', roomMessages);
})
socket.on('message-room', async(room, content, sender, time, date) => {
const newMessage = await Message.create({content, from: sender, time, date, to: room});
let roomMessages = await getLastMessagesFromRoom(room);
roomMessages = sortRoomMessagesByDate(roomMessages);
// sending message to room
io.to(room).emit('room-messages', roomMessages);
// And send notification when sending new message
socket.broadcast.emit('notifications', room)
})
app.delete('/logout', async(req, res)=>{
try {
const {_id, newMessages} = req.body;
const user = await User.findById(_id);
user.status = "Hors Ligne";
user.newMessages = newMessages;
await user.save();
const members = await User.find();
socket.broadcast.emit('new-user', members);
res.status(200).send();
} catch (e) {
console.log(e);
res.status(400).send();
}
})
})
server.listen(PORT, () => {
console.log('listening to port ', PORT);
})
Edit
Adding the App.js that I forgot
App.js
import './App.css';
import {
BrowserRouter as Router,
Route,
Routes
} from 'react-router-dom'
import Home from './pages/Home';
import Login from './pages/Login';
import Signup from './pages/Signup';
import Chat from './pages/Chat';
import GlobalStyle from './GlobalStyles';
import { useSelector } from 'react-redux';
import { useState } from 'react';
import { AppContext, socket } from './context/appContext'
function App() {
const [rooms, setRooms] = useState([]);
const [currentRoom, setCurrentRoom] = useState([]);
const [members, setMembers] = useState([]);
const [messages, setMessages] = useState([]);
const [privateMemberMsg, setPrivateMemberMsg] = useState({});
const [newMessages, setNewMessages] = useState({});
const user = useSelector((state) => state.user);
return (
<AppContext.Provider value={{
socket,
currentRoom,
setCurrentRoom,
members,
setMembers,
messages,
setMessages,
privateMemberMsg,
setPrivateMemberMsg,
newMessages,
setNewMessages,
rooms,
setRooms
}} >
<Router>
<GlobalStyle/>
<Routes>
<Route path="/" element={<Home/>} />
{!user && (
<>
<Route path="/login" element={<Login/>} />
<Route path="/signup" element={<Signup/>} />
</>
)}
<Route path="/chat" element={<Chat/>} />
</Routes>
</Router>
</AppContext.Provider>
);
}
export default App;

Related

Why is message double sending to the other user in my react app?

I'm building a react app with socket-io and I'm currently experiencing a problem where when there is two users in the same room, the message from the sender will double send to the receiver. I've parsed through the code for a bit and can't figure out what is causing it. Here is the code.
Chat.js
import React, { useEffect, useState } from 'react'
function Chat({socket, username, room}) {
const [currentMessage, setCurrentMessage] = useState("");
const [messageList, setMessageList] = useState([]);
const sendMessage = async () => {
if (currentMessage != ""){
const messageData = {
room: room,
author: username,
message: currentMessage,
time: new Date(Date.now()).getHours() +
":" + new Date(Date.now()).getMinutes(),
};
await socket.emit("send_message", messageData);
setMessageList((list) => [...list, messageData]);
}
};
useEffect(() => {
socket.on("recieve_message", (data) => {
setMessageList((list) => [...list, data]);
})
},[socket]);
return (
<div className='chat-window'>
<div className='chat-header'>
<p>Live Chat</p>
</div>
<div className='chat-body'>
{messageList.map((messageContent) => {
return <h1>{messageContent.message}</h1>;
})}
</div>
<div className='chat-footer'>
<input
type="text"
placeholder="Hey..."
onChange={(event) => {
setCurrentMessage(event.target.value);
}}
/>
<button onClick={sendMessage}>â–º</button>
</div>
</div>
)
}
export default Chat
App.js
import './App.css';
import io from 'socket.io-client'
import {useState} from "react";
import Chat from './Chat'
const socket = io.connect("http://localhost:3001");
function App() {
const [username, setUsername] = useState("");
const [room, setRoom] = useState("");
const [showChat, setShowChat] = useState(false);
const joinRoom = () => {
if (username != "" && room != ""){
socket.emit("join_room", room)
setShowChat(true);
}
};
return (
<div className="App">
{!showChat ? (
<div className='joinChatContainer'>
<h3>Join a chat</h3>
<input type='text'
placeholder='Name?'
onChange={(event) => {setUsername(event.target.value);
}}/>
<input type='text'
placeholder='Room ID'
onChange={(event) => {setRoom(event.target.value);
}}/>
<button onClick={joinRoom}>Join A Room</button>
</div>
)
: (
<Chat socket={socket} username={username} room={room}/>
)}
</div>
);
}
export default App;
index.js
const express = require('express');
const app = express();
const http = require("http");
const cors = require("cors");
const { Server } = require("socket.io")
app.use(cors());
const server = http.createServer(app);
const io = new Server(server, {
cors:{
origin: "http://localhost:3000",
methods: ["GET", "POST"]
}
});
io.on("connection", (socket) => {
console.log('User Connected: ' + socket.id);
socket.on("join_room", (data) => {
socket.join(data);
console.log("User with ID: " + socket.id + " joined room " + data)
})
socket.on("send_message", (data) => {
console.log(data);
socket.to(data.room).emit("recieve_message", data);
});
socket.on("disconnect", () => {
console.log("User Disconnected", socket.id);
})
})
server.listen(3001, () => {
console.log("SERVER RUNNING");
});
I'm assuming there is something that calls twice when the receiver gets the message but I can't seem to find it

Could not make POST request to add comments to my backend server

I am new to express and router. When I try to add a comment to the database, the console returned the error ("Post 400 Bad request" and "Uncaught in Promise"). I've tried many solutions but it doesn't work. I think it is to do with my routing.
Below is my profilesRouter.js in the backend folder:
const express = require("express");
const router = express.Router();
class profilesRouter {
constructor(controller) {
this.controller = controller;
}
routes() {
router.get("/", this.controller.getAll.bind(this.controller));
router.get(
"/:profileId/comments",
this.controller.getComments.bind(this.controller)
);
router.post(
"/:profileId/comments",
this.controller.addComment.bind(this.controller)
);
return router;
}
}
module.exports = profilesRouter;
Here is my profilesController.js in the backend folder.
const BaseController = require("./baseController");
class profilesController extends BaseController {
constructor(model) {
super(model);
}
async getComments(req, res) {
const { profileId } = req.params;
try {
const comments = await this.models.comment.findByPk(profileId);
return res.json(comments);
} catch (err) {
return res.status(400).json({ error: true, msg: err });
}
}
async addComment(req, res) {
try {
const comment = { ...req.body };
const newComment = await this.models.comment.create(comment);
return res.json(newComment);
} catch (err) {
return res.status(400).json({ error: true, msg: err });
}
}
}
module.exports = profilesController;
On the other hand, for my frontend folder:
Here is my App.js:
import React from "react";
import { useState, useEffect } from "react";
import Single from "./Single";
import { Routes, Route } from "react-router-dom";
export default function App() {
const [profiles, setprofiles] = useState([]);
const getInitialData = async () => {
let initialAPICall = await axios.get(
`${process.env.REACT_APP_API_SERVER}/profiles`
);
setprofiles(initialAPICall.data);
};
useEffect(() => {
getInitialData();
}, []);
return (
<div className="App">
<Routes>
<Route exact path="/" element={<Home />}></Route>
<Route
exact
path="/profiles"
element={<Card profiles={profiles} />}
></Route>
<Route
path="/profiles/:profileIndex"
element={<Single profiles={profiles} />}
></Route>
</Routes>
</div>
);
}
Upon clicking on individual profiles, it will bring me to Single.js
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";
import { BACKEND_URL } from "../src/constant";
const Single = ({ profiles }) => {
const [comments, setComments] = useState();
const [commentContent, setCommentContent] = useState("");
const { profileIndex } = useParams();
const profile = profiles[profileIndex];
console.log(profile);
useEffect(() => {
// If there is a profiles.id, retrieve the profile data
if (profile.id) {
axios
.get(`${BACKEND_URL}/profiles/${profile.id}/comments`)
.then((response) => {
setComments(response.data);
});
}
// Only run this effect on change to profiles.id
}, [profile.id]);
console.log(profile.id);
if (!profile) {
return "No Profile";
}
const handleChange = (event) => {
setCommentContent(event.target.value);
};
const handleSubmit = (event) => {
// Prevent default form redirect on submission
event.preventDefault();
// Send request to create new comment in backend
axios
.post(
`${BACKEND_URL}/profiles/${profile.id}/comments`,
{
content: commentContent,
}
)
.then((res) => {
// Clear form state
setCommentContent("");
// Refresh local comment list
return axios.get(
`${BACKEND_URL}/profiles/${profile.id}/comments`
);
})
.then((response) => {
setComments(response.data);
});
};
// Store a new JSX element for each comment
const commentElements = comments
? comments.map((comment) => (
<ol key={comment.id}>
{comment.createdAt} | {comment.content}
</ol>
))
: [];
return (
<div className="App">
<form onSubmit={handleSubmit}>
<input
// Use textarea to give user more space to type
as="textarea"
name="content"
value={comments}
onChange={handleChange}
/>
<button variant="primary" type="submit">
Submit
</button>
</form>
</div>
);
};
export default Single;
Data is being stored in profile.json without the comments: [{"PROFILE_NUMBER": "A123", "NAME": "X", "AGE" : "21", "HOBBY" : "RUN"} , .....]
I am quite new and not sure how to debug it and push my comments into the database.
Try in this method
router.post('/add', (req, res) => {
let { userid, comment } = req.body;
commentModel.create({
userid: userid,
comment: comment
}).then((data) => {
console.log("data-add", data);
return res.json({
success: true,
message: "Comment added",
data: data
})
}).catch((error) => {
console.log("error: ", error);
return res.json({
success: false,
message: error.message,
data: []
})
})
});

Trying to submit a user score with a button in react. Getting error message of saveScore is not a function

I am building a react application and part of the application is a quiz section. At the end of the quiz there is a button which can save the user score in the quiz to the database.
This is my express route
// #route Put api/profile/saveScore/:id
// #desc Save users quiz score to profile
// #access Private
router.put('/saveScore/:topic_id', checkObjectId('topic_id'), auth, async (req, {params: {topic_id } }, res) => {
const score = req.body.score
const topic = topic_id
const newUserTopic = {
score,
topic,
}
try {
const profile = await Profile.findOne({ user: req.user.id });
profile.topics.unshift(newUserTopic);
await profile.save();
res.json(profile)
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
})
The express route works no bother in postman so thinking the issue must be more on the react side.
This is my action route
// Save Quiz Score to users profile
export const saveScore = (topicId, payload) => async (dispatch) => {
try {
const res = await api.put(`/profile/saveScore/${topicId}`, payload);
dispatch({
type: GET_PROFILE,
payload: res.data
});
dispatch(setAlert('Topic Saved', 'success'));
} catch (err) {
const errors = err.response.data.errors;
if(errors) {
errors.forEach(error => dispatch(setAlert(error.msg, 'danger')))
}
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
This is my component
import React, { useEffect, useState, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import QuizItem from './QuizItem';
import { getTopicById } from '../../actions/topic';
import { saveScore} from '../../actions/profile';
import { SaveScoreForm } from './SaveScoreForm';
const Quiz = ({ getTopicById, saveScore, topic: { topic, loading }, match }) => {
useEffect(() => {
getTopicById(match.params.id);
}, [getTopicById, match.params.id])
const [currentIndex, setCurrentIndex] = useState(0);
const [score, setScore] = useState(0);
const [showAnswers, setShowAnswers] = useState(false)
const [formData, setFormData] = useState({ score })
const handleAnswer = (answer) => {
if(!showAnswers) {
if(answer === topic[currentIndex].correct_answer) {
setScore(score + 1);
}
}
setShowAnswers(true);
};
const handleNextQuestion = () => {
setShowAnswers(false);
setCurrentIndex(currentIndex + 1);
}
console.log(currentIndex)
const onChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value })
}
const onSubmit = (e) => {
e.preventDefault();
const payload = new FormData();
payload.append('score', formData.score)
saveScore(payload, match.params.id);
}
return topic.length > 0 ? (
<div className='container'>
{currentIndex >= topic.length ? (
<Fragment>
<SaveScoreForm topic={topic} score={score} />
<form
onSubmit={e => onSubmit(e)}
>
<input
type='hidden'
value={score}
onChange={(e) => onChange(e)}
/>
<input type='submit' className='btn btn-primary1 my-1' />
</form>
</Fragment>
) : (
<QuizItem
key={topic.question}
topic={topic[currentIndex]}
showAnswers={showAnswers}
handleNextQuestion={handleNextQuestion}
handleAnswer={handleAnswer}
/>
)}
</div>
) : (
<Spinner/>
)
}
Quiz.prototype = {
getTopicById: PropTypes.func.isRequired,
topic: PropTypes.object.isRequired
}
const mapStateToProps = state => ({
topic: state.topic,
showAnswers: state.showAnswers,
handleNextQuestion: state.handleNextQuestion,
handleAnswer: state.handleAnswer
})
export default connect(mapStateToProps, { getTopicById })(Quiz)
Child component
import React from 'react'
export const SaveScoreForm = ({ score, topic, }) => {
return (
<div>
<div className='bg-primary1 p-2 my-4'>
<h1 className='large'>Review Your Score</h1>
<p className="lead">Quiz ended! Your score is: {(score/topic.length) * 100}%</p>
<p>Save your score to your profile or take the quiz again!</p>
</div>
</div>
);
};
export default SaveScoreForm;
TypeError: saveScore is not a function
Any help or pointers in the right direction would be very much appreciated.
Thanks
You are importing import { saveScore} from '../../actions/profile';
But then you have this prop
const Quiz = ({ getTopicById, saveScore
// ----------------------------^
which is overriding saveScore in your components context. Unless you are passing a saveScore prop while initialising <Quiz> it'll be undefined.
If you want to import the saveScore module just remove this prop variable.

redux request failed with status code 400(bad request)

i am getting an error "400 bad request" i can't find out what is my error
below i share my frontend and backend code..
i am also share my image error link that i came
https://ibb.co/swQPgYG
################### backend ######################
todoschema.js
this is a todoschema
var mongoose = require('mongoose');
var todoSchema = new mongoose.Schema({
name: {
type: String,
required: true,
maxlength:32,
trim:true
}
},{timestamps:true}
)
module.exports = mongoose.model('Todo',todoSchema)
auth.js/router
var express = require('express');
var router = express.Router();
const {addTodo} = require('../controller/auth');
router.post('/addtodo',addTodo);
module.exports = router;
auth.js/controller
const Todo = require('../model/todo');
exports.addTodo = (req,res) =>{
const todo = new Todo(req.body)
todo.save((err,todo) => {
if(err || !todo){
return res.status(400).json({
err : 'NOT able to store data in database'
})
}
res.json(todo);
})
}
################## frontEnd ###########################
API == http://localhost:8000/api/
here i fetch request from my backend
index.js
import {API} from '../backend'
import axios from 'axios'
export const getdata = (todo) => {
return (dispatch) => {
axios.post(`${API}addtodo`)
.then(res => {
console.log(res)
dispatch({
type : 'FETCH_TODO',
payload : todo
})
})
.catch(err =>{
console.log(err);
})
}
}
This is the todoForm where i add my todo
todoform.js
import React,{useState, useEffect} from 'react'
import '../App.css'
import {
FormGroup,
Input,
Button,
Form,
InputGroup,
InputGroupAddon
} from 'reactstrap';
import {v4} from 'uuid';
import 'bootstrap/dist/css/bootstrap.min.css';
import {getdata } from '../Auth'
//redux
import {connect, useDispatch} from 'react-redux'
import {addTodo} from '../Action/todo';
const TodoForm = ({addTodo}) => {
const [title,setTitle] = useState('')
const dispatch = useDispatch();
useEffect(() => {
dispatch(getdata())
}, []);
return(
<Form>
<FormGroup>
<InputGroup>
<Input
type='text'
name='todo'
id='todo'
placeholder='Your next Todo'
value={title}
onChange={e => setTitle(e.target.value)}
/>
<InputGroupAddon addonType='prepend'>
<Button color='primary' onClick={()=>{
if(title === ''){
return alert('please add a todo')
}
const todo = {
title,
id:v4(),
}
addTodo(todo);
setTitle('');
}}>
ADD
</Button>
</InputGroupAddon>
</InputGroup>
</FormGroup>
</Form>
)
}
const mapStateToProps = state => ({
})
const mapDispatchToProps = dispatch =>({
addTodo : todo =>{
dispatch(addTodo(todo))
},
})
export default connect(mapStateToProps,mapDispatchToProps)(TodoForm)
getData(todo) - action creator function require argument
export const getdata = (todo) => {
return (dispatch) => {
axios.post(`${API}addtodo`)
.then(res => {
console.log(res)
dispatch({
type : 'FETCH_TODO',
payload : todo
})
})
.catch(err =>{
console.log(err);
})
}
}
and you call it without argument
const dispatch = useDispatch();
useEffect(() => {
dispatch(getdata())
}, []);

How to prevent React state update for asynchronous request on unmounted component? [duplicate]

This question already has answers here:
How to cancel a fetch on componentWillUnmount
(16 answers)
Closed 2 years ago.
I'm working on a mernstack app where I have a custom hook for API requests with useReducer's state and dispatch functions that is loaded into the context api. Usually GET request runs smoothly on page load, but every time I use the POST, PATCH, PUT, and DELETE request functions it causes a component to unmount and get this error:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
The error goes away whenever I refresh the page and see the changes. How to prevent React state update for asynchronous request on unmounted component?
Database Setup
const mongodb = require('mongodb');
const { MongoClient, ObjectID } = mongodb;
require('dotenv').config();
const mongourl = process.env.MONGO_URI;
const db_name = process.env.DB_NAME;
let db;
async function startConnection(cb) {
let client;
try {
client = await MongoClient.connect(mongourl, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
db = client.db(db_name);
await cb();
} catch (err) {
await cb(err);
}
}
const getDb = () => {
return db;
};
const getPrimaryKey = (_id) => {
return ObjectID(_id);
};
module.exports = { db, startConnection, getDb, getPrimaryKey };
Server:
const express = require('express');
require('dotenv').config();
const port = process.env.PORT || 8000;
const db = require('./db');
const db_col = process.env.DB_COL;
const router = express.Router();
let status;
db.startConnection((err) => {
if (err) {
status = `Unable to connect to the database ${err}`;
console.log(status);
} else {
status = 'Connected to the database';
console.log(status);
}
});
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/list', router);
router.get('/', (req, res) => {
db.getDb()
.collection(db_col)
.find({})
.toArray((err, docs) => {
if (err) {
console.log(err);
}
res.json(docs);
console.log(docs);
});
});
router.post('/', (req, res) => {
const newlist = req.body;
const { list_name, list_items } = newlist;
db.getDb()
.collection(db_col)
.insertOne({ list_name, list_items }, (err, docs) => {
if (err) {
console.log(err);
}
res.redirect('/');
console.log(docs);
});
});
router.patch('/:id', (req, res) => {
const paramID = req.params.id;
const listname = req.body.list_name;
db.getDb()
.collection(db_col)
.updateOne(
{ _id: db.getPrimaryKey(paramID) },
{ $set: { list_name: listname } },
(err, docs) => {
if (err) {
console.log(err);
}
res.redirect('/');
console.log(docs);
}
);
});
router.put('/:id', (req, res) => {
const paramID = req.params.id;
const listitems = req.body.list_items;
db.getDb()
.collection(db_col)
.updateOne(
{ _id: db.getPrimaryKey(paramID) },
{ $set: { list_items: listitems } },
(err, docs) => {
if (err) {
console.log(err);
}
res.redirect('/');
console.log(docs);
}
);
});
router.delete('/:id', (req, res) => {
const paramID = req.params.id;
db.getDb()
.collection(db_col)
.deleteOne({ _id: db.getPrimaryKey(paramID) }, (err, docs) => {
if (err) {
console.log(err);
}
res.redirect('/');
console.log(docs);
});
});
app.listen(port, console.log(`Server listening to port: ${port}`));
Actions:
import { LOADING, PROCESSING_REQUEST, HANDLING_ERROR } from './actionTypes';
const loading = () => {
return {
type: LOADING,
};
};
const processingRequest = (params) => {
return {
type: PROCESSING_REQUEST,
response: params,
};
};
const handlingError = () => {
return {
type: HANDLING_ERROR,
};
};
export { loading, processingRequest, handlingError };
Reducer:
import {
LOADING,
PROCESSING_REQUEST,
HANDLING_ERROR,
} from './actions/actionTypes';
export const initialState = {
isError: false,
isLoading: false,
data: [],
};
const listReducer = (state, { type, response }) => {
switch (type) {
case LOADING:
return {
...state,
isLoading: true,
isError: false
};
case PROCESSING_REQUEST:
return {
...state,
isLoading: false,
isError: false,
data: response,
};
case HANDLING_ERROR:
return {
...state,
isLoading: false,
isError: true
};
default:
throw new Error();
}
};
export default listReducer;
Custom Hook for API Requests:
import { useEffect, useCallback, useReducer } from 'react';
import axios from 'axios';
import listReducer, { initialState } from '../../context/reducers/reducers';
import {
loading,
processingRequest,
handlingError,
} from '../../context/reducers/actions/actionCreators';
const useApiReq = () => {
const [state, dispatch] = useReducer(listReducer, initialState);
const getRequest = useCallback(async () => {
dispatch(loading());
try {
const response = await axios.get('/list');
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
const postRequest = useCallback(async (entry) => {
dispatch(loading());
try {
const response = await axios.post('/list', entry);
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
const patchRequest = useCallback(async (id, updated_entry) => {
dispatch(loading());
try {
const response = await axios.patch(`/list/${id}`, updated_entry);
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
const putRequest = useCallback(async (id, updated_entry) => {
dispatch(loading());
try {
const response = await axios.put(`/list/${id}`, updated_entry);
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
const deleteRequest = useCallback(async (id) => {
dispatch(loading());
try {
const response = await axios.delete(`/list/${id}`);
dispatch(processingRequest(response.data));
} catch (err) {
dispatch(handlingError);
}
}, []);
return [
state,
getRequest,
postRequest,
patchRequest,
putRequest,
deleteRequest,
];
};
export default useApiReq;
Context API
import React, { createContext } from 'react';
import useApiReq from '../components/custom-hooks/useApiReq';
export const AppContext = createContext();
const AppContextProvider = (props) => {
const [
state,
getRequest,
postRequest,
patchRequest,
putRequest,
deleteRequest,
] = useApiReq();
return (
<AppContext.Provider
value={{
state,
getRequest,
postRequest,
patchRequest,
putRequest,
deleteRequest,
}}
>
{props.children}
</AppContext.Provider>
);
};
export default AppContextProvider;
App:
import React from 'react';
import AppContextProvider from './context/AppContext';
import Header from './components/header/Header';
import Main from './components/main/Main';
import './stylesheets/styles.scss';
function App() {
return (
<AppContextProvider>
<div className='App'>
<Header />
<Main />
</div>
</AppContextProvider>
);
}
export default App;
Main:
This is where the GET request happens on initial load.
import React, { useEffect, useContext } from 'react';
import { AppContext } from '../../context/AppContext';
import Sidebar from '../sidebar/Sidebar';
import ParentListItem from '../list-templates/ParentListItem';
function Main() {
const { state, getRequest } = useContext(AppContext);
const { isError, isLoading, data } = state;
useEffect(() => {
getRequest();
}, [getRequest]);
return (
<main className='App-body'>
<Sidebar />
<div className='list-area'>
{isLoading && (
<p className='empty-notif'>Loading data from the database</p>
)}
{isError && <p className='empty-notif'>Something went wrong</p>}
{data.length == 0 && <p className='empty-notif'>Database is empty</p>}
<ul className='parent-list'>
{data.map((list) => (
<ParentListItem key={list._id} {...list} />
))}
</ul>
</div>
</main>
);
}
export default Main;
Sidebar
import React, { useState } from 'react';
import Modal from 'react-modal';
import AddList from '../modals/AddList';
import DeleteList from '../modals/DeleteList';
/* Modal */
Modal.setAppElement('#root');
function Sidebar() {
const [addModalStatus, setAddModalStatus] = useState(false);
const [deleteModalStatus, setDeleteModalStatus] = useState(false);
const handleAddModal = () => {
setAddModalStatus((prevState) => !prevState);
};
const handleDeleteModal = () => {
setDeleteModalStatus((prevState) => !prevState);
};
return (
<aside className='sidebar'>
<nav className='nav'>
<button className='btn-rec' onClick={handleAddModal}>
Add
</button>
<button className='btn-rec' onClick={handleDeleteModal}>
Delete
</button>
</nav>
<Modal isOpen={addModalStatus} onRequestClose={handleAddModal}>
<header className='modal-header'>Create New List</header>
<div className='modal-body'>
<AddList exitHandler={handleAddModal} />
</div>
<footer className='modal-footer'>
<button onClick={handleAddModal} className='btn-circle'>
×
</button>
</footer>
</Modal>
<Modal isOpen={deleteModalStatus} onRequestClose={handleDeleteModal}>
<header className='modal-header'>Delete List</header>
<div className='modal-body'>
<DeleteList exitHandler={handleDeleteModal} />
</div>
<footer className='modal-footer'>
<button onClick={handleDeleteModal} className='btn-circle'>
×
</button>
</footer>
</Modal>
</aside>
);
}
export default Sidebar;
Add Modal
This is where the post request is called
import React, { useContext, useEffect, useState, useRef } from 'react';
import { AppContext } from '../../context/AppContext';
const AddList = ({ exitHandler }) => {
const { postRequest } = useContext(AppContext);
const [newList, setNewList] = useState({});
const inputRef = useRef(null);
/* On load set focus on the input */
useEffect(() => {
inputRef.current.focus();
}, []);
const handleAddList = (e) => {
e.preventDefault();
const new_list = {
list_name: inputRef.current.value,
list_items: [],
};
setNewList(new_list);
};
const handleSubmit = (e) => {
e.preventDefault();
postRequest(newList);
exitHandler();
};
return (
<form onSubmit={handleSubmit} className='generic-form'>
<input
type='text'
ref={inputRef}
placeholder='List Name'
onChange={handleAddList}
/>
<input type='submit' value='ADD' className='btn-rec' />
</form>
);
};
export default AddList;
Delete Modal
This is where the Delete Request is called.
import React, { useContext, useEffect, useState, useRef } from 'react';
import { AppContext } from '../../context/AppContext';
const DeleteList = ({ exitHandler }) => {
const { state, deleteRequest } = useContext(AppContext);
const { data } = state;
const selectRef = useRef();
const [targetListId, setTargetListId] = useState();
useEffect(() => {
selectRef.current.focus();
}, []);
useEffect(() => {
setTargetListId(data[0]._id);
}, [data]);
const handleDeleteList = (e) => {
e.preventDefault();
deleteRequest(targetListId);
exitHandler();
};
const handleChangeList = (e) => {
setTargetListId(e.target.value);
};
return (
<form onSubmit={handleDeleteList} className='generic-form'>
<label>
<select
ref={selectRef}
value={targetListId}
onChange={handleChangeList}
className='custom-select'
>
{data.map((list) => (
<option key={list._id} value={list._id}>
{list.list_name}
</option>
))}
</select>
</label>
<input type='submit' value='DELETE' className='btn-rec' />
</form>
);
};
export default DeleteList;
Parent List:
This is where the PUT, PATCH request is called
import React, { useContext, useState, useEffect, useRef } from 'react';
import { FaPen, FaCheck } from 'react-icons/fa';
import ChildListItem from './ChildListItem';
import { AppContext } from '../../context/AppContext';
import displayDate from '../../utilities/utilities';
import { v4 } from 'uuid';
function ParentListItem({ _id, list_name, list_items }) {
const { patchRequest, putRequest } = useContext(AppContext);
const [activeListItems, setActiveListItems] = useState([]);
const [completedListItems, setCompletedListItems] = useState([]);
const [listItems, setListItems] = useState({});
const [disabledInput, setDisabledInput] = useState(true);
const [title, setTitle] = useState({});
const [status, setStatus] = useState(false);
const titleRef = useRef();
const { day, date, month, year, current_time } = displayDate();
const handleCreateNewItem = (e) => {
const newItem = {
item_id: v4(),
item_name: e.target.value,
item_date_created: `${day}, ${date} of ${month} ${year} at ${current_time}`,
isComplete: false,
};
const new_list_items = [...list_items, newItem];
setListItems({ list_items: new_list_items });
};
/* Handles the edit list title button */
const toggleEdit = () => {
setDisabledInput(!disabledInput);
};
/* Handles the edit list title button */
const toggleStatus = (item_id) => {
const target = list_items.find((item) => item.item_id == item_id);
let updated_list = [...list_items];
updated_list.map((list) => {
if (list == target) {
list.isComplete = !list.isComplete;
}
});
const update = { list_items: updated_list };
putRequest(_id, update);
};
/* Handles the edit list title button */
const deleteItem = (item_id) => {
const target = list_items.find((item) => item.item_id == item_id);
let updated_list = [...list_items].filter((list) => {
if (target.isComplete == true) {
return list !== target;
}
});
const update = { list_items: updated_list };
putRequest(_id, update);
};
/* Handles the edit list tile input */
const handleTitleChange = (e) => {
const newTitle = { list_name: e.target.value };
setTitle(newTitle);
};
/* Handles the submit or dispatched of edited list tile*/
const handleUpdateTitle = (e) => {
e.preventDefault();
patchRequest(_id, title);
setDisabledInput(!disabledInput);
};
const handleSubmitItem = (e) => {
e.preventDefault();
putRequest(_id, listItems);
[e.target.name] = '';
};
useEffect(
(e) => {
if (disabledInput === false) titleRef.current.focus();
},
[disabledInput]
);
useEffect(() => {
setTitle(list_name);
}, [list_name]);
useEffect(() => {
/* On load filter the active list */
let active_list_items = list_items.filter(
(item) => item.isComplete === false
);
setActiveListItems(active_list_items);
}, [list_items]);
useEffect(() => {
/* On load filter the completed list */
let completed_list_items = list_items.filter(
(item) => item.isComplete === true
);
setCompletedListItems(completed_list_items);
}, [list_items]);
return (
<li className='parent-list-item'>
<header className='p-li-header'>
<input
type='text'
className='edit-input'
name='newlist'
ref={titleRef}
defaultValue={list_name}
onChange={handleTitleChange}
disabled={disabledInput}
/>
{disabledInput === true ? (
<button className='btn-icon' onClick={toggleEdit}>
<FaPen />
</button>
) : (
<form onSubmit={handleUpdateTitle}>
<button className='btn-icon' type='submit'>
<FaCheck />
</button>
</form>
)}
</header>
<div id={_id} className='p-li-form-container'>
<form className='generic-form clouds' onSubmit={handleSubmitItem}>
<input
type='text'
placeholder='Add Item'
name='itemname'
onChange={handleCreateNewItem}
/>
<input type='submit' value='+' className='btn-circle' />
</form>
</div>
<div
className={list_items.length === 0 ? 'p-li-area hidden' : 'p-li-area'}
>
<section className='pi-child-list-container'>
<h6>Active: {activeListItems.length}</h6>
{activeListItems.length === 0 ? (
<p className='empty-notif'>List is empty</p>
) : (
<ul className='child-list'>
{activeListItems.map((list) => (
<ChildListItem
key={list.item_id}
{...list}
list_id={_id}
toggleStatus={toggleStatus}
deleteItem={deleteItem}
/>
))}
</ul>
)}
</section>
<section className='pi-child-list-container'>
<h6>Completed: {completedListItems.length}</h6>
{completedListItems.length === 0 ? (
<p className='empty-notif'>List is empty</p>
) : (
<ul className='child-list'>
{completedListItems.map((list) => (
<ChildListItem
key={list.item_id}
{...list}
list_id={_id}
toggleStatus={toggleStatus}
deleteItem={deleteItem}
/>
))}
</ul>
)}
</section>
</div>
</li>
);
}
export default ParentListItem;
Child List
import React from 'react';
import { IconContext } from 'react-icons';
import { FaTrashAlt, FaRegCircle, FaRegCheckCircle } from 'react-icons/fa';
function ChildListItem({
item_name,
item_id,
item_date_created,
isComplete,
toggleStatus,
deleteItem,
}) {
const handleIsComplete = (e) => {
e.preventDefault();
toggleStatus(item_id);
};
const handleDeleteItem = (e) => {
e.preventDefault();
deleteItem(item_id);
};
return (
<li className='c-li-item' key={item_id}>
<div className='c-li-details'>
<p className='item-name'>{item_name}</p>
<p className='date-details'>Date created: {item_date_created}</p>
</div>
<div className='c-li-cta'>
<label htmlFor={item_id} className='custom-checkbox-label'>
<input
type='checkbox'
id={item_id}
checked={isComplete}
onChange={handleIsComplete}
/>
<span className='btn-icon'>
<IconContext.Provider
value={{ className: 'react-icon ri-success' }}
>
{isComplete === false ? <FaRegCircle /> : <FaRegCheckCircle />}
</IconContext.Provider>
</span>
</label>
<button
className='btn-icon btn-delete'
disabled={!isComplete}
onClick={handleDeleteItem}
>
<IconContext.Provider
value={{
className:
isComplete === false
? 'react-icon ri-disabled'
: 'react-icon ri-danger',
}}
>
<FaTrashAlt />
</IconContext.Provider>
</button>
</div>
</li>
);
}
export default ChildListItem;
The warning occurred because your component received the response but it was already unmounted(stoped rendering)
To fix this you have to cancel the request after the component is unmounted like this using useEffect()'s cleanup function (by return cancel function) and axios like the example below
useEffect( () => {
const CancelToken = axios.CancelToken;
let cancel;
const callAPI = async () => {
try {
let res = await axios.post(`.....`, { cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
}) });
}
catch (err) {
console.log(err)
}
}
callAPI();
return (cancel);
}, []);
you can read more in axios docs: https://github.com/axios/axios
However, keep in mind this only solves the warning and not the reason it redirects after your post request.

Categories