Race conditions in React and beyond. A race condition guard with TypeScript

栏目: IT技术 · 发布时间: 4年前

内容简介:Fetching resources is a bread and butter of the frontend development. When doing so, we might encounter a set of issues related toThe term “Our code does not need to utilize multiple threads to have a race condition. A similar issue might occur with asynch

Fetching resources is a bread and butter of the frontend development. When doing so, we might encounter a set of issues related to race conditions . In this article, we identify them and provide a solution.

Defining a race condition

The term “ race condition ” dates back to as far as 1954. We use it in the field of not only software but also electronics. It describes a situation when the behavior of our system is dependant on a sequence of events, but its order is uncontrollable. One of the apparent examples of race conditions is present in  multithreading when two or more threads attempt to change shared data. Therefore, they are  racing to access it.

Our code does not need to utilize multiple threads to have a race condition. A similar issue might occur with asynchronous code. Imagine the following situation:

  • The user visits the  / posts / 1  page intending to view the post number 1.
    • we start fetching it, and it takes a while
  • Meanwhile, the user changes the page to  / posts / 2
    • we begin to fetch post number 2 that loads almost immediately
  • After the above completes, the first request finishes and the content of the page displays the post number 1

Unfortunately, once we create a Promise, we can’t cancel it. There has been quite a lot of discussion about whether or not it should be implemented. There even was a proposal for it , but it has been withdrawn.

If you want to know more about promises, check out Explaining promises and callbacks while implementing a sorting algorithm

Reproducing the issue in React

The above issue is quite easy to reproduce, unfortunately.

All of the below examples use React with TypeScript.

While making this article I use an API that I’ve built with Express. If you want to know more about it, check out this TypeScript Express series

interface Post {
  id: number;
  title: string;
  body: string;
}
import React from 'react';
import usePostLoading from './usePostLoading';
 
const Post = () => {
  const { isLoading, post } = usePostLoading();
  return (
    <div>
      {
        isLoading ? 'Loading...' : post && (
          <>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </>
        )
      }
    </div>
  );
};

The above component loads the post based on the URL of the application. The whole logic happens in the usePostLoading hook.

import { useRouteMatch } from 'react-router-dom';
import { useEffect, useState } from 'react';
import Post from '../interfaces/Post';
 
function usePostLoading() {
  const { params } = useRouteMatch();
  const [post, setPostState] = useState<Post>();
  const [isLoading, setLoadingState] = useState(false);
 
  useEffect(
    () => {
      setLoadingState(true);
      fetch(`http://localhost:5000/posts/${params.id}`)
        .then((response) => {
          if (response.ok) {
            return response.json();
          }
          return Promise.reject();
        })
        .then((fetchedPost) => {
          setPostState(fetchedPost);
        })
        .finally(() => {
          setLoadingState(false);
        });
    },
    [params],
  );
 
  return {
    post,
    isLoading,
  };
}

If you want to know more about how to design custom hooks, check out JavaScript design patterns #3. The Facade pattern and applying it to React Hooks

Unfortunately, the above code is prone to race conditions. The most straightforward fix for it is to introduce an additional didCancel variable, as Dan Abramov described in his article on overreacted.io .

useEffect(
  () => {
    let didCancel = false;
    
    setLoadingState(true);
    fetch(`http://localhost:5000/posts/${params.id}`)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
        return Promise.reject();
      })
      .then((fetchedPost) => {
        if (!didCancel) {
          setPostState(fetchedPost);
        }
      })
      .finally(() => {
        setLoadingState(false);
      });
    
    return (() => {
      didCancel = true;
    })
  },
  [params],
);

By returning a function in our useEffect hook, we can perform a  cleanup . React runs this function when the component unmounts, as well as after every render. Thanks to that, it prevents setting the post from a previous request.

If you want to know more about how the useEffect hook works, check out Understanding the useEffect hook in React

Creating a race condition guard

The above solution might not work for you in a more complex situation. It also gets a bit messy, because we need to remember always to check the didCancel variable. Instead, we can create a custom race condition guard. It’s a concept that I took from the article of Sebastien Lorber and built on top of it. Let’s walk through it step by step.

First, we globally keep a reference to a promise:

let lastPromise: Promise<any>;

We update it every time we start making a request.

let currentPromise = fetch(`http://localhost:5000/posts/${params.id}`);
lastPromise = currentPromise;

