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.