Skip to content
Advertisement

how to properly handle input dependent async data calls in react component?

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:

  1. The input is changed and a an update promise (A) is created and now pending.
  2. The input is changed again and a new update promise (B) is created.
  3. Promise B is resolved while promise A is still pending.
  4. The state will now be changed to the result of promise B.
  5. Promise A is now resolved and will override the result of Promise B in the state.
  6. 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.

User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement