React set state error outside of componentDidMount - reactjs

Im getting an error Unhandled Rejection (Error): Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. when I try to set state in my retrieveRoleMembers function not sure how to fix it, any feedback is appreciated!
class MainCard extends Component {
state = {
userResponseData:[] ,
roleResponseDataID:[]
}
handleChange = (tab) => {
window.alert(`Tab changed to: ${tab}`);
};
retrieveRoleMembers(){
var i,j;
for (i = 0; i < this.props.userRoleDataValue.length; i++) {
if(this.props.userRoleDataValue[i].role_id === this.state.roleResponseDataID.id){
for(j=0;j<this.state.userResponseData.length;j++){
if(this.props.userRoleDataValue[i].user_id === this.state.userResponseData[j].id)
{
this.setState({ // This is where the error is happening
outputRoleMembers: this.state.userResponseData[j],
})
}
}
}}}
componentDidMount() {
this.props.getComponentById(VIEW_ROLES, Roles, this.props.searchValue.value).then(() => {
return this.setState({
roleResponseDataID: this.props.roles.data,
cardHandle: false,
})
});
this.props.fetchComponent([IS_FETCHING_DBUSERS, FETCH_DBUSERS_SUCCESS], users)
.then(() => {
return this.setState({
userResponseData: this.props.users.data,
})
});
}
render() {
if (this.props.cardHandle) {
return null
}
else {
if (this.props.sendOptionSelected === 'Role') {
this.retrieveRoleMembers()
return (
<Card mr={'0px'}>
<Tabs defaultActiveTab="Members" onChange={this.handleChange} >
{/* Group of tabs */}
<Tabs.Tab label="Members">Members</Tabs.Tab>
<Tabs.Tab label="Access">Access</Tabs.Tab>
{/* Tab panels */}
<Tabs.Panel label="Members">
<Table
data={Array.isArray(this.state.outputRoleMembers) ? this.state.outputRoleMembers : [this.state.outputRoleMembers]}
defaultPageSize={[this.state.outputRoleMembers].length}
columns={
[
{
Header: 'Fisrt Name',
accessor: 'first_name'
},
{
Header: 'Last Name',
accessor: 'last_name'
}
]
}
sortable={false}
resizable={false}
showPagination={false}
onSortedChange={() => { }}
/>
</Tabs.Panel>
</Tabs>
</Card>
)
}
}
}
const mapStateToProps = state => {
return {
roles: state.roles.item,
users: state.users
}
}
export default connect(mapStateToProps, { getComponentById,fetchComponent })(MainCard);
and when I change retrieveRoleMembers to look like so, my code works but when I inspect the console log I see a infinite loop / renders for VIEW_DBUSERS
retrieveRoleMembers(){
var i;
for (i = 0; i < this.props.userRoleDataValue.length; i++) {
if(this.props.userRoleDataValue[i].role_id === this.state.roleResponseDataID.id){
this.props.getComponentById(VIEW_DBUSERS, users, this.props.userRoleDataValue[i].user_id).then(() => {
return this.setState({
outputRoleMembers: this.props.users.data,
})
});
}}}

The problem is you are calling function inside render method. That sets the State and calls the render method again and so on. So it created a loop.
Hence you get
Unhandled Rejection (Error): Maximum update depth exceeded
I put everything inside componentDidMount by making an async function.
componentDidMount() {
this.preFetchData();
}
preFetchData async () { // made this async function.. using await to make code sync
await this.props.getComponentById(VIEW_ROLES, Roles, this.props.searchValue.value);
await this.props.fetchComponent([IS_FETCHING_DBUSERS, FETCH_DBUSERS_SUCCESS], users);
this.setState({ roleResponseDataID: this.props.roles.data, cardHandle: false, userResponseData: this.props.users.data }, () => {
this.retrieveRoleMembers(); // call your method here
});
}

Related

React - Render happening before data is returned and not updating component

I can't get this to work correctly after several hours.
When creating a component that needs data from Firebase to display, the data is returning after all actions have taken place so my component isn't showing until pressing the button again which renders again and shows correctly.
Currently my function is finishing before setState, and setState is happening before the data returns.
I can get setState to happen when the data is returned by using the callback on setState but the component would have already rendered.
How do i get the component to render after the data has returned?
Or what would the correct approach be?
class CoffeeList extends Component {
constructor(props) {
super(props);
this.state = {
coffeeList: [],
}
}
componentDidMount() {
this.GetCoffeeList()
}
GetCoffeeList() {
var cups = []
coffeeCollection.get().then((querySnapshot) => {
querySnapshot.forEach(function (doc) {
cups.push({ name: doc.id})
});
console.log('Updating state')
console.log(cups)
})
this.setState({ coffeeList: cups })
console.log('End GetCoffeeList')
}
render() {
const coffeeCups = this.state.coffeeList;
console.log("Rendering component")
return (
<div className="coffee">
<p> This is the Coffee Component</p>
{coffeeCups.map((c) => {
return (
<CoffeeBox name={c.name} />
)
})}
</div >
)
}
}
Thanks
The problem is that you set the state before the promise is resolved. Change the code in the following way:
GetCoffeeList() {
coffeeCollection.get().then((querySnapshot) => {
const cups = []
querySnapshot.forEach(function (doc) {
cups.push({ name: doc.id})
});
console.log('Updating state')
console.log(cups)
this.setState({ coffeeList: cups })
console.log('End GetCoffeeList')
})
}

React app render function executes before state is set?

I am building a chatting app in React, but it crashes right after sending a message. Here is part of the code
state ={
conversationData: null,
message: ''
};
componentDidMount() {
this.setState({conversationData: this.props.conversationData});
};
onSendClicked = () => {
con.addMessage(this.state.conversationData.id, data).then(() => {
this.setState({message: ''}, () => {
this.setState({conversationData: this.state.conversationData.message.push(data)});
});
});
};
and in my render function, I have this
<List>
{this.state.conversationData.message !== null && this.state.conversationData.message !== undefined ?
this.state.conversationData.message.map((mes, index)
........
: <div/>....
The problem is in that loop, it works fine when I load it, but once I press send message and set the state on conversationData, it either returns an empty div and never updates or crashes with .map() is undefined if I eliminate that div. What am I doing wrong?
Based on your current code this is how I would set it up:
state ={
conversationData: this.props.conversationData, // no need to set it as null then update on mount
message: ''
};
onSendClicked = () => {
con.addMessage(this.state.conversationData.id, data).then(() => {
this.setState(({ conversationData }) => ({
message: '',
conversationData: {
...conversationData,
message: [ ...conversationData.message, data ] // where is this data coming from?
} // update conversationData with message
});
};
assuming that 'con' is defined and has a method named 'addMessage' that isn't returning an error
i'm also assuming that you have a type-o and data is being passed in from onSendClicked(data)
then i would do this. take special notice to the spread operator
[...this.state.conversationData.message, data.id]
as opposed to mutating state with .push()
as well as initializing conversationData.message state with an [] to avoid iteration errors on null (check for conversationData.message.length instead of conversationData.message !== null)
state ={
conversationData: {message:[]},
message: ''
};
componentDidMount() {
this.setState({conversationData: this.props.conversationData});
};
onSendClicked = (data) => {
con.addMessage(this.state.conversationData.id, data)
.then(() => {
this.setState({
message: '',
conversationData: [...this.state.conversationData.message, data]
})
});
};
in render:
<List>
{
this.state.conversationData.message.length && this.state.conversationData.message.map((mes, index)=>{
//dosomething with mes and index
})
}
</List>

Interupt code and wait for user interaction in a loop - React

I am trying to implement an "add all" button in my react app. to do that, i pass this function to the onClick method of the button :
for (element in elements) {
await uploadfunction(element)
}
const uploadfunction = async (element) => {
if (valid) {
// await performUpload(element)
}
else if (duplicate) {
//show dialog to confirm upload - if confirmed await performUpload(element)
}
else {
// element not valid set state and show failed notification
}
}
const performUpload = async (element) => {
// actual upload
if(successful){
// set state
}else{
// element not successful set state and show failed notification
}
}
the uploadfunction can have three different behaviors :
Add the element to the database and update the state
Fail to add the element and update the state
Prompt the user with the React Dialog component to ask for confirmation to add duplicat element and update the state accordingly
My problem now is since i'm using a for loop and despite using Async/await , i can't seem to wait for user interaction in case of the confirmation.
The behavior i currently have :
The for loop move to the next element no matter what the result
The Dialog will show only for a second and disappear and doesn't wait for user interaction
Wanted behavior:
Wait for user interaction (discard/confirm) the Dialog to perform the next action in the loop.
How can i achieve that with React without Redux ?
Here is an example of a component that might work as an inspiration for you.
You might split it in different components.
class MyComponent extends Component {
state = {
items: [{
// set default values for all booleans. They will be updated when the upload button is clicked
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_2',
}],
performUpload: false,
};
onUploadButtonClick = () => {
this.setState(prevState => ({
...prevState,
items: prevState.items.map((item, index) => ({
isValid: validationFunction(),
isDuplicate: prevState.items.slice(0, index).some(i => i.data === item.data),
shouldUploadDuplicate: false,
data: item.data
})),
performUpload: true,
}), (nextState) => {
this.uploadToApi(nextState.items);
});
};
getPromptElement = () => {
const firstDuplicateItemToPrompt = this.getFirstDuplicateItemToPrompt();
const firstDuplicateItemIndexToPrompt = this.getFirstDuplicateItemIndexToPrompt();
return firstDuplicateItemToPrompt ? (
<MyPrompt
item={item}
index={firstDuplicateItemIndexToPrompt}
onAnswerSelect={this.onPromptAnswered}
/>
) : null;
};
getFirstDuplicateItemToPrompt = this.state.performUpload
&& !!this.state.items
.find(i => i.isDuplicate && !i.shouldUploadDuplicate);
getFirstDuplicateItemIndexToPrompt = this.state.performUpload
&& !!this.state.items
.findIndex(i => i.isDuplicate && !i.shouldUploadDuplicate);
onPromptAnswered = (accepted, item, index) => {
this.setState(prevState => ({
...prevState,
items: prevState.items
.map((i, key) => (index === key ? ({
...item,
shouldUploadDuplicate: accepted,
}) : item)),
performUpload: accepted, // if at last an item was rejected, then the upload won't be executed
}));
};
uploadToApi = (items) => {
if (!this.getFirstDuplicateItemToPrompt()) {
const itemsToUpload = items.filter(i => i.isValid);
uploadDataToApi(itemsToUpload);
}
};
render() {
const { items } = this.stat;
const itemElements = items.map((item, key) => (
<MyItem key={key} {...item} />
));
const promptElement = this.getPromptElement();
return (
<div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{itemElements}
</div>
<Button onClick={this.onUploadButtonClick}>Upload</Button>
{promptElement}
</div>
)
}
}

Mock multiple fetch calls with state updates in ReactJS

I am having a ReactJS component which does two things:
- on ComponentDidMount it will retrieve a list of entries
- on Button click it will submit the select entry to a backend
The problem is that i need to mock both requests (made with fetch) in order to test it properly. In my current testcase i want to test a failure in the submit on the button click. However due some odd reason the setState is triggered however the update from that is received after i want to compare it.
Dumps i did for the test. First one is the state as listen in the test. The second is from the code itself where it is setting state().error to the error received from the call
FAIL react/src/components/Authentication/DealerSelection.test.jsx (6.689s)
● Console
console.log react/src/components/Authentication/DealerSelection.test.jsx:114
{ loading: true,
error: null,
options: [ { key: 22, value: 22, text: 'Stationstraat 5' } ] }
console.log react/src/components/Authentication/DealerSelection.jsx:52
set error to: my error
The actual test code:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(() => mockComponentDidMount);
const element = mount(<DealerSelection />);
process.nextTick(() => {
jest.spyOn(global, 'fetch').mockImplementation(() => mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
process.nextTick(() => {
console.log(element.state()); // state.error null even though it is set with setState but arrives just after this log statement
global.fetch.mockClear();
done();
});
});
});
This is the component that i actually use:
import React, { Component } from 'react';
import { Form, Header, Select, Button, Banner } from '#omnius/react-ui-elements';
import ClientError from '../../Error/ClientError';
import { fetchBackend } from './service';
import 'whatwg-fetch';
import './DealerSelection.scss';
class DealerSelection extends Component {
state = {
loading: true,
error: null,
dealer: '',
options: []
}
componentDidMount() {
document.title = "Select dealer";
fetchBackend(
'/agent/account/dealerlist',
{},
this.onDealerListSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerListSuccessHandler = json => {
const options = json.data.map((item) => {
return {
key: item.key,
value: item.key,
text: item.value
};
});
this.setState({
loading: false,
options,
dealer: json.default
});
}
onFetchErrorHandler = err => {
if (err instanceof ClientError) {
err.response.json().then(data => {
this.setState({
error: data.error,
loading: false
});
});
} else {
console.log('set error to', err.message);
this.setState({
error: err.message,
loading: false
});
}
}
onSubmitHandler = () => {
const { dealer } = this.state;
this.setState({
loading: true,
error: null
});
fetchBackend(
'/agent/account/dealerPost',
{
dealer
},
this.onDealerSelectSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerSelectSuccessHandler = json => {
if (!json.error) {
window.location = json.redirect; // Refresh to return back to MVC
}
this.setState({
error: json.error
});
}
onChangeHandler = (event, key) => {
this.setState({
dealer: event.target.value
});
}
render() {
const { loading, error, dealer, options } = this.state;
const errorBanner = error ? <Banner type='error' text={error} /> : null;
return (
<div className='dealerselection'>
<Form>
<Header as="h1">Dealer selection</Header>
{ errorBanner }
<Select
label='My dealer'
fluid
defaultValue={dealer}
onChange={this.onChangeHandler}
maxHeight={5}
options={options}
/>
<Button
primary
fluid
onClick={this.onSubmitHandler}
loading={loading}
>Select dealer</Button>
</Form>
</div>
);
}
}
export default DealerSelection;
Interesting, this one took a little while to chase down.
Relevant parts from the Node.js doc on Event Loop, Timers, and process.nextTick():
process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop.
...any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues.
In other words, Node starts processing the nextTickQueue once the current operation is completed, and it will continue until the queue is empty before continuing with the event loop.
This means that if process.nextTick() is called while the nextTickQueue is processing, the callback is added to the queue and it will be processed before the event loop continues.
The doc warns:
This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.
...and as it turns out you can starve your Promise callbacks as well:
test('Promise and process.nextTick order', done => {
const order = [];
Promise.resolve().then(() => { order.push('2') });
process.nextTick(() => {
Promise.resolve().then(() => { order.push('7') });
order.push('3'); // this runs while processing the nextTickQueue...
process.nextTick(() => {
order.push('4'); // ...so all of these...
process.nextTick(() => {
order.push('5'); // ...get processed...
process.nextTick(() => {
order.push('6'); // ...before the event loop continues...
});
});
});
});
order.push('1');
setTimeout(() => {
expect(order).toEqual(['1','2','3','4','5','6','7']); // ...and 7 gets added last
done();
}, 0);
});
So in this case the nested process.nextTick() callback that logs element.state() ends up running before the Promise callbacks that would set state.error to 'my error'.
It is because of this that the doc recommends the following:
We recommend developers use setImmediate() in all cases because it's easier to reason about
If you change your process.nextTick calls to setImmediate (and create your fetch mocks as functions so Promise.reject() doesn't run immediately and cause an error) then your test should work as expected:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = () => Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = () => Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(mockComponentDidMount);
const element = mount(<DealerSelection />);
setImmediate(() => {
jest.spyOn(global, 'fetch').mockImplementation(mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
setImmediate(() => {
console.log(element.state()); // state.error is 'my error'
global.fetch.mockClear();
done();
});
});
});
There are several asynchronous calls required to update the state, so your process.nextTick() isn't sufficient. To update the state, this needs to happen:
your test code clicks, and the event handler callback is queued
the event handler callback runs, runs fetch, gets a promise rejection, and runs the error handler
the error handler runs setState, which queues the state update (setState is asynchronous!)
your test code runs, checking the element's state
the state update runs
In short, you need to wait longer before asserting on the state.
A useful idiom to "wait" without nested process.nextTick() calls is to define a test helper
function wait() {
return new Promise((resolve) => setTimeout(resolve));
}
and then do
await wait();
as many times as required in your test code. Note that this requires you to define test functions as
test(async () => {
})
rather than
test(done => {
})

Unable to pass params successfully to another .js file/screen

I'm trying to pass params from one screen to another screen using react-navigation, the problem I'm encountering is that when I console.log the param itself, the console returns 'undefined'. I can't seem to pinpoint what I'm doing wrong exactly. Any help or guidance would be much appreciated.
I tried the following, with no success:
-this.props.navigation.getParam('biometryStatus')
-this.props.navigation.state.params('biometryStatus')
This is my AuthenticationEnroll screen where my param is being initialised as the state of the component:
export default class AuthenticationEnroll extends Component {
constructor() {
super()
this.state = {
biometryType: null
};
}
async _clickHandler() {
if (TouchID.isSupported()){
console.log('TouchID is supported');
return TouchID.authenticate()
.then(success => {
AlertIOS.alert('Authenticated Successfuly');
this.setState({biometryType: true })
this.props.navigation.navigate('OnboardingLast', {
pin: this.props.pin,
biometryStatus: this.state.biometryType,
});
})
.catch(error => {
console.log(error)
AlertIOS.alert(error.message);
});
} else {
this.setState({biometryType: false });
console.log('TouchID is not supported');
// AlertIOS.alert('TouchID is not supported in this device');
}
}
_navigateOnboardingLast() {
this.props.navigation.navigate('OnboardingLast', {pin: this.props.pin})
}
render () {
return (
<View style={{flex: 1}}>
<Slide
icon='fingerprint'
headline='Secure authentication'
subhead='To make sure you are the one using this app we use authentication using your fingerprints.'
buttonIcon='arrow-right'
buttonText='ENROLL'
buttonAction={() => this._clickHandler()}
linkText={'Skip for now.'}
linkAction={() => this._navigateOnboardingLast()}
slideMaxCount={4}
slideCount={2}
subWidth={{width: 220}}
/>
</View>
)
}
}
And this is my OnboardingLast Screen where my param is being passed down and printed through console.log:
class OnboardingLast extends Component {
async _createTokenAndGo () {
let apiClient = await this._createToken(this.props.pin)
this.props.setClient(apiClient)
AsyncStorage.setItem('openInApp', 'true')
const { navigation } = this.props;
const biometryStatus = navigation.getParam('biometryStatus', this.props.biometryStatus);
console.log(biometryStatus);
resetRouteTo(this.props.navigation, 'Home')
}
/**
* Gets a new token from the server and saves it locally
*/
async _createToken (pin) {
const tempApi = new ApiClient()
let token = await tempApi.createToken(pin)
console.log('saving token: ' + token)
AsyncStorage.setItem('apiToken', token)
return new ApiClient(token, this.props.navigation)
}
render () {
return (
<View style={{flex: 1}}>
<Slide
icon='checkbox-marked-circle-outline'
headline={'You\'re all set up!'}
subhead='Feel free to start using MyUros.'
buttonIcon='arrow-right'
buttonText='BEGIN'
buttonAction={() => this._createTokenAndGo()}
slideMaxCount={4}
slideCount={3}
/>
</View>
)
}
}
Expected Result is that console.log(biometryStatus); returns 'true' or 'false', however it returns 'undefined'.
Since setState is asynchron, you send null (declared in your constructor) to your next page. By doing so, you will send true:
this.setState({ biometryType: true })
this.props.navigation.navigate('OnboardingLast', {
pin: this.props.pin,
biometryStatus: true,
});
You could also do this, since setState can take a callback as param:
this.setState({ biometryType: true }, () => {
this.props.navigation.navigate('OnboardingLast', {
pin: this.props.pin,
biometryStatus: true,
});
})
In your second page this.props.biometryStatus is undefined.
The second argument of getParam is the default value. You should change it like that
const biometryStatus = navigation.getParam('biometryStatus', false);

Resources