Cancel a promise when a component is unmounted in ReactJS - reactjs

I've a component named "Item" which creates and calls a promise when it has been mounted.
class Item extends React.Component{
constructor(props){
super(props)
this.onClick = this.onClick.bind(this)
this.prom = new Promise((resolve, reject) => {
setTimeout(() => resolve("PROMISE COMPLETED "+this.props.id),6000)
})
}
componentDidMount(){
this.prom.then((success) => {
console.log(success)
})
}
componentWillUnmount(){
console.log("unmounted")
}
onClick(e){
e.preventDefault()
this.props.remove(this.props.id)
}
render(){
return (
<h1>Item {this.props.id} - <a href="#" onClick={this.onClick}>Remove</a></h1>
)
}
}
As you can see, the promise calls the resolve 6 seconds after it has been called.
There is another component named "List" that is responsible for showing those items on the screen. The "List" is the parent of the "Item" component.
class List extends React.Component{
constructor(props){
super(props)
this.state = {
items : [1,2,3]
}
this.handleRemove = this.handleRemove.bind(this)
}
handleRemove(id){
this.setState((prevState, props) => ({
items : prevState.items.filter((cId) => cId != id)
}));
}
render(){
return (
<div>
{this.state.items.map((item) => (
<Item key={item} id={item} remove={this.handleRemove} />
))
}
</div>
)
}
}
ReactDOM.render(<List />,root)
On the example above, it shows three Item on the screen.
If I remove any of those components, componentWillUnmount() is called but also the promise which has been created in the removed component is run.
For example, I can see the promise of the second item is run even if I remove the second item.
unmounted
PROMISE COMPLETED 1
PROMISE COMPLETED 2
PROMISE COMPLETED 3
I have to cancel the promise when a component is unmounted.

A variation of this https://hshno.de/BJ46Xb_r7 seemed to work for me.
I made an HOC with the mounted instance variable and wrapped all async components in it.
Below is what my code roughly loks like.
export function makeMountAware(Component) {
return class MountAwareComponent extends React.Component {
mounted = false;
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
return (
<Component
mounted = {this.mounted}
{...this.props}
{...this.state}
/>
);
}
}
class AsyncComponent extends React.Component {
componentDidMount() {
fetchAsyncData()
.then(data => {
this.props.mounted && this.setState(prevState => ({
...prevState,
data
}));
});
}
}
export default makeMountAware(AsyncComponent);

You can't cancel native ES6 promises. Read more at https://medium.com/#benlesh/promise-cancellation-is-dead-long-live-promise-cancellation-c6601f1f5082
What you can do, however, is maybe use non-native promise libraries like Bluebird or Q, that give you promises that can be cancelled.

There are various things you can do. The simplest is to reject the promise:
this.prom = new Promise((resolve, reject) => {
this.rejectProm = reject;
...
});
and then
componentWillUnmount(){
if (this.rejectProm) {
this.rejectProm();
this.rejectProm = nil;
}
console.log("unmounted")
}

