Is there a way to make setState synchronous to solve render issue? - javascript

I'm facing a problem. I just found out that setState is asynchronous.
I'm rendering a component in my render method if a certain condition is true.
Reder():
render() {
const { isFetchingSubject, isFetchingTemplate } = this.props;
return (
...
{this.state.showLabelDetails && <Details template={this.props.match.params.templatename} close={this.toggleShowLabelDetails} data={this.state.labelDetails} />}
...
);
}
Function Call at onclick button:
toggleShowLabelDetails = (event) => {
if (!this.state.showLabelDetails) this.setState({ labelDetails: JSON.parse(event.target.value) })
this.setState({ showLabelDetails: !this.state.showLabelDetails });
if (this.state.showLabelDetails) this.setState({ labelDetails: {} })
}
state:
state = {
showLabelDetails: false,
labelDetails: {},
}
Explaination of what code is doing:
When user hits button X it calls the function toggleShowLabelDetails()
It changes the boolean value in the state to true and adds the value from the button the the state labelDetails which is an object.
State changes which means component will render again and when the condition is true it will show up a new component on the screen.
50% of the times it's working well, but sometimes i'm getting the following error:
Uncaught SyntaxError: Unexpected token u in JSON at position 0
at Object.parse (<anonymous>)
Uncaught Error: A cross-origin error was thrown. React doesn't have access to the actual error object in development.
Any solution for this?

Can you try doing something like this:
class MyComponent extends Component {
function setStateSynchronous(stateUpdate) {
return new Promise(resolve => {
this.setState(stateUpdate, () => resolve());
});
}
async function foo() {
// state.count has value of 0
await setStateSynchronous(state => ({count: state.count+1}));
// execution will only resume here once state has been applied
console.log(this.state.count); // output will be 1
}
}

You can pass a callback to setState.
this.setState(
(state, props) => ({showLabelDetails : !state.showLabelDetails}),
() => { // Executed when state has been updated
// Do stuff with new state
if (this.state.showLabelDetails) {
this.setState({ labelDetails: {} })
}
}
)
And BTW: You cannot rely on this.state inside setState (mentionned in the react docs), since react may batch the state updates.

Related

How to update state of component in componentDidUpdate() without being stuck in an infinite re render?

I have a component with a componentDidMount() method that calls a method called getData() which gets the initial data and sets the initial state of the component.
class LogsSettings extends React.Component {
constructor(props) {
super(props);
this.settingsUrls = [
"/ui/settings/logging"
];
this.state = {
configSettings: {},
formSchema: formSchema
};
this.configSettings = {};
this.selected = "general";
}
getData = (url, selectedSetting) => {
fetch(url)
.then((response) => {
if (response.status !== 200) {
console.log('Looks like there was a problem. Status Code: ' +
response.status);
return;
}
response.json().then((response) => {
//pass formschema here
console.log(selectedSetting);
let newFormSchema = this.setNonDefaultValues(response.data, formSchema.subsections);
Object.assign(this.configSettings, response.data);
this.setState({
configSettings : this.configSettings,
formSchema: newFormSchema
});
});
}
)
.catch((err) => {
console.log('Fetch Error :-S', err);
});
};
componentDidMount() {
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.selected)
})
}
componentDidUpdate() {
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.props.selectedSetting)
})
}
render() {
return (
<div className="card-wrapper">
<h2>{formSchema.label.toUpperCase()}</h2>
{
formSchema.subsections.map((subSection) => {
return (
<>
<h3>{subSection['description']}</h3>
<div style={{marginBottom: '10px'}}></div>
{
subSection['input_fields'].map((inputField) => {
return buildForm(inputField, this.handleChange)
})
}
<hr></hr>
</>
)
})
}
<button className="button button-primary">Save Changes</button>
</div>
)
}
}
The selectedSetting parameter that gets passed to the getData() method in this component will change however and when this changes, I need to change the state of the component and get new data specific to the changed selectedSetting parameter.
The new selectedSetting is passed into the component as a prop. The problem is that I can't pass the new selectedSetting parameter to my getData method to update the state of the component as it gets caught in an infinite loop.
How do I go about passing the new selectedSetting to the getData() method without getting caught in an infinite loop? Is this even possible? If not, what is the best approach I should take?
note the selectedSetting parameter isn't used in the getData() function yet but will be and it will be used to get data from an API call and a new form schema which will then lead to the ConfigSettings and formSchema states being changed
If you look closely on the lifecycle of your component, after mount, you'll fetch then update the component. This will trigger the componentDidUpdate lifecycle method which will do the same thing, causing the infinite loop. You need to have a flag that checks whether this.props.selected changed. If it didn't, don't fetch the data else fetch as normal. In the update method, you have access to the previous props. (You may also do this in componentShouldUpdate method, but it'll be just outright risky)
componentDidUpdate(prevProps) {
if( prevProps.selectedSetting !== this.props.selectedSetting ){
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.props.selectedSetting)
})
}
}
also just a heads up, I noticed that your didMount method, uses a default of "general" as the selected setting, since you want to be using this.props.selectedSetting might be better if it was the one being used instead and just set default props to "general".

