useEffect only on pageload AND page refresh - javascript

I have a functional component that should subscribe to events when the page loads and when the page refreshes and unsubscribe each time the component unmounts. I want to use useEffect for that, unfortunately not successfully. The first attempt removes eventlisteners and adds eventlisteners again each time a new event is passed:
import { useEffect } from 'react';
import { useSnackbar } from '../../context/SnackbarContext';
import { parseRigData } from '../data.utils';
import sse from '../../lib/sse';
export function useRig(config) {
const { openSnackbar } = useSnackbar();
useEffect(() => {
const createListeners = () => {
const newListeners = {};
config.forEach(instance => {
const { name, handleData, showMessage, type } = instance;
newListeners[name] = ({ data }) => {
// eslint-disable-next-line no-console
console.log('[RIG] received', JSON.parse(data, 0, 2));
const messageBody = parseRigData(data);
handleData(messageBody);
if (showMessage) {
const message = showMessage(messageBody);
if (message) openSnackbar(message, type);
}
};
});
return newListeners;
};
const initializeListeners = listeners => {
config.forEach(instance => {
const { events, name } = instance;
sse.listenForUserMessage(events, listeners[name]);
});
};
const removeListeners = listeners => {
config.forEach(instance => {
const { events, name } = instance;
sse.removeListener(events, listeners[name]);
});
};
const createConnection = async (events, listeners) => {
if (!events.length) return;
await sse.connect(events);
initializeListeners(listeners);
};
function getEvents() {
const result = [];
config.forEach(instance => {
const { events, config } = instance;
result.push({
eventTypes: events,
config
});
});
return result;
}
const events = getEvents();
const listeners = createListeners();
createConnection(events, listeners);
return () => {
removeListeners(listeners);
if (events.length) sse.disconnect(events);
};
}, [config, openSnackbar]);
}
I understand the behaviour - it happens each time the component updates. Thats why I tried to solve this problem by removing config and openSnackbar from the useEffect-array that defines what changes are being watched for the useEffect to take place:
export function useRig(config) {
const { openSnackbar } = useSnackbar();
useEffect(() => {
// ...same code
return () => {
removeListeners(listeners);
if (events.length) sse.disconnect(events);
};
}, []);
}
Unfortunately, this is not working when I reload the page with f5. In that case, the events are not passed ("canĀ“t perform a React state update on an unmounted component) and the listeners from the return cleanup are not removed. Otherwise, on a normal page load or leaving page it works great.
Is there any way how can I include page reload(but not componentUpdate) to perform useEffect?

as i can see there is a problem of calling the create listeners
you created the function inside the useEffect but not calling it ,
try to create it outside and call it on the useEffect like you did with removeListeners
useEffect(() => {
console.log("onMounte with useEffect");
const createListeners = () => {
console.log("createListeners called"); // this will not work ...
};
return () => {
console.log("unmount") // will be called when the component going to be unmounted
};
}, []);

Related

React: ES2020 dynamic import in useEffect blocks rendering

From what I understand, async code inside useEffect runs without blocking the rendering process. So if I have a component like this:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
const data = await response.json();
console.log("end");
};
getData();
}, []);
return null;
};
The console output is (in order):
begin
window loaded
end
However if I use ES2020 dynamic import inside the useEffect:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const data = await import("./data.json");
console.log("end");
};
getData();
}, []);
return null;
};
The output is (in order):
begin
end
window loaded
This is a problem because when data.json is very large, the browser hangs while importing the large file before React renders anything.
All the necessary and useful information is in Terry's comments. here is the implementation of what you want according to the comments:
First goal:
I would like to import the data after window has loaded for SEO reasons.
Second goal:
In my case the file I'm trying to dynamically import is actually a function that requires a large dataset. And I want to run this function whenever some state has changed so that's why I put it in a useEffect hook.
Let's do it
You can create a function to get the data as you created it as getData with useCallback hook to use it anywhere without issue.
import React, {useEffect, useState, useCallback} from 'react';
function App() {
const [data, setData] = useState({});
const [counter, setCounter] = useState(0);
const getData = useCallback(async () => {
try {
const result = await import('./data.json');
setData(result);
} catch (error) {
// do a proper action for failure case
console.log(error);
}
}, []);
useEffect(() => {
window.addEventListener('load', () => {
getData().then(() => console.log('data loaded successfully'));
});
return () => {
window.removeEventListener('load', () => {
console.log('page unmounted');
});
};
}, [getData]);
useEffect(() => {
if (counter) {
getData().then(() => console.log('re load data after the counter state get change'));
}
}, [getData, counter]);
return (
<div>
<button onClick={() => setCounter((prevState) => prevState + 1)}>Increase Counter</button>
</div>
);
}
export default App;
Explanation:
With component did mount, the event listener will load the data.json on 'load' event. So far, the first goal has been met.
I use a sample and simple counter to demonstrate change in the state and reload data.json scenario. Now, with every change on counter value, the getData function will call because the second useEffect has the counter in its array of dependencies. Now the second goal has been met.
Note: The if block in the second useEffect prevent the getData calling after the component did mount and first invoking of useEffect
Note: Don't forget to use the catch block for failure cases.
Note: I set the result of getData to the data state, you might do a different approach for this result, but the other logics are the same.

