React-Native Making setInterval Declarative with React Hooks - reactjs

I'm playing with React Hooks for more than a few hours, I'm probably ran into an intriguing problem: using setInterval just doesn’t work as I'd expect with react-native
function Counter() {
const [time, setTime] = useState(0);
const r = useRef(null);
r.current = { time, setTime };
useEffect(() => {
const id = setInterval(() => {
console.log("called");
r.current.setTime(r.current.time + 1);
}, 1000);
return () => {
console.log("cleared");
clearInterval(id);
};
}, [time]);
return <Text>{time}</Text>;
}
The code above should clearInterval every time that time state changes
It works fine on ReactJS but on React-native I'm getting an error says "Callback() it's not a function"
enter image description here
It's working as expected in Reactjs
https://codesandbox.io/s/z69z66kjyx
"dependencies": {
"react": "16.8.3",
"react-native": "^0.59.6",
...}
Update:
I tried to use the ref like this example but still getting same error
const [time, setTime] = useState(0);
useInterval(() => {
setTime(time +1);
});
return (<Text>{time}</Text>);
}
function useInterval(callback) {
const savedCallback = useRef();
// Remember the latest function.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
let id = setInterval(()=>savedCallback.current(), delay);
return () => clearInterval(id);
});
}

Because you are mutating the DOM via a DOM node refs and the DOM mutation will change the appearance of the DOM node between the time that's rendered and your effects mutates it. then you don't need to use useEffect you will want to use useLayoutEffect
useLayoutEffect this runs synchronously immediately after React has performed all the DOM mutations.
import React, {useState, useLayoutEffect,useRef} from 'react';
import { Text} from 'react-native';
const [time, setTime] = useState(0);
useInterval(() => {
setTime(time +1);
});
return (<Text>{time}</Text>);
}
function useInterval(callback) {
const savedCallback = useRef();
// Remember the latest function.
useLayoutEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useLayoutEffect(() => {
let id = setInterval(()=>{
console.log('called');
return savedCallback.current();
}, delay);
return () => {
console.log('cleared');
return clearInterval(id);
}
});
}
if you just using useEffect and getting this error
Uncaught TypeError: callback is not a function
at flushFirstCallback (scheduler.development.js:348)
at flushWork (scheduler.development.js:441)
at MessagePort.channel.port1.onmessage (scheduler.development.js:188)
This is a bug in RN because of wrong scheduler version, Unfortunately RN didn't have an explicit dependency on scheduler version by mistak. Dan Abramov already fixed this bug on scheduler version "0.14.0"
To solve the problem just run the following command
npm install scheduler#0.14.0 --save
Or Try adding "scheduler": "0.14.0" to your package.json in dependencies and re-running your package manager

You should be able to still use the state hook variables in the effect hook as they are in scope.
useRef: mutations here aren't tracked, so they don't trigger re-renders.
CodeSandbox Counter Example
I feel that using refs in the way you're trying to is more verbose than just using the state and setter directly. The useRef ref is intended for mutable values over time, but you already get that with the useState hook. The ref works because you're not really mutating the ref, but instead are simply overwriting it each render cycle with the contents of the useState hook that are updated.
I've updated my sandbox to use useRef as the way you have, your useEffect hook was causing your cleanup function to fire on every render, so removed the dependency. You'll notice now you only see "called" until you refresh.

Related

Why does useEffect run the first time even when it is listening to a prop?

I admit I will probably never understand hooks but is this assertion correct that a prop based useEffect should only run when the prop changes? In the following code both useEffects run the first time comp is rendered. Why?
function Comp() {
const [search, setSearch] = useState();
useEffect(() => {
console.log("this should run once on comp load")
}, [])
useEffect(() => {
console.log("this should only run if search prop changes")
}, [search])
return (
<div>
</div>
);
}
Sample Code
It seem that you misundersood how hooks work.
The official hook effect documentation states:
Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. ...
So all the useEffect hooks will run on first render. By adding search dependency to your hook, you only stated that the hook should additionally run on each change of search.
If you want to disable the functionality of a hook effect on the first render then you can construct a custom hook which utilises useRef hook to conditionally block the initial run of the useEffect:
const useDidUpdate = (callback, dependencies) => {
const didMountRef = useRef(false);
useEffect(() => { // block first call of the hook and forward each consecutive one
if (didMountRef.current) {
callback();
} else {
didMountRef.current = true;
}
}, dependencies);
};
Then you would call it like:
useDidUpdate(() => {
console.log("this should only run if search prop changes")
}, [ search ]);

