Can I use dynamic properties in dependency array for useEffect? - reactjs

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.

Related

How setState affects setInterval?

I've coded simple timer, but when I try to console.log(time)(It is in handleStart), I get the same output 0, even if setTime() is called.
Here's the part of code, where I console.log(time) (It is in handleStart, you should click the Start button in order to see console.log):
const handleStart = () => {
setIsCounting(true);
timerIntervalRef.current = setInterval(() => {
console.log(time);
setTime((prevState) => {
localStorage.setItem("time", `${prevState + 1}`);
return prevState + 1;
});
}, 1000);
};
Link to Sandbox
Please, explain me, why it works that way, cause I think, that, the callback in setInterval has a reference to a time, which is defined above, so every time this callback gets called, it should climb through closure to the the point, where time is defined, so it should get renewed value.
time is a local const, which your interval function closes over. As a const, it can never change, so you're always logging out the original value. Even if you used let the behavior would be the same, because calling setTime does not change the value in the local variable. Rather it asks react to rerender your component. On that new render, a new local variable will be created with the new value, but code in the old render (including the code in the setInterval) still only has the old variable in its scope and cannot access the new one.
If you'd like to verify that the component is rerendering, you can move your log statement into the body of the component.
console.log('rendering with', time); // moved outside of handle start
const handleStart = () => {
// ... normal handleStart function, minus the console.log
}
Or if you want a log statement at the time you set the state, you could move it inside the set state function, since that gets passed the latest value of the state and doesn't depend on a closure variable:
setTime((prevState) => {
console.log(prevState);
localStorage.setItem("time", `${prevState + 1}`);
return prevState + 1;
});
inside of settime you are getting the renewed value (prevState), the "time" is referencing to the initial time, consoling will obviously refer to initial value, I think you should console.log(prevState)

How do I make a list updates only on the second render?

I have a state list that is called 'journal' and I want to add a state object that is called 'record' to the list after the user enters the data and set the state of the record.
Here's my states:
const [journal, setJournal] = useState([]);
const [record, setRecord] = useState({});
And here's the method that takes the data from user to set the record:
function AddRecord(debitAccount, debitValue, creditAccount, creditValue, description){
setRecord({date: new Date().getDate().toString(), debit: {[debitAccount]: debitValue},
credit: {[creditAccount]: creditValue}, description, id: new Date().getTime().toString()});
}
I'm using a useEffect to update the journal every time the record changes like this:
useEffect(()=>{
setJournal([...journal, record])
}, [record])
But it adds an empty object at the beginning of the array.
Can someone please tell me how to fix this, I'm still trying to figure my way around states in react, and they're just getting complicated
This is a misuse of useEffect. Effects should be used to react to, and tie together, things which happen outside the business logic of the component (i.e. prop changes, multiple concurrent fetch calls), or resubscribe listeners which are dependent on state values. Just move all the relevant code into AddRecord:
function AddRecord(debitAccount, debitValue, creditAccount, creditValue, description){
const newRecord = {
date: new Date().getDate().toString(),
debit: {
[debitAccount]: debitValue
},
credit: {
[creditAccount]: creditValue
},
description,
id: new Date().getTime().toString()
};
setRecord(newRecord);
setJournal([...journal, newRecord]);
}
useEffect will be called on component initialization try to use some other way maybe useCallback function
useEffect runs on mount, and each time record changes, that's why it runs when record is empty. Just add a condition:
Also do not use journal directly, it's not guarantee that it has the expected value. Use set state callback instead:
useEffect(() => {
if (record && record.id) {
setJournal(journals => ([...journals, record]))
}
}, [record])

React JS component declare a variable required to assign value on useEffect to use across multiple functions

In a React Component, Need to declare variable to access across multiple functions. Two approaches tried to achieve this - useState (less good because of rendering) or let/var. Example:
const [userName, setuserName] = useState("");
let root = "";
problem (part 1): on useEffect after setting setuserName hook I can not access userName immediately. but after modifying root I can use the variable immediately. Example:
useEffect(() => {
//get name from firebase doc ref...
let name = user.claims.userName; //"AName"
setuserName(name);
root = name ;
console.log("userName: " + userName+ " , " + root); //here userName is empty but root has the name.
return () => {
// db.ref("").off("value", listener);
};
}, []);
problem (part 2): if both used in a function declared in the component problem part 1 reverses, I mean, the userName (hook) will have name in it but the root variable will be empty. Example:
async function handleSubmit(e) {
// e.preventDefault();
console.log("root = " + root + " , userName : " + userName );//here root is empty but userName has the name.
return; //or call methods to firebase ref.
}
Tested with assigning variables and state with string literals from code within (Eg: root = "testName"). Just need a temporary variable to call firebase functions. Why this behaviour happens and what approach should I use here?
Update:
Re rendering of component will reset the variable, using state hook is kinda overkill but only option.
Now to set the value of state (userName), useEffect is used in the first place. It also calls and fetches other variables from firebase based on the state (userName). If you dont have that reference nothing else works. You dont have that reference because useState will have it on next render, so not usable right here. You can store userName on a variable. Again render will reset that variable, so wont work with variable either. Hope this make sense.
Now I have solved this, thought it would be interesting to put on SO.
first issue is related to the fact the setState doesn't update state synchronous, which is a common doubt, hence console.log will not print next state. And you can't await on some state fwiw, since it's not an actually promise. If you need to perform some action based on a state change you should create another use useEffect with that state as dependency:
useEffect(() => {
//get name from firebase doc ref...
let name = user.claims.userName; //"AName"
setuserName(name);
return () => {
// db.ref("").off("value", listener);
};
}, []);
useEffect(() => {
// another useEffect based on userName as dependency
//do something on userName update...
console.log(userName);
}, [userName]);
second issue is that on rerenders only the state value is reflected correctly. Once setuserName is resolved and your component is rerendered, any declared variables like root will be recreated based on its original logic (unless you memoize it with useMemo to avoid some expensive calculation).
you should better use state across places/functions to control the logic of your component, not variables declared at your component body which would 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 */}
);
}

