Maximum update depth exceeded with recompose - javascript

i saw several topics talking about this common mistake, but i didn't find nothing related to the use of recompose.
Context
I've this withStateHandler
withStateHandlers(
{
bookingValidation: false,
},
{
setBookingValidation: ({ bookingValidation }) => () => ({
bookingValidation: !bookingValidation,
}),
},
),
and i've this lifeCycle
lifecycle({
componentDidMount() {
const { getFamily } = this.props;
getFamily();
},
componentWillReceiveProps(prevProps) {
const { formValues } = prevProps;
const { setBookingValidation } = this.props;
const {
locationId,
specialityId,
serviceId,
cancellationDetails,
personId,
date,
} = formValues;
const allFormValuesSelected = !!(
locationId &&
specialityId &&
serviceId &&
cancellationDetails &&
personId &&
date
);
if (allFormValuesSelected) setBookingValidation();
},
}),
it's a simple validation, when i've all the selectedValues, the state of bookingValidation will change on true and you will be able to click on a button.
Problem
When you enter in this if if (allFormValuesSelected) setBookingValidation(); the maximum update depth exceeded because of the function setBookingValidation()
Question
How can i avoid this behavior ?
There is a way to maintain this function ?

It happens because setBookingValidation() changes property value and calls componentWillReceiveProps. And you got an infinite loop of calls.
If you want to perform form validation, you should move that functionality into a separate withHandlers() method and call onChange event.

Related

Polling parameters are refreshed on every poll in React

I have been trying to figure something out in my React cryptocurrency project. So I have a function that basically polls for a response from the api every 6 seconds. It is supposed to start polling the moment the component renders, so I have it in a useEffect. It has default parameters being passed in for the polling to happen right after render. The polling works in terms that it polls and get the response based on the default values. It also takes in the user inputs and returns a response based on it, however, after the next poll, the arguments being passed to the api for polling are back to their defaults. In other words, the arguments being passed into the polling function on the next polls are all back to the default values I passed in, and not a continuation of the current state of the arguments.
Here is where I define pair, which lives above the useEffect:
const baseAsset = transactionType === TRANSACTION_TYPES.BUY ? selectedCurrencyState.selectedToCurrency : selectedCurrencyState.selectedFromCurrency;
const quoteAsset = transactionType === TRANSACTION_TYPES.SELL ? selectedCurrencyState.selectedToCurrency : selectedCurrencyState.selectedFromCurrency;
const pair = baseAsset && quoteAsset ? `${baseAsset}/${quoteAsset}` : '';
Here is the function that gets polled:
const handleInitPoll = useCallback((baseAndQuote, side, value) => {
setIsLoading(true);
getSwapPrice(baseAndQuote, side, value || 0)
.then((res) => {
if (res.error) {
setErrorMessage(res.error);
} else if (res.price) {
setIsLoading(false);
setSwapPriceInfo(res);
}
});
}, [pair, transactionType, fromCurrencyAmount]);
And here is the useEffect that polls it:
useEffect(() => {
if (isLoggedIn) {
getSwapPairs()
.then((res) => {
setSwapInfo(res.markets);
// Set state of selected currencies on render
if (transactionType === TRANSACTION_TYPES.BUY) {
setSelectedCurrencyState({
...selectedCurrencyState,
selectedToCurrency: (quoteAsset !== symbol) ? symbol : ''
});
}
});
if (symbol === selectedCurrencyState.selectedToCurrency) {
actions.updateChartQuote(selectedCurrencyState.selectedFromCurrency);
}
if (pair && transactionType && symbol === baseAsset) {
handleInitPoll(pair, transactionType, fromCurrencyAmount);
}
const timer = setInterval(handleInitPoll, 6000, pair, transactionType, fromCurrencyAmount);
return () => {
clearInterval(timer);
};
}
setSelectedCurrencyState({ ...selectedCurrencyState, selectedFromCurrency: 'SHIB', selectedToCurrency: 'DASH' });
}, [pair, transactionType, fromCurrencyAmount, symbol]);
Here is the debouncing method:
const debounceOnChange = useCallback(debounce(handleInitPoll, 500, pair, transactionType, fromCurrencyAmount), []);
And here is where the user is entering the input to debounce the api call when a user input is detected onChange:
const handleAssetAmount = (e) => {
const { value } = e.target;
const formattedAmount = handleAssetAmountFormat(value);
setFromCurrencyAmount(formattedAmount);
validateInputAmount(formattedAmount);
debounceOnChange(pair, transactionType, formattedAmount);
};
You should put pair in a useState hook. otherwise, everytime when this component is redenrering. the following part will be executed:
const pair = baseAsset && quoteAsset ? `${baseAsset}/${quoteAsset}` : '';
That's why you got a initial value again.
useState can help you to preserve the value for the whole component lifecircle unless you call set***().

