disable default Scroll to top in web application - javascript

I'm new to React and Web.
I am trying to load some chats data, whenever I add some chats to the top of the chat list, automatically chats container scrolled to the top of chats, I mean first chat in term of creating time.
how to disable this scrolling behavior?
I also tried a library called reverse-infinite-search.
useChatLoad.ts
import {useEffect, useState} from "react";
import axios, {Canceler} from "axios";
import {API_KEY, BASE_SANGRIA_URL} from "../apis/BaseApi";
import {Message} from "../models/Message";
export default function useChat(visitorId: string, fetchBeforeTimestampMillis: number, scrollToBottom: () => void) {
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<boolean>(false);
const [chats, setChats] = useState<Message[]>([]);
const [hasMore, setHasMore] = useState<boolean>(false);
useEffect(() => {
setChats([]);
}, [visitorId]);
const onNewMessageReceived = (m: Message) => {
setChats([...chats, m]);
}
useEffect(() => {
setLoading(true);
setError(false);
let canceler: Canceler;
axios({
method: "POST",
url: BASE_URL,
data: {userId: visitorId, fetchBeforeTimestampMillis: fetchBeforeTimestampMillis},
headers: {'Content-Type': 'application/json', "API-KEY": API_KEY},
cancelToken: new axios.CancelToken(c => canceler = c)
}).then(res => {
console.log("fetch called")
if (res.status != 200 || res.data.error != null) {
console.log(res.status + " " + res.data.error);
setError(true);
return;
}
let messages: Message[] = res.data.messages;
messages = messages.reverse();
if (chats.length == 0 && messages.length != 0)
scrollToBottom()
setChats(prevChats => {
return [...messages, ...prevChats]
})
setHasMore(res.data.messages.length > 0);
setLoading(false);
}).catch(e => {
if (axios.isCancel(e)) return;
console.log("l41 useChatLoad ", e);
setError(true);
})
}, [visitorId, fetchBeforeTimestampMillis])
return {loading, error, chats, hasMore, onNewMessageReceived};
}
ChatContent.tsx
import React, {createRef, useCallback, useEffect, useRef, useState} from "react";
import styles from './ChatContent.module.css';
import ChatItem from "../chat-item/ChatItem";
import {Message} from "../../models/Message";
import InputField from "../input-field/InputField";
import useChatsLoad from "../../custom-hooks/useChasLoad";
import InfiniteScrollReverse from "../inf-scroll/InfiniteScrollReverse";
import {OtherOperatorMessagePayload, Sangria, VisitorMessagePayload} from "../../sangria/Sangria";
import {isEmptyText} from "../../tools/Tools";
interface Props {
isOnline: boolean
visitorId: string,
onOperatorMessageReceived: (m: OtherOperatorMessagePayload) => void
onCustomerMessageReceived: (m: VisitorMessagePayload) => void
}
const ChatContent: React.FC<Props> = (props: Props) => {
const [msg, setMsg] = useState<string>("");
const [fetchBeforeTimeStamp, setFetchBeforeTimeStamp] = useState<number>(Date.now());
const [sangria, setSangria] = useState<Sangria>();
const messagesEndRef = useRef<HTMLDivElement>(null);
function scrollToBottom() {
console.log("scroll to bottom called")
messagesEndRef.current?.scrollIntoView({behavior: "auto"})
}
const {
chats,
hasMore,
loading,
error,
onNewMessageReceived
} = useChatsLoad(props.visitorId, fetchBeforeTimeStamp, scrollToBottom);
useEffect(() => {
let sg = new Sangria();
sg.setReceiveMessageFromOtherOperatorCallback(data => {
props.onOperatorMessageReceived(data);
if (data.message.customerId != props.visitorId) return;
let newMessage: Message = {
meta: data.message.meta,
messageType: data.message.messageType,
body: data.message.body,
id: data.message.id,
createdAt: data.message.createdAt,
sender: data.sender
}
onNewMessageReceived(newMessage);
});
sg.setVisitorTypingCallback(d => {
})
sg.setReceiveMessageFromVisitorCallback(data => {
props.onCustomerMessageReceived(data);
if (data.customerId != props.visitorId) return;
let newMessage: Message = {
sender: data.sender,
createdAt: data.createdAt,
id: data.id,
body: data.body,
messageType: data.messageType,
meta: data.meta,
}
onNewMessageReceived(newMessage);
})
setSangria(sg);
}, [])
// const observer = useRef<IntersectionObserver>();
//
// const firstChatItemElement = useCallback(node => {
// if (loading) return;
//
// if (observer.current) observer.current.disconnect();
//
// observer.current = new IntersectionObserver(entries => {
// if (entries[0].isIntersecting && hasMore) {
// updateFetchBeforeTimeStamp();
// }
// })
// if (node) observer.current?.observe(node);
// }, [loading, hasMore])
//
function sendMessage() {
if (isEmptyText(props.visitorId)) {
setMsg("");
return;
}
sangria?.sendMessage("text", msg, "", props.visitorId);
setMsg("")
}
function sendOperatorTypingEvent() {
if (isEmptyText(props.visitorId)) {
return;
}
sangria?.sentOperatorTypingEvent(props.visitorId);
}
function sendImage() {
}
useEffect(() => {
updateFetchBeforeTimeStamp();
},[chats.length])
function updateFetchBeforeTimeStamp() {
console.log("fetch beforeTimeStamp called")
if (chats.length == 0)
setFetchBeforeTimeStamp(Date.now());
else
setFetchBeforeTimeStamp(chats[0].createdAt);
}
return (
<div className={styles.main__chatcontent}>
<div className={styles.content__header}>
</div>
<div className={styles.content__body}>
<InfiniteScrollReverse className={styles.chat_items} loadMore={() => {
updateFetchBeforeTimeStamp()
}} hasMore={hasMore} isLoading={loading} loadArea={30}>
{chats.map((itm, index) => {
return (<ChatItem key={itm.id} message={itm} ref={index == 0 ? null : null}/>)
})}
<div key="end" ref={messagesEndRef}/>
</InfiniteScrollReverse>
{/*<div className={styles.chat_items}>*/}
{/* */}
{/*</div>*/}
</div>
<InputField onAttach={sendImage} onSend={sendMessage} onTextChanged={e => {
setMsg(e.target.value);
sendOperatorTypingEvent()
}} text={msg}/>
</div>
)
}
export default ChatContent;
InfiniteScrollReverse.js
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
function InfiniteScrollReverse({ className, isLoading, hasMore, loadArea, loadMore, children }) {
const infiniteRef = useRef();
const [currentPage, setCurrentPage] = useState(1);
const [scrollPosition, setScrollPosition] = useState(0);
// Reset default page, if children equals to 0
useEffect(() => {
if (children.length === 0) {
setCurrentPage(1);
}
}, [children.length]);
useEffect(() => {
console.log("useEffect in inf -scroll called")
let { current: scrollContainer } = infiniteRef;
function onScroll() {
// Handle scroll direction
console.log(scrollContainer.scrollTop," $$$")
if (scrollContainer.scrollTop > scrollPosition) {
// Scroll bottom
} else {
// Check load more scroll area
if (scrollContainer.scrollTop <= loadArea && !isLoading) {
// Check for available data
if (hasMore) {
// Run data fetching
const nextPage = currentPage + 1;
setCurrentPage(nextPage);
loadMore(nextPage);
}
}
}
// Save event scroll position
setScrollPosition(scrollContainer.scrollTop);
}
scrollContainer.addEventListener("scroll", onScroll);
return () => {
scrollContainer.removeEventListener("scroll", onScroll);
};
}, [currentPage, hasMore, isLoading, loadArea, loadMore, scrollPosition]);
useEffect(() => {
let { current: scrollContainer } = infiniteRef;
if (children.length) {
// Get available top scroll
const availableScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
// Get motion for first page
if (currentPage === 1) {
// Move data to bottom for getting load more area
if (availableScroll >= 0) {
scrollContainer.scrollTop = availableScroll;
}
} else {
// Add scroll area for other pages
if (hasMore) {
scrollContainer.scrollTop = scrollContainer.clientHeight;
}
}
}
}, [children.length, currentPage, hasMore]);
return (
<div className={className} ref={infiniteRef}>
{children}
</div>
);
}
InfiniteScrollReverse.propTypes = {
className: PropTypes.string.isRequired,
children: PropTypes.array,
hasMore: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
loadMore: PropTypes.func.isRequired,
loadArea: PropTypes.number,
};
InfiniteScrollReverse.defaultProps = {
className: "InfiniteScrollReverse",
children: [],
loadArea: 30,
};
export default InfiniteScrollReverse;

