Refreshing UI after deleting item in React - javascript

I'm new to React and I have the issue that my UI ain't refreshing once I send a delete fetch in my React app. I tried to use a useEffect on my deleteTaskHandler but it broke my code. Any ideas how to accomplish this refresh?
This is my Task.js file, which is receiving props from a TaskList.js file, and TaskList.js file sends a component to App.js:
import React, { useState } from 'react';
import classes from './Task.module.css';
const Task = (props) => {
const [isCompleted, setIsCompleted] = useState(props.isCompleted);
const changeCompleteStatus = () => {
setIsCompleted(!isCompleted);
}
const deleteTaskHandler = async () => {
try {
const key = props.id
const response = await fetch('http://localhost:5050/delete-task/' + key, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Something went wrong!');
};
const data = await response.json();
console.log(data);
} catch (error) {
console.log(error);
}
};
const updateTaskHandler = async () => {
const id = props.id
const taskData = {
id: id,
content: props.content,
isCompleted: !props.isCompleted,
dateCreation: props.dateCreation,
};
try {
const response = await fetch('http://localhost:5050/edit-task/' + id, {
method: 'PATCH',
body: JSON.stringify(taskData),
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Something went wrong!');
};
const data = await response.json();
console.log(data);
} catch (error) {
console.log(error);
}
};
let task;
if (props.isAllView) {
task = <div >
<input type="checkbox" onClick={updateTaskHandler} onChange={changeCompleteStatus} checked={isCompleted} />
<h2>{props.content}</h2>
<h3>{props.dateCreation}</h3>
<button onClick={deleteTaskHandler}>X</button>
</div>
} else {
task = <div >
<h2>{props.content}</h2>
<h3>{props.dateCreation}</h3>
</div>
}
return (
<li>{task}</li>
);
};
export default Task;
This is TaskList.js:
import React, { useState } from 'react';
import classes from './TaskList.module.css';
import Task from './Task';
const TaskList = (props) => {
const [taskView, setTaskView] = useState('all');
const getCompleteURL = () => {
setTaskView('complete')
props.onChangeTaskURL('http://localhost:5050/completed');
};
const getAllURL = () => {
setTaskView('all')
props.onChangeTaskURL('http://localhost:5050/');
};
const getPendingURL = () => {
setTaskView('pending')
props.onChangeTaskURL('http://localhost:5050/pending');
};
let taskList;
if (taskView != 'all') {
taskList = props.taskData.map((task) => (
<Task
key={task.id}
content={task.content}
dateCreation={task.dateCreation}
isCompleted={task.isCompleted}
isAllView={false}
/>
));
} else {
taskList = props.taskData.map((task) => (
<Task
key={task.id}
id={task.id}
content={task.content}
dateCreation={task.dateCreation}
isCompleted={task.isCompleted}
isAllView={true}
/>
));
}
return (
<div>
<ul >
{taskList}
</ul>
<button onClick={getCompleteURL}>Completed</button>
<button onClick={getAllURL}>All</button>
<button onClick={getPendingURL}>Pending</button>
</div>
);
};
export default TaskList;
This is App.js:
import React, { useState, useEffect, useCallback } from 'react';
import './App.css';
import TaskList from './components/Tasks/TaskList';
import NewTask from './components/NewTask/NewTask';
function App() {
const [tasks, setTasks] = useState([]);
const [taskURL, setTaskURL] = useState('http://localhost:5050/');
const fetchTasksHandler = useCallback(async (url) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Something went wrong!');
}
const data = await response.json();
const loadedTasks = [];
for (const key in data) {
loadedTasks.push({
id: data[key]._id,
content: data[key].content,
isCompleted: data[key].isCompleted,
dateCreation: data[key].dateCreation
});
}
console.log(loadedTasks)
setTasks(loadedTasks);
} catch (error) {
// throw new Error('Something went wrong!');
console.log(error)
}
}, []);
useEffect(() => {
fetchTasksHandler(taskURL);
}, [fetchTasksHandler, taskURL]);
const changeTaskURL = url => {
console.log(url)
setTaskURL(url);
};
return (
<React.Fragment>
<TaskList taskData={tasks} onChangeTaskURL={changeTaskURL}></TaskList>
<NewTask></NewTask>
</React.Fragment>
);
}
export default App;

