React/Redux Multiple referrals are carried out with a single click - reactjs

When I add const serverTime = useSelector(state => state.serverTime);, when I click once, dispatch occurs multiple times. But this problem disappears when I remove the const serverTime = useSelector(state => state.serverTime); code. What would be the reason? What am I doing wrong?
Component.js
import { useSelector, useDispatch } from "react-redux";
const Buy = () => {
const dispatch = useDispatch();
const serverTime = useSelector(state => state.serverTime);
document.body.addEventListener("click", function() {
dispatch(SERVER_TIME(Date.now()))
})
return (
<main>
...
</main>
)
}
Action.js
export const SERVER_TIME = (serverTime) => ({
type: "SERVER_TIME",
serverTime: serverTime
})
Reducer.js
let initialState = {
serverTime: ""
}
const serverTime = (state = initialState, action) => {
switch (action.type) {
case "SERVER_TIME":
return {
...state,
serverTime: action.serverTime
}
default:
return state
}
}
export default serverTime;

The problem is not really the serverTime, although that is triggering the problem. The real problem is how you are setting up your event listener.
In your Buy component, you are adding an event listener every time the component is updated rather than just once. As a result, whenever state changes (like whenever you update the server time), react will add another event listener. To only add one event listener, you should wrap it in useEffect inside of your Buy component.
The first argument to useEffect is a function to run on first render, and the second argument is an array of dependencies that will trigger reruns of that function when those dependencies change. In your case, since you only want this to run once, you can use an empty array so that nothing will trigger a rerun.
The function argument in useEffect can also return a function, which will be used to "clean up" when your component is unmounted. This is useful for unsubscribing to event listeners. So for example:
useEffect( () => {
// Define what should happen on click.
function onClickStuff(){
dispatch(SERVER_TIME(Date.now()))
}
// Add the event listener to the document.
document.body.addEventListener("click", onClickStuff)
return () => {
// Remove the event listener on unmount.
document.body.removeEventListner("click", onClickStuff)
}
}, [] /* No dependencies, so useEffect will only run once. */ )

Related

React - set state doesn't change in callback function

