background

The fuzzy search component (hereafter referred to as the ES component) is a very common business component throughout the team. On almost every list page, there is a search function. However, even though we had encapsulated the business component as well as we could, we repeatedly stumbled through a lot of component callbacks and complex business requirements.

After this, I never want to hear QA come to me with front-end problems with ES search.

The code required for this sharing can be taken with the article: git.garena.com/shopee/bg-l…

So, before we start the body of the text, we need to take a look at our previous callback code. Note that don’t read all the code, just get the whole picture.

EsSearch components.

// ES searches for 1
<span style={{ marginRight: 8 }}>{translate('filtersku.product')}</span>
<EsSearch
  url={` /api/es/autocomplete/product_name?include_delete=${includeDelete}`}
  esDocName="product_name"
  onChange={(value: any) = >{ setSearchModel({ ... searchModel, product_name: value, search_type: value ? 1 : undefined }); }} onSelect={(value: any) => { const newPager = { pageno: 1 }; const newSearchType = value ? 0 : lastSearchTypeSku; const newSearchModel = { product_name: value, search_type: newSearchType }; lastSearchTypeProduct = newSearchType; beforeLoadList(newPager, newSearchModel); }} onPressEnter={(value: any) => { const pressLastSelected = searchModel.product_name === value && ! searchModel.search_type; if (pressLastSelected) return; const newPager = { pageno: 1 }; const newSearchType = value ? 1 : lastSearchTypeSku; const newSearchModel = { product_name: value, search_type: newSearchType }; lastSearchTypeProduct = newSearchType; beforeLoadList(newPager, newSearchModel); }} / >

// ES search for 2
<span style={{ marginRight: 8}} >{translate('filtersku.variation')}</span>
<EsSearch
  url={` /api/es/autocomplete/sku_name?include_delete=${includeDelete}`}
  esDocName="sku_name"
  onChange={(value: any) = >{ setSearchModel({ ... searchModel, sku_name: value, search_type: value ? 1 : undefined }); }} onSelect={(value: any) => { const newPager = { pageno: 1 }; const newSearchType = value ? 0 : lastSearchTypeProduct; const newSearchModel = { sku_name: value, search_type: newSearchType }; lastSearchTypeSku = newSearchType; beforeLoadList(newPager, newSearchModel); }} onPressEnter={(value: any) => { const pressLastSelected = searchModel.sku_name === value && ! searchModel.search_type; if (pressLastSelected) return; const newPager = { pageno: 1 }; const newSearchType = value ? 1 : lastSearchTypeProduct; const newSearchModel = { sku_name: value, search_type: newSearchType }; lastSearchTypeSku = newSearchType; beforeLoadList(newPager, newSearchModel); }} / >
Copy the code

As you can see, it takes 100 lines of code just to implement the two ES functions on the page, which is sometimes confusing if nested within the business logic.

In addition, careful students have found that although there are two ES functions, the processing logic in the onChange, onSelect and onPressEnter callback functions are surprisingly similar, and surprisingly difficult to read.

SelectInput component, a component that can be configured to support ES search, or precise search.

<SelectInput
  customQuery={customQuery}
  onSelect={() = > {
    constnewPager = { ... pager,current: 1 };
    constnewSearchModel = { ... searchModel,platform_product_name: null.physical_product_name: null.physical_sku_id_barcode: null}; setPager(newPager); setSearchModel(newSearchModel); getSalesOrderList(newPager, filterModel, sorterModel, newSearchModel); }} onEsSearchSelect={(value: any) = > {
    constnewPager = { ... pager,current: 1 };
    constnewSearchModel = { ... searchModel,platform_product_name: null.physical_product_name: null.physical_sku_id_barcode: null. value,search_type: 0
    };
    setPager(newPager);
    setSearchModel(newSearchModel);
    getSalesOrderList(newPager, filterModel, sorterModel, newSearchModel);
  }}
  onPressEnter={(value: any) = > {
    constnewPager = { ... pager,current: 1 };
    constnewSearchModel = { ... searchModel,platform_product_name: null.physical_product_name: null.physical_sku_id_barcode: null. value,search_type: 1
    };
    setPager(newPager);
    setSearchModel(newSearchModel);
    getSalesOrderList(newPager, filterModel, sorterModel, newSearchModel);
  }}
  onChange={(value: any) = > {
    if (Object.keys(value).includes('physical_sku_id_barcode')) {
      constnewPager = { ... pager,current: 1 };
      constnewSearchModel = { ... searchModel,platform_product_name: null.physical_product_name: null.physical_sku_id_barcode: null. value }; setPager(newPager); setSearchModel(newSearchModel); delayedFetch(newPager, filterModel, sorterModel, newSearchModel, activeKey); }}} / >Copy the code

