I am updating the setStates I am using from class based components to functional ones. I have a few usecases where theres a closure. the code is
this.setState(
({ notifications }) => ({
notifications: [...notifications, notification],
}),
() =>
timeout !== undefined && window.setTimeout(closeNotification, timeout)
);
How do I use the useState hook to update this. I assume useState doesnt return a promise, so I cant use a .then. If thats the case, whats the best way to update this function?
Maybe something like so?
const [notifications, setNotifications] = useState([]);
const doSomething = () => {
setNotifications([ /* ... */ ]);
}
useEffect(() => {
let timer;
if(timeout !== undefined) {
timer = setTimeout(closeNotification, timeout);
}
return () => clearTimeout(timer)
}, [notifications]);
You can use the useState and the useEffect hooks to achieve the same result like so:
const [notifications, setNotifications] = useState([]);
useEffect(() => {
// Close Notifications Here
}, [notifications]);
// To update the state use the setNotifications function like so
setNotifications(prevState => [...prevState, newNotification]);
the useEffect hook will run whenever you update the notifications.
Related
useEvent solves the problem of reading latest props/state in a callback inside useEffect, but can't be used in production yet [Nov 22].
It's use case is also documented in beta docs as well
The problem
const SomeComponent = ({ prop1, ...}) => {
const [state, setState] = useState('initial')
useEffect(() => {
// inside the connect callback I want to refer to latest state and props and I dont want to reconnect on state change
// here event handler, depends on prop1 and state
const connection = createConnection(...)
connection.on('connect', () => {
// will refer to prop, state
// without these varaibles in depedency array
// this effect will not see the latest values
})
return () => {
connection.disconnect()
}
}, [])
useEffect depends on depends on prop1 and state, causing unnecessary reconnections.
Some patch work like solution using useRef
const SomeComponent = ({ prop1, ...}) => {
const [state, setState] = useState()
const someFunction = () => {
// use latest props1
// use latest state
}
const someFunctionRef = useRef()
someFunctionRef.current = someFunction;
useEffect(() => {
const someFunctionRefWrapper = () => {
someFunctionRef.current()
}
// need to read the latest props/state variables
// but not rerender when those values change
const connection = createConnection(...)
connection.on('connect', someFunctionRefWrapper)
return () => {
connection.disconnect()
}
}, [])
Right now useEvent can't be used in production, I am thinking of creating a custom hook to solve the problem
const usePoorMansUseEvent(callback) {
const itemRef = useRef();
itemRef.current = callback;
const stableReference = useCallback((..args) => {
itemRef.current(...args)
}, []);
return stableReference;
}
Is there any better approach, am I reinventing the wheel
You should be able to just make two useEffects, one for connecting / disconnecting and one for refreshing the callback.
If you just want a connection on mount:
const [connection] = useState(createConnection(...));
useEffect(() => {
return () => {
connection.disconnect()
}
}, [connection])
useEffect(() => {
connection.on('connect', someCallback)
return () => {
// Disconnect previous callback on change, idk the actual syntax
connection.off('connect', someCallback)
}
}, [connection, someCallback]
That follows the whole immutability principle.
I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.
i have five a snapshot listener in useEffect and i have another call api to get data from firestore and update state
but I am facing a problem is every initial mount all listener got called , my goal is i want to all listener called only when document changed
i tried with useRef it works but listener do not trigger
As you can see in the example below, onSnapshot is printed during the initial mounted
useEffect(() => {
if (isFirstMount.current) return;
someFirestoreAPICall.onSnapshot((snap) => {
//called every initial mount
});
someFirestoreAPICall.onSnapshot((snap) => {
//called every initial mount
});
}, []);
useEffect(() => {
if (isFirstMount.current) {
isFirstMount.current = false;
return;
}
}, []);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
let snap = await someFirestoreAPICall.get();
setData(snap.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
setLoading(false);
};
fetchData();
}, []);
Codesandbox
You can use a condition inside your useEffect block as you are doing, I think. But perhaps useState would be more appropriate here than useRef.
eg:
const [state, setState] = useState(null)
useEffect(()=>{
if (state) {
// do something
}
}, [state])
The useEffect will run on mount and every time you change the value of state, but code inside the condition will only run if you change the state to a truthy value.
I've got stuck with a problem of hook reusage when state and redux store should work together. I've simplified a code to show the problem.
There is a component where I want to use multiple hooks (for simplicity I re-use useMouseDown hook here):
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const plusSighRef = useRef<HTMLButtonElement>(null);
useMouseDown({
ref: plusSighRef,
onMouseDown: () => {
console.log('in first hook');
dispatch(increment());
}
});
useMouseDown({
ref: plusSighRef,
onMouseDown: () => { console.log('in second hook'); }
});
return <button ref={plusSighRef}>+</button>;
}
Each hook has inner state and has own callback on mouse down event:
const useMouseDown = ({ ref, onMouseDown }) => {
const [isClicked, setIsClicked] = useState(false);
useEffect(() => {
const element = ref.current;
const down = (e: MouseEvent) => {
setIsClicked(true);
onMouseDown(e);
}
element.addEventListener('mousedown', down);
return (): void => {
element.removeEventListener('mousedown', down);
};
}, [onMouseDown, ref]);
}
As a result a mousedown event in second hook is never triggered. The problem is that re-render occurs earlier than second hook is started.
I found some solutions but don't like both:
use something like setTimeout(() => dispatch(increment()), 0) inside the first hook mousedown prop. But it seems to be not obvious in terms of re-usage.
rewrite two hooks into one and manipulate with one "big" mousedown handler. But in that case a combined hook could be difficult for maintaining.
So I need a solution that allow to retain structure as is (I mean two separate hooks), but has second hook is working too. Could someone help how to get it?
Can't you just keep your logic in two different functions and execute it sequentially in the hook's onMouseDown function?
const executeFirstFlow = () => {
console.log('in first function');
dispatch(increment());
};
const executeSecondFlow = () => {
console.log('in second function');
};
useMouseDown({
ref: plusSighRef,
onMouseDown: () => {
executeFirstFlow();
executeSecondFlow();
}
});
I never find a solution for my useEffect probleme :
I'm useing firebase and create a listener (onSnapshot) on my database to get the last state of my object "Player" I can get when the states currentGroup and currentUser are available
const [currentGroup, setCurrentGroup] = useState(null)
const [currentUser, setCurrentUser] = useState(null)
const [currentPlayer, setCurrentPlayer] = useState(null)
const IDefineMyListener = () =>{
return firebase.doc(`group/${currentGroup.id}${users/${currentUser.id}/`)
.onSnpashot(snap =>{
//I get my snap because it changed
setCurrentPlayer(snap.data())
})
}
Juste above, i call a useEffect when the currentGroup and currentUser are available and (IMPORTANT) if I didn't already set the currentPlayer
useEffect(() => {
if (!currentGroup || !currentUser) return
if (!currentPlayer) {
let unsubscribe = IDefineMyListener()
return (() => {
unsubscribe()
})
}
},[currentGroup,currentUser])
As you can think, unsubscribe() is called even if the IDefineMyListener() is not redefined. In other words, when currentGroup or currentUser changes, this useEffect deleted my listener whereas I NEED IT.
How can i figure out ?!
PS :if I remove if (!currentPlayer), of course it works but will unlessly get my data
PS2 : If I remove the unsubscribe, my listener is called twice each time.
You can use useCallback hook to work around this.
First we'll define your listener using useCallback and give the dependency array the arguments as currentGroup and currentUser.
const IDefineMyListener = useCallback(event => {
return firebase.doc(`group/${currentGroup.id}${users/${currentUser.id}/`)
.onSnpashot(snap =>{
//I get my snap because it changed
setCurrentPlayer(snap.data())
})
}, [currentGroup, currentUser]);
And we will only use useEffect to register and deregister your listener.
useEffect(() => {
//subscribe the listener
IDefineMyListener()
return (() => {
//unsubscribe the listener here
unsubscribe()
})
}
},[])
Since we passed an [] to useEffect, it will only run once when the component is mounted. But we have already registered the callback. So your callback will run everytime the currentGroup or currentUser changes without deregistering your listener.
The problem was about my bad understanding of the unsubscribe() .
I didn't return anything in my useEffect, but save my unsusbcribe function to call it when i need.
let unsubscribe = null //I will save the unsubscription listener inside it
const myComponent = () => {
const [currentGroup, setCurrentGroup] = useState(null)
const [currentUser, setCurrentUser] = useState(null)
const [currentPlayer, setCurrentPlayer] = useState(null)
useEffect(() => {
if (!currentGroup || !currentUser) {
if(unsubscribe){
unsubscribe() // 2 - when I dont need of my listener, I call the unsubscription function
}
return
}
if (!currentPlayer && !unsubscribe) {
unsubscribe = IDefineMyListener() // 1 - I create my listener and save the unsubscription in a persistant variable
}
},[currentGroup,currentUser])
const IDefineMyListener = () =>{
return firebase.doc(`group/${currentGroup.id}${users/${currentUser.id}/`)
.onSnpashot(snap =>{
//I get my snap because it changed
setCurrentPlayer(snap.data())
})
}
...