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;
Related
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
I have used more than two react hooks together and I get this issue where there is no error with my frontend other than render hooks. I don't know how to resolve this error. I tried even using useState method.
If there is a possible fix can you let me know?
const Login = () => {
const { register, handleSubmit, setValue } = useForm();
const onSubmit = useCallback((formData) => {
const { email, password } = formData;
console.log(formData);
}, []);
const windowWidth = Dimensions.get("window").width;
const windowHeight = Dimensions.get("window").height;
const [loaded] = useFonts({
Lato: require("../assets/fonts/Lato-Regular.ttf"),
});
if (!loaded) {
return null;
}
const onChangeField = useCallback(
(name) => (text) => {
setValue(name, text);
},
[]
);
useEffect(() => {
register("email");
register("password");
}, [register]);
return (
);
};
export default Login;
Your app must call the same number of hooks every render. So you need to move
if (!loaded) {
return null;
}
to be below all of your hooks:
const Login = () => {
const { register, handleSubmit, setValue } = useForm();
const onSubmit = useCallback((formData) => {
const { email, password } = formData;
console.log(formData);
}, []);
const windowWidth = Dimensions.get("window").width;
const windowHeight = Dimensions.get("window").height;
const [loaded] = useFonts({
Lato: require("../assets/fonts/Lato-Regular.ttf"),
});
const onChangeField = useCallback(
(name) => (text) => {
setValue(name, text);
},
[]
);
useEffect(() => {
register("email");
register("password");
}, [register]);
if (!loaded) {
return null;
}
return (
);
};
export default Login;
I am trying to filter my results via button onClicks in React, but for some reason its not working?
https://stackblitz.com/edit/react-x7tlxr?file=src/App.js
import React, {useEffect, useState} from "react";
import axios from 'axios';
import "./style.css";
export default function App() {
const [fetchData, setFetchData] = useState([]);
const [loading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const url = 'https://jsonplaceholder.typicode.com/todos';
useEffect(() => {
let mounted = true;
const loadData = async () => {
try {
const response = await axios(url);
if (mounted) {
setFetchData(response.data);
setLoading(false);
setIsError(false);
console.log('data mounted')
}
} catch (err) {
setIsError(true)
setLoading(false);
setFetchData([]);
console.log(err);
}
};
loadData();
return () => {
mounted = false;
console.log('cleaned');
};
},
[url]
);
const renderdata = () => {
if (fetchData) {
return (
<div>{fetchData.map(inner => {
return (
<React.Fragment key={inner.id}>
<p>{inner.title}</p>
</React.Fragment>
)
})}</div>
)
} else {
<p>No data to display!</p>
}
}
const handle1 = () => {
const result1 = fetchData.filter((inner) => inner.id === '1');
setFetchData(result1);
}
const handle2 = () => {
const result2 = fetchData.filter((inner) => inner.id === '2');
setFetchData(result2);
}
const handleAll = () => {
setFetchData(fetchData);
}
return (
<div>
<button onClick={handleAll}>Show all</button>
<button onClick={handle1}>Filter by id 1</button>
<button onClick={handle2}>Filter by id 2</button>
{renderdata()}
</div>
);
}
The id is a number, not a string, so you filter(s) need to be changed like that:
const result1 = fetchData.filter((inner) => inner.id === 1);
You have another problem, is that you change the whole data set when you filter, so once you've filtered, you can't "unfilter" or filter again on another id.
You need to maintain the original fetchedData unchanged.
This example shows how it can be fixed.
I found 2 issues in your code
Id in the API result is numeric not string
inner.id === '2'. so this will return false inner.id === 2 you need to use like this
when you assign the filtered value to the original array it will of course change the original array so when you try to filter it second time you don't have the original API result in the fetch data array because it is already filtered
So i have created one more array filteredData
This will work.
import React, {useEffect, useState} from "react";
import axios from 'axios';
import "./style.css";
export default function App() {
const [fetchData, setFetchData] = useState([]);
const [filteredData, setFileredData] = useState([]);
const [loading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const url = 'https://jsonplaceholder.typicode.com/todos';
useEffect(() => {
let mounted = true;
const loadData = async () => {
try {
const response = await axios(url);
if (mounted) {
setFetchData(response.data);
setFileredData(response.data)
setLoading(false);
setIsError(false);
console.log('data mounted')
}
} catch (err) {
setIsError(true)
setLoading(false);
setFetchData([]);
console.log(err);
}
};
loadData();
return () => {
mounted = false;
console.log('cleaned');
};
},
[url]
);
const renderdata = () => {
if (filteredData) {
return (
<div>{filteredData.map(inner => {
return (
<React.Fragment key={inner.id}>
<p>{inner.title}</p>
</React.Fragment>
)
})}</div>
)
} else {
<p>No data to display!</p>
}
}
const handle1 = () => {
const result1 = fetchData.filter((inner) => inner.id === 1);
setFileredData(result1);
}
const handle2 = () => {
const result2 = fetchData.filter((inner) => inner.id === 2);
setFileredData(result2);
}
const handleAll = () => {
setFileredData(fetchData);
}
return (
<div>
<button onClick={handleAll}>Show all</button>
<button onClick={handle1}>Filter by id 1</button>
<button onClick={handle2}>Filter by id 2</button>
{renderdata()}
</div>
);
}
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)
}
}, [])
I am trying to migrate my class based react component to react-hooks. The purpose of the component is to fetch stories from HackerNews API and after each 5000 milliseconds to do a polling by hitting the API again for new data.
The problem I am facing is in using the custom hooks below usePrevious() to compare my previous state with current state and only after the comparison to execute some other function inside useEffect()
I am most probably missing some basic implementation here of the custom hooks or of useEffect()
And I am following this official guide
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Here's the code for my class based component and this is perfectly working.
And below is my hooks based component
The problem is with this line
const fromPrevStoriesIds = usePrevious(prevStoriesIds);
The variable fromPrevStoriesIds is giving me good value inside return(), but inside useEffect() its undefined.
import React, { Component, useState, useEffect, useRef } from "react";
import axios from "axios";
import MUIDataTable from "mui-datatables";
import "./Dashboard.css";
import NewItemAddedConfirmSnackbar from "./NewItemAddedConfirmSnackbar";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
const isEqual = require("lodash.isequal");
const differenceWith = require("lodash.differencewith");
const omit = require("lodash.omit");
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const getEachStoryGivenId = (id, index) => {
return new Promise((resolve, reject) => {
axios
.get(`https://hacker-news.firebaseio.com/v0/item/${id}.json`)
.then(res => {
let story = res.data;
let result = omit(story, ["descendants", "time", "id", "type"]);
if (
result &&
Object.entries(result).length !== 0 &&
result.constructor === Object
) {
resolve(result);
} else {
reject(new Error("No data received"));
}
});
});
};
const Dashboard = () => {
const [prevStoriesIds, setPrevStoriesIds] = useState([]);
const [fetchedData, setFetchedData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [tableState, setTableState] = useState({});
const [
openNewItemAddedConfirmSnackbar,
setOpenNewItemAddedConfirmSnackbar
] = useState(false);
const [noOfNewStoryAfterPolling, setNoOfNewStoryAfterPolling] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const onChangeRowsPerPage = rowsPerPage => {
setRowsPerPage(rowsPerPage);
};
const closeNewItemConfirmSnackbar = () => {
setOpenNewItemAddedConfirmSnackbar(false);
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
setPrevStoriesIds(storyIds.data.slice(0, 2));
getAllNewStory(storyIds);
});
};
const getAllNewStory = storyIds => {
setIsLoading(true);
let topStories = storyIds.data.slice(0, 2).map(getEachStoryGivenId);
let results = Promise.all(topStories);
results
.then(res => {
setFetchedData(res);
setIsLoading(false);
})
.catch(err => {
console.log(err);
});
};
const fromPrevStoriesIds = usePrevious(prevStoriesIds);
useEffect(() => {
const fetchData = () => {
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
// console.log("STORY IDs FETCHED ", storyIds.data.slice(0, 2));
setPrevStoriesIds(storyIds.data.slice(0, 2));
getAllNewStory(storyIds);
});
};
fetchData();
const doPolling = () => {
var timer = setInterval(() => {
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
console.log(
"fromPrevStoriesIds INSIDE doPolling() ",
fromPrevStoriesIds
);
if (
fromPrevStoriesIds !== undefined &&
!isEqual(fromPrevStoriesIds.sort(), storyIds.data.slice(0, 2).sort())
) {
setPrevStoriesIds(storyIds.data.slice(0, 2));
setNoOfNewStoryAfterPolling(
differenceWith(
prevStoriesIds.sort(),
storyIds.data.slice(0, 2).sort(),
isEqual
).length
);
getAllNewStory(storyIds);
setOpenNewItemAddedConfirmSnackbar(true);
}
});
}, 5000);
};
doPolling();
// return () => {
// console.log("cleaning up");
// clearInterval(timer);
// };
}, [rowsPerPage, noOfNewStoryAfterPolling]);
let renderedStoriesOnPage = [];
const getDataToRender = (() => {
renderedStoriesOnPage = fetchedData.map(i => {
return Object.values(i);
});
return renderedStoriesOnPage;
})();
const columnsOptions = [
{
name: "Author",
sortDirection: tableState
? tableState.columns && tableState.columns[0].sortDirection
: null
},
{
name: "score",
sortDirection: tableState
? tableState.columns && tableState.columns[1].sortDirection
: null
},
{
name: "title",
sortDirection: tableState
? tableState.columns && tableState.columns[2].sortDirection
: null
},
{
name: "URL",
options: {
filter: false,
customBodyRender: (value, tableMeta, updateValue) => {
// console.log("TABLE META IS ", JSON.stringify(tableMeta));
return (
<a target="_blank" href={value}>
{value}
</a>
);
}
}
}
];
const options = {
filter: true,
selectableRows: false,
filterType: "dropdown",
responsive: "stacked",
selectableRows: "multiple",
rowsPerPage: tableState ? tableState.rowsPerPage : 10,
onChangeRowsPerPage: onChangeRowsPerPage,
activeColumn: tableState ? tableState.activeColumn : 0,
onTableChange: (action, tableState) => {
// console.log("taBLE STATE IS ", JSON.stringify(tableState));
setTableState(tableState);
}
};
return (
<React.Fragment>
{console.log("fromPrevStoriesIds INSIDE RETURN --- ", fromPrevStoriesIds)}
<div
style={{
marginLeft: "15px",
marginTop: "80px",
display: "flex",
flexDirection: "row"
}}
>
<h4 style={{ width: "400px", paddingRight: "15px" }}>
Hacker News top 2
</h4>
</div>
<div>
{isLoading ? (
<div className="interactions">
<div className="lds-ring">
<div />
<div />
<div />
<div />
</div>
</div>
) : fetchedData.length !== 0 && renderedStoriesOnPage.length !== 0 ? (
<MUIDataTable
title={"Hacker News API top 2 result"}
data={renderedStoriesOnPage}
columns={columnsOptions}
options={options}
/>
) : null}
<NewItemAddedConfirmSnackbar
openNewItemAddedConfirmSnackbar={openNewItemAddedConfirmSnackbar}
closeNewItemConfirmSnackbar={closeNewItemConfirmSnackbar}
noOfNewStoryAfterPolling={noOfNewStoryAfterPolling}
/>
</div>
</React.Fragment>
);
};
export default Dashboard;
Instead of returning ref.current from usePrevious return, ref since ref.current will be mutated at its reference and you will be able to receive the updated value within useEffect otherwise it will receive the value from its closure
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref;
}
Code:
const fromPrevStoriesIds = usePrevious(prevStoriesIds);
useEffect(() => {
const fetchData = () => {
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
// console.log("STORY IDs FETCHED ", storyIds.data.slice(0, 2));
setPrevStoriesIds(storyIds.data.slice(0, 2));
getAllNewStory(storyIds);
});
};
fetchData();
const doPolling = () => {
var timer = setInterval(() => {
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
console.log(
"fromPrevStoriesIds INSIDE doPolling() ",
fromPrevStoriesIds.current
);
if (
fromPrevStoriesIds.current !== undefined &&
!isEqual(fromPrevStoriesIds.current.sort(), storyIds.data.slice(0, 2).sort())
) {
setPrevStoriesIds(storyIds.data.slice(0, 2));
setNoOfNewStoryAfterPolling(
differenceWith(
prevStoriesIds.sort(),
storyIds.data.slice(0, 2).sort(),
isEqual
).length
);
getAllNewStory(storyIds);
setOpenNewItemAddedConfirmSnackbar(true);
}
});
}, 5000);
};
doPolling();
// return () => {
// console.log("cleaning up");
// clearInterval(timer);
// };
}, [rowsPerPage, noOfNewStoryAfterPolling]);