It’s terrible! That’s more than 100 lines of Code, and a typical VS Code editor can only view about 50 lines of Code on one screen, which means it’s impossible to see the logic in a single glance. Having to scroll up and down repeatedly in order to understand the logic is something I think most developers would be put off by.

If no one has explained to you what onSelect, onEsSearchSelect, onPressEnter, and onChange mean, and the life cycle of the callback, you will not be able to read on.

What’s more, if you write similar features every time, these hidden logic will never appear in your checklist. If there is a bug, you will be left crying and blaming the product.

Of course, the above two cases are only A small sample. What you can see is that the EsSearch component has been used 25 times (20 files) and the SelectInput component has been used 15 times (13 files) by searching the code of project A.

Moreover, it is possible to make 40 changes, read at least (25 * 50 + 15 * 100) lines of code, and repeatedly skip between 33 files as long as one problem is out of alignment with the business. Who wants to make such optimization requirements?

“Is there already a good excuse [doge] for forced delay in demand?”

The body of the

Of course, the above statement is just a joke. When a problem is identified, we need to find a solution.

I don’t think anyone likes to spend time in constant revision, just like a “beautiful” PRD can make people have a “beautiful” “good” mood for a day.

After careful accounting and alignment with business and other front-end development, I have identified the following issues that need to be addressed in a unified way:

  1. Unified anti-shake strategy.
  2. An exception request scenario: select and enter.
  3. A backward compatible handle: an unfriendly use of the SelectInput component.
  4. The input is triggered to realize the unification of system interactive operation.
  5. Special handling of empty and blank strings.
  6. Compatible with scenarios where multiple ES components exist (as wellEsSearch,SelectInputMixed scenarios).
  7. Thoroughly decouple with the business logic.
  8. Say goodbye to 4 or 5 callbacks, greatly reducing the cost of using ES components; independenthooksThe ES query logic has the maintainability, and greatly improves the extensibility.

Before I begin, I’d like to give you a taste of the 2.0 refactored demo code:

Note: The 1.0 refactoring code was written as onereducerTo handle complex callbacks, but not to decouple from business code and use customizationhook

 // The calculation is done inside the hook and updated to esState
const [esState, esProps] = useEsSearchOnChange(selectOptions); // selectOptions is configured to use the SelectInput component

<SelectInput
  selectOptions={selectOptions}
  {. esProps}
  selectProps={{
    defaultValue: 'product_name',
      style: { width: 160 },
        dropdownMatchSelectWidth: 336}} / >. <ProTable searchModel={esState}// Complete the request automatically with Protable. />Copy the code

Has there ever been a moment when your happiness level soared?

No more managing complex callback lifecycles, no more extra checklists in every business requirement, no more time consuming reading a 100-line piece of code…

The overall train of thought

The problems have been sorted out and the front-end use of ES component has been felt after reconstruction. Let me introduce you to the whole idea of refactoring.

With the react. useReducer, we can use a reducer function as the state feature to integrate several complex callbacks of ES components inside the function. The user only needs to care about the state returned by the useReducer. See esState for the demo code above.

We use the dispatch returned by the useReducer to provide a method object to update state, such as the esProps of the demo code:

{
      onChange: (value: any) = >
        debounceDispatch({ payload: value, type: 'onChange', isEsSearch }),
      onPressEnter: (value: any) = >
        debounceDispatch({ payload: value, type: 'onPressEnter', isEsSearch }),
      onEsSearchSelect: (value: any) = >
        debounceDispatch({ payload: value, type: 'onEsSearchSelect', isEsSearch }),
      onSelect: (key: any) = >
        debounceDispatch({ payload: key, type: 'onSelect', isEsSearch })
}
Copy the code

When a callback is triggered due to the behavior of the ES component, it goes to the corresponding calculation logic based on the type of the callback. For example, the onPressEnter callback corresponds to the onPressEnter type.

Next, we should focus on how to write this reducer.

Unified anti-shake strategy

Our team has already packaged a useDebounce hook based on LoDash/Debounce. Passing in the callback to be buffered, buffered time, buffered function configuration, and dependencies to care about will return a buffered function.

About the hook after use feeling, within our group of friend have a summary of an article, interested can learn: confluence was. Shopee. IO/pages/viewp…

import { DependencyList, useEffect, useRef } from 'react';
import debounce from 'lodash/debounce';
import { DebounceSettings } from 'lodash';

