Why React Class Component and Function Component output is different in setTimeout? - javascript

A class component and a function component, both of which can click on the div to modify the value of num.
export default function App() {
const [num, setNum] = useState(0);
const click = () => {
setTimeout(() => {
console.log(num, "3000");
}, 3000);
setNum(num + 1);
};
return <div onClick={click}>click {num}</div>;
}
export default class App extends React.Component {
state = {
num: 0
};
click = () => {
setTimeout(() => {
console.log(this.state.num);
}, 3000);
this.setState({ num: this.state.num + 1 });
};
render() {
return <div onClick={this.click}>click {this.state.num}</div>;
}
}
Why is the output of num different in the above two components after clicking

In class components, when state is set, when the component updates, the new state will be assigned to the state property of the instance. To oversimplify, doing
this.setState(someNewState);
eventually results in React doing something like
theInstance.state = mergedNewState;
As a result, referencing this.state will result in changes being seen if the state updates, because this.state points to something new..
Functional components are different. Unlike class components, nothing gets (visibly) mutated when using their state. Instead, when you call a state setter, the whole function (component) runs again, and the calls to useState return a new value.
In your code, the
const [num, setNum] = useState(0);
results in a num that the click function closes over.. If there's a click, when the timeout callback runs, it'll still close over the same num that was created before the click handler runs. The component will re-render with a new click function, which has a new num number that it closes over, but the old click function will still be referencing the num from the render in which that old function was created.

Related

What gets updated in React?

I am new to React and I am a little confused about what gets updated every time the state or props change. For instance:
const Foo = props => {
const [someState, setSomeState] = useState(null)
let a
useEffect(() => {
a = 'Some fetched data'
}, [])
}
Now if the state (i.e. someState) or props get updated, does it run through the function again, making a undefined? Does only JSX elements that depend on the state/props and the hooks that use them get affected? What changes exactly?
To understand how the state affect your component, you could check this example on Stackblitz. It shows you how the local variable of your component act when a state changes. This behavior is exactly the same when the component receive a new prop. Here is the code just in case :
import React, { Component } from "react";
import { render } from "react-dom";
const App = () => {
const [state, setState] = React.useState('default');
// see how the displayed value in the viewchanges after the setTimeout
let a = Math.floor(Math.random() * 1000);
React.useEffect(() => {
a = "new a"; // Does nothing in the view
setTimeout(() => setState("hello state"), 2000);
}, []);
return (
<div>
{a} - {state}
</div>
);
};
render(<App />, document.getElementById("root"));
What you can see is :
a is initialized with a random value and it is displayed in the view
useEffect is triggered. a is edited but does not re-render your component
After 2 seconds, setState is called, updating the state state
At this point, a value has changed for a new one because your component is re-rendered after a state update. state also changed for what you gave to it
So for your question :
if the state (i.e. someState) or props get updated, does it run through the function again, making a undefined? Does only JSX elements that depend on the state/props and the hooks that use them get affected? What changes exactly?
The answer is yes, it does run through the function again, and the state/props that has been updated are updated in the JSX. Local variable like a will be set to their default value.

Locking behavior of object in JS?

