I'm trying to access React state inside a function enclosed inside of useRef. However, even with a helper function bound to App to access the state, the state never updates inside of the useRef function.
getCount outside 0
// after clicking, getCount inside is still 0, even though count now equals 1
getCount outside 1
getCount inside 0
import React, { useState, useRef } from 'react'
import ReactDOM from 'react-dom'
const App = function () {
const [count, setCount] = useState(0)
const getCount = function () {
return count
}.bind(App)
console.log('getCount outside', getCount())
const onClick = useRef(() => {
console.log('getCount inside', getCount())
})
return (
<>
<div onClick={() => setCount(count + 1)}>
increment count
</div>
<div onClick={onClick.current}>
{count}
</div>
</>
)
}
const wrapper = document.getElementById('root')
ReactDOM.render(<App />, wrapper)
The argument passed to useRef is only considered when the component mounts. Only at that time is the value assigned to the ref; it won't update when the component re-renders.
When the component mounts, the count variable that the ref's function closes over is the initial state value, which is 0. No matter how many times the component re-renders, the ref's function, if not reassigned, will still close over that original state value.
If you want the ref's function to result in an up-to-date value, assign it anew each time there's a re-render.
// assign nothing notable on mount, just create a ref
const onClickRef = useRef();
// on mount and on re-render, assign a function
// with an up-to-date reference to the state variable
onClickRef.current = () => {
console.log(count);
};
Though, in React, usually it'd be better to pass the state itself down and use it, rather than a ref - refs are generally for things that you can't accomplish using the more functional tools provided by React.
Using callback with previousValue in setter solves this problem:
const onClick = useRef(() => {
console.log(count); <----- always 0 (initial state)
setCount((previousValue)=> {
console.log(previousValue); <---- correct current value each time
return previousValue+1;
}
})
Source: idea was from #bogdanoff's first comment from the question. Upvoted.
Related
The gap variable should just give a difference once, but it gives a diff every second. I am not even updating its state. Even if i use settime in useEffect still the other variable that are changing in background are still effecting the page and are updating.
const ftime = dayjs('Dec 31,2021').unix();
const dateVar = dayjs().unix();
const gap = ftime - dateVar;
const [time, settime] = useState(dayjs().format('DD/MM/YYYY'));
useEffect(() => {
setInterval(() => settime(dayjs().second()), 1000);
}, []);
return (
<div className="App">
<div>{time}</div>
<div>{gap}</div>
</div>
)
useEffect(() => {
setInterval(() => settime(dayjs().second()), 1000)
}, [])
settime is invoked for every 1000ms. This will update the state (time) and trigger a rerender.
const ftime = dayjs('Dec 31,2021').unix();
const dateVar = dayjs().unix();
const gap = ftime - dateVar;
As these three variables are initialised at the top level of the function, the rerender will initialise them every time.
If you want to prevent this, you can move the variables outside the function component.
It is updating every second because you change time every second, which the component renders.
This rerendering will also cause the constants ftime and dateVar to reinitialise every second. If this is not intended, you need to put them outside the function or wrap them in a hook, such as useState or useMemo.
You can solve the rerendering issue by making the time a React component and placing the interval effect in that component. A child's rerendering normally doesn't trigger a rerender in the parent.
This is because in useEffect you are updating the state every second. When a state updates in a react function component the variables will be re-initialized and react doesn't track the values of the variables. It only keeps track on state and props.
Here you can make use of useRef hook since its is mutable and react does not re-initialize its value, which guarantees its value to be same during state update
import { useState, useEffect, useRef } from "react";
import dayjs from "dayjs";
import ReactDOM from "react-dom";
function App() {
const gap = useRef(dayjs("Dec 31,2021").unix() - dayjs().unix());
const [time, settime] = useState(dayjs().format("DD/MM/YYYY"));
useEffect(() => {
setInterval(() => settime(dayjs().second()), 1000);
}, []);
return (
<div className="App">
<div> {time}</div>
**<div>{gap.current}</div>**
</div>
);
}
Here's my react component and my reducer function:
const testReducer = (state) => {
const newState = {...state}
newState.counts[0] += 1
return newState
}
function App() {
const [countState, dispatchCount] = useReducer(testReducer, {counts: [0]})
return (
<div className="App">
<h1>{countState.counts[0]}</h1>
<button onClick={dispatchCount}>up</button>
</div>
);
}
When the button is clicked and the reducer is executed, I expect the count displayed in the H1 to increment by one. This happens when the button is clicked the first time, but every subsequent click increments it by 2.
This happens no matter what the count is initialized to. If the value I'm incrementing is not in an array, it works normally.
Can anyone tell me why this is happening?
Issue
newState.counts[0] = += 1 isn't valid syntax. Assuming you meant newState.counts[0] += 1 then you are mutating the state object.
const testReducer = (state) => {
const newState = {...state}
newState.counts[0] += 1 // <-- mutates newState.counts!!
return newState
}
In all likelihood this mutation is being exposed in your app by being rendered within a React.StrictMode component.
StrictMode - Detecting unexpected side effects
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer <-- this
Solution
Even though you are shallow copying state you still need to return a new counts array reference.
const testReducer = (state) => {
const newState = {
...state,
counts: [state.counts[0] + 1]
};
return newState;
};
Issue
I have a child component that gets some button id-name configs as props, renders selectable HTML buttons according to those configs and returns the selected button's value(id) to the callback function under a useEffect hook. However it causes an infinite render loop because I need to pass the props as a dependency array. Note that React.js wants me to destructure props, but it still causes an infinite render loop even if I do that.
Child Component
import React, {createRef, useState, useEffect} from "react";
const OptionButton = ({ buttons, buttonClass, callback }) => {
const [value, setValue] = useState()
const refArray = []
const buttonWidth = ((100 - (Object.keys(buttons).length - 1)) - ((100 - (Object.keys(buttons).length - 1)) % Object.keys(buttons).length)) / Object.keys(buttons).length
useEffect(() => {
if (callback) {
callback(value);
}
}, [value, callback])
const select = (event) => {
event.target.style.backgroundColor = "#10CB81"
refArray.forEach((currentRef) => {
if (currentRef.current.id !== event.target.id) {
currentRef.current.style.backgroundColor = "#F5475D"
}
})
setValue(event.target.id)
}
return(
<span style={{display: "flex", justifyContent: "space-between"}}>
{Object.entries(buttons).map((keyvalue) => {
const newRef = createRef()
refArray.push(newRef)
return <button ref={newRef} id={keyvalue[0]} key={keyvalue[0]} className={buttonClass} onClick={select} style={{width: `${buttonWidth}%`}}>{keyvalue[1]}</button>
})}
</span>
)
}
export default OptionButton
So as you can see here my child component gets button configs as key-value (button value-button name) pairs, renders these buttons and when user clicks one of these buttons it gets the id of that button, sets it to 'value' constant using useState hook and then passes that value to parent component callback.
Parent Component
return(
<OptionButton buttons={{"ButtonValue": "ButtonName", "Button2Value": "Button2Name"}} callback={(value) => this.setState({buttonState: value})} buttonClass="buttonclass"/>
)
It's just fine if I don't use this.setState at the callback function. For example if I just do
(value) => console.log(value)
there is no infinite loop. As far as I can tell it only happens if I try to use setState.
What am I doing wrong? How can I fix this? Thanks in advance.
The reason why there is infinite loop is because you have callback as dependency in useEffect. And what you are passing to the component is you pass a new callback function on each new render, hence it always enters useEffect because of that. Since you are using classes consider passing instance method as callback instead of arrow function as here.
Also I think you are overusing refs. You could also achieve what you are doing by just storing say the id of clicked button, and then dynamically styling all buttons, e.g. inside map if id of current button is same as the one stored, use #10CB81 bg color in style object, otherwise different one.
Also there are better ways to check which btn was clicked, see here.
I'm having trouble to lift the state up in that case:
I have a button which on each click adds element to 'mylist' through 'setMyList'. These elements are 'Child' components (the div className "site" is just a CSS square), and I want that each time I click on one of those components/elements it increments 'count' thanks to 'setCount' in the callback that is given to the 'Child' component in 'setMyList'.
But what happen is that, for instance, I create a square then click on it, 'count' doesn't go further than 1, then with another square created, clicking on it makes 'count' goes up to 2 but not more, and so on.
And when I click back on the first square 'count' is 1, it looks like the state is saved for each 'Child' components instead of incrementing it.
Here are my components:
import React, { useState } from 'react'
import Child from './Child.js'
function Parent() {
const [count, setCount] = useState(0);
const [mylist, setMyList] = useState([])
function increment() {
console.log("TEST")
setCount(count + 1)
}
return (
<div className="App">
<button onClick={() => setMyList([...mylist, <Child onClick={increment}></Child>])}> </button>
<h2>count {count}</h2>
{mylist}
(count should be updated from child)
</div>
);
}
export default Parent
Note that each time a square is clicked on, It does go in 'increment' function because the console log displays, but it goes through setCount withtout doing anything.
import React, { useState } from 'react'
import "./Site.css"
function Child(props) {
return (
<div>
<div className="Site" onClick={props.onClick}></div>
</div>
)
}
export default Child
Thanks in advance
You should use setCount((count) => count + 1);
This will ensure that setCount can get the latest count value every time.
This is because every time you push Child, the increment function will be recreated, and then the count in the increment function is actually the value when the increment function was created.
In addition, you can use React.useCallback to optimize this.
const increment = React.useCallback(() => {
setCount((count) => count + 1);
}, []);
I'm still getting my head around react hooks but struggling to see what I'm doing wrong here. I have a component for resizing panels, onmousedown of an edge I update a value on state then have an event handler for mousemove which uses this value however it doesn't seem to be updating after the value has changed.
Here is my code:
export default memo(() => {
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
console.log(activePoint); // is null but should be 'top|bottom|left|right'
};
const resizerMouseDown = (e, point) => {
setActivePoint(point); // setting state as 'top|bottom|left|right'
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', cleanup); // removed for clarity
};
return (
<div className="interfaceResizeHandler">
{resizePoints.map(point => (
<div
key={ point }
className={ `interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${ point }` }
onMouseDown={ e => resizerMouseDown(e, point) }
/>
))}
</div>
);
});
The problem is with the handleResize function, this should be using the latest version of activePoint which would be a string top|left|bottom|right but instead is null.
How to Fix a Stale useState
Currently, your issue is that you're reading a value from the past. When you define handleResize it belongs to that render, therefore, when you rerender, nothing happens to the event listener so it still reads the old value from its render.
There are a several ways to solve this. First let's look at the most simple solution.
Create your function in scope
Your event listener for the mouse down event passes the point value to your resizerMouseDown function. That value is the same value that you set your activePoint to, so you can move the definition of your handleResize function into resizerMouseDown and console.log(point). Because this solution is so simple, it cannot account for situations where you need to access your state outside of resizerMouseDown in another context.
See the in-scope function solution live on CodeSandbox.
useRef to read a future value
A more versatile solution would be to create a useRef that you update whenever activePoint changes so that you can read the current value from any stale context.
const [activePoint, _setActivePoint] = React.useState(null);
// Create a ref
const activePointRef = React.useRef(activePoint);
// And create our custom function in place of the original setActivePoint
function setActivePoint(point) {
activePointRef.current = point; // Updates the ref
_setActivePoint(point);
}
function handleResize() {
// Now you'll have access to the up-to-date activePoint when you read from activePointRef.current in a stale context
console.log(activePointRef.current);
}
function resizerMouseDown(event, point) {
/* Truncated */
}
See the useRef solution live on CodeSandbox.
Addendum
It should be noted that these are not the only ways to solve this problem, but these are my preferred methods because the logic is more clear to me despite some of the solutions being longer than other solutions offered. Please use whichever solution you and your team best understand and find to best meet your specific needs; don't forget to document what your code does though.
You have access to current state from setter function, so you could make it:
const handleResize = () => {
setActivePoint(activePoint => {
console.log(activePoint);
return activePoint;
})
};
useRef for the callback
A similar approach to Andria's can be taken by using useRef to update the event listener's callback itself instead of the useState value. This allows you to use many up-to-date useState values inside one callback with only one useRef.
If you create a ref with useRef and update its value to the handleResize callback on every render, the callback stored in the ref will always have access to up-to-date useState values, and the handleResize callback will be accessible to any stale callbacks like event handlers.
function handleResize() {
console.log(activePoint);
}
// Create the ref,
const handleResizeRef = useRef(handleResize);
// and then update it on each re-render.
handleResizeRef.current = handleResize;
// After that, you can access it via handleResizeRef.current like so
window.addEventListener("mousemove", event => handleResizeRef.current());
With this in mind, we can also abstract away the creation and updating of the ref into a custom hook.
Example
See it live on CodeSandbox.
/**
* A custom hook that creates a ref for a function, and updates it on every render.
* The new value is always the same function, but the function's context changes on every render.
*/
function useRefEventListener(fn) {
const fnRef = useRef(fn);
fnRef.current = fn;
return fnRef;
}
export default memo(() => {
const [activePoint, setActivePoint] = useState(null);
// We can use the custom hook declared above
const handleResizeRef = useRefEventListener((event) => {
// The context of this function will be up-to-date on every re-render.
console.log(activePoint);
});
function resizerMouseDown(event, point) {
setActivePoint(point);
// Here we can use the handleResizeRef in our event listener.
function handleResize(event) {
handleResizeRef.current(event);
}
window.addEventListener("mousemove", handleResize);
// cleanup removed for clarity
window.addEventListener("mouseup", cleanup);
}
return (
<div className="interfaceResizeHandler">
{resizePoints.map((point) => (
<div
key={point}
className={`interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${point}`}
onMouseDown={(event) => resizerMouseDown(event, point)}
/>
))}
</div>
);
});
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
setActivePoint(currentActivePoint => { // call set method to get the value
console.log(currentActivePoint);
return currentActivePoint; // set the same value, so nothing will change
// or a different value, depends on your use case
});
};
Just small addition to the awe ChrisBrownie55's advice.
A custom hook can be implemented to avoid duplicating this code and use this solution almost the same way as the standard useState:
// useReferredState.js
import React from "react";
export default function useReferredState(initialValue) {
const [state, setState] = React.useState(initialValue);
const reference = React.useRef(state);
const setReferredState = value => {
reference.current = value;
setState(value);
};
return [reference, setReferredState];
}
// SomeComponent.js
import React from "react";
const SomeComponent = () => {
const [someValueRef, setSomeValue] = useReferredState();
// console.log(someValueRef.current);
};
For those using typescript, you can use this function:
export const useReferredState = <T>(
initialValue: T = undefined
): [T, React.MutableRefObject<T>, React.Dispatch<T>] => {
const [state, setState] = useState<T>(initialValue);
const reference = useRef<T>(state);
const setReferredState = (value) => {
reference.current = value;
setState(value);
};
return [state, reference, setReferredState];
};
And call it like that:
const [
recordingState,
recordingStateRef,
setRecordingState,
] = useReferredState<{ test: true }>();
and when you call setRecordingState it will automatically update the ref and the state.
You can make use of the useEffect hook and initialise the event listeners every time activePoint changes. This way you can minimise the use of unnecessary refs in your code.
When you need to add event listener on component mount
Use, useEffect() hook
We need to use the useEffect to set event listener and cleanup the same.
The use effect dependency list need to have the state variables which are being used in event handler. This will make sure handler don't access any stale event.
See the following example. We have a simple count state which gets incremented when we click on given button. Keydown event listener prints the same state value. If we remove the count variable from the dependency list, our event listener will print the old value of state.
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const clickHandler = () => {
console.log({ count });
setCount(c => c + 1);
}
useEffect(() => {
document.addEventListener('keydown', normalFunction);
//Cleanup function of this hook
return () => {
document.removeEventListener('keydown', normalFunction);
}
}, [count])
return (
<div className="App">
Learn
<button onClick={clickHandler}>Click me</button>
<div>{count}</div>
</div>
);
}
export default App;