Since you are using a timeout in this example you should clear it when unmounting.
class Item extends React.Component{
constructor(props){
super(props)
this.onClick = this.onClick.bind(this)
// attribute for the timeout
this.timeout = null;
this.prom = new Promise((resolve, reject) => {
// assign timeout
this.timeout = setTimeout(() => resolve("PROMISE COMPLETED "+this.props.id),6000)
})
}
componentDidMount(){
this.prom.then((success) => {
console.log(success)
})
}
componentWillUnmount(){
// clear timeout
clearTimeout(this.timeout);
console.log("unmounted")
}
My guess is this will result in a rejection and you won't see that console log.

Related

How can I chain asynchronous Firebase updates in my React app?

React & Firebase newbie here. I have a React component that needs to look up some stuff in Firebase before rendering. My database design requires first getting the correct doohick ids and subsequently looking up the doohick details, but I'm not sure how to do that with the asynchronous nature of Firebase database access. This doesn't work:
class Widget extends React.Component {
componentDidMount() {
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
});
this.state.doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
render() {
if (this.state.doohick-ids) {
return null;
} else {
// render the Doohick subcomponents
}
}
}
I can think of a few solutions here, but none that I like. What's the recommended way to chain together Firebase calls, or perhaps redesign this to eliminate the problem?
I think you should split one component Widget to two WidgetList and WidgetItem.
WidgetItem
subscribe and unsubscribe to firebase.database().ref(/doohick-details/${id})
class WidgetItem extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
}
constructor(props) {
super(props);
this.state = {};
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
const { id } = this.props;
this.dbRef = firebase.database().ref(`/doohick-details/${id}`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
// update state
this.setState(dataSnapshot);
}
render() {
return (
<pre>{JSON.stringify(this.state, null, 2)}</pre>
);
}
}
WidgetList
subscribe and unsubscribe to firebase.database().ref(/users/${username}/doohick-ids)
class WidgetItem extends React.Component {
constructor(props) {
super(props);
this.state = { doohick_ids: [] };
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
// Note: I've just copied your example. `username` is undefined.
this.dbRef = firebase.database().ref(`/users/${username}/doohick-ids`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
this.setState({ doohick_ids: dataSnapshot });
}
render() {
const { doohick_ids } = this.state;
if (doohick_ids.length === 0) {
return 'Loading...';
}
return (
<React.Fragment>
{doohick_ids.map(id => <WidgetItem key={id} id={id} />)}
</React.Fragment>
);
}
}
And code that requires the data from the database needs to be inside the callback that is invoked when that data is available. Code outside of the callback is not going to have the right data.
So:
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
});
There's many optimizations possible here, but they all boil down to the code being inside the callback and updating the state when a value comes from the database.

Rendering a new component inside componentDidMount - React

I will have to render a new component after all the expected components are loaded. I will need a timeout based on which the the new component has to be rendered. So this new component has to show up after 5 minutes after the page has loaded.
I need to render a component called new_component that extends React.component
public componentDidMount(): void {
if (visited) {
setTimeout(() => {
console.log('Reached the timeout')
//Render the new conponent here. (Not sure how to call the render function of another component here)
}, timeout);
}
Can someone help me call the render function of new_component inside componentDidMount please. i tried new_component.render(). But that does not seem to work.
You can use state to track this.
componentDidMount() {
setTimeout(() => {
this.setState({ showNewComponent: true })
})
}
and in render:
render() {
if (this.state.showNewComponent) {
return <NewComponent />
}
return null
}
You can go with this code, wait and then render new one:
cosnt FIVE_MIN = 5 * 60 * 1000
class Example {
this.state = { isShowComponent: false }
timer
componentDidMount() {
this.timer = setTimeout(() => {
this.setState({ isShowComponent: true })
}, FIVE_MIN)
}
componentWilllUnmount() {
clearTimeout(this.timer)
}
render() {
if (this.state.isShowComponent) return <NewComponent />
return <Component />
}
}
:)
you can render your component by your state.
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
isTimeout: false,
};
}
componentDidUpdate(prevProps, prevState) {
this.checkTimeout = setTimeout(() => {
this.setState(() => ({isTimeout: true}))
}, 500);
}
componentWillUnmount() {
// clean it up when the component is unmounted.
clearTimeout(this.checkTimeout);
}
render () {
if (isTimeout) {
return (k<h1>time is running out</h1>)
}
return (<h1>hello world.</h1>)
}
}

ReactJS - Pass Updated Value To Sub-Component Method

