How to get `useEffect` code reuse with custom hook that uses refs? - reactjs

I need to provide some custom functionality after render that uses refs.
It seems to be a perfect use case for useEffect, but it needs to be in a custom hook so that it can be reused.
Unfortunately, passing either a ref or its current to a custom hook that does useEffect appears to result in different behaviour than calling useEffect directly.
A working example looks like this:
const useCustomHookWithUseEffect = (el1, el2) => {
React.useEffect(() => {
console.log("CUSTOM Use effect...");
console.log("firstRef element defined", !!el1);
console.log("secondRef element", !!el2);
}, [el1, el2]);
}
const RefDemo = () => {
const [vis, setVis] = React.useState(false);
const firstRef = React.useRef(null);
const secondRef = React.useRef(null);
useCustomHookWithUseEffect(firstRef.current, secondRef.current);
React.useEffect(() => {
console.log("Standard Use effect...");
console.log("firstRef element defined", !!firstRef.current);
console.log("secondRef element ", !!secondRef.current);
}, [firstRef.current, secondRef.current]);
console.log("At RefDemo render", !!firstRef.current , !!secondRef.current);
return (
<div>
<div ref={firstRef}>
My ref is created in the initial render
</div>
<div className="clickme" onClick={() => setVis(true)}>
click me
</div>
{vis &&
<div ref={secondRef}>boo (second ref must surely be valid now?)</div>
}
</div>
)
}
After the first render, the custom hook does not have the defined value of firstRef, but the in-line useEffect does.
After clicking the click-me, once again the custom hook does not get the most-recent update (though now it has the firstRef value).
Is this expected?
How could I achieve the goal: be able to re-usably supply useEffect-based code that uses refs?
https://jsfiddle.net/GreenAsJade/na1Lstwu/34/
Here's the console log:
"At RefDemo render", false, false
"CUSTOM Use effect..."
"firstRef element defined", false
"secondRef element", false
"Standard Use effect..."
"firstRef element defined", true
"secondRef element ", false
Now I click the clickme
"At RefDemo render", true, false
"CUSTOM Use effect..."
"firstRef element defined", true
"secondRef element", false
"Standard Use effect..."
"firstRef element defined", true
"secondRef element ", true

