Is this useEffect() unecessarily expensive? - reactjs

This is a very simple version of my code:
const MyComponent = (props)=>{
const [randVar, setRandVar] = useState(null);
const randomFunction = ()=>{
console.log(randVar, props);
};
useCustomHook = ()=>{
useEffect(()=>{
document.addEventListener('keydown', randomFunction);
return ()=>{
document.removeEventListener('keydown', randomFunction);
}
}, [props, randVar]);
}
useCustomHook();
...
};
I want randomFunction to log accurate values for randVar and props (i.e. logs update when those variables change values), but I'm concerned that adding an event listener and then dismounting it every time they change is really inefficient.
Is there another way to get randomFunction to log updated values without adding props and randVar as dependencies in useEffect?

A few things, your useEffect does not need to be in that custom hook at all... It should probably look like this:
const MyComponent = (props)=>{
const [randVar, setRandVar] = useState(null);
const randomFunction = useCallback(()=>{
console.log(randVar, props);
}, [props, randVar]);
useEffect(()=>{
document.addEventListener('keydown', randomFunction);
return ()=>{
document.removeEventListener('keydown', randomFunction);
}
}, [randomFunction]);
...
};
useCallback will keep your function from being redefined on every render, and is the correct dependency for that useEffect as well. The only thing bad about the performance ehre is that you are logging props so it needs to be in the dependency array of the useCallback and since it is an object that may get redefined a lot, it will cause your useCallback to get redefined on nearly every render, which will then cause your useEffect to be fired on nearly every render.
My only suggestion there would be to separate your logging of props from where you log changes to randVar.