Related

Adjust user to point in slider

I try to build sliders with different categories that each user has his point.
The informant comes from the json server
What I need I do not succeed in having the customer choose a user that is numbered and the dot will be colored in the slider How do I do that?
In addition he has the option to delete and return the point.
I was able to delete the points by deleting them in the object. But I could not return, is there a possibility to return?
Broker.jsx
import React, { useEffect, useState } from 'react';
import './style.css';
import Combo from '../components/Combo/Combo';
import Sliders from '../components/Sliders/Sliders';
const GetUsersDataFromManipulation = (users, field) => {
const state = users.reduce((store, user) => {
const userId = user.user
const currentManipulationUserData = user.profileManipulation[field]
if (currentManipulationUserData.length === 0) {
return store
}
store[userId] = currentManipulationUserData[0].bid
return store;
}, {})
return state;
};
function Broker({ manipulations }) {
const users = manipulations[2].users
const [hiddenUser, setHiddenUser] = useState(() => {
const visible = {};
for (let user of users) {
visible[user.user] = true;
}
return visible;
})
const GetUsersBid = (profileManipulation) => {
const data = GetUsersDataFromManipulation(users, `${profileManipulation}`); if (!Object.keys(data).length) {
return null
}
return data;
};
const gender = GetUsersBid('gender');
const age = GetUsersBid('age');
const marital = GetUsersBid('marital');
const children = GetUsersBid('children');
const education = GetUsersBid('education');
const interests = GetUsersBid('interests');
const dynamicInterests = GetUsersBid('dynamicInterests');
const showUser = (user_id) => {
const new_hidden = { ...hiddenUser }
new_hidden[user_id] = true;
setHiddenUser(new_hidden);
}
const hideUser = (user_id) => {
const new_hidden = { ...hiddenUser }
console.log(user_id)
new_hidden[user_id] = false;
setHiddenUser(new_hidden);
}
const [userInformation, setUserInformation] = useState([
{ name: 'gender', bids: gender },
{ name: 'age', bids: age },
{ name: 'marital', bids: marital },
{ name: 'children', bids: children },
{ name: 'education', bids: education },
{ name: 'interests', bids: interests },
{ name: 'dynamicInterests ', bids: dynamicInterests },
]);
useEffect(() => {
const curret_User_Info = [...userInformation]
for (let user of Object.keys(hiddenUser)) {
for (let i = 0; i < curret_User_Info.length; i++) {
if (curret_User_Info[i].bids !== null) {
if (hiddenUser[user] === false) {
delete curret_User_Info[i].bids[user]
}
else {
//What am I returning here? So that the bids will return?
}
}
}
}
setUserInformation(curret_User_Info)
}, [hiddenUser])
return (
<div>
<div className="button" >
{userInformation && <Combo users={users} showUser={showUser} hideUser={hideUser} userInformation={userInformation} />}
</div>
<div className='slid'>
{userInformation.map(sliderDetails => {
return (
<div className={sliderDetails.name} key={sliderDetails.name} >
{sliderDetails.bids && (<Sliders className="sliders" hiddenUserChange={hiddenUser} name={sliderDetails.name} userBids={sliderDetails.bids} setUserInformation={setUserInformation} userInformation={userInformation} />)}
</div>
)
})}
</div>
</div>
);
}
export default Broker;
ComboBox.jsx
import React, { useEffect, useRef, useState } from 'react';
import ComboBox from 'react-responsive-combo-box';
import { Button } from '#mui/material';
import 'react-responsive-combo-box/dist/index.css';
import "./style.css"
function Combo({ users, showUser, hideUser, userInformation }) {
const [selectedOption, setSelectedOption] = useState();
const [choosing, setChoosing] = useState();
useEffect(() => {
}, [users])
const onShow = () => {
showUser(users[selectedOption - 1].user)
}
const onHide = () => {
hideUser(users[selectedOption - 1].user)
}
const colorChange = (numOption) => {
const id = users[numOption - 1].user
}
return (
<div className="combo_box">
<ComboBox
onSelect={(option) => { setSelectedOption(option); colorChange(option) }}
options={[...Array.from({ length: users.length }, (_, i) => i + 1)]}
/>
<div className='button' >
<Button style={{ "marginRight": 20 }} variant="contained" onClick={onShow}>Show</Button>
<Button variant="contained" onClick={onHide}>Hide</Button>
</div>
</div>
);
}
export default Combo;
Sliders.jsx
import React, { useEffect, useState } from 'react'
import "./style.css"
import 'rc-slider/assets/index.css';
import Slider from 'rc-slider';
const Sliders = ({ hiddenUserChange, name, userBids, setUserInformation, userInformation }) => {
const [bids, setBids] = useState()
useEffect(() => {
setBids(Object.values(userBids))
}, [hiddenUserChange, userBids])
const updateFieldChanged = (newValue, e) => {//OnChanged Slider
setUserInformation(state => {
return state.map(manipulation => {
if (manipulation.name === name) {
Object.entries(manipulation.bids).forEach(([userId, bidValue], index) => {
manipulation.bids[userId] = newValue[index]
console.log(manipulation.bids[userId])
})
}
return manipulation
})
});
}
const handleChange = (event, newValue) => {
setBids(event)
};
return (
<>
<h1 className='headers'>{name}</h1>
{
<Slider
style={{ "marginRight": "20rem", "width": "30rem", "left": "20%" }}
range={true}
trackStyle={[{ backgroundColor: '#3f51b5' }]}
max={100}
RcSlider={true}
railStyle={{ backgroundColor: '#3f51b5' }}
activeDotStyle={{ left: 'unset' }}
ariaLabelForHandle={Object.keys(hiddenUserChange)}
tabIndex={(Object.keys(userBids))}
ariaLabelledByForHandle={bids}
value={(bids)}
onChange={handleChange}
onAfterChange={updateFieldChanged}
tipProps
tipFormatter
/>
}
</>
)
}
export default Sliders
enter image description here
Thank you all!