The problem is that you pass the ref.current while rendering to the custom hook. Now when you change the state of vis, the component is executed from top to bottom again (effects are read, not yet executed). But in this rerender at the point you call the custom hook, your ref has not yet updated (since we have not yet actually rerendered and therefore assigned the ref to the second div). And since you specifically pass the value of the ref, it won`t show the actual updated ref value but the value you passed at the time of the function call (null). When the effect is then run later on, you only have access to the explicit value you passed, unlike if you would pass the ref itself, whose value will always be up to date. This code sandbox should illustrate the issue.

I realised that I asked two questions. This is the answer I found to "how should I achieve side effects from refs?"
The answer is "do not use refs in useEffect". Of course, there are situations where it might be fine, but it is certainly asking for trouble.
Instead, use a ref callback to achieve the side effect of the ref being created and destroyed.
The semantics of ref callback are much simpler to understand: you get a call when the node when it is created and another when it is destroyed.

Related

useEffect dependency list and its best practice to include all used variables

According to the react docs at https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect when using the useEffect dependency array, you are supposed to pass in all the values used inside the effect.
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Learn more about how to deal with functions and what to do when the array values change too often.
I don't know how the hook works behind the scenes, so I'm going to guess here.
Since the variables inside the closure might go stale, that would imply that the function is cached somewhere. But why would you cache the function since its not being called unless the dependencies changed and the function needs to be recreated anyways?
I've made a small test component.
function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffect(() => {
console.log("b", b);
}, [a]);
return (
<div>
<div>App {a}</div>
<button
onClick={() => {
setA(a + 1);
}}
>
AddA
</button>
<button
onClick={() => {
setB(b + 1);
}}
>
AddB
</button>
</div>
);
}
You can try it out here: https://codesandbox.io/s/react-hooks-playground-forked-m5se8 and it works just fine, with no stale values. Can someone please explain what I'm missing?
Edit:
After feedback that my question is not entirely clear, adding a more specific question:
When after a page load I click on AddB button and then click on AddA button, value displayed in console is 1. According to the docs, I should get a stale value (0). Why is this not the case?
When after a page load I click on AddB button and then click on AddA
button, value displayed in console is 1. According to the docs, I
should get a stale value (0). Why is this not the case?
The reason why you don't see stale value in that case is that when you click AddA a re-render happens. Inside that new render since value of a is different from previous render, the useEffect will be scheduled to run after react updates the UI (however the values it will reference will be from current render - because the function passed to useEffect is re-created each time one of its dependencies change, hence it captures values from that render).
Due to above reasons, that is why you see fresh value of b.
But if you had such code
React.useEffect(() => {
const timer = window.setInterval(() => {
setB(b + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
b would be a stale closure. Since useEffect didn't re-run, b always has value from the render it was created in, that is the first render and above code wouldn't work as expected, which you can check yourself.
I will add this comment by Patrick Roberts from above because it explains well IMHO what react docs may mean when they say "Otherwise, your code will reference stale values from previous renders":
the problem is that you had to click on AddA button in the first
place. Between the time you click AddB and AddA, the effect references
b from a stale closure

Using refs to modify an element in React.js

Is it wrong to use refs to modify an element's properties? If so, why?
Example:
myRef.current.innerHTML = "Some content";
That's wrong if it's possible to modify the component's JSX to implement the change instead. Whenever possible, one should be able to determine the JSX that gets rendered solely from the current state of the component; direct DOM mutation side-effects like .innerHTML should only be done when there's no other possible option.
For this case, put the content into a state variable instead, like:
const [spanContents, setSpanContents] = useState('foobar');
const changeSpanContents = () => {
setSpanContents('Some content');
};
return (
<div>
<button onClick={changeSpanContents}>click</button>
<span>{spanContents}</span>
</div>
);
In some unusual cases, there exists no JSX syntax for the DOM mutation you want - for example, for putting a resize listener on the window. In such a case, you will have to resort to using vanilla DOM methods instead of doing it solely through React. The following pattern is common for such a case:
useEffect(() => {
const handler = () => {
// resize detected
};
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}, []);
I wouldn't say wrong but it depends on what you are going to do after changing the html. As React will loose control over that element, you would have a hard time if you need to work on that DOM. Also, it is risky because React have controls over DOM, when you change it manually, it could lead to unexpected behaviors.

useEffect not triggering when object property in dependence array

I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}

Can I use dynamic properties in dependency array for useEffect?

So I have a question regarding useEffect dependenices
This is from the react docs:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
What does this mean exactly, does React keep track of the count variable and its value, and reacts when the value changes, or does React keep track of the first element in the array and its value.
What do I mean by this? Let me explain more. So if we have something like this [name] as dependencies. At the moment of evaluation, the array might result with ['Bob'] or ['Steve']. Clearly this is a change and the useEffect will rerender the component. But how does it check it?
Does it keep track of name or does it keep track of dependencyArray[0]. If we take a look in the previous example, both of these would result to true, name and the first element both changed their values from 'Bob' to 'Steve'. But how does it actually work?
Currently in my code I am using something like this [employees[selectedEmployee].name], where selectedEmployee is something clickable on the UI and it becomes 'Bob' or 'Steve'
ex:
const employees = {
Bob: {
name: 'Bob'
},
Steve: {
name: 'Steve'
}
}
This means that in the end, when evaluated, the dependency array will still result with ['Bob'] --> ['Steve'], and if React is evaluating the dependencyArray[0] then that has clearly changed and component should rerender, but If it keeps track of the reference, then I am changing the reference altogether and it may cause problems.
So what's the correct approach? Can I use dynamic properties like employees[selectedEmployee].name as a dependency?
count is a value, not a reference.
It's just good old Javascript, nothing fancy:
const myArray = [ count ]; // new array containing the value of variable 'count'
const myFunction = () => {
document.title = `You clicked ${count} times`;
}
useEffect(
myFunction,
myArray
);
// Means actually:
// "Run this function if any value in the array
// is different to what it was last time this useEffect() was called"
does React keep track of the ... value, or ... the reference ?
React doesn't really 'keep track' of any of them. It only checks the difference to a previous call, and forgets about everything else.
Can I use dynamic properties as a dependency?
Yes, you can (because they are not as 'dynamic' as you think).
So what's the correct approach?
Better think less of any react-magic going on, but
understand that the component is a function, and believe React calls it when necessary and
think about the variables (properties and state) used inside it, from a plain Javascript perspective.
Then your 'dynamic properties' become 'constant variables during one function call'. No matter which variables change dynamically and how, it will always be one value last time and one value now.
Explaination:
The important 'trick' here is, that the component is just a javascript function, that is called like 'whenever anything might have changed', and consequently useEffect() is also called (as useEffect() is just a function call inside the component).
Only the callback function passed to useEffect is not always called.
useEffect does not render the component, useEffect is called when the component is called, and then just calls the function given to it, or not, depending on if any value in the dependencies array is different to what it was last time useEffect() was called.
React might rerender the component if in the function given to useEffect there are any changes made to the state or something (anything that makes React to think it has to rerender), but that's as a result of this state change, where ever it came from, not because of the useEffect call.
Example:
const MyComponent = (props) => {
// I'm assigning many const here to show we are dealing with local constants.
// Usually you would use this form (using array destructuring):
// const [ selectedEmployee, setSelectedEmployee ] = useState( someInitialValue );
const myStateValueAndSetter = useState( 'Bob' );
const selectedEmployee = myStateValueAndSetter[0];
const setSelectedEmployee = myStateValueAndSetter[1];
const employees = {
Bob: { name: 'Bob' },
Steve: { name: 'Steve' }
};
const currentName = employees[ selectedEmployee ].name;
useEffect(() => {
document.title = 'current name: ' + currentName;
}, [ currentName ]);
return <MyClickableComponent onClick={( newValue ) => {
setSelectedEmployee( newValue )
}}>;
};
click on MyClickableComponent calls the current setSelectedEmployee( newValue ) function.
(The constant selectedEmployee is not changed!)
MyComponent() is called again.
(This is a new function call. All the constants are gone! Only React stores some state in the background.)
useState() is called, the result is stored in a new constant selectedEmployee.
useEffect() is called, and decides if its callback should be called, depending on the previous and the current value of selectedEmployee.
If the callback is not called and nothing else is changed, you might not notice that anything has happened at all.
<MyClickableComponent ... /> is rendered.

React warning Maximum update depth exceeded

This is a follow up question to this question which is the nearest to my issue:
Infinite loop in useEffect
I am creating a small React.js app to study the library. I'm getting this warning:
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
I got a functional component, in which there is this code:
const propertiesMap2 = new Map([ //There is also propertiesMap1 which has the same structure
["TITLE4",
{
propertyValues: {
myProperty10 : "myVal1",
myProperty11 : "myVal2",
myProperty12 : "myVal3",
},
isOpen: true
}
],
["TITLE5",
{
propertyValues: {
myProperty13 : "myVal4",
myProperty14 : "myVal5",
myProperty15 : "myVal6",
},
isOpen: true
}
],
["TITLE6",
{
propertyValues:{
myProperty16 : "myVal7",
myProperty17 : "myVal8",
myProperty18 : "myVal9",
},
isOpen: true
}
]
]);
const [properties, setPropertiesMapFunc] = useState(new Map());
useEffect(()=>
{
let mapNum = Number(props.match.params.id);
setPropertiesMapFunc(mapNum === 1 ?propertiesMap1 : propertiesMap2);
}, [properties]);
The correct properties map is chosen each time, but like I said I get this error. Why do I get it, if the propertiesMap is constant without anything changing, and properties was passed as a parameter to setEffect, so I thought it would only re render when something there changes..
Because you are creating the map objects inside of your component function they will be recreated on every render. Because of that your effect will set a new map as the new state which will in turn trigger another re-render and your effect being called again which leads to an infinite update loop.
You can move the definition of your map objects outside of your component to fix this.
properties is a Map, and when passed in dependency array to useEffect, it will be recreated on every render, and when compared, it will always be not equal to itself, since the map, same as other non-primitive types in JS are compared by reference and not by value. So this will cause the function inside the useEffect to be run on every re-render.
You'd need to wrap it into some kind of deep compare function: https://stackoverflow.com/a/54096391/4468021

Resources