Browser tab freezes after using useState hook in React - javascript

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)
}
}, [])

Related

Render a component only when a state array is not empty (React Router 6)

I am dealing with search parameters and redirecting of URLs in a not so pretty way (only way I could come up with). And, because of the first useEffect right under handleSubmit(), there are way too many unnecessary renders of Search component. For instance, when the page is refreshed on the search page, the Search component gets rendered 7 times (5 renders of allImages being empty, 2 renders of allImages filled with fetched images).
So, I am thinking of adding a conditional for Search component to render Search component only when allImages is not empty (when it is filled with fetched images). Let me know if this is doable.
import React from 'react'
import Navbar from './Navbar'
import create from 'zustand'
import ErrorMsg, { useError } from './ErrorMsg'
import { useEffect } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
// Zustand
let store = (set) => ({
input: '',
setInput: (value) => set({ input: value }),
allImages: [],
setAllImages: (images) => set({ allImages: images}),
totalResults: null,
setTotalResults: (num) => set({ totalResults: num}),
})
export const useHeader = create(store)
function Header() {
// global state and search params
let navigate = useNavigate()
const location = useLocation()
const [searchParams] = useSearchParams()
const query = searchParams.get('query')
const page = Number(searchParams.get('page') || 1)
const input = useHeader(state => state.input)
const setInput = useHeader(state => state.setInput)
const allImages = useHeader(state => state.allImages)
const setAllImages = useHeader(state => state.setAllImages)
const setTotalResults = useHeader(state => state.setTotalResults)
const error = useError(state => state.error)
const setError = useError(state => state.setError)
const showError = useError(state => state.showError)
const setShowError = useError(state => state.setShowError)
const setFadeOut = useError(state => state.setFadeOut)
function handleChange(event) {
setInput(event.target.value)
}
async function fetchImages() {
try {
const res = await fetch(`https://api.unsplash.com/search/photos?&page=${page}&per_page=30&query=${input}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
const data = await res.json()
if (data.total !== 0) {
setAllImages(data.results)
setTotalResults(data.total)
} else {
setAllImages([])
setTotalResults(0)
}
} catch(error) {
setError(error)
}
}
const handleSubmit = async (event) => {
event.preventDefault()
navigate(`/search?query=${input}&page=1`)
}
// this useEffect causes Search.js to render too many times
// especially the second conditional need improvement
useEffect(() => {
if (location.pathname === '/search' && allImages.length === 0) {
if (query) {
setInput(query)
}
navigate(`/search?query=${input}&page=${page}`)
fetchImages()
}
// need this to deal with page not refreshing when submitting or changing pages
if (location.pathname === '/search' && allImages.length !== 0) {
fetchImages()
}
// eslint-disable-next-line
}, [searchParams])
// error
useEffect(() => {
if (error) {
setShowError(true)
setTimeout(() => {
setFadeOut(true)
setTimeout(() => {
setShowError(false)
}, 1000)
}, 5000)
}
}, [error, setFadeOut, setShowError])
return (
<div className='header'>
<Navbar />
<h2 className='header--heading text-center text-light'>Find Images</h2>
<div className='header--form'>
<form onSubmit={handleSubmit}>
<input
className='header--form--input'
autoComplete='off'
type='text'
placeholder='Search'
onChange={handleChange}
name='input'
value={input}
/>
</form>
</div>
{showError && <ErrorMsg />}
</div>
)
}
export default Header
import React from 'react'
import Header from '../Header'
import Image from '../Image'
import { useHeader } from '../Header';
import { useSearchParams } from 'react-router-dom';
function Search() {
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page') || 1)
const allImages = useHeader(state => state.allImages)
const totalResults = useHeader(state => state.totalResults)
console.log(allImages)
console.log('Search.js rendered')
// pages
function handlePrev() {
setSearchParams(params => {
params.set("page", Math.max(1, page - 1))
return params
})
}
function handleNext() {
setSearchParams(params => {
params.set("page", page + 1)
return params
})
}
return (
<div>
<Header />
{/* {totalResults === 0 && <p>Nothing Found</p>} */}
<div className='image-list mt-5 pb-5'>
{allImages.map(el => (
<Image
key={el.id}
// do need spread operator below for img's src to work in Image.js
{...el}
el={el}
/>
))}
</div>
{allImages.length !== 0 && <div className='pagination'>
<button disabled={page === 1} onClick={handlePrev}>
Prev
</button>
<h5 className='pagination--h5'>{page}</h5>
<button disabled={totalResults < 31} onClick={handleNext}>
Next
</button>
</div>}
</div>
)
}
export default Search
Finally figured it out and all I had to do was to improve the fetchImages function and simplify the useEffect.
import React from 'react'
import Navbar from './Navbar'
import create from 'zustand'
import ErrorMsg, { useError } from './ErrorMsg'
import { useEffect, useRef } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
// Zustand
let store = (set) => ({
input: '',
setInput: (value) => set({ input: value }),
allImages: [],
setAllImages: (images) => set({ allImages: images}),
totalResults: null,
setTotalResults: (num) => set({ totalResults: num}),
})
export const useHeader = create(store)
function Header() {
// global state and search params, and some others
let navigate = useNavigate()
const inputRef = useRef(null)
const [searchParams] = useSearchParams()
const query = searchParams.get('query')
const page = Number(searchParams.get('page') || 1)
const input = useHeader(state => state.input)
const setInput = useHeader(state => state.setInput)
const setAllImages = useHeader(state => state.setAllImages)
const setTotalResults = useHeader(state => state.setTotalResults)
const error = useError(state => state.error)
const setError = useError(state => state.setError)
const showError = useError(state => state.showError)
const setShowError = useError(state => state.setShowError)
const setFadeOut = useError(state => state.setFadeOut)
function handleChange(event) {
setInput(event.target.value)
}
const handleSubmit = async (event) => {
event.preventDefault()
navigate(`/search?query=${input}&page=1`)
}
let realShit
if (input === '') {
realShit = query
} else {
realShit = input
}
useEffect(() => {
async function fetchImages() {
try {
const res = await fetch(`https://api.unsplash.com/search/photos?&page=${page}&per_page=30&query=${realShit}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
const data = await res.json()
if (data.total === 0) {
setTotalResults(0)
} else {
setAllImages(data.results)
setTotalResults(data.total)
}
} catch(error) {
setError(error)
}
}
fetchImages()
// eslint-disable-next-line
}, [searchParams])
// input
useEffect(() => {
inputRef.current.focus()
}, [])
// error
useEffect(() => {
if (error) {
setShowError(true)
setTimeout(() => {
setFadeOut(true)
setTimeout(() => {
setShowError(false)
}, 1000)
}, 5000)
}
}, [error, setFadeOut, setShowError])
return (
<div className='header'>
<Navbar />
<h2 className='header--heading text-center text-light'>Find Images</h2>
<div className='header--form'>
<form onSubmit={handleSubmit}>
<input
className='header--form--input'
autoComplete='off'
type='text'
placeholder='Search'
onChange={handleChange}
name='input'
value={input}
ref={inputRef}
/>
</form>
</div>
{showError && <ErrorMsg />}
</div>
)
}
export default Header