How do I run functions on page unload only if user confirms page reload/exit?

I have this code that runs 2 functions before the page is unloaded. However, if the user presses 'cancel', the code still runs obviously. How can I only call the functions if the user confirms the page reload/exit? Or how can I call the functions without having a popup at all? If I remove the return value, it doesn't seem to have enough time to run the functions.
window.addEventListener('beforeunload', (e) => {
e.preventDefault();
e.returnValue = '';
removeUserSocket(false);
stopSearching();
})
This is the react component that houses the listener:
import React, { useState, useEffect } from 'react';
import Game from './components/boards/Game';
import HomeBoard from './components/homeBoard/HomeBoard';
import FindMatch from './components/findMatch/FindMatch';
import Friends from './components/friends/Friends';
import FriendsHome from './components/friends/FriendsHome';
import HomeText from './components/homeText/HomeText';
import Login from './components/logReg/Login';
import Register from './components/logReg/Register';
import Leaderboard from './components/leaderboard/Leaderboard';
import Navigation from './components/navigation/Navigation';
import Footer from './components/footer/Footer';
import { socket } from './socket/socketImport';
import './homePage.css';
import './homePageLogged.css';
import './gamePage.css';
import './leaderboard.css';
function App() {
const [route, setRoute] = useState('login');
const [user, setUser] = useState({username: '', wins: 0});
const [currentSocket, setCurrentSocket] = useState(null);
const [friendSocket, setFriendSocket] = useState(null);
const [unsortedFriends, setUnsortedFriends] = useState([]);
useEffect(() => {
socket.on('connect', () => {
setCurrentSocket(socket.id);
})
return () => {
socket.off('connect');
}
},[])
const onRouteChange = (e) => {
switch(e.target.value) {
case 'goToRegister':
setUser({username: '', wins: 0});
setRoute('register');
removeUserSocket(true);
break;
case 'goToLogin':
setUser({username: '', wins: 0});
setRoute('login');
removeUserSocket(true);
break;
case 'goHome':
setRoute('loggedIn');
break;
case 'goToLeaderboard':
setRoute('leaderboard');
break;
case 'login':
setRoute('loggedIn');
break;
case 'register':
setRoute('loggedIn');
break;
case 'game':
setRoute('game');
break;
default:
setRoute('login');
removeUserSocket(true);
}
}
const showOnlineStatusToFriends = async () => {
let allFriendNames = [];
try {
const response1 = await fetch(`https://calm-ridge-60009.herokuapp.com/getFriends?username=${user.username}`)
if (!response1.ok) {
throw new Error('Error')
}
const friends = await response1.json();
if (friends !== null && friends !== '') {
allFriendNames = friends.split(',');
}
for (let friend of allFriendNames) {
const response2 = await fetch(`https://calm-ridge-60009.herokuapp.com/findFriend?username=${friend}`)
if (!response2.ok) {
throw new Error('Error')
}
const user = await response2.json();
if (user.socketid) {
socket.emit('update user status', user.socketid);
}
}
} catch(err) {
console.log(err);
}
}
const loadUser = (user) => {
setUser({ username: user.username, wins: user.wins })
}
const removeUserSocket = async (show) => {
const res = await fetch('https://calm-ridge-60009.herokuapp.com/removeUserSocket', {
method: 'put',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: user.username
})
})
const socketRemoved = await res.json();
if (socketRemoved && show) {
showOnlineStatusToFriends();
}
}
const stopSearching = async () => {
try {
const response = await fetch('https://calm-ridge-60009.herokuapp.com/updateSearching', {
method: 'put',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: user.username,
search: false
})
})
if (!response.ok) {throw new Error('Problem updating searching status')}
} catch(err) {
console.log(err);
}
}
window.addEventListener('beforeunload', (e) => {
e.preventDefault();
removeUserSocket(false);
stopSearching();
e.returnValue = '';
})
document.onkeydown = (e) => {
const loginBtn = document.querySelector('.loginBtn');
const registerBtn = document.querySelector('.registerBtn');
const friendRequestBtn = document.querySelector('.friendRequestBtn');
if ((e.keyCode === 13) && (route === 'login')) {
e.preventDefault();
loginBtn.click();
} else if ((e.keyCode === 13) && (route === 'register')) {
e.preventDefault();
registerBtn.click();
} else if ((e.keyCode === 13) && (route === 'loggedIn')) {
e.preventDefault();
friendRequestBtn.click();
}
};
return (
route === 'login' || route === 'register'
?
<div className='homePage'>
<FriendsHome onRouteChange={onRouteChange}/>
<HomeBoard route={route}/>
<HomeText />
{
route === 'login'
? <Login currentSocket={currentSocket} loadUser={loadUser} onRouteChange={onRouteChange}/>
: <Register currentSocket={currentSocket} loadUser={loadUser} onRouteChange={onRouteChange}/>
}
<Footer />
</div>
:
<>
{
route === 'loggedIn'
?
<div className='homePageLogged'>
<Navigation setUnsortedFriends={setUnsortedFriends} socket={socket} username={user.username} onRouteChange={onRouteChange} route={route} />
<Friends unsortedFriends={unsortedFriends} setUnsortedFriends={setUnsortedFriends} socket={socket} route={route} setFriendSocket={setFriendSocket} currentSocket={currentSocket} showOnlineStatusToFriends={showOnlineStatusToFriends} username={user.username} setRoute={setRoute} />
<div className='matchAndBoard'>
<FindMatch username={user.username} setFriendSocket={setFriendSocket} setRoute={setRoute} />
<HomeBoard route={route}/>
</div>
<Footer />
</div>
:
<>
{
route === 'leaderboard'
?
<div className='leaderboard'>
<Leaderboard route={route} onRouteChange={onRouteChange} setUnsortedFriends={setUnsortedFriends} socket={socket} username={user.username} />
<Footer />
</div>
:
<Game setRoute={setRoute} setUnsortedFriends={setUnsortedFriends} socket={socket} username={user.username} onRouteChange={onRouteChange} route={route} friendSocket={friendSocket} />
}
</>
}
</>
);
}
export default App;

