React State Cleared After Button Click? - reactjs

I am trying to keep track of an array of clicked IDs. Whenever I click a new ID (after clicking the first one), it just replaces the old one in the array instead of adding it to the array. I even tried to sanity check by doing the React example of incrementing a "count" state, and that just stays at 1, so clearly I am not sane LOL. Help please!
const Example = () => {
const [downloadedBatches, setDownloadedBatches] = useState<any>([]);
const [count, setCount] = useState<number>(0);
const _handleDownload = ({ currentTarget: { dataset: { id } } }: any) => {
// TODO: Add call
if (!downloadedBatches.includes(id)) {
setDownloadedBatches([...downloadedBatches, id])
}
console.log(`Downloaded Batch #${id}`)
}
const _testCount = () => setCount(count + 1);
return (
<Button icon={'download'} onClick={_handleDownload} data-id={1}/>
<Button icon={'download'} onClick={_handleDownload} data-id={2}/>
<Button icon={'download'} onClick={_handleDownload} data-id={3}/>
<Button icon={'download'} onClick={_testCount} data-id={3}/>
)
}

I don't see any overt issues with the shared code snippet but I suspect the Button component is doing something like memoizing the passed onClick handler it when it mounts. Regardless of this it's often the case in React you may be working with stale enclosures over a React state value from one render cycle when executing a callback in a later render cycle (after the state has since updated). To resolve issues like these you should use functional state updates. Functional updates are where you pass a callback function to the state updater function. When the enqueued state update is processed the previous state's value is passed to the callback to be updated from.
Example:
const _handleDownload = ({ currentTarget: { dataset: { id } } }: any) => {
setDownloadedBatches(downloadedBatches => {
if (!downloadedBatches.includes(id)) {
// shallow copy previous state and append new id value
return [...downloadedBatches, id];
}
// else just return previous state value, nothing to update
return downloadedBatches;
});
}
const _testCount = () => setCount(
// return previous state value + 1
count => count + 1
);
Code:
const Example = () => {
const [downloadedBatches, setDownloadedBatches] = useState<any>([]);
const [count, setCount] = useState<number>(0);
const _handleDownload = ({ currentTarget: { dataset: { id } } }: any) => {
// TODO: Add call
setDownloadedBatches(downloadedBatches => {
if (!downloadedBatches.includes(id)) {
return [...downloadedBatches, id];
}
return downloadedBatches;
});
console.log(`Downloaded Batch #${id}`);
}
const _testCount = () => setCount(count => count + 1);
return (
<Button icon={'download'} onClick={_handleDownload} data-id={1}/>
<Button icon={'download'} onClick={_handleDownload} data-id={2}/>
<Button icon={'download'} onClick={_handleDownload} data-id={3}/>
<Button icon={'download'} onClick={_testCount} data-id={3}/>
);
};

You can try setting the state with a Callback.
setDownloadedBatches((prevState) => [...prevState, id])

Related

Memoized functions to custom hook not picking up updated value from hook

Given a small hook defined as
const useCount = ({ trigger }) => {
const [count, setCount] = useState(1)
const increment = () => setCount(count => count + 1);
return {
data: count,
increment,
trigger,
}
}
and being consumed as
function App() {
let data;
const log = useCallback(() => {
console.log(data);
}, [data]);
const hooked = useCount({
trigger: log
});
({ data } = hooked);
const { trigger, increment } = hooked;
return (
<div className="App">
<div>{data}</div>
<button onClick={increment}>Increment</button>
<button onClick={trigger}>Log</button>
</div>
);
}
If we click on increment, the data is updated. If we click on Log, the memoized value of 1 is logged.
Q1. Is it an anti-pattern to consume data returned by the hook in the memoized callback that is itself passed to the hook?
Q2. Why does it log 1 rather than undefined. If the log fn has picked up 1, why doesn't it pick up subsequent updates?
Q3. Removing the memoization for log fn would make it work. Is there any other apporach to work around this that doesn't involve removing memoization?
A small reproduction is available in this codesandbox.

Run event handler functions synchronously after a React state change

