Separate debounce for each React component instance - javascript

I'm using a debounce library (tried different ones but currently the one from lodash) in a react component in order to avoid executing code too often while scrolling in the browser.
The problem is that I have multiple instances of the react component and it seems that the debounce function is accidentally shared between those instances. Consequently the function code with '... some code here' is only executed in one instance and not in all instances of the react component. The debounce functionality works great if I have only one instance of my component rendered.
useEffect(() => {
document.querySelector(props.scrollSelector!)?.addEventListener('scroll', e => {
setViewport(props, state, e.target as HTMLDivElement, ref)
}, true)
}, [state.obj])
const setViewport = debounce((p: Props, s: State, rowHeaderObj: any, scrollContainer: HTMLDivElement, ref: any) => {
// ... some code here
}, 20)
Is there some way to change the code so the debounce function works for each instance separately? Please consider that the react component instances have unique keys assigned so that should not be the issue.

One approach could be to create a new debounced function each time you register the event listener instead of reusing the same function, in which case the event handler would be debounced independently within each instance of your component.
const _setViewport = () => (
p: Props,
s: State,
rowHeaderObj: any,
scrollContainer: HTMLDivElement,
ref: any
) => {
// ... some code here
}
const MyComponent: React.FC<Props> = (props) => {
const [state, setState] = useState<State>()
const ref = useRef<any>()
useEffect(() => {
const srollableElement = document.querySelector(props.scrollSelector!)
if (!srollableElement) {
return
}
const setViewport = debounce(_setViewport, 20)
const scrollHandler = (e: Event) =>
setViewport(props, state, e.target as HTMLDivElement, ref)
srollableElement.addEventListener('scroll', scrollHandler, true)
return () => {
srollableElement.removeEventListener('scroll', scrollHandler, true)
}
}, [state, props, ref])
return <></>
}
As a side note, be careful with this usage of useEffect, as (I think) the props parameter that's passed to your component will change each time the parent component re-renders, causing useEffect to potentially re-run very often. One fix for this is making sure the dependencies array passed to useEffect only contains primitive or stable values. Feel free to read this section of the React docs for a discussion of this topic. Taking this into consideration, you might want to re-write the above example as follows (depending on the shape of the Props type):
interface Props {
scrollSelector?: string
b: string
c: number
}
const _setViewport = () => (
p: Props,
s: State,
rowHeaderObj: any,
scrollContainer: HTMLDivElement,
ref: any
) => {
// ... some code here
}
const MyComponent: React.FC<Props> = ({ scrollSelector, b, c }) => {
const [state, setState] = useState<State>()
const ref = useRef<any>()
useEffect(() => {
if (!scrollSelector) {
return
}
const srollableElement = document.querySelector(scrollSelector)
if (!srollableElement) {
return
}
const setViewport = debounce(_setViewport, 20)
const scrollHandler = (e: Event) =>
setViewport(
{ scrollSelector, b, c },
state,
e.target as HTMLDivElement,
ref
)
srollableElement.addEventListener('scroll', scrollHandler, true)
return () => {
srollableElement.removeEventListener('scroll', scrollHandler, true)
}
}, [state, scrollSelector, b, c, ref])
return <></>
}

Related

Rerendering component and useReducer initialization