Extract deleteTaskHandler and updateTaskHandler in your App.js and pass them down to the TaskList => Task. In both methods, on successful operation update the tasks state array (for delete - filter out the deleted task, for update - swap the old task with the updated one). This way, the Task component will call the relevant handler which will update the parent tasks state, which in turn will spill down to the TaskList and Task and everything will get updated automatically.
Here is a sample. Consider it more as a pseudo code as you'll have to modify some of the parts to handle your case appropriately.
Your Task.js:
import React, { useState } from 'react';
import classes from './Task.module.css';
const Task = (props) => {
const {
updateTaskHandler,
deleteTaskHandler
} = props;
const [isCompleted, setIsCompleted] = useState(props.isCompleted);
const changeCompleteStatus = () => {
setIsCompleted(!isCompleted);
}
const updateHandler = () => {
const taskData = {
id: id,
content: props.content,
isCompleted: !props.isCompleted,
dateCreation: props.dateCreation,
};
updateTaskHandler(props.id, taskData);
};
const deleteHandler = () => {
deleteTaskHandler(props.id);
};
let task;
if (props.isAllView) {
task = <div >
<input type="checkbox" onClick={updateHandler} onChange={changeCompleteStatus} checked={isCompleted} />
<h2>{props.content}</h2>
<h3>{props.dateCreation}</h3>
<button onClick={deleteHandler}>X</button>
</div>
} else {
task = <div >
<h2>{props.content}</h2>
<h3>{props.dateCreation}</h3>
</div>
}
return (
<li>{task}</li>
);
};
export default Task;
TaskList.js:
import React, { useState } from 'react';
import classes from './TaskList.module.css';
import Task from './Task';
const TaskList = (props) => {
const [taskView, setTaskView] = useState('all');
const getCompleteURL = () => {
setTaskView('complete')
props.onChangeTaskURL('http://localhost:5050/completed');
};
const getAllURL = () => {
setTaskView('all')
props.onChangeTaskURL('http://localhost:5050/');
};
const getPendingURL = () => {
setTaskView('pending')
props.onChangeTaskURL('http://localhost:5050/pending');
};
let taskList;
if (taskView != 'all') {
taskList = props.taskData.map((task) => (
<Task
key={task.id}
content={task.content}
dateCreation={task.dateCreation}
isCompleted={task.isCompleted}
isAllView={false}
updateTaskHandler={props.updateTaskHandler}
deleteTaskHandler={props.deleteTaskHandler}
/>
));
} else {
taskList = props.taskData.map((task) => (
<Task
key={task.id}
id={task.id}
content={task.content}
dateCreation={task.dateCreation}
isCompleted={task.isCompleted}
isAllView={true}
updateTaskHandler={props.updateTaskHandler}
deleteTaskHandler={props.deleteTaskHandler}
/>
));
}
return (
<div>
<ul >
{taskList}
</ul>
<button onClick={getCompleteURL}>Completed</button>
<button onClick={getAllURL}>All</button>
<button onClick={getPendingURL}>Pending</button>
</div>
);
};
export default TaskList;
And App.js:
import React, { useState, useEffect, useCallback } from 'react';
import './App.css';
import TaskList from './components/Tasks/TaskList';
import NewTask from './components/NewTask/NewTask';
function App() {
const [tasks, setTasks] = useState([]);
const [taskURL, setTaskURL] = useState('http://localhost:5050/');
const fetchTasksHandler = useCallback(async (url) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Something went wrong!');
}
const data = await response.json();
const loadedTasks = [];
for (const key in data) {
loadedTasks.push({
id: data[key]._id,
content: data[key].content,
isCompleted: data[key].isCompleted,
dateCreation: data[key].dateCreation
});
}
setTasks(loadedTasks);
}
catch (error) {
// throw new Error('Something went wrong!');
console.log(error)
}
}, []);
const deleteTaskHandler = async (taskID) => {
try {
const response = await fetch(`http://localhost:5050/delete-task/${taskID}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Something went wrong!');
};
const data = await response.json();
setTasks(tasks => {
return tasks.filter(task => task.id !== taskID)
});
}
catch (error) {
console.log(error);
}
};
const updateTaskHandler = async (taskID, taskData) => {
const id = props.id
const taskData = {
id: id,
content: props.content,
isCompleted: !props.isCompleted,
dateCreation: props.dateCreation,
};
try {
const response = await fetch(`http://localhost:5050/edit-task/${taskID}`, {
method: 'PATCH',
body: JSON.stringify(taskData),
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Something went wrong!');
};
const data = await response.json();
setTasks(tasks => {
return tasks.map(task => {
if (task.id !== taskID) {
return task;
}
else {
return data; // The updated task
}
})
});
}
catch (error) {
console.log(error);
}
};
useEffect(() => {
fetchTasksHandler(taskURL);
}, [fetchTasksHandler, taskURL]);
const changeTaskURL = url => {
console.log(url)
setTaskURL(url);
};
return (
<React.Fragment>
<TaskList
taskData={tasks}
onChangeTaskURL={changeTaskURL}
deleteTaskHandler={deleteTaskHandler}
updateTaskHandler={updateTaskHandler}
/>
<NewTask />
</React.Fragment>
);
}
export default App;