Dispatch and Listen Action with Hooks in React (without Redux)

I wanted to share actions through different places in the component tree like:
// in one component, listen an action
useListener("DELETE", (payload) => {
console.log("DELETE EVENT 1", payload);
});
// in another component, dispatch action
const dispatch = useDispatch();
dispatch("DELETE", payload);
I come up with such implementation :
const globalListener = {};
export const useListener = (actionName = "default", fn, dependencies = []) => {
const listenerRef = useRef(Symbol("listener"));
useEffect(() => {
const listener = listenerRef.current;
return () => {
delete globalListener[actionName][listener];
};
}, []);
useEffect(() => {
const listener = listenerRef.current;
if (!globalListener?.[actionName]) {
globalListener[actionName] = {};
}
globalListener[actionName][listener] = fn;
}, dependencies);
};
export const useAction = () => {
const dispatch = (actionName, payload) => {
const actionListener = globalListener?.[actionName];
if (!actionListener) {
return;
}
const keys = Reflect.ownKeys(actionListener);
keys.forEach((key) => {
actionListener[key]?.(payload);
});
};
return dispatch;
};
I would like to hear what kind of risks/flaws can such implementation bring. I think I could use useReducer's middleware for such usage but it would be overkill to use the whole useReducer code to do such implementation.
Also; what could be other approaches to solve this? With or without any extra library...

How do deal with React hooks returning stale event handlers

