preface

A “search list” is a business scenario with a search box and a list of search results. The ability to display search-related information in a list as the user enters text. This business scenario should be done by many students, today the author will lead you to gradually optimize this business scenario. This article suggests browsing on the PC end, the author has prepared a demo for you, the audience can open the demo of the following chapter 🔗 for debugging. First, there are a few requirements:

  1. Instant response to user input
  2. Try to quickly display a list of relevant user searches
  3. Page flow, performance as high as possible
  4. Unnecessary requests need to be reduced
  5. Asynchronous request timing needs to be considered

Simple business completion

First, the first version does business first. I use React as the column

import {useState} from 'react';

function FirstDemo() {
  // const [inputText, setInputText] = useState('');
  const [list, setList] = useState([]);

  const onChange = (e) = > {
    console.log('onInput')
    fetch(`${api}&q=${e.target.value}`)
      .then(async res => {
        let result = await res.json();
        setList(Array.isArray(result) ? result : []);
      })
      .catch(e= > {
      console.log(e)
    })
  }

  return <div className="box">
    <div className="search-bar">Please enter the name of the warehouse you want to search:<input 
        onChange={onChange} 
      /></div>
    <ul className="list-box">{list? .map((v, i) => { return<li>{v.name}</li>
      })}
    </ul>
  </div>
}
Copy the code

Ok, we have finished the first version of Demo1, let’s see what the problems are.

Anti-shake optimized version

Think about this scenario. When users enter a word or phrase quickly, they can request it without adjusting the API. Some students may think of anti-shake and throttling to optimize the frequency of service invocation. In the case of not affecting the user experience, GENERALLY 30-100 ms I think is more reasonable, ok, this optimization method should be used by everyone, I will not do the code demonstration here.

Optimized Chinese, Japanese and Korean text input

Let’s take a look at the requirement “To respond to user searches in real time”. What events do we need to bind to the search box? Native onchange and onInput are available as alternatives, and their execution timing is slightly different. You can check the difference on MDN. For React users, React already encapsulates native events to ensure that onChange events are triggered at the appropriate time. But what if you tried typing CJK(Chinese, Japanese, Korean) text? What happens? When you switch to The Chinese input method, try typing “zhongguo”, you will find 8 invalid API calls before typing “China”. Why is it invalid? I think users don’t really care about these intermediate search results. I think it’s a bug.

const onCompositionStart = (e) = > {
    console.log('onCompositionStart');
  }
  const onCompositionUpdate = (e) = > {
    console.log('onCompositionUpdate');
  }
  const onCompositionEnd = (e) = > {
    console.log('onCompositionEnd');
  }

  return <div className="box">
    <div className="search-bar">
      <input
        onChange={onChange}
        onCompositionStart={onCompositionStart}
        onCompositionUpdate={onCompositionUpdate}
        onCompositionEnd={onCompositionEnd}
      /></div>
    <ul className="list-box">{list? .map((v, i) => { return<li>{v.text}</li>
      })}
    </ul>
  </div>
Copy the code

I started by adding onCompositionStart, onCompositionUpdate, onCompositionEnd events to the code. You can test the log printed by the console when typing non-CJK and CJK text in Demo2.

We can take advantage of the fact that only onInput events are executed when non-CJK text is entered, When entering CJK text, onCompositionStart, onCompositionUpdate and onInput are executed successively (onCompositionUpdate and onInput are repeated processes). Finally, onCompositionEnd is executed after selecting the text to be selected. Based on the above tests, should our event be executed in onCompositionEnd when the user enters CJK text? Code implementation

function SearchDemo() {
  const isCompositionRef = useRef(false);
  const [list, setList] = useState([]);

  const getData = (e) = > {
    fetch(`${api}&q=${e.target.value}`)
      .then(res= > {
      console.log(res);
      // setList(res? .data || []);
      })
      .catch(e= > {
      console.log(e)
    })
  }
  const onChange = (e) = > {
    // enter CJK without executing getData
    if(isCompositionRef.current) return;
    getData(e)
  }
  const onCompositionStart = (e) = > {
    isCompositionRef.current = true;
  }
  const onCompositionEnd = (e) = > {
    isCompositionRef.current = false;
    getData(e)
  }

  return <div className="box">
    <div className="search-bar">
      <input
        onChange={onChange}
        onCompositionStart={onCompositionStart}
        onCompositionEnd={onCompositionEnd}
      /></div>
    <ul className="list-box">{list? .map((v, i) => { return<li>{v.text}</li>
      })}
    </ul>
  </div>
}
Copy the code

Now that our code has optimized this scenario, you can try it out. demo3

I’ve also seen this optimization in the el-Input source for Element-UI. Of course, CompositionEvent is an event that occurs when the user indirectly enters text (such as using input methods). It also supports voice input, etc. You can explore it yourself.