I don't think you need randVar as a dependency and if you had ESLint, it would tell you the same since you never acutally reference randVar in the effect.
If you don't want the function to get rebuilt over and over, you need to either memoize it or useRef. Unfortunately, once it's a ref it's not reactive. Maybe I'm overthinking this, but you could have a ref that you update in an effect with the new value and print out the value of that ref in the callback you pass to randFunction. See my example below.
I don't like how I did this, but it doesn't remake the function. I'm trying to think how I could do it better, but I think this works how you want.
const {
useRef,
useState,
useEffect
} = React;
const useCustomHook = (fn) => {
useEffect(() => {
document.addEventListener("keydown", fn);
return () => {
document.removeEventListener("keydown", fn);
};
}, [fn]);
};
const MyComponent = (props) => {
const [randVar, setRandVar] = useState("");
const randVarRef = useRef("");
useEffect(() => {
randVarRef.current = randVar;
}, [randVar]);
const randFn = useRef(() => {
console.log(randVarRef.current);
});
useCustomHook(randFn.current);
return React.createElement("input", {
type: "text",
value: randVar,
onChange: e => {
setRandVar(e.target.value);
}
});
};
ReactDOM.render(
React.createElement(MyComponent),
document.getElementById("app")
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="app"></div>

Related

Create `setTimeout` Loop In React Without Cyclic Dependencies

I have the following situation and don't know what's the best way to approach it. I want a component with two states, playing and items, when playing is set to true, it should add a new item to items every second, where the new item depends on the content in items so far. So my naive approach would be the following:
function App() {
const [playing, setPlaying] = useState(false);
const [items, setItems] = useState([]);
const addItem = useCallback(
function () {
/* adding new item */
},
[items]
);
useEffect(
function () {
let timeout;
playing &&
(function loop() {
timeout = window.setTimeout(loop, 1000);
addItem();
})();
return function () {
window.clearTimeout(timeout);
};
},
[addItem, playing]
);
/* render the items */
}
(I could use setInterval here, but I want to add another state later on, to change the interval while the loop is running, for this setTimeout works better.)
The problem here is that the effect depends on addItem and addItem depends on items, so as soon as playing switches to true, the effect will be caught in an infinite loop (adding a new item, then restarting itself immediately because items has changed). What's the best way to avoid this?
One possibility would be using a ref pointing to items, then have an effect only updating the ref whenever items changes, and using the ref inside addItem, but that doesn't seem like the React way of thinking.
Another possibility is to not use items in addItem but only setItems and using a callback to get access to the current items value. But this method fails when addItem manipulates more than a single state (a situation I've encountered before).
Implements functional approach to setting state, while defining the function to invoke the same within useState to remove dependency. ultimately allows setInterval to be used which feels more natural for this case.
import * as React from "react";
import { useState, useEffect, useCallback } from "react";
import { render } from "react-dom";
function App() {
const [playing, setPlaying] = useState(true);
const [items, setItems] = useState([1]);
useEffect(
function () {
const addItem = () => (
setItems((arr) => [...arr, arr[arr.length - 1] + 1])
);
setTimeout(() => setPlaying(false), 10000)
let interval: any;
if (playing) {
(function() {
interval = window.setInterval(addItem, 1000);
})();
}
return function () {
clearInterval(interval);
};
},
[playing]
);
return (
<div>
{items.map((item) => (<div key={item}>{item}</div>))}
</div>
)
}
render(<App />, document.getElementById("root"));

React linter suggesting to wrap constant array with useMemo inside useEffect react-hooks/exhaustive-deps warning

React linter is giving a warning:
The 'someUnchangingArray' array makes the dependencies of useEffect Hook (at line 42) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'someUnchangingArray' in its own useMemo() Hook react-hooks/exhaustive-deps
Why? someUnchangingArray is a constant and wont change, why is React linter suggesting that it will trigger the useEffect on every render?
export default function Component(
const [initialized, setInitialized] = useState(false);
const someUnchangingArray = ["1", ""];
useEffect(() => {
if (!initialized) {
// do some iniitializing
console.log(someUnchangingArray, initialized);
setInitialized(true);
}
}, [someUnchangingArray, initialized]);
...
v-dom rerender basically means it will rerun all your component(function)
hence you will create new array reference for every render
simply move the constant outside
const someUnchangingArray = ["1", ""];
export default function Component(
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (!initialized) {
// do some iniitializing
console.log(someUnchangingArray, initialized);
setInitialized(true);
}
}, [someUnchangingArray, initialized]);
...
or for whatever reason it need to be inside(rely on your state), then you need the useMemo hook
Your component is a function that is called on every render.
It means that on every render someUnchangingArray will point to a newly created reference of an array.
useEffect will receive a new reference and will decide that it's time to be called.
function render() {
const someUnchangingArray = ["1", ""];
return someUnchangingArray
}
console.log(render() === render()); // false
Here is an illustration:
function Component() {
const [counter, setCounter] = React.useState(0);
const supposedlySomeUnchangingArray = ["1", ""];
React.useEffect(() => {
console.count('USE EFFECT HAS BEEN CALLED');
}, [supposedlySomeUnchangingArray]);
return (
<div>
{counter}
<button onClick={() => setCounter(counter + 1)}>do something to rerender</button>
</div>
);
}
ReactDOM.render(<Component />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root">
</div>
Check the console and you will see that useEffect is called on each rerender.

Function never uses updated state

In the sample component below, myFunc's state1 is never updated while the console.log in the useEffect outputs the updated state correctly. What could be the reason for this?
const TestComponent = () => {
const [state1, setState1] = useState();
useEffect(() => {
console.log(state1);
}, [state1]);
const myFunc(() => {
const newState1 = getNewState1();
console.log(state1); // never outputs updated state, even when myFunc is called multiple times
if (state1 !== newState1) {
console.log('updated state');
setState1(newState1);
}
});
}
Obviously, my real component is much more complicated, but the only time setState1 is called is in myFunc which is confirmed by the useEffect.
Edit:
const TestComponent2 = () => {
useFocusEffect(
useCallback(() => {
myFunc();
}),
);
};
I am trying to call myFunc when TextComponent2 is focused/loaded. I realize that useEffect with no dependencies may be the best option here. Thanks!
I was using useCallback with no relevant dependencies which most likely caused state1 to be the same.

Nested react hooks exposing static references

I was looking the implementation of eslint-plugin-react-hooks and it looks like useState's setState and useReducer's dispatch functions are static references which are not required to be declared in the dependency array in useEffect.
However, this rule does not seem to work properly when you write a custom rule that abstracts an internal logic. for example, I create a custom hook:
const useCustom = () => {
const [number, setNumber] = React.useState(0);
return [number, setNumber];
};
which is then used the following way:
const [number, setNumber] = useCustom();
React.useEffect(() => {
if (something) {
setNumber(1);
}
}, [something]); // useEffect has a missing dependency: 'setNumber'
Adding the dependency to the array does not seem to cause extra render cycles. However, it raises a another question: What if my hook returns a reference to an value returned from useRef() would that be safe to add it to the dependencies array?
The value from useRef will never change unless you unmount and remount the component.
Changing value.current will not cause the component to re render but if the value.current is a dependency of your effect then your effect will re run when the component re renders.
Not sure if this answers your question, I added sample code below to demonstrate:
const useTimesRendered = () => {
const rendered = React.useRef(0);
rendered.current++;
return rendered;
};
const useCustomRef = () => {
const customRef = React.useRef(0);
return customRef;
};
const App = () => {
const rendered = useTimesRendered();
const custom = useCustomRef();
const [, reRender] = React.useState({});
React.useEffect(
() => console.log('rendered ref only on mount', rendered.current),
[rendered]
);
const customVal = custom.current;
React.useEffect(
() => console.log('custom ref.current only when re rendering', customVal),
[customVal]
);
return (
<div>
<h1>Times rendered: {rendered.current}</h1>
<button onClick={() => custom.current++}>
increase custom
</button>
<button onClick={() => reRender({})}>
Re render app
</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
If I've understood your question correctly, than will go with yes. If ref never changes (capturing some node on component mount), or if it changes frequetly (like in usePrevious hook) it should be added to the list of dependencies. Only potential issue I could think of is when someone, for example use ref on input and than expect that will get a new updated ref after user type somenthing. But that is a topic for some other time

Call a Redux Action inside a useEffect

My objective here is to call an action inside a useEffect.
const ShowTodos = (props) =>{
useEffect(()=>{
props.fetchTodos()
},[])
}
const mapStateToProps = (state)=>{
return {
todos:Object.values(state.todos),
currentUserId:state.authenticate.userId
}
}
export default connect(mapStateToProps,{fetchTodos})(ShowTodos)
It works fine but I got a warning
React Hook useEffect has a missing dependency: 'props'. Either include it or remove the dependency array react-hooks/exhaustive-deps.
But if I'm going to add props as my second parameter in my useEffects then it will run endlessly.
My first workaround here is to use the useRef but it seems that it will always re-render thus re-setup again the useRef which I think is not good in terms of optimization.
const ref = useRef();
ref.current = props;
console.log(ref)
useEffect(()=>{
ref.current.fetchTodos()
},[])
Is there any other workaround here ?
That is an eslint warning that you get if any of the dependency within useEffect is not part of the dependency array.
In your case you are using props.fetchTodos inside useEffect and the eslint warning prompts you to provide props as a dependency so that if props changes, the useEffect function takes the updated props from its closure.
However since fetchTodos is not gonna change in your app lifecycle and you want to run the effect only once you can disable the rule for your case.
const ShowTodos = (props) =>{
const { fetchTodos } = props
useEffect(()=>{
fetchTodos()
// eslint-disable-next-line import/no-extraneous-dependencies
},[])
}
const mapStateToProps = (state)=>{
return {
todos:Object.values(state.todos),
currentUserId:state.authenticate.userId
}
}
export default connect(mapStateToProps,{fetchTodos})(ShowTodos)
You however can solve the problem without disabling the rule like
const ShowTodos = (props) =>{
const { fetchTodos } = props
useEffect(()=>{
fetchTodos()
},[fetchTodos])
}
I however will recommend that you know when exactly should you disable the rule or pass the values to the dependency array.
You have to add fetchTodos to dependencies.
const ShowTodos = ({ fetchTodos }) => {
useEffect(() => {
fetchTodos();
}, [fetchTodos])
...
}
or like this.
const ShowTodos = (props) => {
const { fetchTodos } = props;
useEffect(() => {
fetchTodos();
}, [fetchTodos])
...
}

Resources