const useDebounce = (
  initialFn: any,
  delay = 200,
  options: DebounceSettings = { leading: true },
  deps: DependencyList = [delay, initialFn]
) = > {
  const fnRef = useRef(debounce(initialFn, delay, options));
  useEffect(() = > {
    fnRef.current = debounce(initialFn, delay, options);
    return fnRef.current.cancel;
  }, deps); // eslint-disable-line react-hooks/exhaustive-deps
  return fnRef.current;
};

export default useDebounce;
Copy the code

Using useDebounce in useEsSearchOnChange:

const [state, dispatch] = useReducer(reducer, {});
  const debounceDispatch = useDebounce(
    dispatch, // The dispatch is called debounceDispatch
    300,
    { trailing: true} []);Copy the code

An exception scenario

Review of exception scenarios:

Select one of the ES results and press Enter to see the list refreshed.

The exception is that if the user accidentally hits Enter, two things will happen on the page:

  1. ES component selector expansion;
  2. To launch aFuzzy search.

As a result, users who want to search accurately get fuzzy results.

The root cause of the problem is that the ES business component cannot determine whether it wants to fuzzy search or expand the selector for the return event. The responsibility of the Enter event is fundamentally inaccurate, so it is not very scientific to change the ES business component to achieve the effect.

Finally, in order not to leave users in doubt about the functionality here, our solution is as follows:

If the last time the user did not modify the selected results, then it is assumed that the user still wants to see accurate search results.

Corresponding processing logic:

const reducer = (state: any, action: ActionType) = >{...// Precise search then fuzzy search: select first and then enter. Do not initiate a request.
  if (type= = ='onPressEnter') {
    constpressLastSelected = ! state.search_type && state[key] && state[key] === value;if (pressLastSelected) returnstate; }... };Copy the code

A backward compatible processing

A problem occurred because the author of the SelectInput component manually triggered the onChange on the outer layer of the EsSearch component when wrapping:

const handleValueChange = (value: any) = >{... onChange({// This trigger is meaningless, but causes a lot of trouble for the user
    [selectedOption.value]: value,
  });
};
<EsSearch
 .
  onSelect={(value: string) = >{... handleValueChange(value); . }}... />
Copy the code

In order not to cause bugs on the page, each user should add this code to the SelectInput component to fix the onChange callback:

<SelectInput
  ...
  onChange={(value: any) = > {
    // This code makes business code unnecessarily difficult to read
    if (Object.keys(value).includes('physical_sku_id_barcode')) {... }}}.. />Copy the code

Almost no one can guess: this judgment is intended to prohibit ES searches in order to allow only non-ES searches in onChange.

So, to avoid the extra reading cost in the business logic, I do this in useEsSearchOnChange to solve this backward compatibility problem:

const reducer = (state: any, action: ActionType) = >{...// type:esSearch onChange events are not processed and requests are not initiated.
  if (typeof payload === 'object' && type= = ='onChange') {
    if (isEsSearch(key)) returnstate; }... };Copy the code

Input is request

Here is an example of interaction with the previous specification:

Here is an example of interaction with the current specification:

The difference is whether a request (which is a precise search) is made after the user stops typing.

By reading SelectInput’s code, the onChange callback processing for the EsSearch component provides a onEsSearchChange callback that users of SelectInput can listen for to implement the input-as-request function.

UseEsSearchOnChange (); useEsSearchOnChange ();

const reducer = (state: any, action: ActionType) = >{...// Every time the dispatch is triggered, it clears.
  const newState = {};
  switch (type) {...case 'onEsSearchChange': // Input triggers the request
      Object.assign(
        newState,
        payload,
        value && isEsSearch(key) ? { search_type: 1}, {});break;
    case 'onSelect':
      break;
    default:}return newState;
};
Copy the code

Reducer returned a new object, so the esState was updated to the new state, which triggered the automatic request logic of ProTable, and finally achieved the goal.

Special handling of special characters

According to the interface specification in the background, a parameter with a blank or whitespace character is considered a meaningless pass.

In fact axios already does something similar for us. If we pass {a: null} or {a: undefined}, the parameter a will not be passed to the background.

Here is a relatively simple process, no technical content, please directly look at the code:

const reducer = (state: any, action: ActionType) = >{...// Empty and whitespace characters are not carried to the request parameters.
  if(! value || ! value.trim()) {return{}; }... };Copy the code

Compatible with multiple ES components