How does React Hooks useCallback "freezes" the closure?

I'd like to know how does React "freezes" the closure while using the useCallback hook (and with others as well), and then only updates variables used inside the hook when you pass them into the inputs parameter.
I understand that the "freeze" may not be very clear, so I created a REPL.it that shows what I mean: https://repl.it/repls/RudeMintcreamShoutcast. Once you open the code, open your web browser console and start clicking on the count button.
How come the value outside compared to the one inside, for the same variable, is different, if they're under the same closure and referencing the same thing? I'm not familiar with React codebase and so I suppose I'm missing an under the hood implementation detail here, but I tried to think how that could work for several minutes but couldn't come up with a good understanding on how React is achieving that.
The first time the component is rendered, the useCallback hook will take the function that is passed as its argument and stores it behind the scenes. When you call the callback, it will call your function. So far, so good.
The second time that the component is rendered, the useCallback hook will check the dependencies you passed in. If they have not changed, the function you pass in is totally ignored! When you call the callback, it will call the function you passed in on the first render, which still references the same values from that point in time. This has nothing to do with the values you passed in as dependencies - it's just normal JavaScript closures!
When the dependencies change, the useCallback hook will take the function you pass in and replace the function it has stored. When you call the callback, it will call the new version of the function.
So in other words, there's no "frozen"/conditionally updated variables - it's just storing a function and then re-using it, nothing more fancy than that :)
EDIT: Here's an example that demonstrates what's going on in pure JavaScript:
// React has some component-local storage that it tracks behind the scenes.
// useState and useCallback both hook into this.
//
// Imagine there's a 'storage' variable for every instance of your
// component.
const storage = {};
function useState(init) {
if (storage.data === undefined) {
storage.data = init;
}
return [storage.data, (value) => storage.data = value];
}
function useCallback(fn) {
// The real version would check dependencies here, but since our callback
// should only update on the first render, this will suffice.
if (storage.callback === undefined) {
storage.callback = fn;
}
return storage.callback;
}
function MyComponent() {
const [data, setData] = useState(0);
const callback = useCallback(() => data);
// Rather than outputting DOM, we'll just log.
console.log("data:", data);
console.log("callback:", callback());
return {
increase: () => setData(data + 1)
}
}
let instance = MyComponent(); // Let's 'render' our component...
instance.increase(); // This would trigger a re-render, so we call our component again...
instance = MyComponent();
instance.increase(); // and again...
instance = MyComponent();
I came here with a similar, rather vague uncertainty about the way useCallback works, its interaction with closures, and the way they are "frozen" by it. I'd like to expand a bit on the accepted answer by proposing to look at the following setup, which shows the working of useCallback (the important aspect is to ignore the linter's warning, for pedagogical reasons):
function App() {
const [a, setA] = useState(0)
const incrementWithUseCallback = useCallback(() => {
// As it closes on the first time `App` is called, the closure is "frozen" in an environment where a=0, forever
console.log(a)
setA(a + 1)
}, []) // but.. the linter should complain about this, saying that `a` should be included!
const incrementWithoutUseCallback = () => {
// This will see every value of a, as a new closure is created at every render (i.e. every time `App` is called)
console.log(a)
setA(a + 1)
}
return (
<div>
<button onClick={incrementWithUseCallback}>Increment with useCallback</button>
<button onClick={incrementWithoutUseCallback}>Increment without useCallback</button>
</div>
)
}
So we clearly see that useCallback effectively "freezes" its closure at a certain moment in time, which is a concept that must be understood clearly, in order to avoid confusing problems, which are sometimes also referred as "stale closures". This article probably does a better job of explaining it than me: https://tkdodo.eu/blog/hooks-dependencies-and-stale-closures
Here's a slightly another view on example code provided by Joe Clay, which emphasizes closure context in which callback is called.
//internal store for states and callbacks
let Store = { data: "+", callback: null };
function functionalComponent(uniqClosureName) {
const data = Store.data;//save value from store to closure variable
const callback = Store.callback = Store.callback || (() => {
console.log('Callback executed in ' + uniqClosureName + ' context');
return data;
});
console.log("data:", data, "callback():", callback());
return {
increase: () => Store.data = Store.data + "+"
}
}
let instance = functionalComponent('First render');
instance.increase();
instance = functionalComponent('Second render');
instance.increase();
instance = functionalComponent('Third render');
As you see, callback without dependencies will be always executed in the closure where it was memorized by useCallback, thus 'freezing' closure.
It happens because when function for callback is created, it is created only once, during first 'render'. Later this function is re-used, and use value of data which was recorded from Store.data during first call.
In the next example you can see the closure 'freezing' logic "in essence".
let globalX = 1;
const f = (() => {
let localX = globalX; return () => console.log(localX); }
)();
globalX = 2;//does not affect localX, it is already saved in the closure
f();//prints 1

Resources