I have an issue with react won't update ui, I am trying to update the number of people connected in the same room there's no issue in the backend, my issue is on the front because I saw that the events are reaching the client through chrome dev tools.
as shown below the event is indeed reaching the client.
import React, { useContext, useEffect, useState, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { SocketContext } from '../context/socket';
type Props = {};
export default function Game({}: Props) {
const socket = useContext(SocketContext);
const { id } = useParams();
const [playerCount, setPlayerCount] = useState(0);
const updatePlayerCount = (...args: string[]) => {
console.log(args);
setPlayerCount(args.length);
};
useEffect(() => {
socket.emit('join_room', id);
socket.on('game_result', gameHandler);
socket.on('player_count', updatePlayerCount);
return () => {
socket.off('game_result');
socket.off('player_count');
};
}, []);
const gameHandler = (...args: any) => {
console.log(args);
};
return (
<div>
Game {id}
<div>{playerCount}</div>
</div>
);
}
checking the console I do see my console.log firing...
however the first join event does work cause I don't see 0 I see 1 instead. playerCount = 0 initially
You can try defining both your event handlers inside your useEffect() like this
useEffect(() => {
const updatePlayerCount = (...args: string[]) => {
console.log(args);
setPlayerCount(args.length);
};
const gameHandler = (...args: any) => {
console.log(args);
};
socket.emit('join_room', id);
socket.on('game_result', gameHandler);
socket.on('player_count', updatePlayerCount);
return () => {
socket.off('game_result');
socket.off('player_count');
};
}, [id]);
apparently I didn't pay attention that the data emitted was an array embedded inside an array
a simple fix was
setPlayerCount(args[0].length);
Related
I have a simple React component that registers a new listener in IPC on mounting and should remove the listener when unmounting. The problem is that my listener reference for some reason doesn't match the reference that was used to register it and I have no idea why.
React component simplified code
import React, { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAppDispatch } from '../../redux/storeHooks';
import { setAuthToken } from './state/authSlice';
import { LocationAuthState } from '../common/RequireAuth/types';
export default () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const navigatedFrom = useNavigatedFrom();
useEffect(() => {
const userLoggedInHandler = (_: Electron.IpcRendererEvent, loginToken: string) => {
dispatch(setAuthToken({ token: loginToken }));
navigate(navigatedFrom, { replace: true });
};
const ref1 = userLoggedInHandler;
window.ipc.startLoginListenerHttpServer();
window.ipc.userLoggedIn(userLoggedInHandler);
return () => {
const theSame = ref1 === userLoggedInHandler;
console.log(theSame); // this logs true
window.ipc.stopLoginListenerHttpServer();
window.ipc.removeUserLoggedInListener(userLoggedInHandler);
};
}, []);
return (
<div>
{`Waiting for login result...`}
</div>
);
};
function useNavigatedFrom() {
const location = useLocation();
const locationState = location.state as LocationAuthState;
return locationState?.from?.pathname || `/`;
}
preload.js
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
import IpcEvent from '../ipcEvent';
let originalHandlerReference: any;
contextBridge.exposeInMainWorld(`ipc`, {
startLoginListenerHttpServer: () => ipcRenderer.send(IpcEvent.StartLoginListenerHttpServer),
stopLoginListenerHttpServer: () => ipcRenderer.send(IpcEvent.StopLoginListenerHttpServer),
userLoggedIn: (handler: (event: IpcRendererEvent, loginToken: string) => void) => {
originalHandlerReference = handler;
ipcRenderer.on(IpcEvent.UserLoggedIn, handler);
},
removeUserLoggedInListener: (handler: (event: IpcRendererEvent, loginToken: string) => void) => {
const countBefore = ipcRenderer.listenerCount(IpcEvent.UserLoggedIn); // 1
const theSame = originalHandlerReference === handler; // false
const theSameAsOriginal = originalHandlerReference === ipcRenderer.listeners(IpcEvent.UserLoggedIn)[0]; // true
const theSameAsCurrentHandler = handler === ipcRenderer.listeners(IpcEvent.UserLoggedIn)[0]; // false
ipcRenderer.off(IpcEvent.UserLoggedIn, handler);
const countAfter = ipcRenderer.listenerCount(IpcEvent.UserLoggedIn); // 1
},
});
Taking a look at the Electron documentation on contextBridge, we can see that the communication between the "normal" renderer process and the isolated part executing the preload script is copied both ways. This means that no two objects passed between the renderer and the preload context (and vice versa) will be exactly the same.
Thus, you cannot use === to check if the two functions passed to your preload script are equal. I would suggest generating an ID for each registered listener, passing it to the calling code and storing it in a map alongside the actual function. That way, the component could call your "unmount" function providing the ID it was given when registering its listener, the preload script would lookup the handler function for the given ID and remove it from IPC channels.
import { useEffect } from "react";
const GoogleTranslate = () => {
useEffect(() => {
let addScript = document.createElement("script");
addScript.setAttribute(
"src",
"//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"
);
document.body.appendChild(addScript);
window.googleTranslateElementInit = googleTranslateElementInit;
}, []);
const googleTranslateElementInit = () => {
return new window.google.translate.TranslateElement(
{
pageLanguage: "en",
layout: google.translate.TranslateElement.InlineLayout.BUTTON,
},
"google_translate_element"
);
};
return <div id="google_translate_element"></div>;
};
export default GoogleTranslate;
Here is my Component. I have used in a single page. But instead of once I am getting two separate instance of google translate button.
So, I want to know how to render only once this button.
Here is the UI image
Double Google translate button bug
useEffect may run twice despite an empty dependency array in multiple circumstances (for example, if a parent component is re-rendering, or if you have React StrictMode enabled).
Hence, you are probably creating 2 elements inside useEffect.
You could analyze your code and check for potential unwanted re-renders or simply replace useEffect with this useEffectOnce hook:
import { useEffect, useRef, useState } from 'react';
export const useEffectOnce = (effect: () => void | (() => void)) => {
const effectFn = useRef<() => void | (() => void)>(effect);
const destroyFn = useRef<void | (() => void)>();
const effectCalled = useRef(false);
const rendered = useRef(false);
const [, setVal] = useState<number>(0);
if (effectCalled.current) {
rendered.current = true;
}
useEffect(() => {
if (!effectCalled.current) {
destroyFn.current = effectFn.current();
effectCalled.current = true;
}
setVal(val => val + 1);
return () => {
if (!rendered.current) {
return;
}
if (destroyFn.current) {
destroyFn.current();
}
};
}, []);
};
Just create a file with this code, and import and use it instead of useEffect.
Im getting this error mostly while shifting components. Can anyone help me out. I have tried the abortController and mount state solution but none of them worked
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at FeedVideo
import { FC, useEffect, useState } from 'react';
import ReactPlayer from 'react-player';
import { FeedVideoProps } from '../../utils/types/interfaces';
import { getDecodedMedia } from '../../utils/helpers/getS3URL';
const FeedVideo: FC<FeedVideoProps> = ({ s3Key }: FeedVideoProps) => {
const [videoLink, setVideoLink] = useState<string>('');
const [isPlaying, setIsPlaying] = useState<boolean>(true);
const getURL = async (key: string) => {
const link: string | object = await getDecodedMedia(key);
setVideoLink(link as string);
};
useEffect(() => {
const abortController = new AbortController();
getURL(s3Key).then(() => abortController.abort());
return () => {
abortController.abort();
};
}, [s3Key]);
return (
<ReactPlayer
stopOnUnmount={true}
className={'react-player'}
controls={true}
url={videoLink}
playing={isPlaying}
/>
);
};
export default FeedVideo;
here is the code of the component that im using.
I wouldn't worry much about it, since it's just a warning that React gives you when some state update happens after unmount. In your case, it is happening because getUrl is async and after resolving it sets videoLink.
I'm saying that you shouldn't worry too much because what is happening is that your component is unmounting and then one small operation is "leaking" (which is setVideoLink(link as string)).
Memory leaks are worrisome when you're handling event listeners or intervals.
Even so, if you're worried about this behavior or if you're just too bothered with the warning you could create a logic that checks if your component is mounted before triggering the setState call:
import { FC, useEffect, useState } from 'react';
import ReactPlayer from 'react-player';
import { FeedVideoProps } from '../../utils/types/interfaces';
import { getDecodedMedia } from '../../utils/helpers/getS3URL';
const FeedVideo: FC<FeedVideoProps> = ({ s3Key }: FeedVideoProps) => {
const [videoLink, setVideoLink] = useState<string>('');
const [isPlaying, setIsPlaying] = useState<boolean>(true);
const getURL = async (key: string, isMounted: boolean) => {
const link: string | object = await getDecodedMedia(key);
if (isMounted) setVideoLink(link as string);
};
useEffect(() => {
let isMounted = true;
const abortController = new AbortController();
getURL(s3Key, isMounted).then(() => abortController.abort());
return () => {
abortController.abort();
isMounted = false;
};
}, [s3Key]);
return (
<ReactPlayer
stopOnUnmount={true}
className={'react-player'}
controls={true}
url={videoLink}
playing={isPlaying}
/>
);
};
export default FeedVideo;
const getURL = async (key: string): Promise<string> => {
try {
const link: string | object = await getDecodedMedia(key);
return (link as string) || '';
} catch (e) {
throw new Error(e);
}
};
useEffect(() => {
let feedVideoMounted = true;
getURL(s3Key).then((link) => {
if (feedVideoMounted && link) {
setVideoLink(link);
}
});
return () => {
feedVideoMounted = false;
};
}, [s3Key]);
So, i fixed the issue by changing the logic to this and it worked!
Besides prop value updates in a hook, I need to bind to events that get triggered in the hook too. So the consumer of the hook can bind to the event-like addEventListner, removeEventListener. How do I do this?
What I have so far:
import {useState, useEffect} from 'react';
interface MyHookProps {
name: string;
onChange: () => void;
}
const useNameHook = () : MyHookProps => {
const [name, setName] = useState<string>('Anakin');
const onChange = () => {
}
useEffect(() => {
setTimeout(() => {
setName('Vader');
// how to a raise an onChange event here that consumers could bind to?
}, 1000);
}, []);
return {
name,
onChange,
}
}
export default function App() {
const {name, onChange} = useNameHook();
const handleHookChange = () => {
console.info('hook changed', name);
}
return (
<div className="App">
<h1>Hello {name}</h1>
</div>
);
}
I think you can refer to the 'Declarative' pattern here.
Reading this article about 'Making setInterval Declarative with React Hooks' from Dan Abramov really changed my ways of thinking about the React hooks.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
So, my attempts to make this useName hook declarative is like below:
// hooks/useName.ts
import { useEffect, useRef, useState } from "react";
type Callback = (name: string) => void;
const useName: (callback: Callback, active: boolean) => string = (
callback,
active
) => {
// "Listener"
const savedCallbackRef = useRef<Callback>();
// keep the listener fresh
useEffect(() => {
savedCallbackRef.current = callback;
}, [callback]);
// name state
const [internalState, setInternalState] = useState("anakin");
// change the name after 1 sec
useEffect(() => {
const timeoutID = setTimeout(() => {
setInternalState("vader");
}, 1000);
return () => clearTimeout(timeoutID);
}, []);
// react to the 'name change event'
useEffect(() => {
if (active) {
savedCallbackRef.current?.(internalState);
}
}, [active, internalState]);
return internalState;
};
export default useName;
and you can use this hook like this:
// App.ts
import useName from "./hooks/useName";
function App() {
const name = useName(state => {
console.log(`in name change event, ${state}`);
}, true);
return <p>{name}</p>;
}
export default App;
Note that the 'callback' runs even with the initial value ('anakin' in this case), and if you want to avoid it you may refer to this thread in SO:
Make React useEffect hook not run on initial render
I've read that using multiple instances of Pusher is not recommended and therefore I am using React's context feature.
import React, { useContext, useEffect, useRef } from "react";
import Pusher from "pusher-js";
const PusherContext = React.createContext<Pusher | undefined>(undefined);
export const usePusher = () => useContext(PusherContext)
export const PusherProvider: React.FC = (props) => {
const clientRef = useRef<Pusher>();
useEffect(() => {
if (!clientRef.current) {
clientRef.current = new Pusher('pusher_key', {
cluster: 'pusher_cluster',
authEndpoint: 'auth_endpoint',
})
}
}, [clientRef]);
return (
<PusherContext.Provider value={clientRef?.current}>
{props.children}
</PusherContext.Provider>
)
}
Our pusher instance is of type Pusher or undefined. When we call usePusher it is always undefined.
import { usePusher } from "./services/pusher";
const Chat: React.FC = (props) => {
const pusher = usePusher()
useEffect(() => {
if (pusher) {
console.log("pusher is initialized/defined")
} else {
// it is always undefined
console.log("pusher is not initialized/defined")
}
}, [pusher])
return (
<div>
Test component
</div>
)
}
Where is the problem?
It seems to work. I was just doing something wrong when wrapping the provider. Hope it helps in case someone have similar problem!