I'm working on an environment that is basically set up with a Main Component like this:
class MainComponent extends Component {
constructor(props) {
super(props);
this.state = {
selectedValues: []
};
}
render() {
const { selectedValues } = this.state;
return (
// Other components
<SubComponent selectedValues = {selectedValues} />
// Other components
);
}
}
export default MainComponent;
And a Sub Component like this:
class SubComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isExporting: false,
selectedValues: props.selectedValues
};
}
performTask = () => {
this.setState({ isWorking: true });
const { selectedValues } = this.state;
console.log(`Selected Values: ${selectedValues}`);
fetch('/api/work', {
method: 'GET'
})
.then(res => res.json())
.then((result) => {
// Handle the result
this.setState({ isWorking: false });
})
.catch((error) => {
console.log(error);
this.setState({ isWorking: false });
});
};
render() {
const { isWorking } = this.state;
return (
<Button
bsStyle="primary"
disabled={isWorking}
onClick={() => this.performTask()}
>
{isWorking ? 'Working...' : 'Work'}
</Button>
);
}
}
SubComponent.propTypes = {
selectedValues: PropTypes.arrayOf(PropTypes.string)
};
SubComponent.defaultProps = {
selectedValues: []
};
export default SubComponent;
In the Main Component, there are other components at work that can change the selectedValues. The functionality I'd like to see is that when the performTask method fires, it has the most recent and up to date list of selectedValues. With my current setup, selectedValues is always an empty list. No matter how many values actually get selected in the Main Component, the list never seems to change in the Sub Component.
Is there a simple way to do this?
I would suggest you 2 of the following methods to check this problem:
Maybe the state.selectedItems doesn't change at all. You only declare it in the contractor but the value remains, since you didn't setState with other value to it. Maybe it will work if you will refer to this.props.selectedItems instead.
Try to add the function component WillReceiveProps(newProps) to the sub component and check the value there.
If this method doesn't call, it means the selectedItems doesnt change.
Update if some of it works.
Good luck.
selectedValues in SubComponent state has not updated since it was set in SubComponent constructor. You may need to call setState again in componentWillReceivedProps in SubComponent

How to force children rerendering after axios call in React?

I'm working on a form with interactive inputs. They have to actualise themselves with information into parent state.
I use Axios to get the data to show, getting them from an external API. I tried to set default values, but they never actualise with newer values.
class Form extends React.Component {
getData() {
axios.get('http://xxx/getform/').then(
res => this.setState(res.data)
);
}
componentDidMount() {
this.getData();
setInterval(() => {
this.getData();
}, 36000000)
}
render() {
return (
<div>
<form>
<DatePicker />
</form>
</div>
)
}
}
class DatePicker extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: new Date(),
runMin: new Date(),
runMax: new Date()
};
}
getDate() {
console.log('DAD');
try { // if axios didn't finish, to avoid undefined
this.setState({
runMin: super.state.RunMin,
runMax: super.state.RunMax})
} catch (e) {
this.setState({
runMin: new Date(),
runMax: new Date()})
}
}
componentDidMount() {
this.getDate();
this.setState({selected: this.state.runMax});
}
render() {
return (<div></div>);
}
}
Actually after axios call, the children doesn't rerender. I separated the call for axios and the component using it, because the Form component do a single call for multiple children (not displayed here), and they read the parent's state to render.
Firstly, you should not access the parents state using super and instead pass the required value as props
Secondly, componentDidMount lifecycle is executed on initial mount and hence the logic within it won't execute when the parent state updates.
The correct way to handle your case would be
class Form extends React.Component {
state = {
RunMin: new Date(),
RunMax: new Date()
}
getData() {
axios.get('http://xxx/getform/').then(
res => this.setState({RunMin: res.data.RunMin, RunMax: res.data.RunMax})
);
}
componentDidMount() {
this.getData();
setInterval(() => {
this.getData();
}, 36000000)
}
render() {
return (
<div>
<form>
<DatePicker runMin={this.state.RunMin} runMax={this.state.RunMax}/>
</form>
</div>
)
}
}
class DatePicker extends React.Component {
render() {
console.log(this.props.runMin, this.props.runMax);
return (<div></div>);
}
}
The way you are setting the state is incorrect
Change
this.setState(res.data);
To
this.setState({data: res.data});
You need to set the response to a state field you have in component and make sure you pass the data to the child component

How to test fetch result in componentDidMount

I need to test if the state was set after promise resolved in componentDidMount
class Todos extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const result = todoStore.fetch();
mobx.when(
() => result.state !== mobxUtils.PENDING,
() => (this.setState({todos: result.value}))
)
}
render() {
const { todos } = this.state;
return <div>
{todos && <ul>
{todos.map(t => <li>{t.title}</li>)}
</ul>}
</div>
}
}
So far I have..
const wrapper = shallow(<Todos />);
// TODO Need wait until promise is resolved. But how?
// Maybe listen the setState method ?
// PLEASE don't use setTimout.
assert(wrapper.state('todos'))
See the full example..
https://runkit.com/ridermansb/testing-fetch-result-in-componentdidmount/1.0.0

Resources