using React useEffect to fetch data and controll component

i am trying to query data using useEffect add those data to state and render them but nothing comes up unless a state in the app changes. this is what i have done so far Please help, Thanks in Advance.
useEffect
// fetchCampaigns
(async () => {
dispatch(showTopLoader());
try {
const res = await getAgentCampaigns(authToken, "accepted");
setCampaigns(res.data.campaigns);
let leads: any[] = [];
const fetchCampaignLeads = async (id: string) => {
try {
const res = await getCampaignLeads(authToken, id);
return res.data.campaignLeads;
} catch (error) {}
};
// loop through campaigns and get leads
let resS: any[] = [];
campaigns.forEach((campaign: any, i: number) => {
const id = campaign?.Campaign?.id;
fetchCampaignLeads(id)
.then((leadsRes) => {
leads.push(leadsRes[i]);
if (id === leadsRes[i]?.campaignId)
return resS.push({
...campaign,
leads: leadsRes,
});
return (resS = campaigns);
})
.catch(() => {})
.finally(() => {
console.log(resS);
setCampaigns(resS);
});
});
} catch (error) {
} finally {
dispatch(hideTopLoader());
}
})();
}, []);
whole component
import { useDispatch, useSelector } from "react-redux";
import _ from "lodash";
import styles from "../../../styles/CreateLeads.module.css";
import {
getAgentCampaigns,
getCampaignLeads,
} from "../../../utils/requests/campaign";
import {
hideTopLoader,
showTopLoader,
} from "../../../store/actions/TopLoader/topLoaderActions";
import CampaignSection from "./CampaignSection";
import Empty from "../Empty/Empty";
import SectionHeader from "../SectionHeader/SectionHeader";
import SearchBar from "../SearchBar/SearchBar";
import { RootState } from "../../../store/store";
const CreateLeadsCardsWrapper: React.FC = () => {
const authToken = useSelector(
(store: any) => store.authenticationReducer.authToken
);
const [stateCampaigns, setStateCampaigns] = React.useState<any[]>([]);
const [showCampaigns, setShowCampaigns] = React.useState<boolean>(false);
const [campaigns, setCampaigns] = React.useState<any[]>(stateCampaigns);
const [filter, setFilter] = React.useState<string>("");
const dispatch = useDispatch();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Reset filter
setFilter("");
let campaignSearch = e.target.value.trim();
if (campaignSearch.length === 0) {
return;
}
let campaignSearchLower = campaignSearch.toLowerCase();
let campaignSearchUpper = campaignSearch.toUpperCase();
let campaignSearchSentence =
campaignSearch.charAt(0).toUpperCase() + campaignSearch.slice(1);
let results = stateCampaigns.filter(
({ leads }: { leads: any[] }, i) =>
leads &&
leads?.some(
(lead: any) =>
lead.firstName.includes(campaignSearch) ||
lead.firstName.includes(campaignSearchLower) ||
lead.firstName.includes(campaignSearchUpper) ||
lead.firstName.includes(campaignSearchSentence) ||
lead.lastName.includes(campaignSearch) ||
lead.lastName.includes(campaignSearchLower) ||
lead.lastName.includes(campaignSearchUpper) ||
lead.lastName.includes(campaignSearchSentence) ||
lead.email.includes(campaignSearch) ||
lead.email.includes(campaignSearchLower) ||
lead.email.includes(campaignSearchUpper) ||
lead.email.includes(campaignSearchSentence) ||
lead.phoneNo.includes(campaignSearch) ||
lead.phoneNo.includes(campaignSearchLower) ||
lead.phoneNo.includes(campaignSearchUpper) ||
lead.phoneNo.includes(campaignSearchSentence)
)
);
setCampaigns(results);
};
React.useEffect(() => {
// fetchCampaigns
(async () => {
dispatch(showTopLoader());
try {
const res = await getAgentCampaigns(authToken, "accepted");
setCampaigns(res.data.campaigns);
let leads: any[] = [];
const fetchCampaignLeads = async (id: string) => {
try {
const res = await getCampaignLeads(authToken, id);
return res.data.campaignLeads;
} catch (error) {}
};
// loop through campaigns and get leads
let resS: any[] = [];
campaigns.forEach((campaign: any, i: number) => {
const id = campaign?.Campaign?.id;
fetchCampaignLeads(id)
.then((leadsRes) => {
leads.push(leadsRes[i]);
if (id === leadsRes[i]?.campaignId)
return resS.push({
...campaign,
leads: leadsRes,
});
return (resS = campaigns);
})
.catch(() => {})
.finally(() => {
console.log(resS);
setCampaigns(resS);
});
});
} catch (error) {
} finally {
dispatch(hideTopLoader());
}
})();
}, []);
React.useEffect(() => {
setCampaigns(stateCampaigns);
campaigns.length > 0 && setShowCampaigns(true);
console.log(showCampaigns);
dispatch(hideTopLoader());
}, []);
return (
<div className={styles.wrappers}>
{/* Multi step form select a campaign first then fill info on the next step */}
<SectionHeader text="Campaign Leads" />
{showCampaigns && stateCampaigns.length === 0 && (
<>
<Empty description="No agents yet" />
<p>Join a campaign.</p>
</>
)}
{showCampaigns && stateCampaigns.length > 0 && (
<>
<p className="text-grey-500">Create Leads for Campaigns.</p>
<section className={styles.container}>
<SearchBar
placeholder="Find Campaign Leads"
onChange={handleChange}
/>
{campaigns.map((item: any) => (
<CampaignSection
key={item?.Campaign?.id}
id={item?.Campaign?.id}
name={item?.Campaign?.name}
imageUrl={item?.Campaign?.iconUrl}
campaignType={item?.Campaign?.type}
productType={item?.Campaign?.Products[0]?.type}
/>
))}
</section>
</>
)}
</div>
);
};
export default CreateLeadsCardsWrapper;
there are two things wrong in yoour code :
1- you should not have two useeffects with the same dependencies in your case: [] you have to merge those useeffects into one or change the second one's dependencies
2- doing async code in useeffect can be problematic sometimes. it is better to create an async function which does the query to the backend and sets the state and the call the function in your useeffect like below :
const getData = async()=>{
// do some queries and set the state
}
React.useeffect(()=>{
getData()
},[])

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]
})
}
[...]

