• How to avoid concurrency issues in React

    5adaf9bed8150valentino rossi alvaro bautista motogp racing 62686

    React proposes a simple declarative way to build an application’s view. It’s straightforward to write a hierarchy of stateless components or to change a state synchronously. The whole app works as an integration of two pure functions “state(event)” and “view(state)“. The behavior is testable and predictable. But you can’t make something complex without depending on an external data sources integration: side effects come into the stage. Beware, you’re approaching the Dark Zone of functional programming now :) 

    A network is the most common side effect in any web application. If a component fetches data from a remote location, you can’t predict the time it would take the particular request to finish, and the order of your queries execution, or even the success status of the job. For example, let’s assume the following situation, which is not a rare thing on a slow connection:

    1. A Component mounts and makes a request A with A1 arguments to fetch the data from a server
    2. User goes to another page or tab, so the Component unmounts
    3. User goes back to the first page and the Component mounts again and makes a request B with B1 arguments to fetch the data
    4. Request B was very lucky, and it completes first
    5. Request A completes only after request B

    If you use external state storage, like Redux, your Component will receive old data. Even if you use a local state for a component, there would be an entirely useless waste of user’s bandwidth (and highly probable — a lot of warnings in a console!)

    Simple Component with an external data source

    Let’s build a simple component to fetch an avatar URL from github.com for a given person. And let’s make it as simple as it’s possible while still keeping in mind all the loading and error handling stuff.

    class User extends React.Component {
      render() {
        const { user } = this.props;
        return (
          <div className="user">
            <img src={user.avatar_url} />
          </div>
        );
      }
    }
    
    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          user: null,
          loading: false,
          error: null,
        };
      }
    
      saveInput = (input) => {
        this.input = input;
      };
    
      ask = () => {
        this.setState((state) => ({ ...state, loading: true, error: null }));
        axios({
          method: 'get',
          url: `https://api.github.com/users/${this.input.value}`,
        })
          .then(({ data }) => {
            this.setState((state) => ({
              ...state,
              loading: false,
              user: data,
              error: null,
            }));
          })
          .catch((err) => {
            this.setState((state) => ({
              ...state,
              loading: false,
              user: null,
              error: err.message,
            }));
          });
      };
    
      render() {
        const { user, loading, error } = this.state;
        return (
          <React.Fragment>
            <div>
              <h2>Ask GITHUB about someone:</h2>
              <input ref={this.saveInput} defaultValue="astartsky" />
              <button onClick={this.ask}>Ask</button>
            </div>
            {loading && <div>Loading...</div>}
            {error && <div>Sorry! {error}</div>}
            {!!user && <User user={user} />}
          </React.Fragment>
        );
      }
    }

    Now, if you limit the network speed and try to ask it about different persons, you will see a lot of requests, and none of them would be canceled. Let’s address this problem using a high order component implementing the cancellation logic.

    Component wrapped with Request Watcher

    const RequestWatcher =
      (config = {}) =>
      (BaseComponent) => {
        const {
          // function will be called automatically on component mount
          loadFunc = 'load',
          // `props => args` function allow to specify `loadFunc` arguments
          loadFuncArgs = () => [],
          // `props => <Loader />` renderer
          renderLoader = () => <div>Loading...</div>,
          // `props => message => <Error />` renderer
          renderError = () => (message) => <div>Sorry! {message}</div>,
        } = config;
    
        return class extends React.Component {
          static displayName = `RequestWatcher(${BaseComponent.displayName})`;
    
          constructor(props, context) {
            super(props, context);
            this.state = {
              loading: true,
              error: null,
            };
            this.sources = {};
            this.bindedFuncs = [loadFunc].reduce((map, func) => {
              map[func] = (...args) => {
                // eslint-disable-line no-param-reassign
                if (this.sources[func]) {
                  this.sources[func].cancel('concurent run');
                }
                this.sources[func] = axios.CancelToken.source();
                return this.props[func](this.sources[func].token, ...args);
              };
              return map;
            }, {});
          }
    
          componentDidMount() {
            this.mounted = true;
            const args = loadFuncArgs(this.props);
            this.load(...args);
          }
    
          componentWillReceiveProps(nextProps) {
            const before = loadFuncArgs(this.props);
            const after = loadFuncArgs(nextProps);
            if (!isEqual(before, after)) {
              this.load(...after);
            }
          }
    
          componentWillUnmount() {
            this.mounted = false;
            Object.keys(this.sources)
              .map((k) => this.sources[k])
              .forEach((source) => {
                source.cancel('master component unmounted');
              });
          }
    
          async load(...args) {
            if (!loadFunc) {
              if (this.mounted) {
                this.setState({ loading: false, error: null }); // eslint-disable-line react/no-did-mount-set-state
              }
              return;
            }
            try {
              if (this.mounted) {
                this.setState({ loading: true, error: null }); // eslint-disable-line react/no-did-mount-set-state
              }
              const data = await this.bindedFuncs[loadFunc](...args);
              if (this.mounted) {
                this.setState({ loading: false, error: null, data }); // eslint-disable-line react/no-did-mount-set-state
              }
            } catch (err) {
              if (this.mounted) {
                this.setState({ loading: false, error: err.message }); // eslint-disable-line react/no-did-mount-set-state
              }
            }
          }
    
          render() {
            const { data, error, loading } = this.state;
    
            if (!!loadFunc && error) {
              return renderError(this.props)(error);
            }
            if (!!loadFunc && loading) {
              return renderLoader(this.props);
            }
            return (
              <BaseComponent
                data={data}
                isLoading={loading}
                error={error}
                {...this.props}
                {...this.bindedFuncs}
              />
            );
          }
        };
      };
    
    class User extends React.Component {
      render() {
        const { data } = this.props;
        return (
          <div className="user">
            <img src={data.avatar_url} />
          </div>
        );
      }
    }
    
    const UserBinded = RequestWatcher({
      loadFuncArgs: ({ name }) => [name],
    })(User);
    
    const load = (token, value) =>
      axios({
        method: 'get',
        url: `https://api.github.com/users/${value}`,
        cancelToken: token,
      }).then(({ data }) => data);
    
    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          user: null,
          name: null,
        };
      }
    
      saveInput = (input) => {
        this.input = input;
      };
    
      ask = () => {
        this.setState((state) => ({ ...state, name: this.input.value }));
      };
    
      render() {
        const { user, name } = this.state;
        return (
          <React.Fragment>
            <div>
              <h2>Ask GITHUB about someone:</h2>
              <input ref={this.saveInput} defaultValue="astartsky" />
              <button onClick={this.ask}>Ask</button>
            </div>
            {!!name && <UserBinded name={name} load={load} />}
          </React.Fragment>
        );
      }
    }

    The idea behind this HOC is to create a token and pass it into every function call with network side effects. The request watcher HOC will watch the React lifecycle for you. If Component cease to exist or there are more recent calls to that function, it will cancel the request. As a bonus, it will look if your props are changed to decide whether to run a new query or you’d be fine without it.

One is the pinnacle of evolution; another one is me

One on the picture is the pinnacle of evolution; another one is me: inspired developer, geek culture lover, sport and coffee addict