I have a markets screen where I am using sockets to update the prices of cryptocurrencies in real time. The screen contains an infinite scroller, so when the user scrolls, more cryptocurrencies load and the coins being observed by the socket changes as well. However I am noticing as the coins list is increasing, the app becomes really slow and I cannot navigate to other screens or click anywhere quickly.
I have seen a few apps achieve this infinite-scroll-live-prices logic such as CoinGecko & CoinMarketCap.
Snippet of the relevant code:
const updatePriceOfCoins = (newPrices = {}, coins = []) => {
const updatedCoins = [...coins];
let wasUpdated = false;
for (let i = 0; i < updatedCoins.length; i++) {
let coin = updatedCoins[i];
if (newPrices[coin.id] !== undefined) {
updatedCoins[i] = { ...coin, priceUsd: newPrices[coin.id] };
wasUpdated = true;
}
}
return { wasUpdated, coins: updatedCoins };
};
const MarketsScreen = ({
markets,
getMarkets,
isLoading,
isLoadingMore,
perPage,
getMoreMarkets,
hasMore,
updateMarkets
}) => {
const socket = useLivePrices(markets);
const marketsRef = useRef(markets);
useEffect(() => {
marketsRef.current = markets;
}, [markets]);
const onNewPrices = (newPrices) => {
const { wasUpdated, coins: updatedMarkets } = updatePriceOfCoins(newPrices, marketsRef.current);
if (wasUpdated) {
updateMarkets(updatedMarkets);
}
};
useEffect(() => {
getMarkets();
}, []);
useEffect(() => {
if (socket !== null) {
socket.on("new prices", onNewPrices);
}
return () => {
if (socket !== null) {
socket.off("new prices");
}
};
}, [socket]);
return (
<FlatList
data={data}
renderItem={renderDataItem}
showsVerticalScrollIndicator={false}
onEndReached={getMoreMarkets}
onEndReachedThreshold={0.5}
/>
);
};
useLivePrices hook
const useLivePrices = (coinsToWatch = []) => {
const [socket, setSocket] = useState(null);
const prevCommaSepCoins = useRef("");
useEffect(() => {
//Only initialize socket once then everytime coinsToWatch is different
//update the coins observed
if (coinsToWatch.length > 0) {
if (socket === null) {
const commaSepCoins = coinsToCommaSepIDs(coinsToWatch);
setSocket(connectToLivePricesSocket(commaSepCoins));
prevCommaSepCoins.current = commaSepCoins;
} else {
const newCommaSepCoins = coinsToCommaSepIDs(coinsToWatch);
if (prevCommaSepCoins.current !== newCommaSepCoins) {
socket.emit("update coins", newCommaSepCoins);
prevCommaSepCoins.current = newCommaSepCoins;
}
}
}
}, [coinsToWatch]);
useEffect(() => {
let unsubFocus = () => {};
let unsubBlur = () => {};
if (socket !== null) {
//pause and resume prices based on if screen is in focus
unsubFocus = navigation.addListener("focus", resumePrices);
unsubBlur = navigation.addListener("blur", pausePrices);
}
return () => {
if (socket !== null) {
socket.disconnect();
unsubFocus();
unsubBlur();
}
};
}, [socket]);
return socket;
};
I want to achieve the infinite-scroll-live-prices but not sure how to optimize the performance anymore.
I tried optimizing the performance by reducing the number of renders when price updates. I have also tried to pause and resume the socket based on if the screen is focused so that state updates are not happening while the screen is not focused.
Related
Thanks in advance.
This is my question : It is Quiz website for couple.
Before partner finish quiz, It send get request to server every 5s.
but the problem is even partner's answers are set, setInterval never stops.
but if I refresh my website, It works well.
Can you please give me advise?
const postAnswers = useGetResults();
const postPartnerAnswers = useGetPartnerResults();
const [myResult, setMyResult] = useState<FinalAnswer | undefined>();
const [partnerResult, setPartnerResult] = useState<FinalAnswer | undefined>();
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState<boolean>(false);
const init = async () => {
try {
const email = localStorage.getItem('email');
const partnerEmail = localStorage.getItem('partnerEmail');
if (email !== undefined && partnerEmail !== undefined) {
// localStorage에 이메일 값들이 있으면,
const result = await postAnswers(email, partnerEmail);
const otherResult = await postPartnerAnswers(email, partnerEmail);
if (result.answers !== undefined && otherResult.answers !== undefined) {
// 몽고디비에서 받아온 값이 둘다 있으면
setMyResult(result);
setPartnerResult(otherResult);
} else {
// 몽고디비에서 받아온 값이 없으면
console.log(result.answers, otherResult.answers);
setIsLoading(true);
}
}
} catch (error) {
setErrorMessage('로딩하는 도중 에러가 발생했습니다');
console.error(error);
}
};
useEffect(() => {
init();
}, []);
useEffect(() => {
if (myResult !== undefined && partnerResult !== undefined) {
setIsLoading(false);
console.log('둘다 값이 있어요!');
console.log(isLoading);
}
}, [myResult, partnerResult]);
const timer = () => {
return setInterval(() => {
init();
console.log('isLoading', isLoading);
if (isLoading === false) {
console.log('clear');
clearInterval(timer());
}
}, 5000);
};
useEffect(() => {
if (isLoading === true) {
console.log('둘다 값이 없어요!');
timer();
}
if (isLoading === false) {
console.log('clear');
clearInterval(timer());
}
}, [isLoading]);
deployed website : https://www.couple-quiz.com/
Expanding on #Ethansocal comment:
Your code is calling clearInterval(timer()) which will create a new interval that it will immediately clear. It seems that you are confusing the API of removeEventListener and clearInterval.
clearInterval should be called with the identifier returned by setInterval.
I suggest getting rid of the timer function and rewriting your last useEffect to make it return a cleanup function when isLoading is true:
useEffect(() => {
if (isLoading) {
console.log('둘다 값이 없어요!');
const interval = setInterval(init, 5_000);
return () => { clearInterval(interval) };
} else {
console.log('clear');
}
}, [isLoading]);
Hi im working on a Chrome extension that adds badges to users, now the problem is i have to refresh the page everytime so the badges can load because there are client-side changes . How can i watch events change so i run the function on first time page load ?
I read something about input event listener or MutationObserver but im not sure how can i implement that , Any help would be much appreciated .
CODE:
function Foreground() {
let users = null;
let queries = [];
let userIds = [];
document.addEventListener("DOMContentLoaded", function(event) {});
window.addEventListener('load', () => {
fetchUsersAndQueries();
chrome.runtime.onMessage.addListener(async(message, sender, res) => {
if (message.users) {
fetchUsersAndQueries();
if (users) {
return res(users);
} else {
return res([]);
}
}
if (message.refresh) {
try {
assignBadges();
} catch (error) {
console.log(error.message);
}
}
return true;
});
let done = false;
setInterval(() => {
if (done) {
return;
}
if (users) {
done = true;
try {
assignBadges();
} catch (error) {
console.log(error.message);
}
}
}, 500);
});
async function fetchUsersAndQueries() {
userIds = await getAPIUserIds();
let isStop = false;
setInterval(() => {
if (isStop) {
return;
}
const parasiteContainer = document.querySelector('#parasite-container');
if (parasiteContainer) {
if (parasiteContainer.shadowRoot) {
try {
const roster1 = parasiteContainer.shadowRoot.querySelector(`[name="roster1"]`);
const roster2 = parasiteContainer.shadowRoot.querySelector(`[name="roster2"]`);
if (!roster1) {
return
};
if ([...roster1.children].length === 1) {
if (roster1.firstElementChild.children.length === 1) {
if (roster1.firstElementChild.firstElementChild.length === 1) {
const fTeam = [...roster1.firstElementChild.firstElementChild.firstElementChild.children].map(item => getUsername(item));
const sTeam = [...roster2.firstElementChild.firstElementChild.firstElementChild.children].map(item => getUsername(item));
users = fTeam.concat(sTeam).flat();
queries.push([...roster1.firstElementChild.firstElementChild.firstElementChild.children]);
queries.push([...roster2.firstElementChild.firstElementChild.firstElementChild.children]);
isStop = true;
} else {
const fTeam = [...roster1.firstElementChild.firstElementChild.children].map(item => getUsername(item));
const sTeam = [...roster2.firstElementChild.firstElementChild.children].map(item => getUsername(item));
users = fTeam.concat(sTeam).flat();
queries.push([...roster1.firstElementChild.firstElementChild.children]);
queries.push([...roster2.firstElementChild.firstElementChild.children]);
isStop = true;
}
} else {
const fTeam = [...roster1.firstElementChild.children].map(item => getUsername(item));
const sTeam = [...roster2.firstElementChild.children].map(item => getUsername(item));
users = fTeam.concat(sTeam).flat();
queries.push([...roster1.firstElementChild.children]);
queries.push([...roster2.firstElementChild.children]);
isStop = true;
}
}
queries.forEach(query => {
query.map(item => {
if (item.children.length > 1) {
[...item.children].map(child => {
// const container = child.querySelector('.sc-hCQDas'); // Classname might change in the future.
const container = child.firstElementChild ? .firstElementChild ? .firstElementChild ? .firstElementChild ? .children[2];
if (container) {
container.insertAdjacentHTML(
'beforeend',
createBadge(badgesResponse.data.exists, child.dataset.userId)
);
}
});
} else {
// const container = item.querySelector('.sc-hCQDas'); // Classname might change in the future.
const container = item.firstElementChild ? .firstElementChild ? .firstElementChild ? .children[0];
if (container) {
container.insertAdjacentHTML(
'beforeend',
createBadge(badgesResponse.data.exists, item.dataset.userId)
);
}
}
});
});
}
The problem I'm having is, that I have a useContext in which I provide all logged users. On the initial run of the app or when the users' log in the array gets populated with all the users that are currently on the server... Which works as expected. But I have also the functionality, that whenever the server "user-connected" event runs, the front-end should just push the user to the end of this array. And there lays the problem. From the backend, the right user is sent, but when I access the connectedUsers array, the array is empty... but it should be already populated.
UsersProvider.tsx
export const inState = {
connectedUsers: [],
addUser: (user: any) => {},
sortUsers: (user: any, socketID: string) => {},
setLoggedUsers: () => {},
};
export interface initState {
connectedUsers: any[];
addUser(user: any): void;
sortUsers(users: any, socketID: string): void;
setLoggedUsers: React.Dispatch < React.SetStateAction < any[] >> ;
}
const UsersContext = createContext < initState > (inState);
export const useUsers = () => {
return useContext(UsersContext);
};
const initUserProps = (user: any) => {
user.messages = [];
user.hasNewMessages = false;
};
export const UsersProvider = ({
children
}: Props) => {
const [connectedUsers, setLoggedUsers] = useState < any[] > ([]);
const addUser = (user: any) => {
console.log('add', connectedUsers);
// This is empty, but it should be already populated when next user connected.
};
const sortUsers = (users: any, socketUserID: string) => {
const usersCopy = users;
usersCopy.forEach((u: any) => {
for (let i = 0; i < usersCopy.length; i++) {
const existingUser = usersCopy[i];
if (existingUser.userID === u.userID) {
existingUser.connected = u.connected;
break;
}
}
u.self = u.userID === socketUserID;
initUserProps(u);
});
// put the current user first, and sort by username
let sorted = usersCopy.sort((a: any, b: any) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
setLoggedUsers([...sorted]);
};
return ( <
UsersContext.Provider value = {
{
connectedUsers,
setLoggedUsers,
addUser,
sortUsers
}
} >
{
children
} <
/UsersContext.Provider>
);
};
And the part of ChatBoard.tsx, you can find addUser function initiated whenever user-connected happens. I really don't know why the would array be empty, if it is populated on the first run with users event.
const ChatBoard = (props: Props) => {
const socket = useSocket();
const {
connectedUsers,
setLoggedUsers,
addUser,
sortUsers
} = useUsers();
useEffect(() => {
if (socket == null) return;
socket.on('users', (users) => {
console.log(users);
if (socket.userID) {
const socketID: string = socket ? .userID;
sortUsers(users, socketID);
}
});
socket.on('user-connected', (user: any) => {
console.log(user, 'this user connected!');
const connectingUser = user;
addUser(connectingUser);
});
socket.on('user-disconnected', (userID) => {
console.log('disconnected user');
const users = [...connectedUsers];
users.forEach((u) => {
if (u.userID === userID) {
u.connected = false;
setLoggedUsers([...users]);
}
});
});
return () => {
socket.off('users');
socket.off('user-connected');
};
}, [socket]);
CodeSandbox
So I have found the problem... so with React hooks sometimes a problem occurs called "Stale Closures", which means that React was picking up the old state (empty one, the one that was not yet populated and always returning that one.).
The solution to this problem, in my case is that when you use setState you use it with a callback. Like so, so you always get the latest state.
const addUser = (user: any) => {
setLoggedUsers((oldUsers) => {
const newUsers: any[] = [...oldUsers];
console.log(newUsers);
for (let i = 0; i < newUsers.length; i++) {
const existingUser = newUsers[i];
if (existingUser.userID === user.userID) {
existingUser.connected = true;
return newUsers;
}
}
initReactiveProperties(user);
newUsers.push(user);
return newUsers;
});
};
I want to perform same functionality on hardware button press as on doneClick. My app has some tags. There are tags on the screen whose state is maintained in the backend. Upon clicking them, their state toggles. When I change the state of one tag, I can see that it's really changed. But when I don't change the state the below line makes the tags array to empty
When I use this
const [allData, changeAllData] = useState([]);
My state is changed on single action. But when I don't select or unselect a tag and come back to previous screen, I see that no tags are selected and allData is just an empty array.
But when I use this
const [allData, changeAllData] = useState(props?.selectedLifeStyle)
My state is not changed on single action. I have to either select two tags, or unselect two tags, select or unselect same or different tags, only then my state changes.
Here is full piece of code:
const LifestyleScreen = (props) => {
const [searchUniqueValue, setSearchUniqueValue] = useState("");
const [searchValue, setSearchValue] = useState("");
const [choice, changeChoice] = useState([]);
// const [allData, changeAllData] = useState([]);
const [allData, changeAllData] = useState(props?.selectedLifeStyle);
const [limit, changeLimit] = useState(10000);
const [offset, changeOffset] = useState(0);
const [fetching, changeFetchingStatus] = useState(false);
const [searchArr, newSearchArr] = useState("");
useEffect(() => {
if (props.user !== null) {
changeFetchingStatus(true);
props.loginChange({ selectedLifeStyle: allData });
debugger;
changeAllData(props.selectedLifeStyle);
let params = {
limit,
offset,
};
getData(
getLifeStyleTags,
params,
(res) => {
console.log(res, "res in lifestyle tag");
changeFetchingStatus(false);
changeChoice(res.lifestyle_tags);
newSearchArr(res.lifestyle_tags);
},
(err) => {
changeFetchingStatus(false);
console.log(JSON.stringify(err, null, 2));
},
props.user ? props.user.api_key : masterKey
);
}
}, []);
const doneClick = () => {
if (allData.length === 0) {
showMessage({
message: "Select at least one lifestyle",
type: "warning",
});
} else if (allData.length > 10) {
showMessage({
message: "Select at most ten lifestyles",
type: "warning",
});
} else {
props.navigation.goBack(null);
props.loginChange({ selectedLifeStyle: allData });
}
};
const saveLifestyles = (item) => {
let counter = 0;
let arr = allData;
if (allData.length > 9) {
for (let i = 0; i < allData.length; i++) {
if (allData[i].id === item.id) {
arr.splice(i, 1);
changeAllData([...arr]);
counter = 1;
break;
}
props.loginChange({ selectedLifeStyle: allData });
if (allData.length == i + 1) {
showMessage({
message: "Only 10 lifestyle tags can be added",
type: "warning",
});
}
}
} else {
for (let i = 0; i < allData.length; i++) {
if (allData[i].id === item.id) {
arr.splice(i, 1);
changeAllData([...arr]);
counter = 1;
props.loginChange({ selectedLifeStyle: allData });
}
}
props.loginChange({ selectedLifeStyle: allData });
if (counter === 0) {
arr.push(item);
changeAllData([...arr]);
props.loginChange({ selectedLifeStyle: allData });
}
}
};
}
I am trying to implement feature to jump 15 seconds forward or backward in video.
I am facing hard time to set the update and set the current time.
const videoNode = useRef(null);
const [currentTime, setCurrentTime] = useState(null);
const handleTimeJump = (type) => {
const player = videojs(videoNode.current);
console.log(player)
if (player) {
type === 'inc' && setCurrentTime(player.currentTime() + 15);
player.currentTime() > 15 ? setCurrentTime(player.currentTime() - 15) : setCurrentTime(0)
}
};
useEffect(() => {
const player = videojs(
videoNode.current,
videoJsOptions,
function onPlayerReady() {
console.log('onPlayerReady');
player.on('timeupdate', () => {
setCurrentTime(player.currentTime());
});
},
);
if (!videoJsOptions.sources[0].src) {
console.log('no source found');
}
return () => {
if (player) {
player.dispose();
}
};
}, []);
useEffect(() => {
const player = videojs(videoNode.current)
player.currentTime(currentTime)
}, [currentTime])
handleTimeJump is called after clicking a button.
onClick={() => handleTimeJump('inc')}
Look I haven't tested if it works but looks like it should be player.setCurrentTime(currentTime) instead of player.currentTime(currentTime)
If that works then they should have thrown an error when calling currentTime with an argument because it is not supposed to take an argument (*cough* or you could use a statically typed language *cough*)
Also the currentTime state is already in the videojs-land there's no need to create another in React-land and keep them in sync. You're dispatching a react update EVERY SECOND. Here's a higly recommended and unsolicited refactor (keeping diff as less as possible):
const videoNode = useRef(null);
const playerRef = useRef(null);
const player = playerRef.current;
const handleTimeJump = (type) => {
if (player) {
type === 'inc' && player.setCurrentTime(player.currentTime() + 15);
player.currentTime() > 15 ? player.setCurrentTime(player.currentTime() - 15) : player.setCurrentTime(0)
}
};
useEffect(() => {
playerRef.current = videojs(
videoNode.current,
videoJsOptions
);
if (!videoJsOptions.sources[0].src) {
console.log('no source found');
}
return () => {
if (player) {
player.dispose();
}
};
}, []);