React setState with callback in functional components

I have a very simple example I wrote in a class component:
setErrorMessage(msg) {
this.setState({error_message: msg}, () => {
setTimeout(() => {
this.setState({error_message: ''})
}, 5000);
});
}
So here I call the setState() method and give it a callback as a second argument.
I wonder if I can do this inside a functional component with the useState hook.
As I know you can not pass a callback to the setState function of this hook. And when I use the useEffect hook - it ends up in an infinite loop:
So I guess - this functionality is not included into functional components?
The callback functionality isn't available in react-hooks, but you can write a simple get around using useEffect and useRef.
const [errorMessage, setErrorMessage] = useState('')
const isChanged = useRef(false);
useEffect(() => {
if(errorMessage) { // Add an existential condition so that useEffect doesn't run for empty message on first rendering
setTimeout(() => {
setErrorMessage('');
}, 5000);
}
}, [isChanged.current]); // Now the mutation will not run unless a re-render happens but setErrorMessage does create a re-render
const addErrorMessage = (msg) => {
setErrorMessage(msg);
isChanged.current = !isChanged.current; // intentionally trigger a change
}
The above example is considering the fact that you might want to set errorMessage from somewhere else too where you wouldn't want to reset it. If however you want to reset the message everytime you setErrorMessage, you can simply write a normal useEffect like
useEffect(() => {
if(errorMessage !== ""){ // This check is very important, without it there will be an infinite loop
setTimeout(() => {
setErrorMessage('');
}, 5000);
}
}, [errorMessage])

React: Trying to rewrite ComponentDidUpdate(prevProps) with react hook useEffect, but it fires when the app starts

I'm using a componentDidUpdate function
componentDidUpdate(prevProps){
if(prevProps.value !== this.props.users){
ipcRenderer.send('userList:store',this.props.users);
}
to this
const users = useSelector(state => state.reddit.users)
useEffect(() => {
console.log('users changed')
console.log({users})
}, [users]);
but it I get the message 'users changed' when I start the app. But the user state HAS NOT changed at all
Yep, that's how useEffect works. It runs after every render by default. If you supply an array as a second parameter, it will run on the first render, but then skip subsequent renders if the specified values have not changed. There is no built in way to skip the first render, since that's a pretty rare case.
If you need the code to have no effect on the very first render, you're going to need to do some extra work. You can use useRef to create a mutable variable, and change it to indicate once the first render is complete. For example:
const isFirstRender = useRef(true);
const users = useSelector(state => state.reddit.users);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
console.log('users changed')
console.log({users})
}
}, [users]);
If you find yourself doing this a lot, you could create a custom hook so you can reuse it easier. Something like this:
const useUpdateEffect = (callback, dependencies) => {
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
return callback();
}
}, dependencies);
}
// to be used like:
const users = useSelector(state => state.reddit.users);
useUpdateEffect(() => {
console.log('users changed')
console.log({users})
}, [users]);
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate, and
componentWillUnmount combined.
As from: Using the Effect Hook
This, it will be invoked as the component is painted in your DOM, which is likely to be closer to componentDidMount.

Using React Hooks, why are my event handlers firing with the incorrect state?

