Looking at the docs, there is a note at the bottom of this section [edited link] that makes it seem like only props or state should be used in a hook dependency list. However, when a "complex expression" on props or state is used in the list, the eslint plugin gives the following:
React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked. eslint(react-hooks/exhaustive-deps)
This has me thinking about what is allowed to be used as a dependency. Can we use a local variable that is calculated from props? Do we need to create some sort of new state variable or ref in this case? I'm not certain if hooks are executed in place within the component (so local variables are available) or are hoisted out of the context of the render (so we have to use only state, props, or other hook values like refs/memos).
Examples
A component has a prop, data, that is an object.
data: {
name: 'name',
id: 2
}
1) It looks like data.name can be used in the dependencies. But can we use a local variable that is set to the property?
const { name } = data;
useEffect(fn, [name]);
2) Would we be able to use a variable that is calculated by a prop in a dependency array?
const isOdd = Boolean(data.id % 2);
useEffect(fn, [isOdd]);
Both cases seem to work with small tests. I'm not knowledgable enough with Hooks to know if it's breaking some rules that would leave the results as indeterminate.
Optimizing Performance by Skipping Effects may help understanding hook dependencies.
Note
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 changes too often.
The important bit, "...all values from the component scope...", meaning any value within the component scope used within the hook.
Q: Can we use a local variable that is calculated from props?
Yes
Q: Do we need to create some sort of new state variable or ref in this case?
No
Q: I'm not certain if hooks are executed in place within the component (so local variables are available) or are hoisted out of the context of the render (so we have to use only state, props, or other hook values like refs/memos)
AFAIK, they are just functions with special rules within react. They are invoked within the scope of the functional component.
Related
I started learning to react and came across the code snippet where the function was passed as a dependency array in useEffect. I want to know the use case where such function is being passed as the dependency and why do we need to pass the function as a dependency?
First: This only makes sense if the code in the useEffect callback uses the function. So let's take that as a baseline. :-)
Fundamentally, you'd do that so the code in the useEffect callback is using the most up-to-date version of the function.
Here are a couple of examples where that would be important:
The function is a prop. Since your code doesn't know why it got a new version of the function, it's important to re-run the effect with the up-to-date version of the function.
The function uses state information it closes over (rather than using the callback form of a state setter). If you didn't re-run the effect with the updated function, the function would use stale state information. (But I wouldn't do it that way. Instead, I'd have the function use the callback form of the state setter.)
There are likely others, but they all boil down to ensuring the effect uses the most recent version of the function.
I am also learning React and this article helped me understand functions in the dependency array of useEffect.
Functions in dependency array of useEffect
A function is an object. It has its own identity like an object. A component re-renders each time on of its state changes. When a component re-renders, the function which is defined inside the component gets a new identity.
I made a bit clear code example here. The example from the article was unclear for me to understand. Use the console tab to see the console logs.
It depends on the use of the useEffect and the definition of the function. Basically, if you put a function inside a useEffect array, each time the function will change, or more accurately, it's reference, your effect will be called again, with the new function reference.
This is good in case you want to always use the latest function, but it can also be tricky. If the passed function is defined inside a component, it means that on every component render the function will be redefined, meaning your effect will be called on each component render. It can be heavy sometimes, depends on what your effect does.
It can still be avoided though, if the component in which the function is defined is using useCallback in order to memoize the function. This way, the function will have its own dependencies array, and will only be redefined (and change reference) when you decide it's needed.
I was under the impression that a second "setter" method param was needed for setting variables to state with the useState() hook. For example, in the following line of code, "widgets" would be used to get widgets from local state but setWidgets() would be needed to set widgets to state:
const [widgets, setWidgets] = useState(new Map<string, JSX.Element>());
However, I inadvertently wrote code to set widgets directly to state via the widgets state variable:
widgets.set(...)
I confirmed that this approach is successfully setting widgets to local state. Was the second param required in earlier versions of React hooks and the useState() hook has been simplified to support a single parameter as described above? If so then do you know in which version of React this was introduced? Or am I possibly doing something in my code that is only making me think that the 1-param useState is valid?
Maps, Sets and other complex objects can be updated, but you won't trigger rerenders by calling they're native methods, as the object reference itself would be the same. The overall effect of this becomes evident on these type of state values being passed down through props or context.
The proper way of handling setting state on this type of object is something like this:
setWidgets(prev => {
prev.set(id, value);
return new Map(prev);
})
This ensures that the object reference is updated so that changes are properly propagated.
widgets.set(...) is wrong. It might seem to work depending on how it is used, but the state should always be updated immutably using the second element of the array returned by useState (setWidgets).
You can update the map immutably like this:
setWidgets(prevMap => new Map(prevMap.set(key, value)))
BTW storing JSX as part of the state is bad practice.
I am learning ReactJS and I understand the benefits of functional programming. However, I am a little concerned about the actual useState approach. If we assume a variable to be changed, then why declaring it const in the first place?
I see that I can simply use let number = 4; and then render it like this <p>{number}</p>. What I cannot do however is to re-render it automatically just by changing it, for example using onClick event like this <p onClick={() => ++number }></p>. Why is it that so? Is there a specific reason I am missing why it was implemented the way it is? I mean why the developers have decided that if the value needs to be re-rendered upon change, then it must be a const value declared with the help of useState functionality?
I am asking this because I am suspecting I am missing some good points behind this and I would like to understand them.
The variable is declared as a const because you are not allowed to set it directly. In React the state itself is immutable. You are just allowed to change it over setState (or setNumber in your case) or with actions if you use redux.
But why is that? It may seem unnecessary cumbersome in the beginning
First of all, if your variable number changes, react has to trigger a rerender.
If the state is mutable, it requires data-binding because if the number is set, it has to update the view.
In javascript, data-binding works for simple objects, but not well for arrays. Vue.js for example, as an alternative that uses two-way data binding, had a lot of trouble in its early versions when dealing with arrays. That's why there are now only seven predefined methods to interact with arrays (which they added later to solve that problem). Vue Js Array Methods
So a simple reason to declare the state as const is that it works better with arrays. And if you watch the example you gave, setNumber(number + 1) is not that much more to write than number++. But setState(newArray) works, and newArray[i] = newElement would not work, because due to javascript limitations this cannot trigger a rerender.
Secondly, it is a nicer design concept. Think of your component as a function, that returns a view to a state. And if the state changes, you get a new view. It simplifies relationships between properties in your component. If you were allowed to change your state while rendering your component, it would create a mess.
The problem is that you're thinking of a functional component as if it was stateful. But it isn't. It's a function and once you run it, that's it.
Take this example:
function useState() {
let value = 1
function setValue(v) {
value = v
}
return [value, setValue]
}
function myFunction () {
const [value, setValue] = useState(); // <----- we use const here
return null
}
Even though we're using const, the value variable only exists within the function, once the function returns that's it. It's the same for components.
The actual value of value is stored in a whole different scope, where useEffect has access to.
Here's a deep dive on how react works internally if you're interested
React works in render cycles, i.e. some state is declared, the DOM (UI) is computed during the "render phase", and then flushed to the actual DOM during the "commit phase".
Within each cycle state is considered constant (const in JS simply means a variable can't be assigned a new value, you could just as easily declare it with let or var instead and react would work the same) but for react's purpose, state is constant during a render cycle. When it is updated via one of the state update methods, react then kicks off another render cycle (update state, compute diff, commit to DOM) and re-renders when necessary.
This process is important and the reason why external state mutations are considered anti-pattern, it goes against the react workflow and leads to buggy code or worse, UI that doesn't update as expected.
React component lifecycle
I cannot do however is to re-render it automatically just by changing
it, for example using onClick event like this <p onClick={() => ++number }></p>. Why is it that so?
React state updates use a process called reconciliation to figure out what changed and what to re-render. In really simplistic terms, when react state is updated it is updated with a new object reference so a shallow object comparison can more quickly detect that the component state updated.
Declaring state and doing ++number simply changes the value but not the reference, and would be considered a state mutation, an anti-pattern in react.
Looking at the hooks docs and some blogs we understand that when using useRef() we have a mutable value stored.
Docs:
You might be familiar with refs primarily as a way to access the DOM. If you pass a ref object to React with , React will set its .current property to the corresponding DOM node whenever that node changes.
However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.
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.
What advantages does this give us over just declaring and using a variable with let?
Code ex:
import React, {useRef} from "react";
const MyFunctionalComponent = (props) => {
const refVariable = useRef('value');
//versus
let letVariable = 'value';
}
Follow up:
The answers given were helpful, and combined with some testing gave me the understanding I needed. If anyone comes across this having trouble with the concept, the way I understand it now is:
You can have instance variables, but they are really instant, and every re-render re-initializes them.
useRef() gives you something more permanent, like useState() but updating doesn't trigger re-render, very useful if you are doing a lot of manipulation in a chaining fashion, but wouldn't want to trigger a re-render until the end
useState() should only be tied to variables used by a UI element, as any change to state will trigger re-render of the whole component. Do not have a chain of actions that manipulate state along the way, use refs until the end of the chain.
The last sentence describes it clearly:
useRef will give you the same ref object on every render.
If you declare a simple javascript variable yourself, it will be updated on each render. Refs are used when you need to persist some value during re-renders (Besides using the ref attribute for DOM node reference)
I'm changing my answer and referring people to Arman's below, since it's the more apt one. In essence, for functional components the entire function gets run every time it re-renders. Which means variables that are initialized with a simple let x = 5; in the body of the function will get re-initialized every render, resetting them. That's the reason we need hooks like useRef, it gives a reference to a value that persists between renders
Suppose I have some external object inside my JavaScript file:
var data = [{id: 1, name:'Test1'}, {id:2, name: 'Test2'}];
which I pass to ReactDOM:
ReactDOM.render(<Test data={data}/>, document.getElementById('container'));
and as a property to the state object for Test component:
getInitialState: function () {
return {localState: data};
},
Somewhere along the chain, I use this:
handleClick: function () {
data[0].id=55;
this.setState({localState: data});
}
which causes re-render. Full code here: http://jsfiddle.net/44ff2j4b/
Is this a good idea? Basically, having external data which will be modified in place in the component and re-rendered appropriately. Are there some side effects of doing this? As far as I'm aware, it's not OK to modify state in the React component, but the "state" here does not belong to a component...it belongs to the domain logic.
In a React component, "state" is very much supposed to be modified, in-fact state of a component can only be modified within it. However, what you are doing here is seeding the state with a prop and then maintaining it internally. So long as you are not duplicating the prop every-time, merely seeding it, it is ok. Do read more about this here. You should however rename the prop from data to initialData to indicate that it will only be used for seeding the state.
Although it may work, it is not generally a good idea to do this.
React likes you to treat props as immutable. So if you pass data as a prop, you are not supposed to change the original data, like you do in:
data[0].id=55;
If you want to pass data to a react component, copy it as initial state in getInitialState(), and change only the copy inside the react component.
NB: for this, you will need to make a deep copy (so not only copy the array of objects, but also copy the objects themselves). ImmutableJS is a solution/ library often used in conjunction with react.
If you also want to change the original data, the proper way is to pass a function as a prop (in addition to the data prop) to the react component. From the react component, you can then call this function, which can then change the original data. The original data change or update should leave your data-copy inside react component intact.
The code (outside react) can then decide whether or not to re-render the entire react component with the new data (but it is standard practice to call ReactDOM.render() only once).