Reputation: 1033
I'm playing around with React Hooks and am facing a problem. It shows the wrong state when I'm trying to console log it using a button handled by event listener.
CodeSandbox: https://codesandbox.io/s/lrxw1wr97m
Why does it show the wrong state?
In first card, Button2
should display 2
cards in the console. Any ideas?
const { useState, useContext, useRef, useEffect } = React;
const CardsContext = React.createContext();
const CardsProvider = props => {
const [cards, setCards] = useState([]);
const addCard = () => {
const id = cards.length;
setCards([...cards, { id: id, json: {} }]);
};
const handleCardClick = id => console.log(cards);
const handleButtonClick = id => console.log(cards);
return (
<CardsContext.Provider
value={{ cards, addCard, handleCardClick, handleButtonClick }}
>
{props.children}
</CardsContext.Provider>
);
};
function App() {
const { cards, addCard, handleCardClick, handleButtonClick } = useContext(
CardsContext
);
return (
<div className="App">
<button onClick={addCard}>Add card</button>
{cards.map((card, index) => (
<Card
key={card.id}
id={card.id}
handleCardClick={() => handleCardClick(card.id)}
handleButtonClick={() => handleButtonClick(card.id)}
/>
))}
</div>
);
}
function Card(props) {
const ref = useRef();
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
}, []);
return (
<div className="card">
Card {props.id}
<div>
<button onClick={props.handleButtonClick}>Button1</button>
<button ref={node => (ref.current = node)}>Button2</button>
</div>
</div>
);
}
ReactDOM.render(
<CardsProvider>
<App />
</CardsProvider>,
document.getElementById("root")
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id='root'></div>
I am using React 16.7.0-alpha.0 and Chrome 70.0.3538.110
BTW, if I rewrite the CardsProvider using a сlass, the problem is gone. CodeSandbox using class: https://codesandbox.io/s/w2nn3mq9vl
Upvotes: 103
Views: 85178
Reputation: 6032
Short answer for me
this will trigger re-render ever time myvar changes.
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, []);
This ***WILL ALSO *** trigger render -> putting myvar in []
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, [myvar]);
Upvotes: 6
Reputation: 16082
To build off of Moses Gitau's great answer, if you are developing in Typescript, to resolve type errors make the hook function generic:
function useStateRef<T>(initialValue: T | (() => T)):
[T, React.Dispatch<React.SetStateAction<T>>, React.MutableRefObject<T>] {
const [value, setValue] = React.useState(initialValue);
const ref = React.useRef(value);
React.useEffect(() => {
ref.current = value;
}, [value]);
return [value, setValue, ref];
}
Upvotes: 2
Reputation: 1022
Starting from the answer of @Moses Gitau, I'm using a sligthly different one that doesn't give access to a "delayed" version of the value (which is an issue for me) and is a bit more minimalist:
import { useState, useRef } from 'react';
function useStateRef(initialValue) {
const [, setValueState] = useState(initialValue);
const ref = useRef(initialValue);
const setValue = (val) => {
ref.current = val;
setValueState(val); // to trigger the refresh
};
const getValue = (val) => {
return ref.current;
};
return [getValue , setValue];
}
export default useStateRef;
This is what I'm using
Example of usage :
const [getValue , setValue] = useStateRef(0);
const listener = (event) => {
setValue(getValue() + 1);
};
useEffect(() => {
window.addEventListener('keyup', listener);
return () => {
window.removeEventListener('keyup', listener);
};
}, []);
Edit : It now gives getValue and not the reference itself. I find it better to keep things more encapsulated in that case.
Upvotes: 1
Reputation: 223249
This is a common problem for functional components that use the useState
hook. The same concerns are applicable to any callback functions where useState
state is used, e.g. setTimeout
or setInterval
timer functions.
Event handlers are treated differently in CardsProvider
and Card
components.
handleCardClick
and handleButtonClick
used in the CardsProvider
functional component are defined in its scope. There are new functions each time it runs, they refer to cards
state that was obtained at the moment when they were defined. Event handlers are re-registered each time the CardsProvider
component is rendered.
handleCardClick
used in the Card
functional component is received as a prop and registered once on component mount with useEffect
. It's the same function during the entire component lifespan and refers to stale state that was fresh at the time when the handleCardClick
function was defined the first time. handleButtonClick
is received as a prop and re-registered on each Card
render, it's a new function each time and refers to fresh state.
A common approach that addresses this problem is to use useRef
instead of useState
. A ref is basically a recipe that provides a mutable object that can be passed by reference:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
In this case a component should be re-rendered on a state update like it's expected from useState
, refs aren't applicable.
It's possible to keep state updates and mutable state separately but forceUpdate
is considered an anti-pattern in both class and function components (listed for reference only):
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
One solution is to use a state updater function that receives fresh state instead of stale state from the enclosing scope:
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
In this case a state is needed for synchronous side effects like console.log
, a workaround is to return the same state to prevent an update.
function eventListener() {
setState(freshState => {
console.log(freshState);
return freshState;
});
}
useEffect(() => {
// register eventListener once
return () => {
// unregister eventListener once
};
}, []);
This doesn't work well with asynchronous side effects, notably async
functions.
Another solution is to re-register the event listener every time, so a callback always gets fresh state from the enclosing scope:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
return () => {
// unregister eventListener
};
}, [state]);
Unless the event listener is registered on document
, window
or other event targets that are outside of the scope of the current component, React's own DOM event handling has to be used where possible, this eliminates the need for useEffect
:
<button onClick={eventListener} />
In the last case the event listener can be additionally memoized with useMemo
or useCallback
to prevent unnecessary re-renders when it's passed as a prop:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
useState
hook implementation in React 16.7.0-alpha version but isn't workable in final React 16.8 implementation. useState
currently supports only immutable state.*Upvotes: 173
Reputation: 456
This way your callback will have updated state values always ;)
// registers an event listener to component parent
React.useEffect(() => {
const parentNode = elementRef.current.parentNode
parentNode.addEventListener('mouseleave', handleAutoClose)
return () => {
parentNode.removeEventListener('mouseleave', handleAutoClose)
}
}, [handleAutoClose])
Upvotes: 1
Reputation: 1
after changing the following line in the index.js
file the button2
works well:
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
- }, []);
+ });
you should not use []
as 2nd argument useEffect
unless you want it to run once.
more details: https://reactjs.org/docs/hooks-effect.html
Upvotes: -1
Reputation: 930
Short answer for me was that useState has a simple solution for this:
function Example() {
const [state, setState] = useState(initialState);
function update(updates) {
// this might be stale
setState({...state, ...updates});
// but you can pass setState a function instead
setState(currentState => ({...currentState, ...updates}));
}
//...
}
Upvotes: 7
Reputation: 1381
Check the console and you'll get the answer:
React Hook useEffect has a missing dependency: 'props.handleCardClick'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
Just add props.handleCardClick
to the array of dependencies and it will work correctly.
Upvotes: 3
Reputation: 1057
A much cleaner way to work around this is to create a hook I call useStateRef
function useStateRef(initialValue) {
const [value, setValue] = useState(initialValue);
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return [value, setValue, ref];
}
You can now use the ref
as a reference to the state value.
Upvotes: 48