I'm trying to create a copy of this spinning div example using react hooks. https://codesandbox.io/s/XDjY28XoV
Here's my code so far
import React, { useState, useEffect, useCallback } from 'react';
const App = () => {
const [box, setBox] = useState(null);
const [isActive, setIsActive] = useState(false);
const [angle, setAngle] = useState(0);
const [startAngle, setStartAngle] = useState(0);
const [currentAngle, setCurrentAngle] = useState(0);
const [boxCenterPoint, setBoxCenterPoint] = useState({});
const setBoxCallback = useCallback(node => {
if (node !== null) {
setBox(node)
}
}, [])
// to avoid unwanted behaviour, deselect all text
const deselectAll = () => {
if (document.selection) {
document.selection.empty();
} else if (window.getSelection) {
window.getSelection().removeAllRanges();
}
}
// method to get the positionof the pointer event relative to the center of the box
const getPositionFromCenter = e => {
const fromBoxCenter = {
x: e.clientX - boxCenterPoint.x,
y: -(e.clientY - boxCenterPoint.y)
};
return fromBoxCenter;
}
const mouseDownHandler = e => {
e.stopPropagation();
const fromBoxCenter = getPositionFromCenter(e);
const newStartAngle =
90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
setStartAngle(newStartAngle);
setIsActive(true);
}
const mouseUpHandler = e => {
deselectAll();
e.stopPropagation();
if (isActive) {
const newCurrentAngle = currentAngle + (angle - startAngle);
setIsActive(false);
setCurrentAngle(newCurrentAngle);
}
}
const mouseMoveHandler = e => {
if (isActive) {
const fromBoxCenter = getPositionFromCenter(e);
const newAngle =
90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
box.style.transform =
"rotate(" +
(currentAngle + (newAngle - (startAngle ? startAngle : 0))) +
"deg)";
setAngle(newAngle)
}
}
useEffect(() => {
if (box) {
const boxPosition = box.getBoundingClientRect();
// get the current center point
const boxCenterX = boxPosition.left + boxPosition.width / 2;
const boxCenterY = boxPosition.top + boxPosition.height / 2;
// update the state
setBoxCenterPoint({ x: boxCenterX, y: boxCenterY });
}
// in case the event ends outside the box
window.onmouseup = mouseUpHandler;
window.onmousemove = mouseMoveHandler;
}, [ box ])
return (
<div className="box-container">
<div
className="box"
onMouseDown={mouseDownHandler}
onMouseUp={mouseUpHandler}
ref={setBoxCallback}
>
Rotate
</div>
</div>
);
}
export default App;
Currently mouseMoveHandler is called with a state of isActive = false even though the state is actually true. How can I get this event handler to fire with the correct state?
Also, the console is logging the warning:
React Hook useEffect has missing dependencies: 'mouseMoveHandler' and 'mouseUpHandler'. Either include them or remove the dependency array react-hooks/exhaustive-deps
Why do I have to include component methods in the useEffect dependency array? I've never had to do this for other simpler component using React Hooks.
Thank you
The Problem
Why is isActive false?
const mouseMoveHandler = e => {
if(isActive) {
// ...
}
};
(Note for convenience I'm only talking about mouseMoveHandler, but everything here applies to mouseUpHandler as well)
When the above code runs, a function instance is created, which pulls in the isActive variable via function closure. That variable is a constant, so if isActive is false when the function is defined, then it's always going to be false as long that function instance exists.
useEffect also takes a function, and that function has a constant reference to your moveMouseHandler function instance - so as long as that useEffect callback exists, it references a copy of moveMouseHandler where isActive is false.
When isActive changes, the component rerenders, and a new instance of moveMouseHandler will be created in which isActive is true. However, useEffect only reruns its function if the dependencies have changed - in this case, the dependencies ([box]) have not changed, so the useEffect does not re-run and the version of moveMouseHandler where isActive is false is still attached to the window, regardless of the current state.
This is why the "exhaustive-deps" hook is warning you about useEffect - some of its dependencies can change, without causing the hook to rerun and update those dependencies.
Fixing it
Since the hook indirectly depends on isActive, you could fix this by adding isActive to the deps array for useEffect:
// Works, but not the best solution
useEffect(() => {
//...
}, [box, isActive])
However, this isn't very clean: if you change mouseMoveHandler so that it depends on more state, you'll have the same bug, unless you remember to come and add it to the deps array as well. (Also the linter won't like this)
The useEffect function indirectly depends on isActive because it directly depends on mouseMoveHandler; so instead you can add that to the dependencies:
useEffect(() => {
//...
}, [box, mouseMoveHandler])
With this change, the useEffect will re-run with new versions of mouseMoveHandler which means it'll respect isActive. However it's going to run too often - it'll run every time mouseMoveHandler becomes a new function instance... which is every single render, since a new function is created every render.
We don't really need to create a new function every render, only when isActive has changed: React provides the useCallback hook for that use-case. You can define your mouseMoveHandler as
const mouseMoveHandler = useCallback(e => {
if(isActive) {
// ...
}
}, [isActive])
and now a new function instance is only created when isActive changes, which will then trigger useEffect to run at the appropriate moment, and you can change the definition of mouseMoveHandler (e.g. adding more state) without breaking your useEffect hook.
This likely still introduces a problem with your useEffect hook: it's going to rerun every time isActive changes, which means it'll set the box center point every time isActive changes, which is probably unwanted. You should split your effect into two separate effects to avoid this issue:
useEffect(() => {
// update box center
}, [box])
useEffect(() => {
// expose window methods
}, [mouseMoveHandler, mouseUpHandler]);
End Result
Ultimately your code should look like this:
const mouseMoveHandler = useCallback(e => {
/* ... */
}, [isActive]);
const mouseUpHandler = useCallback(e => {
/* ... */
}, [isActive]);
useEffect(() => {
/* update box center */
}, [box]);
useEffect(() => {
/* expose callback methods */
}, [mouseUpHandler, mouseMoveHandler])
More info:
Dan Abramov, one of the React authors goes into a lot more detail in his Complete Guide to useEffect blogpost.
React Hooks useState+useEffect+event gives stale state.
seems you are having similar problems. basic issue is that "it gets its value from the closure where it was defined"
try that Solution 2 "Use a ref". in your scenario
Add below useRef, and useEffect
let refIsActive = useRef(isActive);
useEffect(() => {
refIsActive.current = isActive;
});
then inside mouseMoveHandler , use that ref
const mouseMoveHandler = (e) => {
console.log('isActive',refIsActive.current);
if (refIsActive.current) {
I have created an NPM module to solve it https://www.npmjs.com/package/react-usestateref and it seems that it may help and answer your question how to fire the current state.
It's a combination of useState and useRef, and let you get the last value like a ref
Example of use:
const [isActive, setIsActive,isActiveRef] = useStateRef(false);
console.log(isActiveRef.current)
More info:
https://www.npmjs.com/package/react-usestateref

With useEffect, how can I skip applying an effect upon the initial render?

With React's new Effect Hooks, I can tell React to skip applying an effect if certain values haven't changed between re-renders - Example from React's docs:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
But the example above applies the effect upon initial render, and upon subsequent re-renders where count has changed. How can I tell React to skip the effect on the initial render?
As the guide states,
The Effect Hook, useEffect, adds the ability to perform side effects from a function component. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes, but unified into a single API.
In this example from the guide it's expected that count is 0 only on initial render:
const [count, setCount] = useState(0);
So it will work as componentDidUpdate with additional check:
useEffect(() => {
if (count)
document.title = `You clicked ${count} times`;
}, [count]);
This is basically how custom hook that can be used instead of useEffect may work:
function useDidUpdateEffect(fn, inputs) {
const didMountRef = useRef(false);
useEffect(() => {
if (didMountRef.current) {
return fn();
}
didMountRef.current = true;
}, inputs);
}
Credits go to #Tholle for suggesting useRef instead of setState.
Here's a custom hook that just provides a boolean flag to indicate whether the current render is the first render (when the component was mounted). It's about the same as some of the other answers but you can use the flag in a useEffect or the render function or anywhere else in the component you want. Maybe someone can propose a better name.
import { useRef, useEffect } from 'react';
export const useIsMount = () => {
const isMountRef = useRef(true);
useEffect(() => {
isMountRef.current = false;
}, []);
return isMountRef.current;
};
You can use it like:
import React, { useEffect } from 'react';
import { useIsMount } from './useIsMount';
const MyComponent = () => {
const isMount = useIsMount();
useEffect(() => {
if (isMount) {
console.log('First Render');
} else {
console.log('Subsequent Render');
}
});
return isMount ? <p>First Render</p> : <p>Subsequent Render</p>;
};
And here's a test for it if you're interested:
import { renderHook } from '#testing-library/react-hooks';
import { useIsMount } from '../useIsMount';
describe('useIsMount', () => {
it('should be true on first render and false after', () => {
const { result, rerender } = renderHook(() => useIsMount());
expect(result.current).toEqual(true);
rerender();
expect(result.current).toEqual(false);
rerender();
expect(result.current).toEqual(false);
});
});
Our use case was to hide animated elements if the initial props indicate they should be hidden. On later renders if the props changed, we did want the elements to animate out.
I found a solution that is more simple and has no need to use another hook, but it has drawbacks.
useEffect(() => {
// skip initial render
return () => {
// do something with dependency
}
}, [dependency])
This is just an example that there are others ways of doing it if your case is very simple.
The drawback of doing this is that you can't have a cleanup effect and will only execute when the dependency array changes the second time.
This isn't recommended to use and you should use what the other answers are saying, but I only added this here so people know that there is more than one way of doing this.
Edit:
Just to make it more clear, you shouldn't use this approach to solving the problem in the question (skipping the initial render), this is only for teaching purpose that shows you can do the same thing in different ways.
If you need to skip the initial render, please use the approach on other answers.
I use a regular state variable instead of a ref.
// Initializing didMount as false
const [didMount, setDidMount] = useState(false)
// Setting didMount to true upon mounting
useEffect(() => { setDidMount(true) }, [])
// Now that we have a variable that tells us wether or not the component has
// mounted we can change the behavior of the other effect based on that
const [count, setCount] = useState(0)
useEffect(() => {
if (didMount) document.title = `You clicked ${count} times`
}, [count])
We can refactor the didMount logic as a custom hook like this.
function useDidMount() {
const [didMount, setDidMount] = useState(false)
useEffect(() => { setDidMount(true) }, [])
return didMount
}
Finally, we can use it in our component like this.
const didMount = useDidMount()
const [count, setCount] = useState(0)
useEffect(() => {
if (didMount) document.title = `You clicked ${count} times`
}, [count])
UPDATE Using useRef hook to avoid the extra rerender (Thanks to #TomEsterez for the suggestion)
This time our custom hook returns a function returning our ref's current value. U can use the ref directly too, but I like this better.
function useDidMount() {
const mountRef = useRef(false);
useEffect(() => { mountRef.current = true }, []);
return () => mountRef.current;
}
Usage
const MyComponent = () => {
const didMount = useDidMount();
useEffect(() => {
if (didMount()) // do something
else // do something else
})
return (
<div>something</div>
);
}
On a side note, I've never had to use this hook and there are probably better ways to handle this which would be more aligned with the React programming model.
Let me introduce to you react-use.
npm install react-use
Wanna run:
only after first render? -------> useUpdateEffect
only once? -------> useEffectOnce
check is it first mount? -------> useFirstMountState
Want to run effect with deep compare, shallow compare or throttle? and much more here.
Don't want to install a library? Check the code & copy. (maybe a star for the good folks there too)
Best thing is one less thing for you to maintain.
A TypeScript and CRA friendly hook, replace it with useEffect, this hook works like useEffect but won't be triggered while the first render happens.
import * as React from 'react'
export const useLazyEffect:typeof React.useEffect = (cb, dep) => {
const initializeRef = React.useRef<boolean>(false)
React.useEffect((...args) => {
if (initializeRef.current) {
cb(...args)
} else {
initializeRef.current = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dep)
}
Here is my implementation based on Estus Flask's answer written in Typescript. It also supports cleanup callback.
import { DependencyList, EffectCallback, useEffect, useRef } from 'react';
export function useDidUpdateEffect(
effect: EffectCallback,
deps?: DependencyList
) {
// a flag to check if the component did mount (first render's passed)
// it's unrelated to the rendering process so we don't useState here
const didMountRef = useRef(false);
// effect callback runs when the dependency array changes, it also runs
// after the component mounted for the first time.
useEffect(() => {
// if so, mark the component as mounted and skip the first effect call
if (!didMountRef.current) {
didMountRef.current = true;
} else {
// subsequent useEffect callback invocations will execute the effect as normal
return effect();
}
}, deps);
}
Live Demo
The live demo below demonstrates the different between useEffect and useDidUpdateEffect hooks
I was going to comment on the currently accepted answer, but ran out of space!
Firstly, it's important to move away from thinking in terms of lifecycle events when using functional components. Think in terms of prop/state changes. I had a similar situation where I only wanted a particular useEffect function to fire when a particular prop (parentValue in my case) changes from its initial state. So, I created a ref that was based on its initial value:
const parentValueRef = useRef(parentValue);
and then included the following at the start of the useEffect fn:
if (parentValue === parentValueRef.current) return;
parentValueRef.current = parentValue;
(Basically, don't run the effect if parentValue hasn't changed. Update the ref if it has changed, ready for the next check, and continue to run the effect)
So, although other solutions suggested will solve the particular use-case you've provided, it will help in the long run to change how you think in relation to functional components.
Think of them as primarily rendering a component based on some props.
If you genuinely need some local state, then useState will provide that, but don't assume your problem will be solved by storing local state.
If you have some code that will alter your props during a render, this 'side-effect' needs to be wrapped in a useEffect, but the purpose of this is to have a clean render that isn't affected by something changing as it's rendering. The useEffect hook will be run after the render has completed and, as you've pointed out, it's run with every render - unless the second parameter is used to supply a list of props/states to identify what changed items will cause it to be run subsequent times.
Good luck on your journey to Functional Components / Hooks! Sometimes it's necessary to unlearn something to get to grips with a new way of doing things :)
This is an excellent primer: https://overreacted.io/a-complete-guide-to-useeffect/
Below solution is similar to above, just a little cleaner way i prefer.
const [isMount, setIsMount] = useState(true);
useEffect(()=>{
if(isMount){
setIsMount(false);
return;
}
//Do anything here for 2nd render onwards
}, [args])
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Example:
useEffectAfterMount(() => {
document.title = `You clicked ${count} times`;
}, [count])

Resources