Multiple ES components exist in several combinations:

  1. EsSearch + EsSearch
  2. SelectInput + SelectInput
  3. EsSearch + SelectInput

Why do you need to do this compatibility work? Again from the actual scene, pay attention to the print on the right side:

Careful students have found that there is no problem when ES component 1 and ES component 2 participate in the query at the same time. Fuzzy search or precise search is subject to the last operation.

However, if you empty the value of one of the two components while both components have values, you can see that an exact search must have been initiated. This is because the callback corresponding to the empty action is onEsSearchSelect, and you can see from onEsSearchChange that this callback type represents an exact search.

I have to say that ES components are very complex, partly because of the number of business scenarios, and partly because the components themselves provide many callbacks with unclear responsibilities.

Aligned with the business side, the result is:

Remember what the user did to each ES component, and be able to retrieve the result of the last operation of the other component when clearing one.

If component 1 last performed a precise search and component 2 last performed a fuzzy search, clear the contents of component 1, and initiate a fuzzy search based on the last operation of component 2 instead of a precise search.

A possible solution:

// Merge esState at the caller
const [esState1, esProps1] = useEsSearchOnChange(selectOptions1);
const [esState2, esProps2] = useEsSearchOnChange(selectOptions2);
// Merged esState
const[totalEsState, setTotalEsState] = React.useState({... esState1, ... esState2}) React.useEffect(() = >{ setTotalEsState({... esState1, ... esState2}); }, [esState1, esState2])Copy the code

The advantage of this is that the search_type belongs to different ESStates, which conforms to the function of memory. The merged object will adopt the side with value later, which conforms to the function of satisfying the user’s habit. The disadvantage is that the merge logic is coupled in the business code.

Decouple with business logic

Custom hook, can be very good non-business care and high stability of the logic encapsulated, and achieve the decoupling of business logic, so that the business code is responsible for the core of the page logic.

This is why I refactored from version 1.0 to 2.0, where the complex Reducer code was still embedded in the business code and the Reducer used by each related component was very similar. The business code has to be bloated, which is unacceptable.

Say goodbye to complex callback functions

See the return from useEsSearchOnChange:

  1. esStateIs the result of data calculation combined with ES component;
  2. {onChange.onPressEnter.onEsSearchSelect.onSelect} is a property passed to the ES component.
const useEsSearchOnChange = (selectOptions: any[]) = > {
  const isEsSearch = (key: any) = >
    selectOptions.some(
      (opt: any) = > opt.type === 'esSearch' && opt.value === key
    );
  const [state, dispatch] = useReducer(reducer, {});
  const debounceDispatch = useDebounce(
    dispatch,
    300,
    { trailing: true} []);// 1.es state
  // 2.es props
  return [
    state,
    {
      onChange: (value: any) = >
        debounceDispatch({ payload: value, type: 'onChange', isEsSearch }),
      onPressEnter: (value: any) = >
        debounceDispatch({ payload: value, type: 'onPressEnter', isEsSearch }),
      onEsSearchSelect: (value: any) = >
        debounceDispatch({ payload: value, type: 'onEsSearchSelect', isEsSearch }),
      onSelect: (key: any) = >
        debounceDispatch({ payload: key, type: 'onSelect', isEsSearch })
    },
  ];
};
Copy the code

Custom hook: useEsSearchOnChange simplifies the processing of complex callback functions to the Reducer. This greatly frees up the time required for users to do their needs.

Other Solutions

The above is not the only solution to the problem caused by THE ES component, it is just a way of thinking to solve the problem. For example, module decoupling is a good way to make code more robust; Secondary encapsulation is a good way to make components easier to use. Clever use of hooks to make components easier to maintain, etc.

The iterative process of components is bound to be a process of constantly appearing and solving problems. It is generally difficult for developers to do a good job of coverage in the packaging stage. Firstly, the time is not allowed, and secondly, the quality of components varies from person to person. So the best way to encapsulate common components is to “find the change and encapsulate the change.”

Other solutions I’ve come up with:

  1. You can also double wrap the ES component and the Table component into the ProTable component, and then do the nasty callback function inside the ProTable component.

  2. Encapsulate the ES component in another layer, and complete the disgusting callback function in this layer.

  3. Refactoring the callback of the ES component to make the callback function more intuitive and easier to use.

And so on…

The last

This is all the sharing of this time, I hope you can see here to gain.

If there is not clear about the place, please correct, common progress.

The resources

  • Insight into the underlying logic of design patterns – mp.weixin.qq.com/s/qRjn_4xZd…
  • Zh-hans.reactjs.org/docs/hooks-…