getDerivedStateFromProps, change of state under the influence of changing props

I click Item -> I get data from url:https: // app / api / v1 / asset / $ {id}. The data is saved in loadItemId. I am moving loadItemId from the component Items to the component Details, then to the component AnotherItem.
Each time I click Item the props loadItemId changes in the getDerivedStateFromProps method. Problem: I'll click Element D -> I see in console.log 'true', then I'll click Element E --> It display in console.log true andfalse simultaneously, and it should display only false.
Trying to create a ternary operator {this.state.itemX ['completed'] ? this.start () : ''}. If {this.state.itemX ['completed'] call the function this.start ()
Code here: stackblitz
Picture: https://imgur.com/a/OBxMKCd
Items
class Items extends Component {
constructor (props) {
super(props);
this.state = {
itemId: null,
loadItemId: ''
}
}
selectItem = (id) => {
this.setState({
itemId: id
})
this.load(id);
}
load = (id) => {
axios.get
axios({
url: `https://app/api/v1/asset/${id}`,
method: "GET",
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
this.setState({
loadItemId: response.data
});
})
.catch(error => {
console.log(error);
})
}
render () {
return (
<div >
<Item
key={item.id}
item={item}
selectItem={this.selectItem}
>
<Details
loadItemId={this.state.loadTime}
/>
</div>
)
}
Item
class Item extends Component {
render () {
return (
<div onClick={() => this.props.selectItem(item.id}>
</div>
)
}
}
Details
class Details extends Component {
constructor() {
super();
}
render () {
return (
<div>
<AnotherItem
loadItemId = {this.props.loadItemId}
/>
</div>
)
}
}
AnotherItem
class AnotherItem extends Component {
constructor() {
super();
this.state = {
itemX: ''
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.loadItemId !== prevState.loadItemId) {
return { itemX: nextProps.loadItemId }
}
render () {
console.log(this.state.itemX ? this.state.itemX['completed'] : '');
{/*if this.state.loadX['completed'] === true, call function this.start()*/ }
return (
<button /*{this.state.loadX['completed'] ? this.start() : ''}*/ onClick={this.start}>
Start
</button>
);
}
}
here:
selectItem = (id) => {
this.setState({
itemId: id
})
this.load(id);
}
you call setState(), then 'Item' and 'Details' and 'AnotherItem' call their render method. so you see log for previous 'loadItemId'.
when 'load' method work done. here:
this.setState({
loadItemId: response.data
});
you setState() again, then 'Item' and 'Details' and 'AnotherItem' call their render method again. in this time you see log for new 'loadItemId'.
solution
setState both state in one place. after load method done, instead of:
this.setState({
loadItemId: response.data
});
write:
this.setState({
itemId: id,
loadItemId: response.data
});
and remove:
this.setState({
itemId: id
})
from 'selectItem' method.
Need some clarification, but think I can still address this at high level. As suggested in comment above, with the information presented, it does not seem that your component AnotherItem actually needs to maintain state to determine the correct time at which to invoke start() method (although it may need to be stateful for other reasons, as noted below).
It appears the functionality you are trying to achieve (invoke start method at particular time) can be completed solely with a comparison of old/new props by the componentDidUpdate lifecycle method. As provided by the React docs, getDerivedStateFromProps is actually reserved for a few 'rare' cases, none of which I believe are present here. Rather, it seems that you want to call a certain method, perhaps perform some calculation, when new props are received and meet a certain condition (e.g., not equal to old props). That can be achieved by hooking into componentDidUpdate.
class AnotherItem extends Component {
constructor(props) {
super(props);
this.state = {}
}
start = () => { do something, perform a calculation }
// Invoked when new props are passed
componentDidUpdate(prevProps) {
// Test condition to determine whether to call start() method based on new props,
// (can add other conditionals limit number of calls to start, e.g.,
// compare other properties of loadItemId from prevProps and this.props) .
if (this.props.loadItemId && this.props.loadItemId.completed === true) {
//Possibly store result from start() in state if needed
const result = this.start();
}
}
}
render () {
// Render UI, maybe based on updated state/result of start method if
// needed
);
}
}
You are encountering this behaviour because you are changing state of Items component on each click with
this.setState({
itemId: id
})
When changing its state, Items component rerenders causing AnotherItem to rerender (because that is child component) with it's previous state which has completed as true (since you've clicked element D before). Then async request completes and another rerender is caused with
this.setState({
loadItemId: response.data
});
which initiates another AnotherItem rerender and expected result which is false.
Try removing state change in selectItem and you'll get desired result.
I'd suggest you read this article and try to structure your code differently.
EDIT
You can easily fix this with adding loader to your component:
selectItem = (id) => {
this.setState({
itemId: id,
loading: true
})
this.load(id);
}
load = (id) => {
axios.get
axios({
url: `https://jsonplaceholder.typicode.com/todos/${id}`,
method: "GET"
})
.then(response => {
this.setState({
loading: false,
loadItemId: response.data
});
})
.catch(error => {
console.log(error);
})
}
render() {
return (
<div >
<ul>
{this.state.items.map((item, index) =>
<Item
key={item.id}
item={item}
selectItem={this.selectItem}
/>
)
}
</ul>
{this.state.loading ? <span>Loading...</span> : <Details
itemId={this.state.itemId}
loadItemId={this.state.loadItemId}
/>}
</div>
)
}
This way, you'll rerender your Details component only when you have data fetched and no unnecessary rerenders will occur.

set multiple states, and push to state of array in one onClick function

I'm running into a recurring issue in my code where I want to grab multiple pieces of data from a component to set as states, and push those into an array which is having its own state updated. The way I am doing it currently isn't working and I think it's because I do not understand the order of the way things happen in js and react.
Here's an example of something I'm doing that doesn't work: jsfiddle here or code below.
import React, {Component} from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
}
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
setCategoryStates = (categoryTitle, categorySubtitle) => {
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
render() {
return (
<CategoryComponent
setCategoryStates={this.setCategoryStates}
categoryTitle={'Category Title Text'}
categorySubtitle={'Category Subtitle Text'}
/>
);
}
}
class CategoryComponent extends Component {
render() {
var categoryTitle = this.props.categoryTitle;
var categorySubtitle = this.props.categorySubtitle;
return (
<div onClick={() => (this.props.setCategoryStates(
categoryTitle,
categorySubtitle,
))}
>
<h1>{categoryTitle}</h1>
<h2>{categorySubtitle}</h2>
</div>
);
}
}
I can see in the console that I am grabbing the categoryTitle and categorySubtitle that I want, but they get pushed as null into this.state.categoryArray. Is this a scenario where I need to be using promises? Taking another approach?
This occurs because setState is asynchronous (https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly).
Here's the problem
//State has categoryTitle as null and categorySubtitle as null.
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
//This gets the correct values in the parameters
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
//This method is using the state, which as can be seen from the constructor is null and hence you're pushing null into your array.
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
Solution to your problem: pass callback to setState
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
}, () => {
/*
Add state to the array
This callback will be called once the async state update has succeeded
So accessing state in this variable will be correct.
*/
this.pushToCategoryArray()
})
}
and change
pushToCategoryArray = () => {
//You don't need state, you can simply make these regular JavaScript variables
this.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
I think React doesn't re-render because of the pushToCategoryArray that directly change state. Need to assign new array in this.setState function.
// this.state.categoryArray.push({...})
const prevCategoryArray = this.state.categoryArray
this.setState({
categoryArray: [ newObject, ...prevCategoryArray],
)}

How can I avoid componentDidCatch() from firing something multiple times?

If a component in rendered 10 times, and has an error, componentDidCatch() will fire 10 times with that same error. Currently I make an API request on catch to log the error, but I only want that error logged once.
My first though was to save my own prevError in state, and check if the error passed to componentDidCatch() was the same. But that won't work since the setState request isn't immediate. Some other lifecycle events are passed the latest state but this isn't. I know setState() takes a callback with up-to-date state but by then the error will always be equal to the prevError. Here's what I mean:
componentDidCatch(error, info) {
this.setState({ error: true });
const isNewError = (error.toString() !== this.state.prevError.toString());
// always true since setState is async
if (isNewError) {
logErrorToMyService(error, info); // should only run once
this.setState({ prevError: error });
}
}
I also don't think I can use componentDidUpdate() somehow because that doesn't know my error.
Am I missing something? Am I just handling this problem wrong and need to rework it (maybe move the check into the logErrorToMyService instead)?
A full React example of what I mean:
const logErrorToMyService = () => console.warn('I should only be run once');
class ErrorBoundary extends React.Component {
state = {
error: false,
prevError: new Error(),
}
componentDidCatch(error, info) {
this.setState({ error: true });
// none of the below code will work correctly
const isNewError = (error.toString() !== this.state.prevError.toString());
// always true since setState is async
if (isNewError) {
logErrorToMyService(error, info); // should only run once
this.setState({ prevError: error });
}
}
render() {
if (this.state.error) {
return <div>Something went (purposefully) wrong.</div>;
}
return this.props.children;
}
}
// below is 10 purposefully buggy component instances
const BuggyComponent = () => (
<div>{purposefully.throwing.error}</div>
)
const App = () => (
<ErrorBoundary>
{[...Array(10)].map((_, i) => <BuggyComponent key={i} />)}
</ErrorBoundary>
)
ReactDOM.render(
<App />,
document.getElementById('root')
);
<div id='root'></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.3.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.3.1/umd/react-dom.development.js"></script>
Update: Along with switching prevError to a field on the class, I also changed it to an array so that repeat errors - but that might have a different error in between - are also skipped.
In the code you posted, componentDidCatch() will fire a dozen or so times in quick succession long before the setstate callback runs for the first time. However prevError doesn't have to be part of state, given that is has no bearing on the appearance of the component.
Just implement it as class variable instead so it is set synchronously.

How can I cancel the next 'then' in a Promise when a React component is unmounted/reused?

I am trying to cancel a promise when a React component is unmounted/reused. Right now I am facing a issue which I can better explain in a little graph below:
** The Promise must finish (executes an AJAX requests and updates the redux store accordingly)
Component is created => Mounted => _onClick() fired => Promise starts => Component gets unmounted but is recycled => Promise finished => calls setState() in the recycled component and rerenders with the wrong state from the previous component.
How can I cancel/interrupt/stop the next 'then' but still execute the Promise?
...
_onClick() {
let dispatch = this.props.dispatch;
if (!this.state.on) {
API.On(dispatch, this.props.id).then(() => {
this.setState({
on: true,
})
}).catch((error) => {
this.setState({
on: false,
})
});
} else {
API.Off(dispatch, this.props.id).then(() => {
this.setState({
on: false,
});
}).catch((error) => {
this.setState({
on: true,
})
})
}
}
...
What I'd do here is just add a conditional to the body of the promise that sets the state. You can use React's lifecycle hooks in order to keep track of whether the element is currently in the DOM or not.
Here's a quick example of a component that keeps track of whether it is mounted or not:
class Test extends React.Component {
render() {
return (<p>hello</p>);
}
componentDidMount () {
this.mounted = true
console.log(`mounted: ${this.mounted}`)
}
componentWillUnmount () {
this.mounted = false
console.log(`mounted: ${this.mounted}`)
}
}
Live example: https://codepen.io/jenius/pen/MmjPJV
Based on this, you should be able to add a simple if/else inside your promise handler to solve your issue!

Categories

Resources