I'm not able to read current state inside refreshWarehouseCallback function. Why?
My component:
export function Schedules({ tsmService, push, pubsub }: Props) {
const [myState, setMyState] = useState<any>(initialState);
useEffect(() => {
service
.getWarehouses()
.then((warehouses) =>
getCurrentWarehouseData(warehouses) // inside of this function I can without problems set myState
)
.catch(() => catchError());
const pushToken = push.subscribe('public/ttt/#');
const pubSubToken = pubsub.subscribe(
'push:ttt.*',
refreshWarehouseCallback // HERE IS PROBLEM, when I try to read current state from this function I get old data, state changed in other functions cannot be read in thi function
);
return () => {
pubsub.unsubscribe(pubSubToken);
push.unsubscribe(pushToken);
};
}, []);
...
function refreshWarehouseCallback(eventName: string, content: any) {
const {warehouseId} = myState; // undefined!!!
case pushEvents.ramp.updated: {
}
}
return (
<Views
warehouses={myState.warehouses}
allRamps={myState.allRamps}
currentWarehouse={myState.currentWarehouse}
pending={myState.pending}
error={myState.error}
/>
I have to use useRef to store current state additionally to be able to rerender the whole component.
My question is - is there any other solution without useRef? Where is the problem? Calback function doesn't work with useState hook?
Your pub/sub pattern does not inherit React's states. Whenever subscribe is triggered, and your callback function is initialized, that callback will not get any new values from myState.
To be able to use React's states, you can wrap refreshWarehouseCallback into another function like below
//`my state` is passed into the first function (the function wrapper)
//the inner function is your original function
const refreshWarehouseCallback =
(myState) => (eventName: string, content: any) => {
const { warehouseId } = myState;
//your other logic
};
And then you can add another useEffect to update subscribe after state changes (in this case, myState updates)
//a new state to store the updated pub/sub after every clean-up
const [pubSubToken, setPubSubToken] = useState();
useEffect(() => {
//clean up when your state updates
if (pubSubToken) {
pubsub.unsubscribe(pubSubToken);
}
const updatedPubSubToken = pubsub.subscribe(
"push:ttt.*",
refreshWarehouseCallback(myState) //execute the function wrapper to pass `myState` down to your original callback function
);
//update new pub/sub token
setPubSubToken(updatedPubSubToken);
return () => {
pubsub.unsubscribe(updatedPubSubToken);
};
//add `myState` as a dependency
}, [myState]);
//you can combine this with your previous useEffect
useEffect(() => {
const pushToken = push.subscribe("public/ttt/#");
return () => {
pubsub.unsubscribe(pushToken);
};
}, []);

React useEffect to have different behavior on first load and subsequent updates

I am using React with typescript and I want to convert my class component to a functional component, but my class component has two different componentDidMount and comonentDidUpdate behaviors:
componentDidMount() {
this.props.turnResetOff();
}
componentDidUpdate() {
if (this.props.resetForm) {
this.resetChangeForm();
this.props.turnResetOff();
}
}
I just want my form to reset every time it loads except the first time, because I have a menu drop-down that allows clearing the form but I want the data to not reset on mount.
I tried using this: componentDidMount equivalent on a React function/Hooks component?
const turnResetOff = props.turnResetOff;// a function
const resetForm = props.resetForm;
const setPersonId = props.setPersonId;// a function
useEffect(() => {
turnResetOff();
}, [turnResetOff]);
useEffect(() => {
const resetChangeForm = () => {/*definition*/};
if (resetForm) {
resetChangeForm();
turnResetOff();
}
}, [resetForm, turnResetOff, setPersonId]);
However, this causes an infinite re-render. Even if I useCallback for turnResetOff:
turnResetOff={useCallback(() => {
if (shouldReset) {
setShouldReset(false);
}
}, [shouldReset])}
I also tried using useRef to count the number of times this has been rendered, with the same result (infinite rerender - this is a simplified version even).
const [shouldReset, setShouldReset] = useState<boolean>(false);
const mountedTrackerRef = useRef(false);
useEffect(() => {
if (mountedTrackerRef.current === false) {
console.log("mounted now!");
mountedTrackerRef.current = true;
// props.turnResetOff();
setShouldReset(false);
} else {
console.log("mounted already... updating");
// if (props.resetForm) {
if (shouldReset) {
// resetChangeForm();
// props.turnResetOff();
setShouldReset(false);
}
}
}, [mountedTrackerRef, shouldReset]);
When you call useEffect() you can return a clean-up function. That cleanup function gets called when the component is unmounted. So, perhaps what you want to do is this: when you are called with turnResetOff then call it, and return a function that calls turnResetOff. The return function will be called when the component unmounts, so next time the component mounts it won't reset.
Something to along these lines:
useEffect(
() => {
turnResetOff()
return () => {setShouldReset(false)}
}
,[turnResetOff, setShouldReset])
Using the logic you have in the class component, the fellowing should give you identical behavior in a functional component
const turnResetOff = props.turnResetOff;// a function
const resetForm = props.resetForm;
const setPersonId = props.setPersonId;// a function
// useEffect with an empty dependency array is identical to ComponentDidMount event
useEffect(() => {
turnResetOff();
}, []);
// Just need resetForm as dependency since only resetForm was checked in the componentDidUpdate
useEffect(() => {
const resetChangeForm = () => {/*definition*/};
if (resetForm) {
resetChangeForm();
turnResetOff();
}
}, [resetForm]);

How to make redux action dispatched in component unmount reach the store before previous page useEffect?

I have 2 react components:
const PageA = () => {
useEffect(()=>{
doXXXX()
}, [])
return ....
}
const PageB = () => {
useEffect(()=>{
return () => dispatch(SomeReduxAction())
}, [])
return ....
}
When the user goes from PageA to PageB, then goes back in history to pageA from PageB. The useEffect block in Page A runs before the SomeReduxAction reaches the redux store which is dispatched when PageB unmounts. This causes a bug. Is there a way to get the SomeReduxAction to reach the redux store before PageA runs useEffect?
Since dispatching an action is asynchronous. And it triggers when component unmount, you can't wait for it to finish on the call side.
But you can listen to state changes in the Redux Store. Use a state as the dependency of the useEffect, so that when the state changed, the useEffect will execute again. Then you can decide whether to execute the code or not like doXXXX() function.
E.g.
const PageA = () => {
const someReduxActionStateSlice = useSelector(state => state.someReduxActionStateSlice);
useEffect(()=>{
if(someReduxActionStateSlice === condition) {
doXXXX()
}
}, [someReduxActionStateSlice])
return ....
}
const PageB = () => {
useEffect(()=>{
return () => dispatch(SomeReduxAction())
}, [])
return ...
}

How to wait for multiple state updates in multiple hooks?

Example
In my scenario I have a sidebar with filters.. each filter is created by a hook:
const filters = {
customerNoFilter: useFilterForMultiCreatable(),
dateOfOrderFilter: useFilterForDate(),
requestedDevliveryDateFilter: useFilterForDate(),
deliveryCountryFilter: useFilterForCodeStable()
//.... these custom hooks are reused for like 10 more filters
}
Among other things the custom hooks return currently selected values, a reset() and handlers like onChange, onRemove. (So it's not just a simple useState hidden behind the custom hooks, just keep that in mind)
Basically the reset() functions looks like this:
I also implemented a function to clear all filters which is calling the reset() function for each filter:
const clearFilters = () => {
const filterValues = Object.values(filters);
for (const filter of filterValues) {
filter.reset();
}
};
The reset() function is triggering a state update (which is of course async) in each filter to reset all the selected filters.
// setSelected is the setter comming from the return value of a useState statement
const reset = () => setSelected(initialSelected);
Right after the resetting I want to do stuff with the reseted/updated values and NOT with the values before the state update, e.g. calling API with reseted filters:
clearFilters();
callAPI();
In this case the API is called with the old values (before the update in the reset())
So how can i wait for all filters to finish there state updated? Is my code just badly structured? Am i overseeing something?
For single state updates I could simply use useEffect but this would be really cumbersome when waiting for multiple state updates..
Please don't take the example to serious as I face this issue quite often in quite different scenarios..
So I came up with a solution by implementing a custom hook named useStateWithPromise:
import { SetStateAction, useEffect, useRef, useState } from "react";
export const useStateWithPromise = <T>(initialState: T):
[T, (stateAction: SetStateAction<T>) => Promise<T>] => {
const [state, setState] = useState(initialState);
const readyPromiseResolverRef = useRef<((currentState: T) => void) | null>(
null
);
useEffect(() => {
if (readyPromiseResolverRef.current) {
readyPromiseResolverRef.current(state);
readyPromiseResolverRef.current = null;
}
/**
* The ref dependency here is mandatory! Why?
* Because the useEffect would never be called if the new state value
* would be the same as the current one, thus the promise would never be resolved
*/
}, [readyPromiseResolverRef.current, state]);
const handleSetState = (stateAction: SetStateAction<T>) => {
setState(stateAction);
return new Promise(resolve => {
readyPromiseResolverRef.current = resolve;
}) as Promise<T>;
};
return [state, handleSetState];
};
This hook will allow to await state updates:
const [selected, setSelected] = useStateWithPromise<MyFilterType>();
// setSelected will now return a promise
const reset = () => setSelected(undefined);
const clearFilters = () => {
const promises = Object.values(filters).map(
filter => filter.reset()
);
return Promise.all(promises);
};
await clearFilters();
callAPI();
Yey, I can wait on state updates! Unfortunatly that's not all if callAPI() is relying on updated state values ..
const [filtersToApply, setFiltersToApply] = useState(/* ... */);
//...
const callAPI = () => {
// filtersToApply will still contain old state here, although clearFilters() was "awaited"
endpoint.getItems(filtersToApply);
}
This happens because the executed callAPI function after await clearFilters(); is is not rerendered thus it points to old state. But there is a trick which requires an additional useRef to force rerender after filters were cleared:
useEffect(() => {
if (filtersCleared) {
callAPI();
setFiltersCleared(false);
}
// eslint-disable-next-line
}, [filtersCleared]);
//...
const handleClearFiltersClick = async () => {
await orderFiltersContext.clearFilters();
setFiltersCleared(true);
};
This will ensure that callAPI was rerendered before it is executed.
That's it! IMHO a bit messy but it works.
If you want to read a bit more about this topic, feel free to checkout my blog post.

React hook useEffect causes initial render every time a component mounts

I am new to React hooks. So, I wanted to implement componentWillReceiveProps with React hooks.
I used React.useEffect() like so:
React.useEffect(() => {
console.log(props.authLoginSuccess); // initially called every time, the component renders
}, [props.authLoginSuccess]);
return ( //JSX...)
onst mapStateToProps = (state: any): StateProps => {
return {
authLoginSuccess: selectAuthLoginSuccess(state) //used selector to select authLoginSuccess
};
};
export default connect(
mapStateToProps,
// mapDispatchToProps
{ authLogin, toggleLoadingStatus }
)(Auth);
The problem is, useEffect is called each time the component renders initially, which I don't want. I only want it to render, when "props.authLoginSuccess" changes.
Since you want the effect to not run on initial render, you can do that by making use of useRef
const initialRender = useRef(true);
React.useEffect(() => {
if(initialRender.current) {
initialRender.current = false;
} else {
console.log(props.authLoginSuccess); // initially called every time, the component renders
}
}, [props.authLoginSuccess]);
Wrap it in an if condition like this:
React.useEffect(() => {
if (props.authLoginSuccess) {
console.log(props.authLoginSuccess);
}
}, [props.authLoginSuccess]);
Note that the effect will still run though - both initially and every time props.authLoginSuccess changes (which is okay!).
The if block will prevent running console.log(props.authLoginSuccess) when props.authLoginSuccess is falsy. So if you don't want it running initially i.e. when component mounts, just make sure that props.authLoginSuccess is false initially.
You could add another state that would monitor whether or not the component has been mounted.
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => {
if (isMounted) {
console.log(props.authLoginSuccess);
} else {
setIsMounted(true);
}
}, [props.authLoginSuccess]);
That way, it will only execute when the component has been mounted.

Resources