I know useEffect allows you to run a function after state is updated.
However, I want to run different logic after a state change based on which different event handler causes a state change.
Context
I have a Parent component that shows or hides a child DialogModal component based on the [isDialogShown, setIsDialogShown] = useState(false) in Parent.
When isDialogShown
The Parent passes setIsDialogShown and 2 event handler callbacks to DialogModal: onDismiss (which adds focus to some element) and onConfirm (which adds focus to another element).
When onDismiss or onConfirm on the DialogModal is pressed, setIsDialogShown(false) should run first to hide the DialogModal, then run the respective callbacks to focus on differing elements of the page.
const Parent = () => {
const [isDialogShown, setIsDialogShown] = useState(false);
// These need to run after Dialog is closed.
// In other words, after isDialogShown state is updated to false.
const focusOnElementA = () => { .... };
const focusOnElementB = () => { .... };
const handleDismiss = () => {
setIsDialogShown(false);
focusOnElementA() // Needs to run after state has changed to close the modal
}
const handleConfirm = () => {
setIsDialogShown(false);
focusOnElementB() // Needs to run after state has changed to close the modal
}
return (
<>
<Button onClick={() => { setIsDialogShown(true) }>Open dialog</Button>
<DialogModal
isOpen={isDialogShown}
onDismiss={handleDismiss}
onConfirm={handleConfirm}
/>
</>
)
}
What's the right pattern for dealing with this scenario?
I would use a separate state for the elements A and B to trigger them by in an additional effect. Enqueueing the toggle A/B state ensures the effect handles the update to call the focus A/B handles on the next render after the modal has closed.
const Parent = () => {
const [isDialogShown, setIsDialogShown] = useState(false);
const [toggleA, setToggleA] = useState(false);
const [toggleB, setToggleB] = useState(false);
useEffect(() => {
if (toggleA) {
focusOnElementA();
setToggleA(false);
}
if (toggleB) {
focusOnElementB();
setToggleB(false);
}
}, [toggleA, toggleB]);
const focusOnElementA = () => { .... };
const focusOnElementB = () => { .... };
const handleDismiss = () => {
setIsDialogShown(false);
setToggleA(true);
}
const handleConfirm = () => {
setIsDialogShown(false);
setToggleB();
}
return (
<>
<Button onClick={() => { setIsDialogShown(true) }>Open dialog</Button>
<DialogModal
isOpen={isDialogShown}
onDismiss={handleDismiss}
onConfirm={handleConfirm}
/>
</>
)
}
A slight difference to Drew's answer but achieved using the same tools (useEffect).
// Constants for dialog state
const DIALOG_CLOSED = 0;
const DIALOG_OPEN = 1;
const DIALOG_CONFIRM = 2;
const DIALOG_CANCELLED = 3;
const Parent = () => {
// useState to keep track of dialog state
const [dialogState, setDialogState] = useState(DIALOG_CLOSED);
// Set dialog state to cancelled when dismissing.
const handleDismiss = () => {
setDialogState(DIALOG_CANCELLED);
}
// set dialog state to confirm when confirming.
const handleConfirm = () => {
setDialogState(DIALOG_CONFIRM);
}
// useEffect that triggers on dialog state change.
useEffect(() => {
// run code when confirm was selected and dialog is closed.
if (dialogState === DIALOG_CONFIRM) {
const focusOnElementB = () => { .... };
focusOnElementB()
}
// run code when cancel was selected and dialog is closed.
if (dialogState === DIALOG_CANCELLED) {
const focusOnElementA = () => { .... };
focusOnElementA()
}
}, [dialogState])
return (
<>
<Button onClick={() => { setDialogState(DIALOG_OPEN) }}>Open dialog</Button>
<DialogModal
isOpen={dialogState === DIALOG_OPEN}
onDismiss={handleDismiss}
onConfirm={handleConfirm}
/>
</>
)
}
You should add another state for which element was triggered and then trigger the effect when the states change:
const [action, setAction] = useState('');
// ...code
const handleDismiss = () => {
setAction('dismiss');
setIsDialogShown(false);
}
const handleConfirm = () => {
setAction('confirm');
setIsDialogShown(false);
}
// Add dependencies to useEffect and it will run only when the states change
useEffect(() => {
if(!isDialogShown) {
if(action === 'dismiss') {
focusOnElementA()
} else {
focusOnElementB()
}
}
}, [action, isDialogShown])

React custom Hook using useRef returns null for the first time the calling component Loads?

I have created a custom hook to scroll the element back into view when the component is scrolled.
export const useComponentIntoView = () => {
const ref = useRef();
const {current} = ref;
if (current) {
window.scrollTo(0, current.offsetTop );
}
return ref;
}
Now i am making use of this in a functional component like
<div ref={useComponentIntoView()}>
So for the first time the current always comes null, i understand that the component is still not mounted so the value is null . but what can we do to get this values always in my custom hook as only for the first navigation the component scroll doesn't work . Is there any work around to this problem .
We need to read the ref from useEffect, when it has already been assigned. To call it only on mount, we pass an empty array of dependencies:
const MyComponent = props => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return <div ref={ref} />;
};
In order to have this functionality out of the component, in its own Hook, we can do it this way:
const useComponentIntoView = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return ref;
};
const MyComponent = props => {
const ref = useComponentIntoView();
return <div ref={ref} />;
};
We could also run the useEffect hook after a certain change. In this case we would need to pass to its array of dependencies, a variable that belongs to a state. This variable can belong to the same Component or an ancestor one. For example:
const MyComponent = props => {
const [counter, setCounter] = useState(0);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, [counter]);
return (
<div ref={ref}>
<button onClick={() => setCounter(counter => counter + 1)}>
Click me
</button>
</div>
);
};
In the above example each time the button is clicked it updates the counter state. This update triggers a new render and, as the counter value changed since the last time useEffect was called, it runs the useEffect callback.
As you mention, ref.current is null until after the component is mounted. This is where you can use useEffect - which will fire after the component is mounted, i.e.:
const useComponentIntoView = () => {
const ref = useRef();
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop );
}
});
return ref;
}