The virtual scroll list optimizes large data volumes

Before React Fiber (React Concurrent Features) is enabled, React may occupy JS threads for a long time. React and ReactDOM are easy versions of React and ReactDOM, which don’t need to be shown in code. Let’s take a look at another solution, the virtual list scrolling scheme, where essentially no matter how much data there is, we just display the items in the container or in the Viewport, and the user dynamically changes the data displayed in the container as they scroll. Zhihu-ele. me team has a good article on the implementation principle of virtual list, recommend you to read and then talk about the implementation of front-end virtual list. Here I recommend a component called React-tiny-virtual-list, which is only 3KB in size after gzip. The author considers many things, such as the height consistency of items in the list, height inconsistency, and the custom buffer for invisible areas of the container (to solve the problem of page blank if you slide too fast). The author has done a lot of demos, so you can try them out, but I’m not going to do the code demo here.

Optimize timing issues for asynchronous requests

Because the Internet is a large network, when we frequently send requests to the back end, it is likely that each request will take a different path (routing and addressing) and some other reason that the request is sent first and the response is received. If the browser makes two requests 1, 2, and the browser receives the response from request 2 and draws it in, then it receives the response from Request 1 and draws it in, it will cause a bug if the search keywords for request 1 and request 2 are different.

function SearchDemo() {
  // fetch returns a promise and saves only the last promise
  const lastPromise = useRef();
  const [list, setList] = useState([]);

  const getData = (e) = > {
    const currentPromise = fetch(`${api}&q=${e.target.value}`)
    lastPromise.current = currentPromise
    // Filter out not the last request
    currentPromise.then(
      res= > {
        if (currentPromise === lastPromise.current) {
          setList(res?.data || []);
        }
      },
      e= > {
        if (currentPromise === lastPromise.current) {
          console.warn('fetch failure', e); }}); }const onChange = (e) = > {
    getData(e)
  }

  return <div className="box">
    <div className="search-bar">
      <input
        onChange={onChange}
      /></div>
    <ul className="list-box">{list? .map((v, i) => { return<li>{v.text}</li>
      })}
    </ul>
  </div>
}
Copy the code

In DEMO4, the technique is relatively strong. The fetch method is used to return different memory address of the Promise object each time, and only the memory address of the last Promise object is always recorded, and only the response of the last request is judged and processed when the response is received. The idea was originally derived from the Handling API request race Conditions in React

Another scheme optimizes the timing problem of asynchronous requests

The Handling API request race conditions in React also provides a way to cancel all previous requests. An added benefit here is that if the request is cancelled in advance, the browser will skip the process of parsing the Response. In the previous one, the browser actually parses the Response, but we didn’t use demo5. The code is as follows:

function SearchDemo() {
  const isCompositionRef = useRef(false);
  const [list, setList] = useState([]);
  const [text, setText] = useState([]);

  const onChange = (e) = > {
    // enter CJK without executing getData
    if(isCompositionRef.current) return; setText(e? .target? .value); }const onCompositionStart = (e) = > {
    isCompositionRef.current = true;
  }
  const onCompositionEnd = (e) = > {
    isCompositionRef.current = false; setText(e? .target? .value); } useEffect(() = > {
    setList([]);
    // Create the current request's abort controller
    const abortController = new AbortController();
    // Issue the request
    fetch(`${api}&q=${e.target.value}`, {
      signal: abortController.signal,
    })
      .then(res= > {
        // IMPORTANT: we still need to filter the results here,
        // in case abortion happens during the delay.
        // In real apps, abortion could happen when you are parsing the json,
        // with code like "fetch().then(res => res.json())"
        // but also any other async then() you execute after the fetch
        if (abortController.signal.aborted) {
          return; } setList(res? .data || []); }) .catch(e= > {
      console.log(e)
    })
    // Trigger the abortion in useEffect's cleanup function
    return () = > {
      abortController.abort();
    };
  }, [text]);

  return <div className="box">
    <div className="search-bar">
      <input
        onChange={onChange}
        onCompositionStart={onCompositionStart}
        onCompositionEnd={onCompositionEnd}
      /></div>
    <ul className="list-box">{list? .map((v, i) => { return<li>{v.text}</li>
      })}
    </ul>
  </div>
}
Copy the code

For example, if you type “123” quickly, you will see something like this on the console:

At the end

The “search list” is just a small business scenario, and there is always a breakthrough if you want to optimize. However, I would like to say that we also need to consider the development cost and income, for income and development cost are not equal to the case we do not need to do so much optimization (it is not impossible to use), but also relatively opposed to “premature optimization”.

This is also the first post of 2022, and I wish you all the best in the New Year “promotion, salary increase, and financial freedom.” If this article is helpful to you, please click “follow” and “like” to support me. Thank you. Welcome to reprint please indicate the source

The Handling API request race conditions in React React-tiny-virtual-list