Strange Sockets Behavior with React - [RESOLVED] - reactjs

I am new to using Web Sockets, and am trying to use the Sockets.io library to finish a chat application I am building. When a message is sent, the recipient receives the message twice, which is obviously not how the app is supposed to work.
I built a Socket context that joins the user into a room that is represented by their unique MongoDB identifier so they can be reached regardless of whichever chat they may be currently viewing. I tried to build it so that each chat the user views enters them into a new room while simultaneously causing them to leave the room for the chat they were viewing previously.
This is my code for the socket-related portion of my server:
io.on('connection', socket => {
socket.on('setup', userData => {
socket.join(userData._id);
});
socket.on('join chat', chatId => {
socket.join(chatId);
});
socket.on('new message', message => {
let chat = message.chat;
chat.users.forEach(user => {
if (user._id === message.sender._id) return;
socket.to(user._id).emit('message received', message);
});
});
socket.on('leave chat', chatId => {
socket.leave(chatId);
});
Here is the relevant code for my socket context (if a new user signs in then it should close the old socket and create a new room representing the new user):
useEffect(() => {
if (!currentUser) return;
const newSocket = io(ENDPOINT);
newSocket.emit('setup', currentUser);
setSocket(newSocket);
return () => newSocket.close();
}, [currentUser]);
And, finally, here is the code within the chat instance component:
useEffect(() => {
if (!socket) return;
socket.emit('join chat', activeChat[0]._id);
return () => socket.emit('leave chat', activeChat[0]._id);
}, [activeChat, socket]);
useEffect(() => {
if (!socket) return;
socket.on('message received', message => {
if (!activeChat[0]._id || message.chat._id !== activeChat[0]._id) {
if (!notifications) return;
setNotifications(prevState => [message, ...prevState]);
return;
} else {
setMessages(prevState => {
return [...prevState, message];
});
}
});
}, [activeChat, fetchChats, notifications, socket, setNotifications]);
Just as a side note, I had the application working previously when I kept the socket instance inside of the chat instance (and did not try importing it from the socket hook), but it inhibited my ability for the user to be contacted while viewing another chat since removed the socket instance when the chat instance unmounted by calling return () => socket.close(). Here is that code:
let socket; // Global scope
useEffect(() => {
socket = io(ENDPOINT);
socket.emit('setup', currentUser);
socket.emit('stop typing', activeChat[0]._id, currentUser);
return () => socket.close();
}, [currentUser, activeChat]);
If there is anything I can clarify, please let me know! Thanks so much for the help :)
EDIT: So I fixed my problem and it had to do with how I was handling the event listeners on the client side, which I was never unmounting. For anyone in the future who faces the same problem, please see the code below that I used to handle user messaging, typing, and handling changes to which users are online. Namaste.
Server.js (relevant portion):
global.onlineUsers = new Map();
io.on('connection', socket => {
socket.on('setup', userId => {
socket.join(userId);
global.onlineUsers.set(userId, socket.id);
for (const [
onlineUserId,
_onlineSocketId,
] of global.onlineUsers.entries()) {
if (onlineUserId === userId) {
socket.emit('logged in user change', [...global.onlineUsers]);
return;
} else {
socket
.to(onlineUserId)
.emit('logged in user change', [...global.onlineUsers]);
}
}
});
socket.on('join room', chatId => {
socket.join(chatId);
});
socket.on('leave room', chatId => {
socket.leave(chatId);
});
socket.on('send-msg', message => {
message.chat.users.forEach(user => {
if (user._id === message.sender._id) return;
socket.to(user._id).emit('msg-received', message);
});
});
socket.on('typing', (room, user) => {
socket.to(room).emit('typing', user.userName);
});
socket.on('stop typing', (room, user) =>
socket.to(room).emit('stop typing', user.userName)
);
socket.on('log out', userId => {
socket.leave(userId);
global.onlineUsers.delete(userId);
for (const [
onlineUserId,
_onlineSocketId,
] of global.onlineUsers.entries()) {
socket
.to(onlineUserId)
.emit('logged in user change', [...global.onlineUsers]);
}
});
});
Socket Context (relevant portion):
useEffect(() => {
if (!currentUser) return;
const newSocket = io(ENDPOINT);
newSocket.emit('setup', currentUser._id);
newSocket.on('logged in user change', users => {
const userIdArr = users.map(([userId, socketId]) => userId);
setOnlineUsers(userIdArr);
});
setSocket(newSocket);
return () => {
newSocket.off('logged in user change');
newSocket.emit('log out', currentUser._id);
};
}, [currentUser]);
Chat Instance (entire component):
import { useCallback, useEffect, useState } from 'react';
import { io } from 'socket.io-client';
import Lottie from 'lottie-react';
import { useChatView } from '../../contexts/chat-view-context';
import Spinner from '../spinner/spinner.component';
import './message-view.styles.scss';
import { useAuthentication } from '../../contexts/authentication-context';
import animationData from '../../animations/typing.json';
import {
defaultToast,
sameSenderAndNotCurrentUser,
TOAST_TYPE,
userSent,
getTyperString,
} from '../../utils/utils';
import { useSocket } from '../../contexts/socket-context';
// Could definitely add timestamp data to the message as well, that would be pretty clean actually
let typingTimer;
const MessageView = () => {
// Somehow we are going to have to get all of the message in a conversation potentially and then mark whether or not they are your messages or someone else's to style accordingly;
const { currentUser } = useAuthentication();
const { activeChat, notifications, setNotifications, fetchChats } =
useChatView();
const { socket } = useSocket();
// const [socketConnected, setSocketConnected] = useState(false);
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [typing, setTyping] = useState(false);
const [typers, setTypers] = useState([]);
// console.log('typers from outside', typers);
// So I am thinking that I can definitely scroll into view whatever message is actually clicked within whatever chat, I don't see why that would not be possible?
// Pretty cool, when the component actually mounts, the ref for the element gets passed into the callback function, could actually do some pretyy coll things with this, like making an animation or shake the screen or bounce the message or anything when the message actually enters the screen...
const handleKeyDown = async e => {
if (!socket) return;
const newMessage = e.target.innerHTML;
if (e.key === 'Enter' && newMessage) {
e.preventDefault();
e.target.innerHTML = '';
try {
const response = await fetch(`http://localhost:4000/api/message`, {
method: 'post',
headers: {
Authorization: `Bearer ${currentUser.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
chatId: activeChat[0]._id,
text: newMessage,
}),
});
const message = await response.json();
socket.emit('send-msg', message);
setMessages(prevState => [...prevState, message]);
setTyping(false);
return;
} catch (error) {
defaultToast(TOAST_TYPE.error, 'Error sending');
}
} else {
if (!typing) {
setTyping(true);
socket.emit('typing', activeChat[0]._id, currentUser);
}
const lastTypingTime = new Date().getTime();
const timerLength = 3000;
if (typingTimer) clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
const timeNow = new Date().getTime();
const timeDiff = timeNow - lastTypingTime;
if (timeDiff >= timerLength) {
socket.emit('stop typing', activeChat[0]._id, currentUser);
setTyping(false);
}
}, timerLength);
}
};
const fetchMessages = useCallback(async () => {
if (!socket) return;
if (!activeChat) return;
setIsLoading(true);
const response = await fetch(
`http://localhost:4000/api/message/${activeChat[0]._id}`,
{
method: 'get',
headers: { Authorization: `Bearer ${currentUser.token}` },
}
);
const messages = await response.json();
setMessages(messages);
setIsLoading(false);
}, [activeChat, currentUser.token, socket]);
useEffect(() => {
fetchMessages();
}, [fetchMessages, activeChat]);
useEffect(() => {
if (!socket) return;
socket.emit('join room', activeChat[0]._id);
socket.emit('stop typing', activeChat[0]._id, currentUser);
return () => socket.emit('leave room', activeChat[0]._id);
}, [activeChat, socket, currentUser]);
useEffect(() => {
if (!socket) return;
socket.on('msg-received', message => {
if (!activeChat[0]._id || message.chat._id !== activeChat[0]._id) {
setNotifications(prevState => [message, ...prevState]);
return;
} else {
setIsTyping(false);
setMessages(prevState => [...prevState, message]);
}
});
return () => socket.off('msg-received');
}, [socket, activeChat, setNotifications]);
useEffect(() => {
if (!socket) return;
socket.on('typing', typer => {
setIsTyping(true);
setTypers(prevState => [...new Set([typer, ...prevState])]);
});
socket.on('stop typing', userName => {
const usersStillTyping = typers.filter(typer => typer !== userName);
if (usersStillTyping.length > 0 && typers.length !== 0) {
setIsTyping(true);
setTypers(usersStillTyping);
return;
}
setIsTyping(false);
setTypers([]);
});
return () => {
socket.off('typing');
socket.off('stop typing');
};
}, [socket, typers]);
const setRef = useCallback(
node => {
if (node && isTyping && isScrolledIntoView(node)) {
node.scrollIntoView({ smooth: true });
} else if (node && !isTyping) {
node.scrollIntoView({ smooth: true });
}
},
[isTyping]
);
function isScrolledIntoView(el) {
var rect = el.getBoundingClientRect();
var elemTop = rect.top;
var elemBottom = rect.bottom;
// Only completely visible elements return true:
var isVisible = elemTop >= 0 && elemBottom <= window.innerHeight;
// Partially visible elements return true:
//isVisible = elemTop < window.innerHeight && elemBottom >= 0;
return isVisible;
}
// What is the best way to make it so that the text bubble can expland if it needs to??
return (
<div className="message-view-container">
{isLoading ? (
<Spinner type="search" />
) : (
<>
<div className="message-view-active-chat-container">
{messages.length > 0 &&
messages.map((message, i) => {
const lastMessageBool = messages.length - 1 === i + 1;
const userSentBool = userSent(currentUser, message);
const sameSenderAndNotCurrentUserBool =
sameSenderAndNotCurrentUser(i, messages, currentUser);
return (
<div
key={i}
ref={lastMessageBool ? setRef : null}
className={`message-view-message-container ${
userSentBool ? 'user-sent' : ''
}`}
>
<div
className="message-view-message-image-container"
style={
sameSenderAndNotCurrentUserBool || userSentBool
? { visibility: 'hidden' }
: { marginTop: '2px' }
}
>
<img
height="100%"
src={message.sender.picture}
alt="profile"
/>
</div>
<div className="message-view-text-container">
<div className="message-view-text">{message.text}</div>
<div
style={
sameSenderAndNotCurrentUserBool || userSentBool
? { display: 'none' }
: {}
}
className="message-view-text-info"
>
<p>
#{!userSentBool ? message.sender.userName : 'You'}
</p>
</div>
</div>
</div>
);
})}
</div>
{isTyping && (
<div className="lottie-container">
{typers.length ? getTyperString(typers) : ''}
<Lottie
animationData={animationData}
loop={true}
autoplay={true}
style={{ height: '16px', display: 'block' }}
/>
</div>
)}
<div
className="send-message-editable"
data-text={`Message `}
contentEditable
onKeyDown={handleKeyDown}
/>
</>
)}
</div>
);
};
export default MessageView;

Related

I need to listen update phase (in life cycle) without constantly sending get requests in react JS

I am trying to write a program that communicates with the client frequently and I need to quickly notice the changes and read the result from the server. But this request is constantly being sent and loaded, even when the user is not interacting. And this causes the user's system to be occupied.
this is mu code:
const AdminDashboard = () => {
const [filterShow, setFilterShow] = useState({ sort: "", read: "", flag: "", skip: 0, limit: 15 });
const [adminItemList, setAdminItemList] = useState([]);
const [searchParams, setSearchParams] = useSearchParams();
async function changeItem(updateItem) {
// update admin item state
await axios.put("{...}/api/admin", { ... });
}
useEffect(() => {
async function resultItem() {
// get result(admin items)
await axios.get(`{...}/api/admin?${searchParams.toString()}`)
.then((res) => {
setAdminItemList(res.data.data);
}).catch((res) => {
console.log(res)
});
}
resultItem();
})
return (<>
{adminItemList.map((ai, i) => {
return (<div key={i}>
<AdminItem item={ai} count={i} skip={filterShow.skip} res={changeItem} />
</div>)
})}
</>);
}
I know that I can avoid resending the request by using "useEffect" and passing an empty array to its second input. But I need to listen the changes so I can't do that.
How can i listening the changes and prevent repeated get requests???
The only solution I could find:
I moved the function "resultItem" outside of useEffect. And I wrote useEffect with an empty array in the second entry. then call "resultItem" in useEffect.
I gave this function "resultItem" an input to receive a query so that it can be flexible
I called it wherever I needed it
Note: In async functions, I first put that function in the Promise and call the "resultItem" in then().
I will put all the written codes here:
const AdminDashboard = () => {
const usePuCtx = useContext(PublicContext);
const { lang, dir } = usePuCtx.language;
// base state
const [allAdminItem, setAllAdminItem] = useState(0);
const [filterShow, setFilterShow] = useState({ sort: "", read: "", flag: "", skip: 0, limit: 3 });
const [adminItemList, setAdminItemList] = useState([]);
// validate for show this page
const [isAdmin, setIsAdmin] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
async function setResalt(radioName, radioState) {
// set setting on query for get result(admin items)
let newFilter = { ...filterShow };
newFilter[radioName] = radioState;
const queryStr = queryString.stringify(newFilter);
setSearchParams(queryStr);
new Promise((res, rej) => { res(setFilterShow(newFilter)) })
.then(() => resultItem(queryStr));
}
async function changeItem(updateItem) {
// update admin item state (read & flag)
await axios.put(usePuCtx.ip_address + "/api/admin",
{ _id: updateItem._id, read: updateItem.read, flag: updateItem.flag })
.then(() => resultItem(searchParams));
}
function showSkipPage(numberPage) {
const nextResult = numberPage * filterShow.limit
setResalt("skip", nextResult)
}
async function resultItem(query) {
// get result(admin items)
await axios.get(usePuCtx.ip_address + `/api/admin?${query.toString()}`)
.then((res) => {
setIsAdmin(true);
setAdminItemList(res.data.data);
}).catch(() => {
// if auth token is wrong
window.location = "/not-found";
});
// get all admin item number
await axios.get(usePuCtx.ip_address + "/api/admin?limit=0&skip=0&flag=&read=&sort=close")
.then((res) => {
setIsAdmin(true);
setAllAdminItem(res.data.data.length);
}).catch(() => {
// if auth token is wrong
window.location = "/not-found";
});
}
useEffect(() => {
resultItem(searchParams);
}, []);
return (!isAdmin ? "" : (<>
<div className="container-fluid" dir={dir}>
<div className="row m-4" dir="ltr">
{/* radio buttons */}
<RadioFilterButton name="read" id1="read-" id2="read-false" id3="read-true"
inner1={txt.radio_filter_button_all[lang]} inner2={txt.radio_filter_button_read_no[lang]}
inner3={txt.radio_filter_button_read[lang]} res={setResalt} />
<RadioFilterButton name="flag" id1="flag-" id2="flag-false" id3="flag-true"
inner1={txt.radio_filter_button_all[lang]} inner2={txt.radio_filter_button_flag_no[lang]}
inner3={txt.radio_filter_button_flag[lang]} res={setResalt} />
<RadioFilterButton name="sort" id1="sort-close" id2="sort-far"
inner1={txt.radio_filter_button_close[lang]} inner2={txt.radio_filter_button_far[lang]}
res={setResalt} />
</div><hr />
<div className="m-4" style={{ minHeight: "100vh" }}>
{
// show result(admin items)
adminItemList.map((ai, i) => {
return (<div key={i}>
<AdminItem item={ai} count={i} skip={filterShow.skip} res={changeItem} />
</div>)
})
}
</div>
<div className="row justify-content-center mt-auto" dir="ltr">
<PageinationTool countAll={allAdminItem} limitCard={filterShow.limit} skipNum={filterShow.skip} res={showSkipPage} />
</div>
<Footer />
</div>
</>));
}

Why am I getting this error: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks?

I'm new to react but I've encountered enough errors to know not to call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks
I'm trying to import data from mongodb atlas into my app.
From the codes written below, I don't think it has violated the rule. Any help would be great thx!
import { useState, useEffect } from "react";
import axios from "axios";
const Events = () => {
const [allEvents, setEvents] = useState([]);
const fetchEvents = async () => {
try {
const { data } = await axios.get(`http://localhost:5000/get/events`);
return data
} catch (err) {
console.log(err);
}
};
useEffect(() => {
fetchEvents();
}, []);
};
export default Events;
function CalendarComponent() {
//useState declarations
const [newEvent, setNewEvent] = useState({
id: "",
title: "",
start: "",
action: "",
});
const [allEvents, setEvents] = useState(events);
const [toggleAdd, setToggleAdd] = useState(false);
const [msg, setMsg] = useState("");
//handle creating new event on button click
function handleAddSlot() {
const title = newEvent.title;
const start = newEvent.start;
const end = start;
let success = 0;
if (title && newEvent.action === control.ADD) {
// add new event into the list
const id = allEvents.length;
if(data.Events.some(i=>i["Serial Number"]===title)){
// let newArr = [...data.Events]
// const res = newArr.find(e=>e["Serial Number"]===title)["Scheduled Sample Date"]
// console.log(res);
// res.push({id:res.length,start,end})
data.Events.map(item=>item["Serial Number"]===title?{...item}["Scheduled Sample Date"].push({"id":{...item}["Scheduled Sample Date"].length,start}):item)
}
else{
setEvents((prev) => [...prev, { start, end, title }]);
}
setToggleAdd(true);
console.log(data.Events)
success = 1;
}
if (newEvent.action === control.UPDATE) {
// find id of existing event to update
setEvents((currEvents) =>
currEvents.map((event) => {
if (event.id === newEvent.id) {
return {
...event,
start: start,
end: start,
title: title,
};
} else {
return event;
}
})
);
success = 1;
console.log(allEvents);
}
// if action successful, close modal and clear all fields
if (success === 1) {
setToggleAdd(false);
clearFields();
}
}
// handle when user select existing scope event
function handleSelectEvent(event) {
setMsg("Update Scope");
const start = new Date(event.start);
setNewEvent({
...newEvent,
action: control.UPDATE,
title: event.title,
start,
id: event.id,
});
setToggleAdd(true);
}
//handle calendar slot click
function handleClick(e) {
newEvent.start = e.start;
newEvent.action = control.ADD;
setMsg("Add New Scope");
if (
// ? Can be better written
document.getElementById("id01")?.classList.contains("setDisplay") === true
) {
setToggleAdd(true);
}
}
//handle modal close when user clicks on x
function toggleClick() {
if (toggleAdd === true) {
setToggleAdd(false);
clearFields();
}
}
//function to clear all fields
function clearFields() {
newEvent.start = "";
newEvent.title = "";
}
return (
<div className={`calendar-container`}>
<AddModal
id="id01"
toggle={toggleAdd}
newEvent={newEvent}
toggleClick={toggleClick}
handleSelectSlot={handleAddSlot}
setNewEvent={setNewEvent}
msg={msg}
/>
<Calendar
localizer={localizer}
events={allEvents}
popup
defaultView="month"
longPressThreshold={1}
views={["month"]}
eventPropGetter={(event) => {
const identifier = data.Events.find(
(item) => item["Type"] === event.type
);
const backgroundColor = identifier
? data.Color.find((item) => item[`${identifier.Type}`])[
`${identifier.Type}`
]
: "";
return { style: { backgroundColor } };
}}
onSelectEvent={handleSelectEvent}
onSelectSlot={(e) => handleClick(e)}
selectable
style={{ height: 500, margin: "30px" }}
components={{
toolbar: CustomToolbar,
}}
/>
<div className="calendar-sidebar">
<Button text="Add New" button="button" onClick={handleClick} />
<Log type='Schedule' />
</div>
</div>
);
}
I've attached the CalendarComponent. I can't really seem to find the problem in CalendarComponent myself. Hopefully it's not in the react big calendar package and I'm not missing something

data rendering issue after button is clicked in react

I am having a data rendering issue in react. Somehow, data is not automatically updated after it's updated in the server side. I can't put all the code in here, cuz the code is kind of lengthy. so i pasted/renamed some variables. Even if some variables are missing, please understand. Basically, I have a button on the page and when the button is clicked, the status changes to 'UPLOADING' and the function checkIfDataExists is called to fetch data from the server side and data should be automatically updated without page refresh, but when I test this, data is successfully retrieved from the server side, but the updated data is not rendered. I see 'successful...' on the Console. Is there anything wrong?
const Settings: React.FC<IProps> = props => {
const { orgId } = props
const password = 'dummy'
const { data } = httpCall(`/${orgId}/${userId}/settings`)
return (
<div>
{data && <SettingsForm data={data} password={password} {...props} />}
</div>
)
}
const SettingsForm: React.FC<Settings & IProps> = ({
data,
password
}) => {
const [status, setStatus] = useState<'ERROR' | 'DONE' | 'UPLOADING'>()
const service = getServiceInstance(data.organizationId)
function checkIfDataExists(user: any) {
return () => {
httpCall
.getClient(user.id)
.then(value => {
console.log('successful...')
data.modeUsername = value.modeUsername
data.modePassword = value.modePassword
})
.catch(() => {
setStatus('ERROR')
})
}
}
useEffect(() => {
if (!status) return
switch (status) {
case 'UPLOADING': {
const timer = setInterval(
checkIfDataExists({ id: data.id }),
2000
)
return () => clearInterval(timer)
}
}
}, [status, client
])
<div className="info-section">
<p className="detail">Username</p>
<p>{data.modeUsername}</p>
</div>
<div className="info-section">
<p className="detail">Password</p>
<p>{data.modePassword}</p>
</div>
The problem I see is that after you setInterval an API you didn't set in the state to trigger the component to rerender. You don't need to be explicit to define resData to data because if you define data already useState already it types.
const SettingsForm: React.FC<Settings & IProps> = ({
data,
password
}) => {
const [resdata,setResData] = useState(data)
const [status, setStatus] = useState<'ERROR' | 'DONE' | 'UPLOADING'>()
const service = getServiceInstance(data.organizationId)
function checkIfDataExists(user: any) {
return () => {
httpCall
.getClient(user.id)
.then(value => {
console.log('successful...')
setResData({
modeUsername: value.modeUsername,
modePassword: value.modePassword,
})
// data.modeUsername = value.modeUsername
// data.modePassword = value.modePassword
})
.catch(() => {
setStatus('ERROR')
})
}
}
useEffect(() => {
if (!status) return
switch (status) {
case 'UPLOADING': {
const timer = setInterval(
checkIfDataExists({ id: data.id }),
2000
)
return () => clearInterval(timer)
}
}
}, [status, client
])
<div className="info-section">
<p className="detail">Username</p>
<p>{resdata.modeUsername}</p>
</div>
<div className="info-section">
<p className="detail">Password</p>
<p>{resdata.modePassword}</p>
</div>

React , custom hook slow render

Hello I made a custom hook that goes hand in hand with a component for generic forms, however, I notice that it is slow when the state changes.
#customHook
export const useFormController = (meta) => {
const { setVisible, setLoading } = useContext(FeedBackContext);
const itemsRef = useRef([]);
const {
control,
setValue,
handleSubmit,
formState: { errors },
} = useForm<Partial<any>>({
mode: "onBlur",
shouldUnregister: true,
resolver: yupResolver(meta.validation),
});
const onRef = function (input) {
this.itemsRef.current[this.index] = input;
};
const onSubmit = (data: any) => {
if(meta.onSubmit){
meta.onSubmit(data);
}else{
setVisible(true);
setLoading(true);
meta.service.submit(data);
}
};
const isJsonEmpty = (val = {}) => {
return Object.keys(val).length == 0;
};
const onSubmitIditing = function () {
let index = ++this.index;
if (isJsonEmpty(errors) && this.end) {
handleSubmit(onSubmit)();
} else if (!this.end) {
this.itemsRef.current[index]._root.focus();
}
};
const setFields = (json) => {
const items = Object.keys(json);
const values = Object.values(json)
console.log('Cambiando fields en formControllser...', json)
for (let i = 0; i < items.length; i++) {
//console.log('Cambiando valores...', items[i], values[i])
setValue(items[i], values[i], { shouldValidate: true })
}
}
const getItems = () => {
console.log('Meta namess', meta.names, meta);
if (!meta && !meta.names) return [];
return meta.names.map(function (item, index) {
const isEnd =
meta.options && meta.options[item] && meta.options[item].end
? true
: false;
const isSecure =
meta.options && meta.options[item] && meta.options[item].secure
? true
: false;
const label = meta.alias ? meta.alias[item] : item;
const visible = meta.invisibles ? (meta.invisibles[item] ? false : true) : true;
const def = meta.defaults ? meta.defaults[item] : "";
const disabled = (val) => {
const b = meta.disableds ? (meta.disableds[item] ? true : false) : false;
return b;
}
return {
name: item,
label: label,
disabled: disabled,
onRef: onRef.bind({ itemsRef: itemsRef, index: index }),
onSubmitEditing: onSubmitIditing.bind({
itemsRef: itemsRef,
index: index,
end: isEnd,
errors: errors,
}),
visible: visible,
setFields,
defaultValue: def,
errors: errors,
secureTextEntry: isSecure,
styles: styles,
control: control,
options: meta.options[item] ? meta.options[item] : null,
};
});
}
const getData = useMemo(() => {
console.log('Get data calback v2', meta);
return {
handleSubmit,
items: getItems(),
onSubmit,
errors,
setFields
};
}, [meta])
return getData;
};
export const Client: React.FC<any> = React.memo(({ navigation, route }) => {
const {
alias,
defaults,
ubigeoSeleccionado,
setUbigeoSeleccionado,
editable,
inputLabel,
search,
getDisabled,
getInvisibles,
getAlias,
getDefaults,
disableds,
invisibles,
searchVisible,
idTypeDocument,
currentTypeDocument,
allTypeDocuments,
onValueChange,
onChangeText } = useContext(CreateClientContext);
const [mode, setMode] = useState(() => {
return route?.params?.mode;
})
const [client, setClient] = useState(() => {
return route?.params?.client;
})
const { dispatchClient } = useContext(GlobalContext);
const clientService = useClientService();
const ref = useRef(0);
const options = useMemo(() => {
return {
names: ["ane_numdoc", "ane_razsoc", "ane_alias", "ane_email", "ane_tel", "ane_tel2", "ane_dir"],
validation: clientValidation,
alias: alias,
defaults: defaults,
disableds: disableds,
service: {
submit: (data) => {
const parse = { ...data, ubigeo_id: ubigeoSeleccionado.ubi_id, ane_tipo_cp: 2, ane_tipdoc: currentTypeDocument.id }
if (mode == "update") {
//console.log('Actualizando...', client.id, parse);
clientService.updateById(client.id, parse)
.then(ok => {
Alert.alert('Actualizaciòn de cliente', "Cliente Actualizado")
dispatchClient({
type: 'create',
payload: ok
});
setTimeout(() => {
navigation.navigate('App', {
screen: "Clients"
})
}, 500)
}).catch(e => {
Alert.alert('Actualizaciòn de cliente', "No se pudo actualizar")
})
} else {
clientService.create(parse)
.then(ok => {
dispatchClient({
type: 'create',
payload: ok
});
Alert.alert('Cliente', "Cliente creado")
setTimeout(() => {
navigation.navigate('App', {
screen: "Clients"
})
}, 500)
})
.catch(e => {
(e);
Alert.alert('Error', "No se pudo crear el cliente")
})
}
}
},
invisibles: invisibles,
options: {
ane_dir: {
end: true
},
}
}
}, [getDisabled,
getInvisibles,
getAlias,
getDefaults])
const { items, handleSubmit, onSubmit, errors, setFields } = useFormController(options);
useEffect(() => {
ref.current++;
})
useEffect(() => {
if (route.params) {
console.log('Ref current', ref.current);
setMode(route.params.mode);
setClient(route.params.client);
}
}, [route.params])
useEffect(() => {
// console.log('Mode', mode, client.id);
if (mode == "update" && client) {
console.log('cambiando fields'), ref;
setFields(client)
}
}, [mode, client])
// useEffect(()=>{
// },[instanceDocument])
useEffect(() => {
console.log('Cambiando cliente...', mode, client);
console.log(ref.current);
}, [client])
useEffect(() => {
//Creación
console.log('set defaults..', ref.current);
if (Object.keys(defaults).length > 0) {
setFields(defaults)
}
}, [getDefaults])
console.log('Current', ref.current);
return (
<StyleProvider style={getTheme(material)}>
<Container style={{ backgroundColor: "#FAF9FE" }}>
<Content style={GlobalStyles.mainContainer}>
<Text style={GlobalStyles.subTitle}>Cliente</Text>
<PickerSearch
search={search}
editable={editable}
style={styles}
searchVisible={searchVisible}
placeholder={inputLabel}
pickerItems={allTypeDocuments}
onValueChange={onValueChange}
selectedValue={idTypeDocument}
onChangeText={onChangeText}
></PickerSearch>
<FormListController
// top={<Top />}
items={items}
style={GlobalStyles}
></FormListController>
<Bottom
ubigeoSeleccionado={ubigeoSeleccionado}
setUbigeoSeleccionado={setUbigeoSeleccionado}
onSubmit={handleSubmit(onSubmit)}
/>
</Content>
<AppFooter2 navigation={navigation} />
</Container>
</StyleProvider>
);
});
export const FormListController: React.FC<any> = React.memo(({ children, items = [], style, top = null, bottom = null }) => {
console.log('%c Form list controlllser...', "background-color:#ccc");
console.log('items', items)
return (
<>
<Form style={!!style.form ? style.form : style.formContainer}>
{top}
{items.map((item: any, index) => {
return <FormItemController {...item} key={index} />;
})}
{bottom}
</Form>
</>
);
});
export const FormItemController: React.FC<any> = React.memo((props: any) => {
console.log('Form item controller print', props)
if (props.visible) {
return (
<>
<Controller
control={props.control}
render={
({ field: { onChange, onBlur, value } }) => {
return (
<Item regular style={props.styles.item}>
<Label style={props.styles.label}>{props.label}</Label>
<Input
onBlur={onBlur}
disabled={props.disabled(value)}
onChangeText={(value) => onChange(value)}
secureTextEntry={props.secureTextEntry}
onSubmitEditing={props.onSubmitEditing}
value={value}
ref={props.onRef}
/>
</Item>
)
}
}
defaultValue={props.defaultValue}
name={props.name}
/>
{props.errors && props.errors[props.name] && (
<TextError value={props.errors[props.name].message} />
)}
{/* {props.options && props.options.errorEmpty && props.errors[""] && (
<TextError value={props.errors[""].message} />
)} */}
</>
);
}
else {
return <></>
}
});
I use the same component to create and edit a client, but when editing and viewing the FormItemController the logs time span is less than 1 second, however it is not rendered until after 8 or 10 seconds.
This is the output of my logs.
Update cliente... 500ms
set defaults.. 77
Client num render 77
Client num render 78
Client num render 79
Client num render 80
Client num render 81
Client num render 82
Client num render 83
Client num render 84
Client num render 85
Client num render 86
Client num render 87
Client num render 88
Client num render 89
Client num render 90
Client num render 91
Client num render 92
Client num render 93
Client num render 94
Client num render 95
Client num render 96
Client num render 97
Client num render 98
Client num render 99
Client num render 100 (6-8 seg)
the problem I have is when I edit, when I use the forms to create I have no problems, I do not find the bottleneck to improve and prevent it from being slow.
After trying several options, I realized that when passing the setValue, I was sending several nulls and objects that did not go with the form, filtering this data, made the final rendering pass from 8 seconds to less than 1 second
const setFields = (json) => {
const items = Object.keys(json);
const values = Object.values(json)
for (let i = 0; i < items.length; i++) {
if (!!values[i] && typeof values[i] != 'object') {
setValue(items[i], values[i])
}
}
}

React useState/setState Error: Not a function when multiple instances of compnent are on same page

Having a bit of trouble here with some useState hooks maybe you know how to get this working! Here is a quick synopsis of what I am trying to do...
I am making a LMS webpage that lets teachers design courses. Teachers can pick from a template and insert video/text/picture. They might pick a two column layout, or three column layout. They can mix and match the content types in the layouts. So potentially the teacher can pick a 3 column layout and put three videos in the template.
I need to make sure that the student watches every second of the video before moving on - and I am super close to getting this to work. So I store some state in the main course file (CourseDash.js) shown below. The useState hook I am having trouble with is const [ videosToWatch, setVidosToWatch ] = useState([]);.
Basically I am passing setVideosToWatch to my video components (also shown below). If a video appears in the template, the video component adds the url to the videosToWatch array. When a video finishes playing, I add the same information to watchedVideos in CourseDash.js. That way I can check to see which videos the student has watched and make sure they watch them before proceeding with the course.
It works fine and dandy when I render one(1) VideoContent component in the template. But when a teacher creates a course that has two different video components on one template... I get the error "setVideosToWatch is not a function". Why does it work when rendering one video component? Why not both? Thanks for your help Here is the code:
//CourseDash.js
import React, { useState, useEffect } from 'react';
import NavBar from '../Layout/NavBar';
import { useAuth0 } from '#auth0/auth0-react'
import Welcome from './Welcome'
import CourseContent from './CourseContent';
import { Button } from 'reactstrap'
import Finish from './Finish';
export default function CourseDash(props) {
const [ currentPanel, setCurrentPanel ] = useState('Welcome')
const { getAccessTokenSilently, user, logout } = useAuth0();
const [ navigation, setNavigation ] = useState()
const [ course, setCourse ] = useState({})
const [ customerInfo, setCustomerInfo ] = useState({})
const [ student, setStudent ] = useState({})
const [ selectedModule, setSelectedModule ] = useState({})
const [ clicked, setClicked ] = useState('')
const [ grade, setGrade ] = useState([])
const [ finalGrade, setFinalGrade ] = useState(0);
const [ allowedModules, setAllowedModules ] = useState([]);
const [ allowedNext, setAllowedNext ] = useState(true)
const [videosToWatch, setVideosToWatch ] = useState([])
const [ watchedVideos, setWatchedVideos ] = useState([])
useEffect(() => {
currentPanel !== 'Welcome' && setSelectedModule(course.modules.filter(mod => mod.id === currentPanel)[0])
currentPanel !== 'Welcome' && currentPanel !== 'Finish' && setClicked(course.modules.filter(mod => mod.id === currentPanel)[0].title)
}, [currentPanel])
useEffect(() => {
setAllowedNext(videosToWatch.every(vid => watchedVideos.includes(vid)))
}, [ watchedVideos, videosToWatch ])
const getCourseContent = async (_id) => {
try {
const token = await getAccessTokenSilently();
const response = await fetch(`/api/GetSingleCourse/${_id}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json; Charset=UTF-8"
}
})
const responseData = await response.json()
setCourse(responseData[0])
let tempNav = []
responseData[0].modules.forEach(mod => {
let navItem = {
buttonLink: mod.id,
buttonAlt: mod.title,
buttonType: 'module',
buttonName: mod.title,
}
tempNav.push(navItem)
})
setNavigation(tempNav)
} catch (error) {
console.log(error)
}
}
const getCustomerInfo = async () => {
try {
const token = await getAccessTokenSilently();
const response = await fetch(`/api/GetACustomer_id/${course.customerId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json; Charset=UTF-8",
},
})
const responseData = await response.json();
setCustomerInfo(responseData[0])
} catch (error) {
console.log(error)
}
}
const getStudentInfo = async () => {
try {
const token = await getAccessTokenSilently();
const response = await fetch(`/api/GetStudentByEmail/${user.name}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json; Charset=UTF-8",
}
})
const responseData = await response.json();
setStudent(responseData[0])
} catch (error) {
console.log(error)
}
}
useEffect(() => {
if(course.customerId){
getCustomerInfo()
}
if(course.modules){
let availablePoints = 0
let quizes = {}
course.modules.forEach(mod => {
if(mod.moduleType === 'quiz'){
quizes[mod.id] = {}
mod.quizContent.forEach(q => {
availablePoints += 1
quizes[mod.id][q.id] = 'studentAnswer'
})
}
})
quizes.pointTotal = availablePoints
setGrade(quizes)
}
}, [course])
useEffect(() => {
if(props.match.params.id){
getCourseContent(props.match.params.id)
}
getStudentInfo()
}, [props.match.params.id])
const display = (panel) => {
setCurrentPanel(panel)
setClicked(course.modules.filter(mod => mod.id === panel)[0].title)
}
if(!navigation){
return <div>Loading...</div>
}
const nextModule = () => {
currentPanel === 'Welcome' && setCurrentPanel(course.modules[0].id)
let indexOfModule = course.modules.findIndex(mod => mod.id === currentPanel)
currentPanel !== 'Welcome' && setCurrentPanel(course.modules[indexOfModule + 1].id)
}
const prevModule = () => {
let indexOfModule = course.modules.findIndex(mod => mod.id === currentPanel)
currentPanel !== 'Welcome' && indexOfModule !== 0 && (setCurrentPanel(course.modules[indexOfModule - 1].id))
}
const finishCourse = async () => {
let total = 0
course.modules.forEach(mod => {
if(mod.moduleType === 'quiz'){
mod.quizContent.forEach( ques => {
if(ques.answer === grade[mod.id][ques.id]){
total += 1
}
})
}
})
let fGrade = total/grade.pointTotal
setFinalGrade(fGrade)
try {
const token = await getAccessTokenSilently();
const response = await fetch(`/api/UpdateStudent/${student._id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json; Charset=UTF-8",
},
body: JSON.stringify({grades: [...student.grades.filter(g => g.course !== course._id), {course: course._id, grade: fGrade}]})
})
} catch (error) {
console.log(error)
}
setCurrentPanel('Finish')
}
const enableButtons = () => {
let indexOfCurrModule = course.modules.findIndex(mod => mod.id === currentPanel)
currentPanel === 'Welcome' && setAllowedModules(mods => [...mods, course.modules[0].title])
currentPanel !== 'Welcome' && currentPanel !== 'Finish' && indexOfCurrModule !== course.modules.length -1 && setAllowedModules(mods => [...mods, course.modules[indexOfCurrModule + 1].title])
indexOfCurrModule === course.modules.length - 1 && setAllowedModules([])
}
if(!student){
return <div className='d-flex w-100 h-100 align-self-center justify-content-center text-light'><h4 style={{
borderRadius: '10px',
backgroundColor: '#0F1D44',
padding: '2%'
}}>It seems like you have not been assigned this course...</h4></div>
}
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
maxWidth: '78%',
zIndex: '10'
}}>
<NavBar newButtons={navigation} display={display} clicked={clicked} allowedModules={allowedModules} />
<div className='w-100 h-100' >
<div className='m-4'>
<h1 className='text-light'>Welcome to {course.courseTitle}!</h1>
<span className='text-light'>For {student.name} at {customerInfo.business}.</span>
</div>
{currentPanel === 'Welcome' && <Welcome nextModule={nextModule} currentPanel={currentPanel} course={course} customerInfo={customerInfo} student={student} enableButtons={enableButtons} /> }
{currentPanel !== 'Welcome' && currentPanel !== 'Finish' && <CourseContent selectedModule={selectedModule} grade={grade} setGrade={setGrade} setAllowedNext={setAllowedNext} setVideosToWatch={setVideosToWatch} videosToWatch={videosToWatch} setWatchedVideos={setWatchedVideos} />}
{currentPanel === 'Finish' && <Finish finalGrade={finalGrade} course={course} customerInfo={customerInfo} student={student} /> }
<div className='w-100 m-4' style={{
display: currentPanel === 'Welcome' || currentPanel === 'Finish' ? 'none' : 'flex'
}}>
<Button onClick={prevModule} color='primary' size='md' alt='previous module' className='m-2' style={{width: '97%'}} disabled={course.modules.findIndex(mod => mod.id === currentPanel) === 0 || currentPanel === 'Welcome'}>←</Button>
<Button onClick={() => {
enableButtons();
nextModule()
}} color='primary' size='md' alt='next module'className='m-2' style={{width: '97%'}} disabled={course.modules.findIndex(mod => mod.id === currentPanel) === course.modules.length - 1 || !allowedNext}>→</Button>
<Button onClick={() => {
finishCourse()
enableButtons();
}} color='success' size='md' alt='next module'
disabled={!allowedNext}
className='m-3'
style={{width: '97%', display: currentPanel === course.modules[course.modules.length - 1].id ? 'block' : 'none'}} >Finish Course!</Button>
</div>
</div>
</div>
)
}
Here is my video content component where each video gets rendered.
//VideoContent.js
import React, { useEffect } from 'react'
export default function VideoContent(props) {
const { content, setAllowedNext, setVideosToWatch, videosToWatch, setWatchedVideos } = props
const checkVideoPlay = () => {
setVideosToWatch(vids => [...vids, content]);
let video = document.getElementById(content);
let timeStarted = -1;
let timePlayed = 0;
let duration = 0;
const getDuration = () => {
duration = video.duration;
document.getElementById("duration").appendChild(new Text(Math.round(duration)+""));
console.log("Duration: ", duration);
}
// If video metadata is laoded get duration
if(video.readyState > 0){
getDuration.call(video);
}
else{
//If metadata not loaded, use event to get it
video.addEventListener('loadedmetadata', getDuration);
}
// remember time user started the video
const videoStartedPlaying = () => {
timeStarted = new Date().getTime()/1000;
}
const videoStoppedPlaying = (event) => {
// Start time less then zero means stop event was fired vidout start event
if(timeStarted>0) {
var playedFor = new Date().getTime()/1000 - timeStarted;
timeStarted = -1;
// add the new number of seconds played
timePlayed+=playedFor;
}
document.getElementById("played").innerHTML = Math.round(timePlayed)+"";
// Count as complete only if end of video was reached
if(timePlayed>=duration && event.type=="ended") {
setWatchedVideos(vids => [...vids, content])
}
}
video.addEventListener("play", videoStartedPlaying);
video.addEventListener("playing", videoStartedPlaying);
video.addEventListener("ended", videoStoppedPlaying);
video.addEventListener("pause", videoStoppedPlaying);
}
useEffect(() => {
checkVideoPlay();
}, [content] )
return (
<div className='d-flex flex-column justify-content-center align-items-center m-2'
style={{
color: 'white',
}}>
<video id={content} src={content} style={{borderRadius: '5px', width: '100%'}} controls />
<div>
<span>Played </span>
<span id="played">0</span><span> seconds out of </span>
<span id="duration"></span><span> seconds. (only updates when the video pauses)</span>
</div>
</div>
)
}
Wow... react developer error. I forgot to drill the props through all the templates... I only did one template.
Each template used the same video content file... Answered!

Resources