You have to pass a callback that removes a task and pass it as a prop to the Task component.
In your App.js:
function deleteTask(id) {
setTasks(loadedTasks.filter(x => x.id !== id);
}
// ...
<TaskList taskData={tasks} onDelete={deleteTask}></TaskList>
In your TaskList.js:
tasks.map(task => <Task id={task.id} key={task.id} onDelete={props.deleteTask}></Task>)
In the Task.js call props.deleteTask(props.id) whenever you need to.
Note that passing a prop through two components or more is called "prop drilling" and should be avoided (by keeping the state in TaskList.js for example).

you can use
window.location.reload();
to refresh the page or
this.setState({});
to refresh the component or
const [value,setValue] = useState();
const refresh = ()=>{
setValue({});
}
to refresh the component using hooks
i hope you found this answer helpful

Related

jest not able to findbyTestID with async await

I am getting error : Unable to find an element by: [data-testid="postLists"]
My ReactJS code is:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const AwaitAsync2 = () => {
const [posts, setPosts] = useState(null);
useEffect(() => {
postApi();
}, []);
const postApi = async () => {
const {data} = await axios.get('https://jsonplaceholder.typicode.com/posts');
setPosts(data);
}
return (
<div>
<h2>Await Async</h2>
{
posts !== null && (<div data-testid="postLists">
{
posts.map((item, index) => {
return <div key={index}>
<h3>{item.title}</h3>
<p>{item.body}</p>
</div>
})
}
</div>)
}
</div>
)
}
and unit test case is
import { render, screen } from "#testing-library/react";
import AwaitAsync2 from "./aa2";
describe("Async Await 2 testing", () => {
it("API Testing", async () => {
render(<AwaitAsync2 />);
const data = await screen.findByTestId("postLists");
expect(data).toBeInTheDocument();
});
});
getting an error for this line
await screen.findByTestId("postLists");

React useEffect hook will cause an infinite loop if I add tasksMap to the depedency array

If I add tasksMap to the useEffect dependency array below an infinite loop will happen. Can someone point me in the right direction on how to fix this? In order for the user to get an updated view of the tasks that have been added or modified, I need the app to call getProjectTasks and assign the returned map to the tasksMap. I do know that anytime you update state the component rerenders. I just havne't figured out how to do this without creating an infinite loop. Any help is greatly appreciated. Thank you.
import { useContext, useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { UserContext } from "../../contexts/user.context";
import { ProjectsContext } from "../../contexts/projects.context";
import { createProjectTask, getProjectTasks } from "../../utils/firebase/firebase.utils";
import OutlinedCard from "../../components/cards/TaskCard.component";
import { TextField, Button, } from "#mui/material";
import "./project.styles.css";
import "./project.styles.css";
const Project = () => {
const params = useParams();
const projectId = params.id;
const { currentUser } = useContext(UserContext);
const { projectsMap } = useContext(ProjectsContext);
const [taskName, setTaskName] = useState("");
const [tasksMap, setTasksMap] = useState({});
const project = Object.keys(projectsMap)
.filter((id) => id.includes(projectId))
.reduce((obj, id) => {
return Object.assign(obj, {
[id]: projectsMap[id],
});
}, {});
useEffect(() => {
console.log("running")
const getTasksMap = async () => {
const taskMap = await getProjectTasks(currentUser, projectId);
taskMap ? setTasksMap(taskMap) : setTasksMap({});
};
getTasksMap();
}, [projectId])
const handleChange = (event) => {
const { value } = event.target;
setTaskName(value);
};
const handleSubmit = async (event) => {
event.preventDefault();
try {
await createProjectTask(currentUser, projectId, taskName);
setTaskName("");
} catch (error) {
console.log(error);
}
};
return (
<div className="project-container">
{project[projectId] ? <h2>{project[projectId].name}</h2> : ""}
<form onSubmit={handleSubmit} className="task-form">
<TextField label="Project Task" onChange={handleChange} value={taskName}></TextField>
<Button type="submit" variant="contained">
Add Task
</Button>
</form>
<div className="tasks-container">
{Object.keys(tasksMap).map((id) => {
const task = tasksMap[id];
return (
<OutlinedCard key={id} projectId={projectId} taskId={id} name={task.name}></OutlinedCard>
);
})}
</div>
</div>
);
};
export default Project;
This is where the taskMap object comes from. For clarification, I'm using Firebase.
export const getProjectTasks = async(userAuth, projectId) => {
if(!userAuth || !projectId) return;
const tasksCollectionRef = collection(db, "users", userAuth.uid, "projects", projectId, "tasks")
const q = query(tasksCollectionRef);
try {
const querySnapshot = await getDocs(q);
const taskMap = querySnapshot.docs.reduce((acc, docSnapshot) => {
const id = docSnapshot.id;
const { name } = docSnapshot.data();
acc[id] = {id, name};
return acc;
}, {});
return taskMap;
} catch (error) {
console.log("Error getting task docs.");
}
};
The useEffect appears to be setting tasksMap when executed. Because this state is an object its reference will change everytime, which will produce an infinite loop

Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component. - useEffect()

I get this error when I try and call a function I have imported within my useEffect() hook in Dashboard.jsx. I am just trying to pull in data from database on the page load pretty much so that when user click button they can send off correct credentials to the api.
I am pulling it in from database for security reasons, so client id is not baked into the code.
I am pretty sure that I am getting this error maybe because the function is not inside a react component? although I am not 100% sure. And if that is the case I am not sure of the best way to restructure my code and get the desired output.
Code below.
mavenlinkCredentials.js
import { doc, getDoc } from "firebase/firestore";
import { useContext } from "react";
import { AppContext } from "../../context/context";
import { db } from "../../firebase";
const GetMavenlinkClientId = async () => {
const {setMavenlinkClientId} = useContext(AppContext)
const mavenlinkRef = doc(db, 'mavenlink', 'application_id');
const mavenlinkDocSnap = await getDoc(mavenlinkRef)
if(mavenlinkDocSnap.exists()){
console.log("mavenlink id: ", mavenlinkDocSnap.data());
console.log(mavenlinkDocSnap.data()['mavenlinkAccessToken'])
setMavenlinkClientId(mavenlinkDocSnap.data()['application_id'])
} else {
console.log("No doc");
}
}
export default GetMavenlinkClientId;
Dashboard.jsx
import React, { useContext, useEffect, useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { useNavigate } from "react-router-dom";
import { query, collection, getDocs, where, setDoc, doc, getDoc } from "firebase/firestore";
import { auth, db, logout } from "../firebase";
import { Button, Container, Grid, Paper } from "#mui/material";
import ListDividers from "../components/ListDividers";
import { AppContext } from "../context/context";
import axios from "axios";
import {SuccessSnackbar, ErrorSnackbar} from '../components/PopupSnackbar';
import GetMavenlinkClientId from "../helpers/firebase/mavenlinkCredentials";
const Dashboard = () => {
const [user, loading, error] = useAuthState(auth);
const [name, setName] = useState("");
const [ accessToken, setAccessToken ] = useState("")
const [errorAlert, setErrorAlert] = useState(false);
const [successAlert, setSuccessAlert] = useState(false);
const [mavenlinkClientId, setMavenlinkClientId] = useState("");
const {isAuthenticated} = useContext(AppContext);
const navigate = useNavigate();
const uid = user.uid
const parsedUrl = new URL(window.location.href)
const userTokenCode = parsedUrl.searchParams.get("code");
const { mavenlinkConnected, setMavenlinkConnected } = useContext(AppContext)
const { maconomyConnected, setMaconomyConnected } = useContext(AppContext)
const { bambooConnected, setBambooConnected } = useContext(AppContext)
const fetchUserName = async () => {
try {
const q = query(collection(db, "users"), where("uid", "==", user?.uid));
const doc = await getDocs(q);
const data = doc.docs[0].data();
setName(data.name);
} catch (err) {
console.error(err);
alert("An error occured while fetching user data");
}
};
//
useEffect(() => {
if (loading) return;
if (!user) return navigate("/");
fetchUserName();
if(userTokenCode !== null){
authorizeMavenlink();
}
if(isAuthenticated){
GetMavenlinkClientId()
}
}, [user, loading]);
///put this into a page load (use effect maybe) so user does not need to press button to connect to apis
const authorizeMavenlink = () => {
console.log(uid);
const userRef = doc(db, 'users', uid);
axios({
//swap out localhost and store in variable like apitool
method: 'post',
url: 'http://localhost:5000/oauth/mavenlink?code='+userTokenCode,
data: {}
})
.then((response) => {
setAccessToken(response.data);
setDoc(userRef, { mavenlinkAccessToken: response.data}, { merge: true });
setMavenlinkConnected(true);
setSuccessAlert(true);
})
.catch((error) => {
console.log(error);
setErrorAlert(true)
});
}
//abstract out client id and pull in from db
const getMavenlinkAuthorization = () => {
window.open('https://app.mavenlink.com/oauth/authorize?client_id='+mavenlinkClientId+'&response_type=code&redirect_uri=http://localhost:3000');
window.close();
}
const authorizeBamboo = () => {
axios({
method: 'get',
url: 'http://localhost:5000/oauth/bamboo',
data: {}
})
.then((response) => {
console.log(response)
})
.catch((error) => {
console.log(error);
});
// console.log('bamboo connected')
setBambooConnected(true);
}
const authorizeMaconomy = () => {
console.log("Maconomy connected")
setMaconomyConnected(true);
}
const syncAccount = async() => {
if(!mavenlinkConnected){
await getMavenlinkAuthorization()
}
if (!bambooConnected){
await authorizeBamboo();
}
if (!maconomyConnected){
await authorizeMaconomy();
}
}
const handleAlertClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setSuccessAlert(false) && setErrorAlert(false);
};
console.log(mavenlinkClientId);
return(
<>
<Container>
<div className="dashboard">
<h1>Dashboard</h1>
<Grid container spacing={2}>
<Grid item xs={12}>
<Paper style={{paddingLeft: "120px", paddingRight: "120px"}} elevation={1}>
<div className="dashboard-welcome">
<h2>Welcome {name}</h2>
<h4>{user?.email}</h4>
<hr/>
<h2>Integrations</h2>
<Button onClick={syncAccount}>
Sync Account
</Button>
{/* <Button onClick={getMavenlinkClientId}>
Bamboo Test
</Button> */}
<ListDividers/>
</div>
</Paper>
</Grid>
</Grid>
</div>
{successAlert === true ? <SuccessSnackbar open={successAlert} handleClose={handleAlertClose}/> : <></> }
{errorAlert === true ? <ErrorSnackbar open={errorAlert} handleClose={handleAlertClose}/> : <></> }
</Container>
</>
);
}
export default Dashboard;
the error is because you’re calling const {setMavenlinkClientId} = useContext(AppContext) inside the file mavenlinkCredentials.js which is not a react components.
you could maybe change the function inside mavenlinkCredentials.js to accept a setMavenlinkClientId and pass it from outside like this.
const GetMavenlinkClientId = async (setMavenlinkClientId) => {
const mavenlinkRef = doc(db, 'mavenlink', 'application_id');
const mavenlinkDocSnap = await getDoc(mavenlinkRef)
if(mavenlinkDocSnap.exists()){
console.log("mavenlink id: ", mavenlinkDocSnap.data());
console.log(mavenlinkDocSnap.data()['mavenlinkAccessToken'])
setMavenlinkClientId(mavenlinkDocSnap.data()['application_id'])
} else {
console.log("No doc");
}
}
and then you can call this function in your dashboard.js like so,
const {setMavenlinkClientId} = useContext(AppContext)
if(isAuthenticated){
GetMavenlinkClientId(setMavenlinkClientId)
}

React functional component re-rendering infinitely when using Axios in useCallback hook?

Here is a file uploading component, everything works at expected, however, when attempting to POST the file using Axios in a useCallback, the ProgressBar component re-renders infinitely if there is an error from Axios. If I comment out the Axios post, the component does not re-render infinitely. How do I avoid the infinite re-rendering of the ProgressBar component?
import { useState, useCallback, useEffect } from 'react'
import { useDropzone } from 'react-dropzone'
import uuid from 'react-uuid'
import axios from 'axios'
import ProgressBar from './ProgressBar'
const FileUploader = ({ setNotifications }) => {
const [fileCount, setFileCount] = useState(0)
const [filesUploaded, setFilesUploaded] = useState([])
const [progress, setProgress] = useState(0)
const [uploaded, setUploaded] = useState(false)
const [exists, setExists] = useState(false)
const [error, setError] = useState(false)
const onDrop = useCallback(acceptedFiles => {
acceptedFiles.forEach(file => {
const reader = new FileReader()
// console.log(file)
reader.onloadstart = () => {
const exists = filesUploaded.find(uploadedFile => uploadedFile.name === file.name)
if (exists) {
return setNotifications(notifications => {
return [...notifications, `'${file.name}' has already been uploaded.`]
})
}
// setStart(true)
return setFilesUploaded(filesUploaded => {
return [...filesUploaded, file]
})
}
reader.onabort = () => {
setError(true)
console.log('file reading was aborted')
}
reader.onerror = () => {
setError(true)
console.log('file reading has failed')
}
reader.onprogress = e => {
// console.log('loaded', e.loaded)
// console.log('total', e.total)
if (e.lengthComputable) {
setProgress((e.loaded / e.total) * 100)
}
}
reader.onload = async () => {
// complete
await axios.post(
'/api/images',
{
file: reader.result
}
)
.then(res => {
if (res) {
setUploaded(true)
if (res === 200) {
// success
setExists(false)
} else if (res === 409) {
// already exists
setExists(true)
}
}
})
.catch(err => {
setError(true)
console.error(err)
})
}
reader.readAsArrayBuffer(file)
})
}, [filesUploaded, setNotifications])
const { getRootProps, getInputProps } = useDropzone({ onDrop, multiple: true })
useEffect(() => {
setFileCount(filesUploaded.length)
}, [setFileCount, filesUploaded, setNotifications])
return (
<div>
<div className='file-uploader'>
<div
className='file-uploader-input'
{...getRootProps()}
>
<input {...getInputProps()} />
<p>Upload Files</p>
</div>
</div>
<div className='progress-bar-container'>
{filesUploaded.map(file => {
return (
<ProgressBar
key={uuid()}
file={file}
progress={progress}
uploaded={uploaded}
exists={exists}
error={error}
/>
)
})}
</div>
</div>
)
}
export default FileUploader
The component re-renders because filesUploaded is modified in the callback each time and is listed as a dependency to the same callback. It looks like you wish to terminate the upload if the file already has been updated, but currently you only terminate the loadstart event handler. I suggest you move some of the functionality out from then loadstart event.
acceptedFiles.forEach(file => {
const exists = filesUploaded.find(uploadedFile => uploadedFile.name === file.name)
if (exists) {
setNotifications(notifications => {
return [...notifications, `'${file.name}' has already been uploaded.`]
})
} else {
const reader = new FileReader()
reader.onloadstart = () => {
return setFilesUploaded(filesUploaded => {
return [...filesUploaded, file]
})
}
[...]

Browser tab freezes after using useState hook in React

I have a blog app and it worked perfectly before I have added the user login feature. After that useState hook methods freeze the application tab in the browser. I am not sure what the problem is, I am guessing it has something to do with re-rendering.
Here is my App.js
import React, { useState, useEffect } from 'react'
import Header from './components/Header'
import Filter from './components/Filter'
import AddNewBlog from './components/AddNewBlog'
import Blogs from './components/Blogs'
import blogService from './services/blogs'
import Notification from './components/Notification'
import Button from './components/Button'
import LoginForm from './components/LoginForm'
import loginService from './services/login'
import './App.css'
const App = () => {
const [ blogs, setBlogs] = useState([])
const [ newTitle, setNewTitle ] = useState('')
const [ newAuthor, setNewAuthor ] = useState('')
const [ newUrl, setNewUrl ] = useState('')
const [ newLike, setNewLike ] = useState('')
const [ blogsToShow, setBlogsToShow] = useState(blogs)
const [ message, setMessage] = useState(null)
const [ notClass, setNotClass] = useState(null)
const [ username, setUsername ] = useState('')
const [ password, setPassword ] = useState('')
const [ user, setUser ] = useState(null)
useEffect(() => {
blogService
.getAll()
.then(initialBlogs => {
setBlogs(initialBlogs)
console.log(initialBlogs)
setBlogsToShow(initialBlogs)
})
.catch(error => {
showMessage(`Error caught: ${error}`, 'error')
})
}, [])
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem('loggedBlogappUser')
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON)
setUser(user)
blogService.setToken(user.token)
}
})
const handleLogin = async (e) => {
e.preventDefault()
try {
const user = await loginService.login({
username, password,
})
window.localStorage.setItem('loggedBlogappUser', JSON.stringify(user))
blogService.setToken(user.token)
setUser(user)
setUsername('')
setPassword('')
} catch (error) {
showMessage('wrong credentials', 'error')
}
}
const handleLogout = () => {
console.log('logging out')
setUser(null)
window.localStorage.clear()
}
const handleAddClick = (e) => {
e.preventDefault()
if(newTitle === '') {
alert("Input Title")
}
else if (newAuthor === '') {
alert("Input Author")
}
else if (newUrl === '') {
alert("Input Url")
} else {
let newObject = {
title: newTitle,
author: newAuthor,
url: newUrl,
likes: 0
}
console.log('step0');
blogService
.create(newObject)
.then(returnedBlog => {
setBlogs(blogs.concat(returnedBlog))
setBlogsToShow(blogs.concat(returnedBlog))
resetForm()
showMessage(`Added ${newTitle}`, 'success')
})
.catch(error => {
console.log(error.response.data)
showMessage(`${error.response.data.error}`, 'error')
})
//}
}
}
const handleDeleteClick = (id, title) => {
let message = `Do you really want to delete ${title}?`
if(window.confirm(message)){
blogService
.deleteBlog(id)
.then(res => {
setBlogs(blogs.filter(b => b.id !== id))
setBlogsToShow(blogs.filter(b => b.id !== id))
})
.catch(error => {
showMessage(`${title} has already been removed from the server`, 'error')
})
}
}
const handleLikeClick = (blog) => {
const updatedObject = {
...blog,
likes: blog.likes += 1
}
blogService
.update(updatedObject)
.then(() => {
setBlogs(blogs)
showMessage(`You liked ${updatedObject.title}`, 'success')
})
}
const resetForm = () => {
setNewTitle('')
setNewAuthor('')
setNewUrl('')
setNewLike('')
document.getElementById('titleInput0').value = ''
document.getElementById('authorInput0').value = ''
document.getElementById('urlInput0').value = ''
}
const showMessage = (msg, msgClass) => {
setMessage(msg)
setNotClass(msgClass)
setTimeout(() => {
setMessage(null)
setNotClass(null)
}, 5000)
}
const handleFilterOnChange = (e) => {
const filtered = blogs.filter(blog => blog.title.toLowerCase().includes(e.target.value.toLowerCase()))
setBlogsToShow(filtered)
//setBlogs(filtered)
}
const handleAddTitleOnChange = (e) => {
console.log(e.target.value)
console.log(newTitle)
setNewTitle(e.target.value)
}
const handleAddAuthorOnChange = (e) => {
setNewAuthor(e.target.value)
}
const handleAddUrlOnChange = (e) => {
setNewUrl(e.target.value)
}
if (user === null) {
return (
<div>
<Header text={'Bloglist'} />
<Notification message={message} notClassName={notClass} />
<LoginForm
handleLogin={handleLogin}
username={username}
setUsername={setUsername}
password={password}
setPassword={setPassword}
/>
</div>
)
}
return (
<div>
<Header text={'Bloglist'} />
<Notification message={message} notClassName={notClass} />
<p>{user.name} logged in</p><Button text={"logout"} handleClick={handleLogout} />
<AddNewBlog
handleAddTitleOnChange={handleAddTitleOnChange}
handleAddAuthorOnChange={handleAddAuthorOnChange}
handleAddUrlOnChange={handleAddUrlOnChange}
handleAddClick={handleAddClick}
/>
<Filter handleFilterOnChange={handleFilterOnChange} />
<Blogs blogs={blogsToShow} handleDeleteClick={handleDeleteClick} handleLikeClick={handleLikeClick} />
</div>
)
}
export default App
Anytime I call anyone of these methods: "setNewTitle, setNewAuthor, setNewUrl, setBlogsToShow", after logging in, the tab of the browser freezes. I tried with Chrome and FireFox.
Thank you for your help.
The issue is with your useEffect
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem('loggedBlogappUser')
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON)
setUser(user)
blogService.setToken(user.token)
}
})
It is executed on each re-render since it has not been provided any dependency and so it send the app in an infinite loop as it itself triggers a re-render. So when you call any state updater, this useEffect is triggered causing you tab to freeze
You can make this useEffect run once on initial render by passing an empty array to it as dependency
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem('loggedBlogappUser')
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON)
setUser(user)
blogService.setToken(user.token)
}
}, [])

Categories

Resources