Why do I see stale data even after invalidating my queries?

I have created a function which adds a specific item to my diary. 9/10 times everything works, which means that there is nothing wrong with the code?
However rarely I add the item to my diary, but I don't see the update values, even thought I activated queryClient.invalidateQueries() method, the value is updated on my server, because when I manually refresh I see the updated diary again.
Does this mean that by the time I activate invalidatequeries method, the update has not reached my server and that is why I am seeing stale data? But what would I do in that case?
Here is the function:
const newAddItemFunction = () => {
const day = newDiary?.[currentDay];
if (day && selectedMealNumber && selectedItem) {
setSavingItem(true);
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
router.push("/diary");
}
};
Here is my custom hook(useFirestoreQuery is just custom wrapped useQuery hook for firebase):
export const useGetCollectionDiary = () => {
const user = useAuthUser(["user"], auth);
const ref = collection(
firestore,
"currentDiary",
user.data?.uid ?? "_STUB_",
"days"
);
return useFirestoreQuery(
["currentDiary"],
ref,
{
subscribe: false,
},
{
select: (data) => {
let fullDaysArray = [] as Day[];
data.docs.map((docSnapshot) => {
const { id } = docSnapshot;
let data = docSnapshot.data() as Day;
data.documentId = id;
fullDaysArray.push(data);
});
fullDaysArray.sort((a, b) => a.order - b.order);
return fullDaysArray;
},
enabled: !!user.data?.uid,
}
);
};
NewAddItemToDiary function is just firebase call to set document:
//...json calculations
setDoc(
doc(
firestore,
"currentDiary",
auth.currentUser.uid,
"days",
day.documentId
),
newDiaryWithAddedItem
);
9/10 times everything works, which means that there is nothing wrong with the code?
It indicates to me that there is something wrong with the code that only manifests in edge cases like race conditions.
You haven't shared the code of what NewAddItemToDiary is doing, but I assume it's asynchronous code that fires off a mutation. If that is the case, it looks like you fire off the mutation, and then invalidate the query without waiting for the query to finish:
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
Mutations in react-query have callbacks like onSuccess or onSettled where you should be doing the invalidation, or, if you use mutateAsync, you can await the mutation and then invalidate. This is how all the examples in the docs are doing it:
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})

getting an error like Too many re-renders. React limits the number of renders to prevent an infinite loop