React Hook useEffect has a missing dependency: 'dispatch'

This is my first time working with react js , im trying to remove the alert when leaving this view cause i don't want to show it on the other view but in case that there is no error i want to keep the success alert to show it when i'm gonna redirect to the other view
but im getting this wearning on google chrome
Line 97:6: React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array react-hooks/exhaustive-deps
if i did include dispatch i get infinite loop
const [state, dispatch] = useUserStore();
useEffect(() => {
let token = params.params.token;
checktoken(token, dispatch);
}, [params.params.token]);
useEffect(() => {
return () => {
if (state.alert.msg === "Error") {
dispatch({
type: REMOVE_ALERT
});
}
};
}, [state.alert.msg]);
//response from the api
if (!token_valide || token_valide_message === "done") {
return <Redirect to="/login" />;
}
this is useUserStore
const globalReducers = useCombinedReducers({
alert: useReducer(alertReducer, alertInitState),
auth: useReducer(authReducer, authInitState),
register: useReducer(registerReducer, registerInitState),
token: useReducer(passeditReducer, tokenvalidationInitState)
});
return (
<appStore.Provider value={globalReducers}>{children}</appStore.Provider>
);
};
export const useUserStore = () => useContext(appStore);
UPDATE 09/11/2020
This solution is no longer needed on eslint-plugin-react-hooks#4.1.0 and above.
Now useMemo and useCallback can safely receive referential types as dependencies.#19590
function MyComponent() {
const foo = ['a', 'b', 'c']; // <== This array is reconstructed each render
const normalizedFoo = useMemo(() => foo.map(expensiveMapper), [foo]);
return <OtherComponent foo={normalizedFoo} />
}
Here is another example of how to safely stabilize(normalize) a callback
const Parent = () => {
const [message, setMessage] = useState('Greetings!')
return (
<h3>
{ message }
</h3>
<Child setter={setMessage} />
)
}
const Child = ({
setter
}) => {
const stableSetter = useCallback(args => {
console.log('Only firing on mount!')
return setter(args)
}, [setter])
useEffect(() => {
stableSetter('Greetings from child\'s mount cycle')
}, [stableSetter]) //now shut up eslint
const [count, setCount] = useState(0)
const add = () => setCount(c => c + 1)
return (
<button onClick={add}>
Rerender {count}
</button>
)
}
Now referential types with stable signature such as those provenients from useState or useDispatch can safely be used inside an effect without triggering exhaustive-deps even when coming from props
---
Old answer
dispatch comes from a custom hook so it doesn't have an stable signature therefore will change on each render (reference equality). Add an aditional layer of dependencies by wrapping the handler inside an useCallback hook
const [foo, dispatch] = myCustomHook()
const stableDispatch = useCallback(dispatch, []) //assuming that it doesn't need to change
useEffect(() =>{
stableDispatch(foo)
},[stableDispatch])
useCallback and useMemo are helper hooks with the main purpose off adding an extra layer of dependency check to ensure synchronicity. Usually you want to work with useCallback to ensure a stable signature to a prop that you know how will change and React doesn't.
A function(reference type) passed via props for example
const Component = ({ setParentState }) =>{
useEffect(() => setParentState('mounted'), [])
}
Lets assume you have a child component which uppon mounting must set some state in the parent (not usual), the above code will generate a warning of undeclared dependency in useEffect, so let's declare setParentState as a dependency to be checked by React
const Component = ({ setParentState }) =>{
useEffect(() => setParentState('mounted'), [setParentState])
}
Now this effect runs on each render, not only on mounting, but on each update. This happens because setParentState is a function which is recreated every time the function Component gets called. You know that setParentState won't change it's signature overtime so it's safe to tell React that. By wrapping the original helper inside an useCallback you're doing exactly that (adding another dependency check layer).
const Component = ({ setParentState }) =>{
const stableSetter = useCallback(() => setParentState(), [])
useEffect(() => setParentState('mounted'), [stableSetter])
}
There you go. Now React knows that stableSetter won't change it's signature inside the lifecycle therefore the effect do not need too run unecessarily.
On a side note useCallback it's also used like useMemo, to optmize expensive function calls (memoization).
The two mai/n purposes of useCallback are
Optimize child components that rely on reference equality to prevent unnecessary
renders. Font
Memoize expensive calculations
I think you can solve the problem at the root but that means changing useCombinedReducers, I forked the repo and created a pull request because I don't think useCombinedReducers should return a new reference for dispatch every time you call it.
function memoize(fn) {
let lastResult,
//initial last arguments is not going to be the same
// as anything you will pass to the function the first time
lastArguments = [{}];
return (...currentArgs) => {
//returning memoized function
//check if currently passed arguments are the same as
// arguments passed last time
const sameArgs =
currentArgs.length === lastArguments.length &&
lastArguments.reduce(
(result, lastArg, index) =>
result && Object.is(lastArg, currentArgs[index]),
true,
);
if (sameArgs) {
//current arguments are same as last so just
// return the last result and don't execute function
return lastResult;
}
//current arguments are not the same as last time
// or function called for the first time, execute the
// function and set last result
lastResult = fn.apply(null, currentArgs);
//set last args to current args
lastArguments = currentArgs;
//return result
return lastResult;
};
}
const createDispatch = memoize((...dispatchers) => action =>
dispatchers.forEach(fn => fn(action)),
);
const createState = memoize(combinedReducers =>
Object.keys(combinedReducers).reduce(
(acc, key) => ({ ...acc, [key]: combinedReducers[key][0] }),
{},
),
);
const useCombinedReducers = combinedReducers => {
// Global State
const state = createState(combinedReducers);
const dispatchers = Object.values(combinedReducers).map(
([, dispatch]) => dispatch,
);
// Global Dispatch Function
const dispatch = createDispatch(...dispatchers);
return [state, dispatch];
};
export default useCombinedReducers;
Here is a working example:
const reduceA = (state, { type }) =>
type === 'a' ? { count: state.count + 1 } : state;
const reduceC = (state, { type }) =>
type === 'c' ? { count: state.count + 1 } : state;
const state = { count: 1 };
function App() {
const [a, b] = React.useReducer(reduceA, state);
const [c, d] = React.useReducer(reduceC, state);
//memoize what is passed to useCombineReducers
const obj = React.useMemo(
() => ({ a: [a, b], c: [c, d] }),
[a, b, c, d]
);
//does not do anything with reduced state
const [, reRender] = React.useState();
const [s, dispatch] = useCombinedReducers(obj);
const rendered = React.useRef(0);
const [sc, setSc] = React.useState(0);
const [dc, setDc] = React.useState(0);
rendered.current++;//display how many times this is rendered
React.useEffect(() => {//how many times state changed
setSc(x => x + 1);
}, [s]);
React.useEffect(() => {//how many times dispatch changed
setDc(x => x + 1);
}, [dispatch]);
return (
<div>
<div>rendered {rendered.current} times</div>
<div>state changed {sc} times</div>
<div>dispatch changed {dc} times</div>
<button type="button" onClick={() => reRender({})}>
re render
</button>
<button
type="button"
onClick={() => dispatch({ type: 'a' })}
>
change a
</button>
<button
type="button"
onClick={() => dispatch({ type: 'c' })}
>
change c
</button>
<pre>{JSON.stringify(s, undefined, 2)}</pre>
</div>
);
}
function memoize(fn) {
let lastResult,
//initial last arguments is not going to be the same
// as anything you will pass to the function the first time
lastArguments = [{}];
return (...currentArgs) => {
//returning memoized function
//check if currently passed arguments are the same as
// arguments passed last time
const sameArgs =
currentArgs.length === lastArguments.length &&
lastArguments.reduce(
(result, lastArg, index) =>
result && Object.is(lastArg, currentArgs[index]),
true
);
if (sameArgs) {
//current arguments are same as last so just
// return the last result and don't execute function
return lastResult;
}
//current arguments are not the same as last time
// or function called for the first time, execute the
// function and set last result
lastResult = fn.apply(null, currentArgs);
//set last args to current args
lastArguments = currentArgs;
//return result
return lastResult;
};
}
const createDispatch = memoize((...dispatchers) => action =>
dispatchers.forEach(fn => fn(action))
);
const createState = memoize(combinedReducers =>
Object.keys(combinedReducers).reduce(
(acc, key) => ({
...acc,
[key]: combinedReducers[key][0],
}),
{}
)
);
const useCombinedReducers = combinedReducers => {
// Global State
const state = createState(combinedReducers);
const dispatchers = Object.values(combinedReducers).map(
([, dispatch]) => dispatch
);
// Global Dispatch Function
const dispatch = createDispatch(...dispatchers);
return [state, dispatch];
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

changes to state issued from custom hook not causing re-render even though added to useEffect

I have a custom hook that keeps a list of toggle states and while I'm seeing the internal state aligning with my expectations, I'm wondering why a component that listens to changes on the state kept by this hook isn't re-rendering on change. The code is as follows
const useToggle = () => {
const reducer = (state, action) => ({...state, ...action});
const [toggled, dispatch] = useReducer(reducer, {});
const setToggle = i => {
let newVal;
if (toggled[i] == null) {
newVal = true;
} else {
newVal = !toggled[i];
}
dispatch({...toggled, [i]: newVal});
console.log('updated toggled state ...', toggled);
};
return {toggled, setToggle};
};
const Boxes = () => {
const {setToggle} = useToggle()
return Array.from({length: 8}, el => null).map((el,i) =>
<input type="checkbox" onClick={() => setToggle(i)}/>)
}
function App() {
const {toggled} = useToggle()
const memoized = useMemo(() => toggled, [toggled])
useEffect(() => {
console.log('toggled state is >>>', toggled) // am not seeing this on console after changes to toggled
}, [toggled])
return (
<div className="App">
<Boxes />
</div>
);
}
It's because you are using useToggle twice.
once in the App
another one in the Boxes.
When you dispatch the action in Boxes, it's updating the toggled instance for Boxes (which is not retrieved in it).
Think of your custom hook like how you use useState. When you use useState, each component gets its own state. Same goes for the custom hook.
So there are a few ways you can address the issue.
Pass the setToggle from App to Boxes via prop-drilling
Use Context API (or Redux or other statement management library to pass
setToggle instance in the App component down)
Here is an example of prop-drilling.
You can follow along
const Boxes = ({ setToggle }) => {
// const { setToggle } = useToggle();
return Array.from({ length: 8 }, el => null).map((el, i) => (
<input key={i} type="checkbox" onClick={() => setToggle(i)} />
));
};
function App() {
const { toggled, setToggle } = useToggle();
useEffect(() => {
console.log("toggled state is >>>", toggled); // am not seeing this on console after changes to toggled
}, [toggled]);
return (
<div className="App">
<Boxes setToggle={setToggle} />
</div>
);
}
Note: that I added key props in Boxes using the index i(and it is a bad practice by the way)
You can see that it's now working as you'd expect.

Resources