I'm trying to figure out how to deal with stale event handlers returned by a hook. First when the component is first rendered, it makes a an asynchronous request to api to fetch credentials. Then these credentials are used when pressing a submit button in a dialog to create a resource. The problem is the credentials for the dialog submit button click event handler are undefined even after the credentials have been fetched.
credentials.js
import { useEffect } from 'react';
import { api } from './api';
export const useCredentials = (setCredentials) => {
useEffect(() => {
const asyncGetCredentials = async () => {
const result = await api.getCredentials();
if (result) {
setCredentials(result);
}
};
asyncGetCredentials().then();
}, []);
return credentials;
}
useComponent.js
import { useEffect, useRef, useCallback, useState } from 'react';
import { useCredentials } from './credentials';
import { createResource } from './resources';
import { useDialog } from './useDialog';
export const useComponent = () => {
const { closeDialog } = useDialog();
const [credentials, setCredentials] = useState();
useCredentials(setCredentials);
const credentialsRef = useRef(credentials);
useEffect(() => {
// logs credentials properly after they have been fetched
console.log(credentials)
credentialsRef.current = credentials;
}, [credentials]);
const createResourceUsingCredentials = useCallback(
async function () {
// credentials and credentialsRef.current are both undefined
// even when the function is called after the credentials
// have already been fetched.
console.log(credentials);
console.log(credentialsRef.current);
createResource(credentialsRef.current);
}, [credentials, credentialsRef, credentialsRef.current]
);
const onDialogSubmit = useCallback(
async function () {
await createResourceUsingCredentials();
closeDialog();
}, [
credentials,
credentialsRef,
credentialsRef.current,
createResourceUsingCredentials,
],
);
return {
onDialogSubmit,
}
}
Try this way
export const useCredentials = (setCredentials) => {
useEffect(() => {
const asyncGetCredentials = async () => {
const result = await api.getCredentials();
if (result) {
setCredentials(result);
}
};
asyncGetCredentials().then();
}, []);
}
export const useComponent = () => {
const { closeDialog } = useDialog();
const [credentials, setCredentials] = useState(); // new add
useCredentials(setCredentials);
....
}
Why are you adding complexity, always return function and check inside the function for credentials
export const useComponent = () => {
const { closeDialog } = useDialog();
const credentials = useCredentials();
// correctly logs undefined at first and updated credentials
// when they are asynchronously received from the api.
console.log(credentials);
async function createResourceUsingCredentials() {
createResource(credentials);
}
let onClickDialogSubmit = async () => {
if (credentials) {
await createResourceUsingCredentials();
closeDialog();
}
};
return {
onClickDialogSubmit,
}
}
I found the problem was in the useCredentials hook implementation. It blocks any further requests for credentials to the api if a request is already in flight. Due to poor implementation of this functionality, if more than 1 component using that hook was rendered, only the component that was rendered first got updated credentials. I changed the useCredentials hooks so that it subscribes to the global state (that has the credentials) so that no matter which component starts the request, all components will get the credentials when the request finishes. https://simbathesailor007.medium.com/debug-your-reactjs-hooks-with-ease-159691843c3a helped a lot with debugging this issue.

How to prevent a state update on a react onClick function (outside useEffect)?