Property "handle" does not exist on type "undefined" - react context and typescript

I'm converting my app from JS to TS. Everything has been working good under JS but when started conversion to TS I'm getting plenty of errors with handle functions like for example handleVideoAdd. Does anyone has idea what am I'm doing wrong? Tried many things without success...
Property 'handleVideoAdd' does not exist on type 'undefined'. TS2339 - and it's pointing out to this fragment of code:
const { handleVideoAdd, inputURL, handleInputURLChange } = useContext(Context)
My code looks like that:
Header.tsx
import { Context } from "../Context";
import React, { useContext } from "react";
import { Navbar, Button, Form, FormControl } from "react-bootstrap";
export default function Header() {
const { handleVideoAdd, inputURL, handleInputURLChange } =
useContext(Context);
return (
<Navbar bg="light" expand="lg">
<Navbar.Brand href="#home">Video App</Navbar.Brand>
<Form onSubmit={handleVideoAdd} inline>
<FormControl
type="text"
name="url"
placeholder="Paste url"
value={inputURL}
onChange={handleInputURLChange}
className="mr-sm-2"
/>
<Button type="submit" variant="outline-success">
Add
</Button>
</Form>
</Navbar>
);
}
Context.tsx
import { useEffect, useMemo, useState } from "react";
import { youtubeApi } from "./APIs/youtubeAPI";
import { vimeoApi } from "./APIs/vimeoAPI";
import React from "react";
import type { FormEvent } from "react";
const Context = React.createContext(undefined);
function ContextProvider({ children }) {
const [inputURL, setInputURL] = useState("");
const [videoData, setVideoData] = useState(() => {
const videoData = localStorage.getItem("videoData");
if (videoData) {
return JSON.parse(videoData);
}
return [];
});
const [filterType, setFilterType] = useState("");
const [videoSources, setVideoSources] = useState([""]);
const [wasSortedBy, setWasSortedBy] = useState(false);
const [showVideoModal, setShowVideoModal] = useState(false);
const [modalData, setModalData] = useState({});
const [showWrongUrlModal, setShowWrongUrlModal] = useState(false);
const createModalSrc = (videoItem) => {
if (checkVideoSource(videoItem.id) === "youtube") {
setModalData({
src: `http://www.youtube.com/embed/${videoItem.id}`,
name: videoItem.name,
});
} else {
setModalData({
src: `https://player.vimeo.com/video/${videoItem.id}`,
name: videoItem.name,
});
}
};
const handleVideoModalShow = (videoID) => {
createModalSrc(videoID);
setShowVideoModal(true);
};
const handleVideoModalClose = () => setShowVideoModal(false);
const handleWrongUrlModalShow = () => setShowWrongUrlModal(true);
const handleWrongUrlModalClose = () => setShowWrongUrlModal(false);
const handleInputURLChange = (e) => {
setInputURL(e.currentTarget.value);
};
const handleVideoAdd = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const source = checkVideoSource(inputURL);
if (source === "youtube") {
handleYouTubeVideo(inputURL);
} else if (source === "vimeo") {
handleVimeoVideo(inputURL);
} else {
handleWrongUrlModalShow();
}
};
const checkVideoSource = (inputURL) => {
if (inputURL.includes("youtu") || inputURL.length === 11) {
return "youtube";
} else if (inputURL.includes("vimeo") || inputURL.length === 9) {
return "vimeo";
}
};
const checkURL = (inputURL) => {
if (!inputURL.includes("http")) {
const properURL = `https://${inputURL}`;
return properURL;
} else {
return inputURL;
}
};
const checkInputType = (inputURL) => {
if (!inputURL.includes("http") && inputURL.length === 11) {
return "id";
} else if (!inputURL.includes("http") && inputURL.length === 9) {
return "id";
} else {
return "url";
}
};
const fetchYouTubeData = async (videoID) => {
const data = await youtubeApi(videoID);
if (data.items.length === 0) {
handleWrongUrlModalShow();
} else {
setVideoData((state) => [
...state,
{
id: videoID,
key: `${videoID}${Math.random()}`,
name: data.items[0].snippet.title,
thumbnail: data.items[0].snippet.thumbnails.medium.url, //default, medium, high
viewCount: data.items[0].statistics.viewCount,
likeCount: data.items[0].statistics.likeCount,
savedDate: new Date(),
favourite: false,
source: "YouTube",
url: inputURL,
},
]);
setInputURL("");
}
};
const handleYouTubeVideo = (inputURL) => {
const inputType = checkInputType(inputURL);
if (inputType === "id") {
fetchYouTubeData(inputURL);
} else {
const checkedURL = checkURL(inputURL);
const url = new URL(checkedURL);
if (inputURL.includes("youtube.com")) {
const params = url.searchParams;
const videoID = params.get("v");
fetchYouTubeData(videoID);
} else {
const videoID = url.pathname.split("/");
fetchYouTubeData(videoID[1]);
}
}
};
const fetchVimeoData = async (videoID) => {
const data = await vimeoApi(videoID);
if (data.hasOwnProperty("error")) {
handleWrongUrlModalShow();
} else {
setVideoData((state) => [
...state,
{
id: videoID,
key: `${videoID}${Math.random()}`,
name: data.name,
thumbnail: data.pictures.sizes[2].link, //0-8
savedDate: new Date(),
viewCount: data.stats.plays,
likeCount: data.metadata.connections.likes.total,
savedDate: new Date(),
favourite: false,
source: "Vimeo",
url: inputURL,
},
]);
setInputURL("");
}
};
const handleVimeoVideo = (inputURL) => {
const inputType = checkInputType(inputURL);
if (inputType === "id") {
fetchVimeoData(inputURL);
} else {
const checkedURL = checkURL(inputURL);
const url = new URL(checkedURL);
const videoID = url.pathname.split("/");
fetchVimeoData(videoID[1]);
}
};
const deleteVideo = (key) => {
let newArray = [...videoData].filter((video) => video.key !== key);
setWasSortedBy(true);
setVideoData(newArray);
};
const deleteAllData = () => {
setVideoData([]);
};
const toggleFavourite = (key) => {
let newArray = [...videoData];
newArray.map((item) => {
if (item.key === key) {
item.favourite = !item.favourite;
}
});
setVideoData(newArray);
};
const handleFilterChange = (type) => {
setFilterType(type);
};
const sourceFiltering = useMemo(() => {
return filterType
? videoData.filter((item) => item.source === filterType)
: videoData;
}, [videoData, filterType]);
const sortDataBy = (sortBy) => {
if (wasSortedBy) {
const reversedArr = [...videoData].reverse();
setVideoData(reversedArr);
} else {
const sortedArr = [...videoData].sort((a, b) => b[sortBy] - a[sortBy]);
setWasSortedBy(true);
setVideoData(sortedArr);
}
};
const exportToJsonFile = () => {
let dataStr = JSON.stringify(videoData);
let dataUri =
"data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
let exportFileDefaultName = "videoData.json";
let linkElement = document.createElement("a");
linkElement.setAttribute("href", dataUri);
linkElement.setAttribute("download", exportFileDefaultName);
linkElement.click();
};
const handleJsonImport = (e) => {
e.preventDefault();
const fileReader = new FileReader();
fileReader.readAsText(e.target.files[0], "UTF-8");
fileReader.onload = (e) => {
const convertedData = JSON.parse(e.target.result);
setVideoData([...convertedData]);
};
};
useEffect(() => {
localStorage.setItem("videoData", JSON.stringify(videoData));
}, [videoData]);
return (
<Context.Provider
value={{
inputURL,
videoData: sourceFiltering,
handleInputURLChange,
handleVideoAdd,
deleteVideo,
toggleFavourite,
handleFilterChange,
videoSources,
sortDataBy,
deleteAllData,
exportToJsonFile,
handleJsonImport,
handleVideoModalClose,
handleVideoModalShow,
showVideoModal,
modalData,
showWrongUrlModal,
handleWrongUrlModalShow,
handleWrongUrlModalClose,
}}
>
{children}
</Context.Provider>
);
}
export { ContextProvider, Context };
App.js (not converted to TS yet)
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import "bootstrap/dist/css/bootstrap.min.css";
import reportWebVitals from "./reportWebVitals";
import { ContextProvider } from "./Context.tsx";
ReactDOM.render(
<React.StrictMode>
<ContextProvider>
<App />
</ContextProvider>
</React.StrictMode>,
document.getElementById("root")
);
reportWebVitals();
This is because when you created your context you defaulted it to undefined.
This happens here: const Context = React.createContext(undefined)
You can't say undefined.handleVideoAdd. But you could theoretically say {}.handleVideoAdd.
So if you default your context to {} at the start like this: const Context = React.createContext({})
Your app shouldn't crash up front anymore.
EDIT: I see you're using TypeScript, in that case you're going to need to create an interface for your context. Something like this:
interface MyContext {
inputURL?: string,
videoData?: any,
handleInputURLChange?: () => void,
handleVideoAdd?: () => void,
deleteVideo?: () => void,
// and all the rest of your keys
}
Then when creating your context do this:
const Context = React.createContext<MyContext>(undefined);