I am trying to set the state in request data method for a string variable like this below
const ViewChangeRequest = () => {
const [requestStageValue, setRequestStage] = useState('');
const { data: requestData, loading: requestDataLoading, error: requestDataError } =
useQuery(GET_SPECIFICREQUEST, {
variables: { requestinputs: { id } },
});
if (requestData != null) {
setRequestStage(requestData.allRequests[0].requestStage.name); // getting error at here
requestData.allRequests.map((code) => {
requestDatasource.push({
id: code.id,
section: code.masterSection.name,
createdBy: code.createdBy,
type: code.requestType.name,
status: code.requestStage.name,
createat: code.createdAt,
});
return null;
});
}
};
export default withRouter(ViewChangeRequest);
Depends upon the requeststage value, i am verifying conditions like this below
if (requestStageValue === 'Request Submitted') {
stepNumber = 0;
} else if (requestStageValue === 'In Review') {
stepNumber = 1;
} else {
stepNumber = 2;
}
I am getting an error Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop at this line setRequestStage(requestData.allRequests[0].requestStage.name)
I am not able to understand where i am doing wrong while setting up the state to string varaiable.
Could any one please help me out from this situation that would be very grateful to me, many thanks in advance.
Well you're running setRequestStage in the function. This will trigger a state update, which means the functions runs again (since state updates trigger re-renders), which means setRequestStage runs again, which means the state updates again, so functions runs again etc. etc. - infinite loop.
If you're wanting to set an initial state based on requestData, do it when declaring it in useState:
const [requestStageValue, setRequestStage] = useState(
requestData !== null ? requestData.allRequests[0].requestStage.name : ''
);

Testing React Components setState overload which takes a function

I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});

binding checkbox with nested array of objects in react

I have a problem with handling checkbox using react, what I want is the state should reflect what condition of the checkbox, and in the end I want to have [id-1, id-2, id-3] to save to the backend. But my demo seems broken, I think I miss one condition, but I can't tell what's my problem.
https://codesandbox.io/s/kpw23v4xv
handleCheckboxChange = (device_id) => {
const upateStatOfZoneCameraMenu = () => {
this.setState({
zones: [...this.state.zones.slice(0, this.state.selectedTab), {
...this.state.zones[this.state.selectedTab],
cameras: [
...this.state.zones[this.state.selectedTab].cameras.map(
o => ({
...o,
checked: this.state.selectedCameras.find(o2 => o2 === o.device_id) || o.device_id === device_id
})
)
]
}, ...this.state.zones.slice(this.state.selectedTab + 1)]
})
}
const updatedSelectedCamera = this.state.selectedCameras.find(obj => obj === device_id)
if (!updatedSelectedCamera) {
this.setState({
selectedCameras: [...this.state.selectedCameras, device_id]
}, () => {
upateStatOfZoneCameraMenu()
})
} else {
this.setState({
selectedCameras: this.state.selectedCameras.filter(obj => obj !== device_id)
}, () => {
upateStatOfZoneCameraMenu()
})
}
}
I think the problem is at line 52.
The problem with your logic was in setting the checked state of your camera.
I have changed the existing code to:
checked: this.state.selectedCameras.find(o2 => o2 === o.device_id) !==undefined
There were also warnings in your code about keys not being present in elements you create iteratively, which I fixed in App component by adding a key to the divs rendered by the map methods.
There was another warning for changing an uncontrolled component to a controlled component, which was fixed when I introduced the checked field in the initial state of your cameras and set it to false.
The detailed code can be found here.
I have forked your sandbox to provide you a working version: https://codesandbox.io/s/k5ml50ky13
Several problems you had:
Prefer to only update your state once instead of several times
Your condition on the checked property was unclear so I rewrote in order to only verify if the current camera is checked
Basically I just changed the function handleCheckboxChange:
handleCheckboxChange = (device_id) => {
const updatedSelectedCamera = this.state.selectedCameras.find(obj => obj === device_id);
const selectedCameras = updatedSelectedCamera ? this.state.selectedCameras.filter(obj => obj !== device_id) : [...this.state.selectedCameras, device_id];
this.setState({
selectedCameras,
zones: [
...this.state.zones.slice(0, this.state.selectedTab),
{
...this.state.zones[this.state.selectedTab],
cameras: [
...this.state.zones[this.state.selectedTab].cameras.map(
o => ({
...o,
checked: selectedCameras.includes(o.device_id),
})
)
]
},
...this.state.zones.slice(this.state.selectedTab + 1)
],
});
}
Hope it helps.

Categories