how to fix limit render loop in react

i am currently building a chat app using react and Firebase real time database.
i have a problem with maximum limit rendering because of usecallback hook and users state (the hooks are addOnConnectLister addOnDisConnectListner).
this is the code :
import React, { useEffect, useState, useCallback, useRef } from "react";
import { Icon, Menu } from "semantic-ui-react";
import firebase from "../../firebase";
const DirectMessages = (props) => {
const [usersRef] = useState(firebase.database().ref("users"));
const [connectedRef] = useState(firebase.database().ref(".info/connected"));
const [presenceRef] = useState(firebase.database().ref("presence"));
const [users, setUsers] = useState([]);
//add presence listner
//create a presence record for current user
const addPressenceListner = useCallback(
(currentUserID) => {
connectedRef.on("value", (snap) => {
console.log("connecting snap value", snap.val());
if (snap.val()) {
const ref = presenceRef.child(currentUserID);
ref.set(true);
ref.onDisconnect().remove((err) => {
if (err) console.log(err);
});
}
});
},
[connectedRef, presenceRef]
);
//init connected users
const initUsers = useCallback(
(currentUserID) => {
const loadedUsers = [];
usersRef.on("child_added", (snap) => {
if (currentUserID !== snap.key) {
let user = snap.val();
user["uid"] = snap.key;
user["status"] = "offline";
loadedUsers.push(user);
setUsers([...loadedUsers]);
}
});
},
[usersRef]
);
//update user status when go online
const addOnConnectListner = useCallback(() => {
presenceRef.on("child_added", (snap) => {
const updatedUsers = users.map((user) => {
if (user.uid === snap.key) {
console.log("user connected", user);
user["status"] = "online";
}
return user;
});
setUsers(...[updatedUsers]);
});
}, [presenceRef, users]);
//update user status when go offline
const addOnDisconnectListner = useCallback(() => {
presenceRef.on("child_removed", (snap) => {
const updatedUsers = users.map((user) => {
if (user.uid === snap.key) {
console.log("user disconnected", user);
user["status"] = "offline";
}
return user;
});
setUsers(updatedUsers);
});
}, [users, presenceRef]);
const addListners = useCallback(
(currentUserID) => {
addPressenceListner(currentUserID);
initUsers(currentUserID);
addOnConnectListner();
addOnDisconnectListner();
},
[
addOnConnectListner,
addOnDisconnectListner,
addPressenceListner,
initUsers,
]
);
useEffect(() => {
if (props.currentUser) {
addListners(props.currentUser.uid);
}
return () => {
presenceRef.off();
usersRef.off();
connectedRef.off();
};
}, [props.currentUser, addListners, connectedRef, presenceRef, usersRef]);
const isUserOnline = (user) => user.status === "online";
return (
<Menu.Menu className="menu" style={{ paddingTop: "2em" }}>
<Menu.Item>
<span>
<Icon name="mail" /> Direct Messages
</span>{" "}
({users.length})
</Menu.Item>
{users.map((user) => (
<Menu.Item key={user.uid}>
<Icon name="circle" color={isUserOnline(user) ? "green" : "red"} />#
{user.username}
</Menu.Item>
))}
</Menu.Menu>
);
};
export default DirectMessages;
after searching i found a solution using useRef to get a reference of users state to avoid the problem but is not working correctly the callback is runnging only once
import React, { useEffect, useState, useCallback, useRef } from "react";
import { Icon, Menu } from "semantic-ui-react";
import firebase from "../../firebase";
const DirectMessages = (props) => {
const [usersRef] = useState(firebase.database().ref("users"));
const [connectedRef] = useState(firebase.database().ref(".info/connected"));
const [presenceRef] = useState(firebase.database().ref("presence"));
const [users, setUsers] = useState([]);
//users state ref
const getUsersRef = useRef(users);
useEffect(() => {
getUsersRef.current = users;
}, [users]);
//add presence listner
//create a presence record for current user
const addPressenceListner = useCallback(
(currentUserID) => {
connectedRef.on("value", (snap) => {
console.log("connecting snap value", snap.val());
if (snap.val()) {
const ref = presenceRef.child(currentUserID);
ref.set(true);
ref.onDisconnect().remove((err) => {
if (err) console.log(err);
});
}
});
},
[connectedRef, presenceRef]
);
//init connected users
const initUsers = useCallback(
(currentUserID) => {
const loadedUsers = [];
usersRef.on("child_added", (snap) => {
if (currentUserID !== snap.key) {
let user = snap.val();
user["uid"] = snap.key;
user["status"] = "offline";
loadedUsers.push(user);
setUsers([...loadedUsers]);
}
});
},
[usersRef]
);
//update user status when go online
const addOnConnectListner = useCallback(() => {
presenceRef.on("child_added", (snap) => {
const updatedUsers = getUsersRef.current.map((user) => {
if (user.uid === snap.key) {
console.log("user connected", user);
user["status"] = "online";
}
return user;
});
setUsers(...[updatedUsers]);
});
}, [presenceRef, getUsersRef]);
//update user status when go offline
const addOnDisconnectListner = useCallback(() => {
presenceRef.on("child_removed", (snap) => {
const updatedUsers = getUsersRef.current.map((user) => {
if (user.uid === snap.key) {
console.log("user disconnected", user);
user["status"] = "offline";
}
return user;
});
setUsers(updatedUsers);
});
}, [getUsersRef, presenceRef]);
const addListners = useCallback(
(currentUserID) => {
addPressenceListner(currentUserID);
initUsers(currentUserID);
addOnConnectListner();
addOnDisconnectListner();
},
[
addOnConnectListner,
addOnDisconnectListner,
addPressenceListner,
initUsers,
]
);
useEffect(() => {
if (props.currentUser) {
addListners(props.currentUser.uid);
}
return () => {
presenceRef.off();
usersRef.off();
connectedRef.off();
};
}, [props.currentUser, addListners, connectedRef, presenceRef, usersRef]);
const isUserOnline = (user) => user.status === "online";
return (
<Menu.Menu className="menu" style={{ paddingTop: "2em" }}>
<Menu.Item>
<span>
<Icon name="mail" /> Direct Messages
</span>{" "}
({users.length})
</Menu.Item>
{users.map((user) => (
<Menu.Item key={user.uid}>
<Icon name="circle" color={isUserOnline(user) ? "green" : "red"} />#
{user.username}
</Menu.Item>
))}
</Menu.Menu>
);
};
export default DirectMessages;
firstly, you do not need them as state
const [usersRef] = useState(firebase.database().ref("users"));
const [connectedRef] = useState(firebase.database().ref(".info/connected"));
const [presenceRef] = useState(firebase.database().ref("presence"));
set them outside the component.
const usersRef = firebase.database().ref("users")
const connectedRef = firebase.database().ref(".info/connected")
const presenceRef = firebase.database().ref("presence")
const DirectMessages = (props) => {
......
Secondly, the correct way to remove the listeners are.
useEffect(() => {
if (props.currentUser) {
addListners(props.currentUser.uid);
return () => {
presenceRef.off('child_added');
presenceRef.off('child_removed');
usersRef.off('child_added');
connectedRef.off('value');
};
}
}, [props.currentUser, addListners, connectedRef, presenceRef, usersRef]);
I will change the following
const [users, setUsers] = useState([]);
//users state ref
const getUsersRef = useRef(users);
useEffect(() => {
getUsersRef.current = users;
}, [users]);
to
const [users, _setUsers] = useState([]);
const getUsersRef = useRef(users);
const setUsers = (array) => {
getUsersRef.current = array;
_setUsers(array)
}
lastly, the usecallbacks are unnecessary, you should create them as regular functions.
const addOnDisconnectListner = () => {
presenceRef.on("child_removed", (snap) => {
const updatedUsers = getUsersRef.current.map((user) => {
if (user.uid === snap.key) {
console.log("user disconnected", user);
user["status"] = "offline";
}
return user;
});
setUsers(updatedUsers);
}
changing this:
const [users, setUsers] = useState([]);
//users state ref
const getUsersRef = useRef(users);
useEffect(() => {
getUsersRef.current = users;
}, [users]);
to this:
//users state ref
const getUsersRef = useRef(users);
useEffect(() => {
getUsersRef.current = users;
}, [users]);
after updating hooks dependencies :
import React, { useEffect, useState, useCallback, useRef } from "react";
import { Icon, Menu } from "semantic-ui-react";
import firebase from "../../firebase";
const usersRef = firebase.database().ref("users");
const connectedRef = firebase.database().ref(".info/connected");
const presenceRef = firebase.database().ref("presence");
const DirectMessages = (props) => {
const [users, _setUsers] = useState([]);
//get users ref
const getUsersRef = useRef(users);
const setUsers = (array) => {
getUsersRef.current = array;
_setUsers(array);
};
//add presence listner
//create a presence record for current user
const addPressenceListner = useCallback((currentUserID) => {
connectedRef.on("value",(snap) => {
console.log("connecting snap value", snap.val());
if (snap.val()) {
const ref = presenceRef.child(currentUserID);
ref.set(true);
ref.onDisconnect().remove((err) => {
if (err) console.log(err);
});
}
});
}, []);
//init connected users
const initUsers = useCallback((currentUserID) => {
const loadedUsers = [];
usersRef.on("child_added", (snap) => {
if (currentUserID !== snap.key) {
let user = snap.val();
user["uid"] = snap.key;
user["status"] = "offline";
loadedUsers.push(user);
setUsers([...loadedUsers]);
}
});
}, []);
//update user status when go online
const addOnConnectListner = useCallback(() => {
presenceRef.on("child_added", (snap) => {
const updatedUsers = getUsersRef.current.map((user) => {
if (user.uid === snap.key) {
user["status"] = "online";
}
console.log("user", user);
return user;
});
setUsers(...[updatedUsers]);
});
}, []);
//update user status when go offline
const addOnDisconnectListner = useCallback(() => {
presenceRef.on("child_removed", (snap) => {
const updatedUsers = getUsersRef.current.map((user) => {
if (user.uid === snap.key) {
console.log("user disconnected", user);
user["status"] = "offline";
}
return user;
});
setUsers(updatedUsers);
});
}, [getUsersRef]);
const addListners = useCallback(
(currentUserID) => {
addPressenceListner(currentUserID);
initUsers(currentUserID);
addOnConnectListner();
addOnDisconnectListner();
},
[
addOnConnectListner,
addOnDisconnectListner,
addPressenceListner,
initUsers,
]
);
useEffect(() => {
if (props.currentUser) {
addListners(props.currentUser.uid);
}
return () => {
presenceRef.off();
usersRef.off();
connectedRef.off();
};
}, [props.currentUser, addListners]);
const isUserOnline = (user) => user.status === "online";
return (
<Menu.Menu className="menu" style={{ paddingTop: "2em" }}>
<Menu.Item>
<span>
<Icon name="mail" /> Direct Messages
</span>{" "}
({users.length})
</Menu.Item>
{users.map((user) => (
<Menu.Item key={user.uid}>
<Icon name="circle" color={isUserOnline(user) ? "green" : "red"} />#
{user.username}
</Menu.Item>
))}
</Menu.Menu>
);
};
export default DirectMessages;

How to fix: Warning: Can't perform a React state update on an unmounted component

I have a function called getRelevates outside the useEffect, the problem is that when I change the states of my components to add the information obtained, the console returns 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.
This is the code of my component:
import React, { useEffect, useState } from 'react';
import './Start.css';
import CardProduct from '../CardProduct/CardProduct';
import { db } from '../../firebase';
import CardEstreno from '../CardEstreno/CardEstreno';
const Start = () => {
const [productsRelevates, setProductsRelevates] = useState([]);
const [productsEstreno, setProductsEstreno] = useState([]);
const getRelevates = () => {
db.collection('Products').onSnapshot((querySnapshot) => {
const docs = [];
const es = [];
querySnapshot.forEach((doc) => {
if (doc.data().Relevancia === true) {
docs.push({ ...doc.data(), id: doc.id });
}
if (doc.data().Estreno === true) {
es.push({ ...doc.data(), id: doc.id });
}
});
setProductsRelevates(docs);
setProductsEstreno(es);
});
};
useEffect(() => {
getRelevates();
}, []);
return <div className="recom">
{productsRelevates !== []
? productsRelevates.map((doc) => {
return (
<div key={doc.id}>
<CardProduct product={doc} />
</div>
);
})
: ''}
</div>
}
You must cleanup the listener in useEffect with the return function. Something like this (untested):
import React, { useEffect, useState } from 'react';
import './Start.css';
import CardProduct from '../CardProduct/CardProduct';
import { db } from '../../firebase';
import CardEstreno from '../CardEstreno/CardEstreno';
const Start = () => {
const [productsRelevates, setProductsRelevates] = useState([]);
const [productsEstreno, setProductsEstreno] = useState([]);
useEffect(() => {
const onSnapshotHandler = querySnapshot => {
const docs = [];
const es = [];
querySnapshot.forEach((doc) => {
if (doc.data().Relevancia === true) {
docs.push({ ...doc.data(), id: doc.id });
}
if (doc.data().Estreno === true) {
es.push({ ...doc.data(), id: doc.id });
}
});
setProductsRelevates(docs);
setProductsEstreno(es);
};
const unsubscribe = db.collection('Products').onSnapshot(onSnapshotHandler);
return (() => unsubscribe());
}, []);
return <div className="recom">
{productsRelevates !== []
? productsRelevates.map((doc) => {
return (
<div key={doc.id}>
<CardProduct product={doc} />
</div>
);
})
: ''}
</div>
}

Categories

Resources