Then, we can use the currentPromise . Since we keep a reference to the last promise, we can check if the one that resolved matches that.

currentPromise.then((response) => {
    if (response.ok) {
      return response.json();
    }
    return Promise.reject();
  })
  .then((fetchedPost) => {
    if (lastPromise === currentPromise) {
      setPostState(fetchedPost);
    }
  })

The above logic makes sure that we don’t update the state with data from an old promise. Unfortunately, we still need to check if the promises match in every callback.

A way to resolve the above issue is to return a promise that never resolves .

currentPromise.then((response) => {
    if (response.ok) {
      return response.json();
    }
    return Promise.reject();
  })
  .then((response) => {
    if (lastPromise !== currentPromise) {
      return new Promise(() => {});
    }
    return response;
  })
  .then((fetchedPost) => {
    setPostState(fetchedPost);
  })

The above logic is still a bit messy. Let’s create a class that takes care of all of the above.

class RaceConditionGuard<PromiseResolveType = any> {
  private lastPromise?: Promise<PromiseResolveType>;
  getGuardedPromise(promise: Promise<PromiseResolveType>) {
    this.lastPromise = promise;
    return this.lastPromise.then(this.preventRaceCondition());
  }
  private preventRaceCondition() {
    const currentPromise = this.lastPromise;
    return (response: PromiseResolveType) => {
      if (this.lastPromise !== currentPromise) {
        console.log('promise cancelled');
        return new Promise(() => null) as Promise<PromiseResolveType>;
      }
      return response;
    };
  }
}

The preventRaceCondition function returns a callback. It returns a promise that never resolves if the data is too old.

Moving all of this logic to a separate class cleans up our code. The only thing left to do is to use our RaceConditionGuard class.

import { useRouteMatch } from 'react-router-dom';
import { useEffect, useState } from 'react';
import Post from '../interfaces/Post';
import RaceConditionGuard from '../utilities/RaceConditionGuard';
 
const raceConditionGuard = new RaceConditionGuard();
 
function usePostLoading() {
  const { params } = useRouteMatch();
  const [post, setPostState] = useState<Post>();
  const [isLoading, setLoadingState] = useState(false);
 
  useEffect(
    () => {
      setLoadingState(true);
      raceConditionGuard.getGuardedPromise(
        fetch(`http://localhost:5000/posts/${params.id}`)
      )
        .then((response) => {
          if (response.ok) {
            return response.json();
          }
          return Promise.reject();
        })
        .then((fetchedPost) => {
          setPostState(fetchedPost);
        })
        .finally(() => {
          setLoadingState(false);
        });
    },
    [params],
  );
 
  return {
    post,
    isLoading,
  };
}
 
export default usePostLoading;

The default value of the PromiseResolveType in our class is  any , but we can easily change that by using an interface:

const raceConditionGuard = new RaceConditionGuard<Post>();
raceConditionGuard.getGuardedPromise(
  fetch(`http://localhost:5000/posts/${params.id}`)
    .then((response) => {
      if (response.ok) {
        return response.json();
      }
      return Promise.reject();
    })
)

This logic would be even cleaner if we would create a separate function that wraps the Fetch API with an additional logic. If you want to know more, check out TypeScript Generics. Discussing naming conventions

Summary

Being aware of the issue discussed in this article is an important thing. We should choose the fix taking a few things into consideration. The first solution that we’ve seen today is strictly limited to React and might be a fitting choice in many situations. The second approach works in many different cases and is completely separate from React. You can also use it regardless if you use TypeScript, or not. Also, you might want to look into solutions like Redux-Saga and RxJS.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C语言名题精选百则技巧篇

C语言名题精选百则技巧篇

冼镜光 / 机械工业出版社 / 2005-7 / 44.00元

《C语言名题精选百则》(技巧篇)收集了100则C语言程序设计题,共分9类。第一类比较简单,主要希望读者了解到《C语言名题精选百则》(技巧篇)的题目、解法与其他书籍之间的差异;第二至六类分别是关于数字、组合数学或离散数学、查找、排序、字符串等方面的题目;第七类列出了一些不太容易归类的题目,如Buffon丢针问题、Dijkstra的三色旗问题等;第八类则收录了一些有趣的、娱乐性的题目,如魔方阵等;第九......一起来看看 《C语言名题精选百则技巧篇》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具