When I use useEffect I can prevent the state update of an unmounted component by nullifying a variable like this
useEffect(() => {
const alive = {state: true}
//...
if (!alive.state) return
//...
return () => (alive.state = false)
}
But how to do this when I'm on a function called in a button click (and outside useEffect)?
For example, this code doesn't work
export const MyComp = () => {
const alive = { state: true}
useEffect(() => {
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return
setSomeState('hey')
// warning, because alive.state is true here,
// ... not the same variable that the useEffect one
}
}
or this one
export const MyComp = () => {
const alive = {}
useEffect(() => {
alive.state = true
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return // alive.state is undefined so it returns
setSomeState('hey')
}
}
When a component re-renders, it will garbage collect the variables of the current context, unless they are state-full. If you want to persist a value across renders, but don't want to trigger a re-renders when you update it, use the useRef hook.
https://reactjs.org/docs/hooks-reference.html#useref
export const MyComp = () => {
const alive = useRef(false)
useEffect(() => {
alive.current = true
return () => (alive.current = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.current) return
setSomeState('hey')
}
}

React Hooks Idle Timeout for Functional Component

I have an application that requires an idle timeout that first warns the user that they will be logged out in one minute, then logs the user out after the one minute has expired.
I have had success using a class component as demonstrated in the following post:
Session timeout warning modal using react
I am moving the code for my application over to React Hooks but I'm having a hard time moving this code over. I have tried the following:
const [signoutTime, setSignoutTime] = useState(0);
let warnTimeout;
let logoutTimeout;
const setTimeout = () => {
warnTimeout = setTimeout(warn, warningTime);
logoutTimeout = setTimeout(logout, signoutTime);
};
const clearTimeout = () => {
if (warnTimeout) clearTimeout(warnTimeout);
if (logoutTimeout) clearTimeout(logoutTimeout);
};
useEffect(() => {
setWarningTime(10000);
setSignoutTime(15000);
const events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
const resetTimeout = () => {
clearTimeout();
setTimeout();
};
for (var i in events) {
window.addEventListener(events[i], resetTimeout);
}
setTimeout();
});
const warn = () => {
console.log('Warning');
};
const destroy = () => {
console.log('Session destroyed');
};
In the end, I would like for a modal to appear to warn the user of impending logout. If the user moves the mouse, clicks, etc (see events) then the timer resets. If the user clicks on the button in the modal the timer is reset.
Thanks so much for the help!
Try this
mport React, { useEffect, useState } from 'react';
const LogoutPopup = () => {
const [signoutTime, setSignoutTime] = useState(10000);
const [warningTime, setWarningTime] = useState(15000);
let warnTimeout;
let logoutTimeout;
const warn = () => {
console.log('Warning');
};
const logout = () => {
console.log('You have been loged out');
}
const destroy = () => {
console.log('Session destroyed');
}
const setTimeouts = () => {
warnTimeout = setTimeout(warn, warningTime);
logoutTimeout = setTimeout(logout, signoutTime);
};
const clearTimeouts = () => {
if (warnTimeout) clearTimeout(warnTimeout);
if (logoutTimeout) clearTimeout(logoutTimeout);
};
useEffect(() => {
const events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
const resetTimeout = () => {
clearTimeouts();
setTimeouts();
};
for (let i in events) {
window.addEventListener(events[i], resetTimeout);
}
setTimeouts();
return () => {
for(let i in events){
window.removeEventListener(events[i], resetTimeout);
clearTimeouts();
}
}
},[]);
return <div></div>
}
export default LogoutPopup;
Based on the first answer, I have another hooks solution when you also need the time until the user gets logged out.
Codesandbox Demo
useAutoLogout.js
import React, { useEffect, useState } from "react";
const useLogout = (startTime) => {
const [timer, setTimer] = useState(startTime);
useEffect(() => {
const myInterval = setInterval(() => {
if (timer > 0) {
setTimer(timer - 1);
}
}, 1000);
const resetTimeout = () => {
setTimer(startTime);
};
const events = [
"load",
"mousemove",
"mousedown",
"click",
"scroll",
"keypress"
];
for (let i in events) {
window.addEventListener(events[i], resetTimeout);
}
return () => {
clearInterval(myInterval);
for (let i in events) {
window.removeEventListener(events[i], resetTimeout);
}
};
});
return timer;
};
export default useLogout;
App.js
import useAutoLogout from "./useAutoLogout";
function App() {
const timer = useAutoLogout(10);
if (timer == 0) {
return <div>Logged Out</div>;
}
if (timer < 8) {
return <div>In {timer} seconds you will be automatically logged out</div>;
}
return <div>Signed in</div>;
}
export default App;
I rewrote your answer a bit to get rid of liniting errors, I thought it would also be handy to extract it into a hook so then you just need to pass in the parameters you want, that eliminates the need to have the states for the timers.
The useCallback I suppose is for efficiency to stop it working itself out on each render,
I've moved some of the other stuff inside the UseEffect to get rid of those linting errors!
import React, { useCallback } from 'react'
function useMonitor({ timeout, warningTimeout, onWarn, onTimeOut }) {
const warn = useCallback(()=> {
onWarn && onWarn()
},[onWarn])
const logout = useCallback(()=> {
onTimeOut && onTimeOut()
},[onTimeOut])
React.useEffect(() => {
let warnTimeout;
let logoutTimeout;
const setTimeouts = () => {
warnTimeout = setTimeout(warn, warningTimeout);
logoutTimeout = setTimeout(logout, timeout);
};
const clearTimeouts = () => {
if (warnTimeout) clearTimeout(warnTimeout);
if (logoutTimeout) clearTimeout(logoutTimeout);
};
const events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
const resetTimeout = () => {
clearTimeouts();
setTimeouts();
};
for (let i in events) {
window.addEventListener(events[i], resetTimeout);
}
setTimeouts();
return () => {
for (let i in events) {
window.removeEventListener(events[i], resetTimeout);
clearTimeouts();
}
}
}, [logout, timeout, warn, warningTimeout]);
}
export default useMonitor

Categories

Resources