On every click of increment button:
Expectation: current count is logged
Reality: initial value of count, i.e. 3 is logged
import React, { useState, useEffect } from "react";
function SomeLibrary(props) {
const [mapState, setMapState] = useState(undefined);
useEffect(() => {
console.log("setting map");
// Run exactly once at mount of component
setMapState(props.map);
}, []);
useEffect(() => {
if (mapState) {
mapState.key();
}
}, [props]);
return <div> ... </div>;
}
export default function App() {
const [count, setCount] = React.useState(3);
const map = { key: () => {
console.log("fn", count);
}};
return (
<div>
Active count: {count} <br />
<button onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
<SomeLibrary map={map} />
</div>
);
}
Run here
Does the object in JS locks the values of variables inside it after initializing?
I want to know the reason why function in object doesn't use the current value of count whenever invoked but React ref gets the current value in that same scenario
I don't understand why this works:
Replace the map variable with this:
const [count, setCount] = React.useState(3);
const stateRef = useRef();
stateRef.current = count;
const map = { key: () => {
console.log("fn", stateRef.current);
}};
Does the object in JS locks the values of variables inside it after initializing?
No.
You're effectively setting state of SomeLibrary with an initial value when it mounts, and never again updating that state, so it continually logs its initial value.
const [mapState, setMapState] = useState(undefined);
useEffect(() => {
console.log("setting map");
// Run only once at mount of component
setMapState(props.map); // <-- no other `setMapState` exists
}, []); // <-- runs once when mounting
By simply adding props.map to the dependency array this effect runs only when map updates, and correctly updates state.
useEffect(() => {
console.log("setting map");
// Run only once at mount of component
setMapState(props.map);
}, [props.map]);
Notice, however, the state of SomeLibrary is a render cycle behind that of App. This is because the value of the queued state update in SomeLibrary isn't available until the next render cycle. It is also an anti-pattern to store passed props in local component state (with few exceptions).
Why React ref gets the current value in that same scenario?
const [count, setCount] = React.useState(3);
const stateRef = useRef();
stateRef.current = count; // <-- stateRef is a stable reference
const map = { key: () => {
console.log("fn", stateRef.current); // <-- ref enclosed in callback
}};
When react component props or state update, a re-render is triggered. The useRef does not, it's used to hold values between or through render cycles, i.e. it is a stable object reference. This reference is enclosed in the callback function in the map object passed as a prop. When the count state updates in App a rerender is triggered and stateRef.current = count; updates the value stored in the ref, i.e. this is akin to an object mutation.
Another piece to the puzzle is functional components are always rerendered when their parent rerenders. The passed map object is a new object when passed in props.
It's this rerendering that allows SomeLibrary to run the second effect to invoke the non-updated-in-state callback mapState.key, but this time the object reference being console logged has been mutated.

React component doesn't rerender with the same props but child state changes

i have the following problem:
I have parent component (where is button, and array of child components to render).
To each child i pass props and child uses it as state, then changes it.
The problem is that children doesn't rerender.
As it may seem not understandable, here is something more clear (i hope):
Here is the simplified version of child.js
export default function ChildComponent(props) {
const [open, setOpen] = React.useState(props.open);
const handleClick = () => {
setOpen(true);
}; /* i actually never use handleClick */
const handleClose = (event) => {
setOpen(open => !open);
};
return (
<div>
<SomeComponent hideAfterTimeMs={1000} onClose={handleClose}/>
</div>
);
}
Parent:
import React from "react";
import Child from "./demo";
class MyClass extends React.Component {
constructor(props) {
super(props);
this.state = {
something: false,
};
}
displayKids = () => {
const a = [];
for (let i = 0; i < 1; i++) {
a.push(<Child open={true} key={i} message={"Abcd " + i} />);
}
return a;
};
handleChange = e => {
this.setState(prevState => ({
something: !prevState.something,
}));
};
render() {
return (
<div>
<button onClick={this.handleChange}>Nacisnij mnie</button>
{this.displayKids()}
</div>
);
}
}
export default MyClass;
So basically the child component renders,
and sets its "open" to false,
and when i click button again
i hoped for displaying child again,
but nothing happens.
Child renders something that disappears after a few seconds.
Keys help React identify which items have changed, are added, or are
removed. Keys should be given to the elements inside the array to give
the elements a stable identity.
You are using the index as a key. Please try to use a unique key. E.g. child id or random hash code.
If the key is unique and new it will re-render. Right now it is not re-rendering because the key is the same.
Check out: https://reactjs.org/docs/lists-and-keys.html#keys
It doesn't look like your components are linked in any meaningful way. Clicking the button on the My Class component updates the something state, but that state is not passed to the Child component.
Similarly, the SomeComponent component handles its own close, and tells the Child component it is closed via handleClose - but that is not communicated to the parent and neither does the parent or Child communicate any open state to SomeComponent.
Changing some state on Child will not rerender it's own children. Something new has to be passed as a prop for that to happen.