EDIT: below the line is the initial problem where I asked if my whole architecture was fine. I later edited the topic (and title) to go straight to what was my issue in order to help the community. Therefore, you might find what you want by jumping directly to the answer while skipping the main post
I am new to react and I am encountering issues back to back. I suspect that something is wrong in my pattern and would really love to have it criticized by someone a bit stronger.
Component A:
It renders (among others) a Component B
Component A has a useState hook on an array of C_obj it renders, called C_obj_arr
const [C_obj_arr, ASetter] = useState<C_obj[]>()
It provides this C_obj_arr and ASetter to Component B as properties to allow the data to go back up.
Component B
It renders each C_obj of the C_obj_arr in a list of Component C.
It has a useReducer hook that controls the states of C_obj_arr
const [C_obj_array, dispatch] = useReducer(reducer, props.C_obj_array);
It has a useEffect hook such that if C_obj_arr changes, data goes back up to Compoennt A
useEffect(() => {
ASetter(C_obj_array);
}, [C_obj_array, ASetter]);
Question: Is it fine so far to use the prop like this to init the reducer?
it also uses a useCallback hook to wrap a function that will allow getting the data back from Component C
const fn_callback = useCallback(
(c: C_obj) =>
dispatch({
kind: Kind.AN_ACTION,
payload: { wells_plan: c },
}),
[]
);
Component C
It has another useReducer that controls the states of C_obj
const [C_obj, dispatch] = useReducer(reducer, props.C_obj);
To send the information back up to Component B, it uses the function fn_callback, created in B thanks to a useEffect hook with dep on C_obj
useEffect(() => {
props.fn_callback(C_obj);
}, [C_obj, props.fn_callback]);
I hope it is not a total brain schmuck to read, I am very new to all of that so I understand I can be doing something totally broken by design.
Many thanks for help
EDIT: as requested, here is a block of code to synthetize
const A = (): JSX.Element => {
const [C_obj_arr, ASetter] = useState<C_obj[]>();
return (
<>
<B>C_obj_arr=C_obj_arr ASetter=ASetter</B>
</>
);
};
const B = (C_obj_arr_init: C_obj[], ASetter: () => void): JSX.Element => {
const [C_obj_array, dispatch] = useReducer(reducer, C_obj_arr_init);
useEffect(() => {
ASetter(C_obj_array);
}, [C_obj_array, ASetter]);
const fn_callback = useCallback(
(c_obj: C_obj) =>
dispatch({
kind: Kind.UPDATE_OBJ,
payload: { wells_plan: c_obj },
}),
[]
);
return C_obj_array.map(C_obj => (
<C C_obj={C_obj} fn_callback={fn_callback}></C>
));
};
const C = (C_obj_init, fn_callback): JSX.Element => {
const [C_obj, dispatch] = useReducer(reducer, C_obj_init);
useEffect(() => {
fn_callback(C_obj);
}, [C_obj, fn_callback]);
return <div>{C.toString()}</div>;
};
I assume, that you mean
import { useState, useEffect, useReducer, useCallback } from "react"
type SomeObj = {
name: string
key: string
}
const A = (): JSX.Element => {
const [items, setitems] = useState<SomeObj[]>([
{
key: "a",
name: "hello"
}
])
return (
<>
<B items={items} setitems={setitems} />
</>
)
}
const B = ({ items, setitems }: { items: SomeObj[]; setitems: (x: SomeObj[]) => void }): JSX.Element => {
const [items2, dispatch] = useReducer(
(
x: SomeObj[],
a: {
kind: string
payload: {}
}
) => {
return x
},
items
)
useEffect(() => {
setitems(items2)
}, [items2, setitems])
const fn_callback = useCallback(
(item: SomeObj) =>
dispatch({
kind: "update",
payload: { wells_plan: item },
}),
[]
)
return (
<div>
{items2.map((item) => (
<C itemInit={item} fn_callback={fn_callback} key={item.key}></C>
))}
</div>
)
}
const C = ({ itemInit, fn_callback }: { itemInit: SomeObj; fn_callback: (c_obj: SomeObj) => void }): JSX.Element => {
const [item, dispatch] = useReducer((x: SomeObj) => x, itemInit)
useEffect(() => {
fn_callback(item)
}, [item, fn_callback])
return <div>{item.name}</div>
}
function App() {
return (
<main>
<A></A>
</main>
)
}
export default App
And, I think, it's basically the reason for using global/atom state managers to react like: redux, recoil, jotai, etc.
In your scenario, you have a couple of prop passing layers through components, and only if I understand correctly, you try to take up changes from deeply nested component.
Using global state, you can have only one array and source of truth, and only one handler in each child component. That probably will remove all unnecessary hooks
The missing bolt for this thing to work was to handle the rerendering of component C. The problem was that useReducer initial state is applied only once, then not when the component is rendered. Therefore, there is a need to force the update of the obj_c when re-rendering it. To do that, I added an extra dispatch action in the Component C body.
useEffect(() => {
dispatch({
kind: Kind.UPDATE,
payload: {
obj_C: C_obj_init,
},
});
}, [C_obj_init]);
which associated case in the reducer is
switch (action.kind) {
case Kind.UPDATE:
return action.payload.obj_C;
An important thing point here is to not return a recreated obj_C, this wouldlead to an infinite loop to this other hook seen earlier:
useEffect(() => {
fn_callback(C_obj);
}, [C_obj, fn_callback]);

Update State of Another Component Within A Generic Component Causes Warning

When designing a generic component, I adopted the principle of SOC where the generic component will not know the implementation details of the user and allow the user to listen for callbacks to handle their own state.
For example:
SortButton.tsx
interface Props {
initialValue?: boolean;
onToggle?: (value: boolean) => void;
}
const Toggle: React.FC<Props> = ({
initialValue = false,
onToggle,
}) => {
const [isActive, setIsActive] = React.useState(initialValue);
const handleToggle = React.useCallback(() => {
let updatedValue = isActive;
// do some computation if needed
if(onToggle) {
onSort(updatedValue);
}
setIsActive(updatedValue);
}, [isActive, onToggle]);
return (
<div onClick={handleToggle}>Sort</div>
);
}
Parent.tsx
const Parent: React.FC<Props> = ({
}) => {
const [parentObject, setParentObject] = React.useState({});
const handleToggle = React.useCallback((value: boolean) => {
let updatedValue = value;
// do some computation to updatedValue if needed
setParentObject((parent) => { ...parent, calculated: updatedValue });
}, []);
return (
<SortButton onToggle={handleToggle} />
);
}
The above implementation allows the generic component (ie. Toggle) to handle their own state and allows parents to implement their own implementation using callbacks. However, I have been getting the following error:
Warning: Cannot update a component (`Parent`) while rendering a different component (`Toggle`). To locate the bad setState() call inside `Toggle`
My understanding from the error is that, within the callback onClick, it triggers state mutation of the parent (in this case, another component) which should not be allowed. The solution I figured was to move the callback within an useEffect like this:
SortButton.tsx
interface Props {
initialValue?: boolean;
onToggle?: (value: boolean) => void;
}
const Toggle: React.FC<Props> = ({
initialValue = false,
onToggle,
}) => {
const [isActive, setIsActive] = React.useState(initialValue);
React.useEffect(() => {
let updatedValue = isActive;
// do some computation if needed
if(onToggle) {
onSort(updatedValue);
}
}, [isActive, onToggle]);
const handleToggle = React.useCallback(() => {
setIsActive((value) => !value);
}, []);
return (
<div onClick={handleToggle}>Sort</div>
);
}
The new implementation works but I have a few questions which I would hope to get some guidance on.
The example works and is easy to refactor because it is a simple state of isActive. What if the value we need is more complicated (for example, mouse position, etc) and does not have a state to store the value and is only available from onMouseMove? Do we create a state to store the `mouse position and follow the pattern?
Is the existing implementation an anti-pattern to any of the React concepts in the first place?
Is there any other possible implementation to solve the issue?
This is a somewhat biased opinion, but I'm a big proponent of Lifting State Up and "Dumb" Components/Controlled Components.
I would design it so that the SortButton does not have any internal state. It would get all of the information that it needs from props. The Parent would be responsible for passing down the correct value of isActive/value, which it will update when the child SortButton calls its onToggle prop.
We can include the event in the onToggle callback just in case the parent wants to use it.
SortButton.tsx
import * as React from "react";
interface Props {
isActive: boolean;
onToggle: (value: boolean, e: React.MouseEvent<HTMLDivElement>) => void;
}
const Toggle: React.FC<Props> = ({ isActive, onToggle }) => {
return (
<div
className={isActive ? "sort-active" : "sort-inactive"}
onClick={(e) => onToggle(!isActive, e)}
>
{isActive ? "Unsort" : "Sort"}
</div>
);
};
export default Toggle;
Parent.tsx
import * as React from "react";
import SortButton from "./SortButton";
interface Props {
list: number[];
}
const Parent: React.FC<Props> = ({ list }) => {
// parent stores the state of the sort
const [isSorted, setIsSorted] = React.useState(false);
// derived data is better as a memo than as state.
const sortedList = React.useMemo(
// either sort the list or don't.
() => (isSorted ? [...list].sort() : list),
// depends on the list prop and the isSorted state.
[list, isSorted]
);
return (
<div>
<SortButton
isActive={isSorted}
// you could use a more complicated callback, but it's not needed here.
onToggle={setIsSorted}
/>
<ul>
{sortedList.map((n) => (
<li>{n.toFixed(3)}</li>
))}
</ul>
</div>
);
};
export default Parent;
Code Sandbox Demo

`useState`, only update component when values in object change

Problem
useState always triggers an update even when the data's values haven't changed.
Here's a working demo of the problem: demo
Background
I'm using the useState hook to update an object and I'm trying to get it to only update when the values in that object change. Because React uses the Object.is comparison algorithm to determine when it should update; objects with equivalent values still cause the component to re-render because they're different objects.
Ex. This component will always re-render even though the value of the payload stays as { foo: 'bar' }
const UseStateWithNewObject = () => {
const [payload, setPayload] = useState({});
useEffect(
() => {
setInterval(() => {
setPayload({ foo: 'bar' });
}, 500);
},
[setPayload]
);
renderCountNewObject += 1;
return <h3>A new object, even with the same values, will always cause a render: {renderCountNewObject}</h3>;
};
Question
Is there away that I can implement something like shouldComponentUpdate with hooks to tell react to only re-render my component when the data changes?
If I understand well, you are trying to only call setState whenever the new value for the state has changed, thus preventing unnecessary rerenders when it has NOT changed.
If that is the case you can take advantage of the callback form of useState
const [state, setState] = useState({});
setState(prevState => {
// here check for equality and return prevState if the same
// If the same
return prevState; // -> NO RERENDER !
// If different
return {...prevState, ...updatedValues}; // Rerender
});
Here is a custom hook (in TypeScript) that does that for you automatically. It uses isEqual from lodash. But feel free to replace it with whatever equality function you see fit.
import { isEqual } from 'lodash';
import { useState } from 'react';
const useMemoizedState = <T>(initialValue: T): [T, (val: T) => void] => {
const [state, _setState] = useState<T>(initialValue);
const setState = (newState: T) => {
_setState((prev) => {
if (!isEqual(newState, prev)) {
return newState;
} else {
return prev;
}
});
};
return [state, setState];
};
export default useMemoizedState;
Usage:
const [value, setValue] = useMemoizedState({ [...] });
I think we would need to see a better real life example of what you are tying to do, but from what you have shared I think the logic would need to move upstream to a point before the state gets set.
For example, you could manually compare the incoming values in a useEffect before you update state, because this is basically what you are asking if React can do for you.
There is a library use-deep-compare-effect https://github.com/kentcdodds/use-deep-compare-effect that may be of use to you in this case, taking care of a lot of the manual effort involved, but even then, this solution assumes the developer is going to manually decide (based on incoming props, etc) if the state should be updated.
So for example:
const obj = {foo: 'bar'}
const [state, setState] = useState(obj)
useEffect(() => {
// manually deep compare here before updating state
if(obj.foo === state.foo) return
setState(obj)
},[obj])
EDIT: Example using useRef if you don't use the value directly and don't need the component to update based on it:
const obj = {foo: 'bar'}
const [state, setState] = useState(obj)
const { current: payload } = useRef(obj)
useEffect(() => {
// always update the ref with the current value - won't affect renders
payload = obj
// Now manually deep compare here and only update the state if
//needed/you want a re render
if(obj.foo === state.foo) return
setState(obj)
},[obj])
Is there away that I can implement something like shouldComponentUpdate with hooks to tell react to only re-render my component when the data changes?
Commonly, for state change you compare with previous value before rendering with functional useState or a reference using useRef:
// functional useState
useEffect(() => {
setInterval(() => {
const curr = { foo: 'bar' };
setPayload(prev => (isEqual(prev, curr) ? prev : curr));
}, 500);
}, [setPayload]);
// with ref
const prev = useRef();
useEffect(() => {
setInterval(() => {
const curr = { foo: 'bar' };
if (!isEqual(prev.current, curr)) {
setPayload(curr);
}
}, 500);
}, [setPayload]);
useEffect(() => {
prev.current = payload;
}, [payload]);
For completeness, "re-render my component when the data changes?" may be referred to props too, so in this case, you should use React.memo.
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
The generic solution to this that does not involve adding logic to your effects, is to split your components into:
uncontrolled container with state that renders...
dumb controlled stateless component that has been memoized with React.memo
Your dumb component can be pure (as if it had shouldComponentUpdate implemented and your smart state handling component can be "dumb" and not worry about updating state to the same value.
Example:
Before
export default function Foo() {
const [state, setState] = useState({ foo: "1" })
const handler = useCallback(newValue => setState({ foo: newValue }))
return (
<div>
<SomeWidget onEvent={handler} />
Value: {{ state.foo }}
</div>
)
After
const FooChild = React.memo(({foo, handler}) => {
return (
<div>
<SomeWidget onEvent={handler} />
Value: {{ state.foo }}
</div>
)
})
export default function Foo() {
const [state, setState] = useState({ foo: "1" })
const handler = useCallback(newValue => setState({ foo: newValue }))
return <FooChild handler={handler} foo={state.foo} />
}
This gives you the separation of logic you are looking for.
You can use memoized components, they will re-render only on prop changes.
const comparatorFunc = (prev, next) => {
return prev.foo === next.foo
}
const MemoizedComponent = React.memo(({payload}) => {
return (<div>{JSON.stringify(payload)}</div>)
}, comparatorFunc);

How to correctly use a curried selector function with react-redux's useSelector hook?

I am using react-redux with hooks, and I need a selector that takes a parameter that is not a prop. The documentation states
The selector function does not receive an ownProps argument. However,
props can be used through closure (see the examples below) or by using
a curried selector.
However, they don't provide an example. What is the proper way to curry as described in the docs?
This is what I've done and it seems to work, but is this right? Are there implications from returning a function from the useSelector function (it seems like it would never re-render?)
// selectors
export const getTodoById = state => id => {
let t = state.todo.byId[id];
// add display name to todo object
return { ...t, display: getFancyDisplayName(t) };
};
const getFancyDisplayName = t => `${t.id}: ${t.title}`;
// example component
const TodoComponent = () => {
// get id from react-router in URL
const id = match.params.id && decodeURIComponent(match.params.id);
const todo = useSelector(getTodoById)(id);
return <span>todo.display</span>;
}
When the return value of a selector is a new function, the component will always re-render on each store change.
useSelector() uses strict === reference equality checks by default, not shallow equality
You can verify this with a super simple selector:
const curriedSelector = state => () => 0;
let renders = 0;
const Component = () => {
// Returns a new function each time
// triggers a new render each time
const value = useSelector(curriedSelector)();
return `Value ${value} (render: ${++renders})`;
}
Even if the value is always 0, the component will re-render on each store action since useSelector is unaware that we're calling the function to get the real value.
But if we make sure that useSelector receives the final value instead of the function, then the component only gets rendered on real value change.
const curriedSelector = state => () => 0;
let renders = 0;
const Component = () => {
// Returns a computed value
// triggers a new render only if the value changed
const value = useSelector(state => curriedSelector(state)());
return `Value ${value} (render: ${++renders})`;
}
Conclusion is that it works, but it's super inefficient to return a new function (or any new non-primitives) from a selector used with useSelector each time it is called.
props can be used through closure (see the examples below) or by using a curried selector.
The documentation meant either:
closure useSelector(state => state.todos[props.id])
curried useSelector(state => curriedSelector(state)(props.id))
connect is always available, and if you changed your selector a little, it could work with both.
export const getTodoById = (state, { id }) => /* */
const Component = props => {
const todo = useSelector(state => getTodoById(state, props));
}
// or
connect(getTodoById)(Component)
Note that since you're returning an Object from your selector, you might want to change the default equality check of useSelector to a shallow equality check.
import { shallowEqual } from 'react-redux'
export function useShallowEqualSelector(selector) {
return useSelector(selector, shallowEqual)
}
or just
const todo = useSelector(state => getTodoById(state, id), shallowEqual);
If you're performing costly computations in the selector or the data is deeply nested and performance becomes a problem, take a look at Olivier's answer which uses memoization.
Here is a solution, it uses memoïzation to not re-render the component on each store change :
First I create a function to make selectors, because the selector depends on the component property id, so I want to have a new selector per component instances.
The selector will prevent the component to re-render when the todo or the id prop hasn't changed.
Lastly I use useMemo because I don't want to have more than one selector per component instance.
You can see the last example of the documentation to have more information
// selectors
const makeGetTodoByIdSelector = () => createSelector(
state => state.todo.byId,
(_, id) => id,
(todoById, id) => ({
...todoById[id],
display: getFancyDisplayName(todoById[id])
})
);
const getFancyDisplayName = t => `${t.id}: ${t.title}`;
// example component
const TodoComponent = () => {
// get id from react-router in URL
const id = match.params.id && decodeURIComponent(match.params.id);
const getTodoByIdSelector = useMemo(makeGetTodoByIdSelector, []);
const todo = useSelector(state => getTodoByIdSelector(state, id));
return <span>todo.display</span>;
}
Yes, it is how it's done, simplified example:
// Curried functions
const getStateById = state => id => state.todo.byId[id];
const getIdByState = id => state => state.todo.byId[id];
const SOME_ID = 42;
const TodoComponent = () => {
// id from API
const id = SOME_ID;
// Curried
const todoCurried = useSelector(getStateById)(id);
const todoCurried2 = useSelector(getIdByState(id));
// Closure
const todoClosure = useSelector(state => state.todo.byId[id]);
// Curried + Closure
const todoNormal = useSelector(state => getStateById(state)(id));
return (
<>
<span>{todoCurried.display}</span>
<span>{todoCurried2.display}</span>
<span>{todoClosure.display}</span>
<span>{todoNormal.display}</span>
</>
);
};
Full example:
This is helper-hook useParamSelector for TypeScript, which implements the official approach of Redux Toolkit.
Hook implementation:
// Define types and create new hook
export type ParametrizedSelector<A, R> = (state: AppState, arg: A) => R;
export const proxyParam: <T>(_: AppState, param: T) => T = (_, param) => param;
export function useParamSelector<A, R>(
selectorCreator: () => ParametrizedSelector<A, R>,
argument: A,
equalityFn: (left: R, right: R) => boolean = shallowEqual
): R {
const memoizedSelector = useMemo(() => {
const parametrizedSelector = selectorCreator();
return (state: AppState) => parametrizedSelector(state, argument);
}, [typeof argument === 'object' ? JSON.stringify(argument) : argument]);
return useSelector(memoizedSelector, equalityFn);
}
Create parametrized selector:
export const selectUserById = (): ParametrizedSelector<string, User> =>
createSelector(proxyParam, selectAllUsers, (id, users) => users.find((it) => it.id === id));
And use it:
const user = useParamSelector(selectUserById, 1001); // in components
const user = selectUserById()(getState(), 1001); // in thunks
You can also use it hook with selectors created with reselect's createSelector.

Wrong React hooks behaviour with event listener

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
Click on 'Add card' button 2 times
In first card, click on Button1 and see in console that there are 2 cards in state (correct behaviour)
In first card, click on Button2 (handled by event listener) and see in console that there is only 1 card in state (wrong behaviour)
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
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.
Mutable 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();
}
State updater function
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.
Manual event listener re-registration
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]);
Built-in event handling
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]);
Previous edition of this answer suggested to use mutable state that was applicable to initial 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.*
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.
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}));
}
//...
}
Short answer for me
this WILL NOT not trigger re-render ever time myvar changes.
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, []);
This WILL trigger render -> putting myvar in []
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, [myvar]);
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.
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])
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];
}
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.
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

Categories

Resources