I am currently building some apps using Typescript and React. To this date i have used some smelly workarounds for the following situation which i would like to get rid of. Maybe you know a better way of doing such.
The Setup: Having a React Component which sould display data which is fetched from server on user input. Example use case: fetch the name of a city from an API based on a postal code entered by user. Example implementation:
import * as React from 'react';
export interface IXmplState {
plc: string;
name: string;
}
export default class Xmpl extends React.Component<{}, IXmplState> {
constructor(props){
super(props);
this.state = {name: "", plc: ""};
}
private fetchName(plc: string): Promise<string> {
//Fetch data from server.
}
private updateName(plc: string): void {
this.fetchName(plc).then(newName => this.setState({name: newName}));
}
public render(): React.ReactElement<{}> {
return(
<div>
<input value={this.state.plc} onChange={(event) => this.updateName(event.target.value)}/>
<div>{this.state.name}</div>
</div>
);
}
}
The problem:
As soon as the user input changes, the updateName()
is called, which then updates the state
on resolved promise. Consider the following case:
- The input is changed and a an update promise (A) is created and now pending.
- The input is changed again and a new update promise (B) is created.
- Promise B is resolved while promise A is still pending.
- The state will now be changed to the result of promise B.
- Promise A is now resolved and will override the result of Promise B in the state.
- The user is now presented with the wrong name (schould be result of B but is result of A)
Are there ways to surpress such behavior? Are there specific ways/libs for doing this in React, typescript or javascript? Or is such kind of input handling generally to be avoided? Which would be a better way or the best way of handling such scenario in gernerally?
Greets and Thanks
EDIT: for the sake of completeness.
My current way of handling such scenarios is introducing a checksum in the component and only update the state if the checksum is still not altered.
export default class Xmpl extends React.Component<{}, IXmplState> {
let nameCkSm: number = 0;
private updateName(plc: string): void {
let ckSm = ++this.nameCkSm;
this.fetchName(plc).then(newName => this.setState(() => {
if(this.nameCkSm === ckSm) return {name: newName};
}));
}
Advertisement
Answer
AbortController
(standard web feature, not a lib) is good for this, see ***
comments:
export default class Xmpl extends React.Component<{}, IXmplState> {
// *** An AbortController for the update
pendingNameController: AbortController | null = null;
constructor(props: IXmplState) {
super(props);
this.state = { name: "", plc: "" };
}
// *** Accept the signal
private fetchName(plc: string, signal?: AbortSignal): Promise<string> {
// Fetch data from server, pass `signal` if the mechanism supports
// it (`fetch` and `axios` do, for instance)
}
private updateName(plc: string): void {
// *** Cancel any outstanding call
this.pendingNameController?.abort();
// *** Get a controller for this call, and its signal
this.pendingNameController = new AbortController();
const { signal } = this.pendingNameController;
// *** Pass the signal to the `fetchName` method
this.fetchName(plc, signal)
.then((name) => {
// *** Don't update if the request was cancelled (ideally you'd
// never get here because a cancelled request won't fulfill the
// promise, but race conditions can mean you would)
if (!signal.aborted) {
this.setState({ name });
}
})
.catch((error) => {
// ...handle.report error...
})
}
public render(): React.ReactElement<{}> {
// ...
}
}
You can write a utility that handles multiple outstanding requests for different things by wrapping the fetching method, etc. But that’s the basic mechanism for using AbortController
for this.