ReactJS element not updating after setState being called

Good Afternoon,
I have a React component that is dynamically rendered in reponse to an API call. I have set the value of one of the elements to a state within the component. During an onClick function (minusOne) this value is supposed to change.
The value is initially rendered successfully based on the state, the function does indeed change the state, however the rendered element stays the same despite the state changing. Does anyone have any ideas of why this might be the case?
If you have any questions, please ask away!
export class Cart extends React.Component {
constructor(props) {
super(props);
this.state={
quantities: []
};
this.minusOne = this.minusOne.bind(this);
}
minusOne(i) {
var self = this;
return function() {
let quantities = self.state.quantities;
if (quantities[i] > 1) {
quantities[i] --;
}
self.setState({
quantities
})
}
}
componentDidMount() {
let cart = this.props.cartTotals;
this.setState({
cart
});
if(cart.lines) {
let cartTotal = [];
let quantities = [];
for (var i = 0; i < cart.lines.length; i++) {
if(cart.lines[i]) {
quantities.push(cart.lines[i].quantity);
}
}
//Initial setting of state
this.setState({
quantities
})
Promise.all(
cart.lines.map(
(cart, i) => axios.get('http://removed.net/article/' + cart.sku)
)
).then(res => {
const allCartItems = res.map((res, i) => {
const data = res.data;
return(
<div key={i} className="cart-item-container">
<img className ="cart-item-picture" src={data.image} name={data.name} />
<div className="cart-item-description">
<p>{data.name}</p>
<p>{data.price.amount} {data.price.currency}</p>
</div>
<div className="cart-item-quantity">
<button onClick={this.minusOne(i)} name="minus">-</button>
//This is the troublesome element
<p className="cart-current-quantity">{this.state.quantities[i]}</p>
<button name="plus">+</button>
</div>
</div>
)
})
this.setState({
allCartItems
})
})
}
}
render() {
return (
{this.state.allCartItems}
);
}
}
Thanks for reading! Any advice will be helpful.
There are two issues:
First, you need to render (including where the onClick is) in render(). ConponentDidMount is only called once and supposed to perform initialization but not render.
Then, there is a problem in minusOne:
quantities points to this.state.quantities. So you are changing the old state, React looks at both the old state and the new one, sees there is no change, and dodesn't render, although the values have changed.
If you will copy this.state.quantities to a new array, like:
newQ = this.state.quantities.slice(0, -1);
Then modify newQ, then do
this.setState({ quantities: newQ });
It should work.
I think you don't need to return a function at minusOne(i) method. Just update the state is enough. You should change the array by specific id.
let quantities = self.state.quantities;
let mutatedQuantities = quantities.map((el, index) => {
return (index === i) ? el - 1 : el;
})
this.setState({quantities: [...mutatedQuantities]})
--- edited ---
I deleted everything I wrote before to make it more concise.
Your problem is that you assign what you want to render to a variable in componentDidMount. This function does only get called once, hence you asigne the variable allCartItems only once. The setState function does not have any effect because it does not trigger componentDidMount and therefore your variable allCartItems does not get reassigned.
What can you do? Well you can do a lot of stuff to enhance your code. First I will let you know about how you can solve your problem and then give you some further improvements
To solve the problem of your component not updating when you call setState you should move your jsx to the render Method. In the componentDidMount you just get all the data you need to render your component and once you have it you can set a flag for example like ready to true. Below you can see an example of how your code could look like.
import React from 'react';
class Cart extends React.Component {
constructor(props) {
super(props);
this.state = {
carts: null,
ready: false,
};
}
componentDidMount() {
fetch('www.google.com').then((carts) => {
this.setState({
carts,
ready: true,
});
});
}
render() {
const myCarts = <h2> Count {this.state.carts} </h2>;
return (
{
this.state.ready
? myCarts
: <h2> Loading... </h2>
}
);
}
}
I made you a demo with a simple counter with some explanations of your case and how you can make it work. You can check it out codesandbox. In the NotWorkingCounter you can see the same problem as in your component of the variable not being updated. In the WorkingCount you can see an example where I implemented what a I wrote above with waiting until your data has arrived and only then render it.
Some more suggestions concerning code:
Those two syntaxes below are identical. One is just a lot more concise.
class Cart extends React.Component {
constructor(props) {
super(props);
this.state = {
carts: null,
ready: false,
};
}
}
class Cart extends React.Component {
state = {
carts: null,
ready: false,
}
}
I would suggest to use arrow function if you want to bind your context. Below you can see your example simplified and an example on how you can achieve the same thing with less syntax.
export class Cart extends React.Component {
constructor(props) {
super(props);
this.minusOne = this.minusOne.bind(this);
}
minusOne(i) {
///
}
}
export class Cart extends React.Component {
constructor(props) {
super(props);
}
minusOne = (i) => {
/// minus one function
}
}
Your minusOne could also be rewritten if you use arrow functions and be a lot smaller, something in the area of
minusOne = (i) => (i) => {
let quant = self.state.quantities[i];
if(quant > 1) {
this.setState({
quantities: quant-1,
})
}
}
In your componentDidMount you call this.setState twice. Every time you call this function your component gets rerender. So what happens in your component is when your mount your component it gets rendered the first time, once it is mounted componentDidMount gets called, in there you call this.setState again twice. This means your component get's rendered in the best case three times before the user sees your component. If you get multiple promises back this means your rerender your state even more. This can create a lot of load for your component to cope with. If you rerender every component three times or more you end up having some performance issues once your application grows. Try to not call setState in your componentDidUpdate more than once.
In your case your first call to setState is totally unnecessary and just creates load. You still have access to quantities in your promise. Just call setState once at the end of your promise.then() with both elements.
In the example below you are using the index i as a key. This is not a good case practice and react should also log you at least a warning in the console. You need to use a unique identifier which is not the index. If you use the index you can get sideeffects and weird rendering which is difficult to debut. Read more on it here
then(res => {
const allCartItems = res.map((res, i) => {
const data = res.data;
return(
<div key={i} className="cart-item-container">
Another suggestion is to replace all var with const or let, as var exposes your variable to the global scope. If you don't understand what that means read this.
Last but not least have a look at object deconstruction. It can help you to clean up your code and make it more resistant to unwanted sideffects.

Lodash debounce not working in React

it would be best to first look at my code:
import React, { Component } from 'react';
import _ from 'lodash';
import Services from 'Services'; // Webservice calls
export default class componentName extends Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value || null
}
}
onChange(value) {
this.setState({ value });
// This doesn't call Services.setValue at all
_.debounce(() => Services.setValue(value), 1000);
}
render() {
return (
<div>
<input
onChange={(event, value) => this.onChange(value)}
value={this.state.value}
/>
</div>
)
}
}
Just a simple input. In the contructor it grabs value from the props (if available) at sets a local state for the component.
Then in the onChange function of the input I update the state and then try to call the webservice endpoint to set the new value with Services.setValue().
What does work is if I set the debounce directly by the onChange of the input like so:
<input
value={this.state.value}
onChange={_.debounce((event, value) => this.onChange(value), 1000)}
/>
But then this.setState gets called only every 1000 milliseconds and update the view. So typing in a textfield ends up looking weird since what you typed only shows a second later.
What do I do in a situation like this?
The problem occurs because you aren't calling the debounce function, you could do in the following manner
export default class componentName extends Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value || null
}
this.servicesValue = _.debounce(this.servicesValue, 1000);
}
onChange(value) {
this.setState({ value });
this.servicesValue(value);
}
servicesValue = (value) => {
Services.setValue(value)
}
render() {
return (
<div>
<input
onChange={(event, value) => this.onChange(value)}
value={this.state.value}
/>
</div>
)
}
}
Solution for those who came here because throttle / debounce doesn't work with FunctionComponent - you need to store debounced function via useRef():
export const ComponentName = (value = null) => {
const [inputValue, setInputValue] = useState(value);
const setServicesValue = value => Services.setValue(value);
const setServicesValueDebounced = useRef(_.debounce(setServicesValue, 1000));
const handleChange = ({ currentTarget: { value } }) => {
setInputValue(value);
setServicesValueDebounced.current(value);
};
return <input onChange={handleChange} value={inputValue} />;
};
This medium article perfectly explains what happens:
Local variables inside a function expires after every call. Every time
the component is re-evaluated, the local variables gets initialized
again. Throttle and debounce works using window.setTimeout() behind
the scenes. Every time the function component is evaluated, you are
registering a fresh setTimeout callback.
So we will use useRef() hook as value returned by useRef() does not get re-evaluated every time the functional component is executed. The only inconvenience is that you have to access your stored value via the .current property.
I've created sandbox with tiny lodash.throttle and lodash.debounce packages so you can experiment with both and choose suitable behavior
For a React functional component, debounce does not work by default. You will have to do the following for it to work:
const debouncedFunction= React.useCallback(debounce(functionToCall, 400), []);
useCallback makes use of the function returned by debounce and works as expected.
Although, this is a bit more complicated when you want to use state variables inside the debounced function (Which is usually the case).
React.useCallback(debounce(fn, timeInMs), [])
The second argument for React.useCallback is for dependencies. If you would like to use a state or prop variable in the debounced function, by default, it uses an an old version of the state variable which will cause your function to use the historical value of the variable which is not what you need.
To solve this issue, you will have to include the state variable like you do in React.useEffect like this:
React.useCallback(debounce(fn, timeInMs), [stateVariable1, stateVariable2])
This implementation might solve your purpose. But you will notice that the debounced function is called every time the state variables (stateVariable1, stateVariable2) passed as dependencies change. Which might not be what you need especially if using a controlled component like an input field.
The best solution I realized is to put some time to change the functional component to a class based component and use the following implementation:
constructor(props)
{
super();
this.state = {...};
this.functionToCall= debounce(this.functionToCall.bind(this), 400, {'leading': true});
}
I wrote a hook for those who are using react functional components.
It's typescript, but you can ignore type annotations to use on your javascript application.
| use-debounce.ts |
import { debounce, DebounceSettings } from 'lodash'
import { useRef } from 'react'
interface DebouncedArgs<T> {
delay?: number
callback?: (value: T) => void
debounceSettings?: DebounceSettings
}
export const useDebounce = <T = unknown>({ callback, debounceSettings, delay = 700 }: DebouncedArgs<T>) => {
const dispatchValue = (value: T) => callback?.(value)
const setValueDebounced = useRef(debounce(dispatchValue, delay, { ...debounceSettings, maxWait: debounceSettings?.maxWait || 1400 }))
return (value: T) => setValueDebounced.current(value)
}
| usage: |
export const MyInput: FC = () => {
const [value, setValue] = useState<string>('')
const debounce = useDebounce({ callback: onChange })
const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
const { value } = evt.currentTarget
setValue(value)
debounce(value)
}
function onChange(value: string) {
// send request to the server for example
console.log(value)
}
return <input value={value} onInput={handleOnInput} />
}
Solution for functional component - use useCallback
export const ComponentName = (value = null) => {
const [inputValue, setInputValue] = useState(value);
const setServicesValue = value => Services.setValue(value);
const setServicesValueDebounced = useCallback(_.debounce(setServicesValue, 500), []);
const handleChange = ({ currentTarget: { value } }) => {
setInputValue(value);
setServicesValueDebounced(value);
};
return <input onChange={handleChange} value={inputValue} />;
};

Categories