React Context API state update leads to infinite loop

I am trying to add Authentication to my app and maintaining Auth State using React Context API.
I am calling my api using a custom hook use-http.
import { useCallback, useReducer } from 'react';
function httpReducer(state, action) {
switch (action.type) {
case 'SEND':
return {
data: null,
error: null,
status: 'pending',
};
case 'SUCCESS':
return {
data: action.responseData,
error: null,
status: 'completed',
};
case 'ERROR':
return {
data: null,
error: action.errorMessage,
status: 'completed',
};
default:
return state;
}
}
function useHttp(requestFunction, startWithPending = false) {
const [httpState, dispatch] = useReducer(httpReducer, {
status: startWithPending ? 'pending' : null,
data: null,
error: null,
});
const sendRequest = useCallback(
async requestData => {
dispatch({ type: 'SEND' });
try {
const responseData = await requestFunction(requestData);
dispatch({ type: 'SUCCESS', responseData });
} catch (error) {
dispatch({
type: 'ERROR',
errorMessage: error.response.data.message || 'Something went wrong!',
});
}
},
[requestFunction]
);
return {
sendRequest,
...httpState,
};
}
export default useHttp;
This is my Login page which calls the api and I need to navigate out of this page and also update my Auth Context.
import { useCallback, useContext } from 'react';
import { makeStyles } from '#material-ui/core';
import Container from '#material-ui/core/Container';
import LoginForm from '../components/login/LoginForm';
import useHttp from '../hooks/use-http';
import { login } from '../api/api';
import AuthContext from '../store/auth-context';
import { useEffect } from 'react';
const useStyles = makeStyles(theme => ({
pageWrapper: {
height: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
},
pageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: '1',
},
}));
function Login() {
const authCtx = useContext(AuthContext);
const { sendRequest, status, data: userData, error } = useHttp(login);
const loginHandler = (email, password) => {
sendRequest({ email, password });
};
if (status === 'pending') {
console.log('making request');
}
if (status === 'completed' && userData) {
console.log('updateContext');
authCtx.login(userData);
}
if (status === 'completed' && error) {
console.log(error);
}
const classes = useStyles();
return (
<div className={classes.pageWrapper}>
<Container maxWidth="md" className={classes.pageContainer}>
<LoginForm status={status} onLoginHandler={loginHandler} />
</Container>
</div>
);
}
export default Login;
The login api -
export const login = async ({ email, password }) => {
let config = {
method: 'post',
url: `${BACKEND_URL}/api/auth/`,
headers: { 'Content-Type': 'application/json' },
data: {
email: email,
password: password,
},
};
const response = await axios(config);
return response.data;
};
The Auth Context -
import React, { useState } from 'react';
import { useEffect, useCallback } from 'react';
import {
getUser,
removeUser,
saveUser,
getExpirationTime,
clearExpirationTime,
setExpirationTime,
} from '../utils/local-storage';
const AuthContext = React.createContext({
token: '',
isLoggedIn: false,
login: () => {},
logout: () => {},
});
let logoutTimer;
const calculateRemainingTime = expirationTime => {
const currentTime = new Date().getTime();
const adjExpirationTime = new Date(expirationTime).getTime();
const remainingDuration = adjExpirationTime - currentTime;
return remainingDuration;
};
const retrieveStoredToken = () => {
const storedToken = getUser();
const storedExpirationDate = getExpirationTime();
const remainingTime = calculateRemainingTime(storedExpirationDate);
if (remainingTime <= 60) {
removeUser();
clearExpirationTime();
return null;
}
return {
token: storedToken,
duration: remainingTime,
};
};
export const AuthContextProvider = ({ children }) => {
const tokenData = retrieveStoredToken();
let initialToken = '';
if (tokenData) {
initialToken = tokenData.token;
}
const [token, setToken] = useState(initialToken);
const userIsLoggedIn = !!token;
const logoutHandler = useCallback(() => {
setToken(null);
removeUser();
clearExpirationTime();
if (logoutTimer) {
clearTimeout(logoutTimer);
}
}, []);
const loginHandler = ({ token, user }) => {
console.log('login Handler runs');
console.log(token, user.expiresIn);
setToken(token);
saveUser(token);
setExpirationTime(user.expiresIn);
const remainingTime = calculateRemainingTime(user.expiresIn);
logoutTimer = setTimeout(logoutHandler, remainingTime);
};
useEffect(() => {
if (tokenData) {
console.log(tokenData.duration);
logoutTimer = setTimeout(logoutHandler, tokenData.duration);
}
}, [tokenData, logoutHandler]);
const user = {
token,
isLoggedIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
export default AuthContext;
The problem is when I call loginHandler function of my AuthContext in Login Component, the Login component re-renders and this login function goes in an infinite loop. What am I doing wrong?
I am new to React and stuck on this issue since hours now.
I think I know what it is.
You're bringing in a bunch of component state via hooks. Whenever authCtx, sendRequest, status, data and error change, the component re-renders. Avoid putting closures into the state. The closures trigger unnecessary re-renders.
function Login() {
const authCtx = useContext(AuthContext);
const { sendRequest, status, data: userData, error } = useHttp(login);
Try looking for all closures that could be causing re-renders and make sure components don't depend on them.
Edit:
Ben West is right- you also have side effects happening during the render, which is wrong.
When you have something like this in the body of a functional component:
if (status === 'completed' && userData) {
console.log('updateContext');
authCtx.login(userData);
}
Change it to this:
useEffect(() => {
if (status === 'completed' && userData) {
console.log('updateContext');
authCtx.login(userData);
}
}, [status, userData]); //the function in arg 1 is called whenever these dependencies change
I made a bunch of changes to your code:
It's down to 2 files. The other stuff I inlined.
I'm not that familiar with useContext(), so I can't say if you're using it correctly.
Login.js:
import { useContext, useEffect } from 'react';
import { makeStyles } from '#material-ui/core';
import Container from '#material-ui/core/Container';
import LoginForm from '../components/login/LoginForm';
import AuthContext from '../store/auth-context';
const useStyles = makeStyles(theme => ({
pageWrapper: {
height: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
},
pageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: '1',
},
}));
function httpReducer(state, action) {
switch (action.type) {
case 'SEND':
return {
data: null,
error: null,
status: 'pending',
};
case 'SUCCESS':
return {
data: action.responseData,
error: null,
status: 'completed',
};
case 'ERROR':
return {
data: null,
error: action.errorMessage,
status: 'completed',
};
default:
return state;
}
}
function Login() {
const [httpState, dispatch] = useReducer(httpReducer, {
status: startWithPending ? 'pending' : null,
data: null,
error: null,
});
const sendRequest = async requestData => {
dispatch({ type: 'SEND' });
try {
let config = {
method: 'post',
url: `${BACKEND_URL}/api/auth/`,
headers: { 'Content-Type': 'application/json' },
data: {
email: requestData.email,
password: requestData.password,
},
};
const response = await axios(config);
dispatch({ type: 'SUCCESS', responseData: response.data });
} catch (error) {
dispatch({
type: 'ERROR',
errorMessage: error.response.data.message || 'Something went wrong!',
});
}
};
const authCtx = useContext(AuthContext);
const loginHandler = (email, password) => {
sendRequest({ email, password });
};
useEffect(() => {
if (httpState.status === 'pending') {
console.log('making request');
}
}, [httpState.status]);
useEffect(() => {
if (httpState.status === 'completed' && httpState.data) {
console.log('updateContext');
authCtx.login(httpState.data);
}
}, [httpState.status, httpState.data]);
useEffect(() => {
if (httpState.status === 'completed' && httpState.error) {
console.log(httpState.error);
}
}, [httpState.status, httpState.error]);
const classes = useStyles();
return (
<div className={classes.pageWrapper}>
<Container maxWidth="md" className={classes.pageContainer}>
<LoginForm status={httpState.status} onLoginHandler={loginHandler} />
</Container>
</div>
);
}
export default Login;
AuthContext.js:
import React, { useState } from 'react';
import { useEffect } from 'react';
import {
getUser,
removeUser,
saveUser,
getExpirationTime,
clearExpirationTime,
setExpirationTime,
} from '../utils/local-storage';
const AuthContext = React.createContext({
token: '',
isLoggedIn: false,
login: () => {},
logout: () => {},
});
const calculateRemainingTime = expirationTime => {
const currentTime = new Date().getTime();
const adjExpirationTime = new Date(expirationTime).getTime();
const remainingDuration = adjExpirationTime - currentTime;
return remainingDuration;
};
// is this asynchronous?
const retrieveStoredToken = () => {
const storedToken = getUser();
const storedExpirationDate = getExpirationTime();
const remainingTime = calculateRemainingTime(storedExpirationDate);
if (remainingTime <= 60) {
removeUser();
clearExpirationTime();
return null;
}
return {
token: storedToken,
duration: remainingTime,
};
};
export const AuthContextProvider = ({ children }) => {
const [tokenData, setTokenData] = useState(null);
const [logoutTimer, setLogoutTimer] = useState(null);
useEffect(() => {
const tokenData_ = retrieveStoredToken(); //is this asynchronous?
if (tokenData_) {
setTokenData(tokenData_);
}
}, []);
const userIsLoggedIn = !!(tokenData && tokenData.token);
const logoutHandler = () => {
setTokenData(null);
removeUser();//is this asynchronous?
clearExpirationTime();
if (logoutTimer) {
clearTimeout(logoutTimer);
//clear logoutTimer state here? -> setLogoutTimer(null);
}
};
const loginHandler = ({ token, user }) => {
console.log('login Handler runs');
console.log(token, user.expiresIn);
setTokenData({ token });
saveUser(token);
setExpirationTime(user.expiresIn);
const remainingTime = calculateRemainingTime(user.expiresIn);
setLogoutTimer(setTimeout(logoutHandler, remainingTime));
};
useEffect(() => {
if (tokenData && tokenData.duration) {
console.log(tokenData.duration);
setLogoutTimer(setTimeout(logoutHandler, tokenData.duration));
}
}, [tokenData]);
const user = {
token: tokenData.token,
isLoggedIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
export default AuthContext;

Apollo Queries triggered and causing rerendering in useContext on URL Change

GIF of Rerender occuring
I'm not sure how to proceed. As you can see, the Header's state (as passed down via context) is switching from the user's data --> undefined --> same user's data. This occurs every time there's a url change, and doesn't happen when I do things that don't change the url (like opening the cart for example).
Is this expected behaviour? Is there any way I can get the query in my context to only be called when there is no user data or when the user data changes? I tried using useMemo, but to no avail.
auth.context
import React, { useState} from "react";
import {
CURRENT_USER,
GET_LOGGED_IN_CUSTOMER,
} from "graphql/query/customer.query";
import { gql, useQuery, useLazyQuery } from "#apollo/client";
import { isBrowser } from "components/helpers/isBrowser";
export const AuthContext = React.createContext({});
export const AuthProvider = ({ children }) => {
const [customer, { data, loading, error }] = useLazyQuery(
GET_LOGGED_IN_CUSTOMER,
{
ssr: true,
}
);
const { data: auth } = useQuery(CURRENT_USER, {
onCompleted: (auth) => {
console.log(auth);
customer({
variables: {
where: {
id: auth.currentUser.id,
},
},
});
},
ssr: true,
});
console.log(data);
const isValidToken = () => {
if (isBrowser && data) {
const token = localStorage.getItem("token");
if (error) {
console.log("error", error);
}
if (token && data) {
console.log("token + auth");
return true;
} else return false;
}
};
const [isAuthenticated, makeAuthenticated] = useState(isValidToken());
function authenticate() {
makeAuthenticated(isValidToken());
}
function signout() {
makeAuthenticated(false);
localStorage.removeItem("token");
}
return (
<AuthContext.Provider
value={{
isAuthenticated,
data,
authenticate,
auth,
signout,
}}
>
{children}
</AuthContext.Provider>
);
};
(In Header, userData is equal to data just passed through an intermediary component (to provide to mobile version)).
header.tsx
import React, { useContext } from "react";
import Router, { useRouter } from "next/router";
import { useApolloClient } from "#apollo/client";
import { openModal } from "#redq/reuse-modal";
import SearchBox from "components/SearchBox/SearchBox";
import { SearchContext } from "contexts/search/search.context";
import { AuthContext } from "contexts/auth/auth.context";
import LoginModal from "containers/LoginModal";
import { RightMenu } from "./Menu/RightMenu/RightMenu";
import { LeftMenu } from "./Menu/LeftMenu/LeftMenu";
import HeaderWrapper from "./Header.style";
import LogoImage from "image/hatchli-reduced-logo.svg";
import { isCategoryPage } from "../is-home-page";
type Props = {
className?: string;
token?: string;
pathname?: string;
userData?: any;
};
const Header: React.FC<Props> = ({ className, userData }) => {
const client = useApolloClient();
const { isAuthenticated, signout } = useContext<any>(AuthContext);
const { state, dispatch } = useContext(SearchContext);
console.log(isAuthenticated);
console.log(userData);
const { pathname, query } = useRouter();
const handleLogout = () => {
if (typeof window !== "undefined") {
signout();
client.resetStore();
Router.push("/medicine");
}
};
const handleJoin = () => {
openModal({
config: {
className: "login-modal",
disableDragging: true,
width: "auto",
height: "auto",
animationFrom: { transform: "translateY(100px)" },
animationTo: { transform: "translateY(0)" },
transition: {
mass: 1,
tension: 180,
friction: 26,
},
},
component: LoginModal,
componentProps: {},
closeComponent: "",
closeOnClickOutside: true,
});
};
const onSearch = (text: any) => {
dispatch({
type: "UPDATE",
payload: {
...state,
text,
},
});
};
const { text } = state;
const onClickHandler = () => {
const updatedQuery = query.category
? { text: text, category: query.category }
: { text };
Router.push({
pathname: pathname,
query: updatedQuery,
});
};
const showSearch = isCategoryPage(pathname);
return (
<HeaderWrapper className={className}>
<LeftMenu logo={LogoImage} />
{showSearch && (
<SearchBox
className="headerSearch"
handleSearch={(value: any) => onSearch(value)}
onClick={onClickHandler}
placeholder="Search anything..."
hideType={true}
minimal={true}
showSvg={true}
style={{ width: "100%" }}
value={text || ""}
/>
)}
<RightMenu
isAuth={userData}
onJoin={handleJoin}
onLogout={handleLogout}
avatar={userData && userData.user && userData.user.avatar}
/>
</HeaderWrapper>
);
};
export default Header;

Categories