preface

In my work, React + TypeScript is the main technology stack. In this article, I want to summarize how to use React techniques to solve practical problems in projects. The code used in this article is simplified and does not represent production environments. The code in production is certainly more complex than the examples in this article, but the simplified idea should be the same.

Cancel the request

The React component that is currently making the request is unloaded from the page. Ideally, the request should also be canceled. How do you correlate the cancellation of the request with the unloading of the page?

Here we consider the return value of the function passed in with useEffect:

useEffect(() = > {
  return () = > {
    // Execute when the page is uninstalled}; } []);Copy the code

Let’s assume that our request uses fetch. AbortController is one more thing we need to use:

const abortController = new AbortController();

fetch(url, {
  // We pass signal here for correlation
  signal: abortController.signal,
});

// Call abort to cancel the request
abortController.abort();
Copy the code

React encapsulates a useFetch hook:

export function useFetch = (config, deps) = >{
  const abortController = new AbortController()
  const [loading, setLoading] = useState(false)
  const [result, setResult] = useState()

  useEffect(() = > {
    setLoading(true) fetch({ ... config,signal: abortController.signal
    })
      .then((res) = > setResult(res))
      .finally(() = > setLoading(false))
  }, deps)

  useEffect(() = > {
    return () = > abortController.abort()
  }, [])

  return { result, loading }
}
Copy the code

For example, in the case of route switching or Tab switching, the requests sent by uninstalled components are also interrupted.

Deep comparison dependence

When using hooks such as useEffect that require passing in dependencies, the ideal situation would be for all dependencies to change their reference addresses only when they actually change. However, some dependencies don’t behave very well. Each rendering will regenerate a reference, but the internal values remain the same. This may make the useEffect not work properly for “shallow comparisons” of dependencies.

Such as:

const getDep = () = > {
  return {
    foo: 'bar'}; }; useEffect(() = > {
  // An infinite loop
}, [getDep()]);
Copy the code

This is an artificial example, because the getDeps function returns an object with a brand new reference every time it executes, causing an infinite update of render ->effect-> render ->effect.

A tricky solution is to convert dependencies to strings:

const getDep = () = > {
  return {
    foo: 'bar'}; };const dep = JSON.stringify(getDeps());

useEffect(() = > {
  // ok!
}, [dep]);
Copy the code

This compares the value of the string “{foo: ‘bar’}”, not the reference to the object, so the update will only be triggered if the value actually changes.

Of course, it is better to use the community-provided solution: useDeepCompareEffect, which uses a deep comparison strategy. For object dependencies, it compares keys and values one by one, at a performance cost.

If one of your dependencies triggers multiple meaningless interface requests, useDeepCompareEffect is preferable, as it is better to spend more time comparing objects than to repeatedly request interfaces.

UseDeepCompareEffect general principle:

import { isEqual } from 'lodash';
export function useDeepCompareEffect(fn, deps) {
  const trigger = useRef(0);
  const prevDeps = useRef(deps);
  if(! isEqual(prevDeps.current, deps)) { trigger.current++; } prevDeps.current = deps;return useEffect(fn, [trigger.current]);
}
Copy the code

The useEffect is actually passed in to update the numeric value trigger. The last dependency passed in is retained with useRef, and each dependency is deeply compared with the old one using lodash’s isEqual. If it changes, the value of trigger is increased.

We can also use the fast-deep-Equal library, which is about 7 times more efficient than LoDash, according to the official benchmark.

Use the URL as the data warehouse

In an internal backend management project, whether you’re building a system for operations or development, it’s always going to involve sharing, so it’s very important to preserve “page state.” For example, I am operating A, using an internal data platform. I must want to share the second page of consumption data of an App with operating B, and filter the page into the status of A user for discussion. State and URL synchronization is especially important.

In traditional state management thinking, we need to use redux, Recoil and other libraries to do a series of data management in the code, but what if the string of queries after the URL is like a data warehouse? React-router encapsulates the router with the react router.

export function useQuery() {
  const history = useHistory();
  const { search, pathname } = useLocation();
  // Save the query state
  const queryState = useRef(qs.parse(search));
  / / set the query
  const setQuery = handler= > {
    const nextQuery = handler(queryState.current);
    queryState.current = nextQuery;
    // replace rerenders the component
    history.replace({
      pathname: pathname,
      search: qs.stringify(nextQuery),
    });
  };
  return [queryState.current, setQuery];
}
Copy the code

In a component, you can use:

const [query, setQuery] = useQuery();

// Interface requests depend on page and size
useEffect(() = > {
  api.getUsers();
}, [query.page, query, size]);

// Paging changes trigger interface rerequests
const onPageChange = page= > {
  setQuery(prevQuery= > ({
    ...prevQuery,
    page,
  }));
};
Copy the code

This way, all page state changes are automatically synchronized to the URL, which is very convenient.

Use AST for internationalization

The hardest part of internationalization is manually replacing the text in the code with i18N.t (key) internationalization method calls, which can be left to Babel AST. Scan the code where text needs to be replaced, modify the AST and convert it to a method call. The tricky part is that you need to consider various boundary cases. I wrote a simple example for reference only:

Github.com/sl1673495/b…

Such a piece of source code:

import React from 'react';
import { Button, Toast, Popover } from 'components';
const Comp = props= > {
  const tips = () = > {
    Toast.info('Here's a hint.');
    Toast({
      text: 'Here's a hint.'}); };return (
    <div>
      <Button onClick={tips}>This is a button</Button>
      <Popover tooltip="Bubble tip" />
    </div>
  );
};
export default Comp;
Copy the code

After processing, it turns into something like this:

import React from 'react';
import { useI18n } from 'react-intl';
import { Button, Toast, Popover } from 'components';
const Comp = props= > {
  const { t } = useI18n();
  const tips = () = > {
    Toast.info(t('tips'));
    Toast({
      text: t('tips')}); };return (
    <div>
      <Button onClick={tips}>{t('btn')}</Button>
      <Popover tooltip={t('popover')} / >
    </div>
  );
};
export default Comp;
Copy the code

Show me the traverse section of the script:

/ / traverse the ast
traverse(ast, {
  Program(path) {
    // i18n import {React} import {React
    path.get('body.0').insertAfter(makeImportDeclaration(I18_HOOK, I18_LIB));
  },
  // Look up the Component function by finding the first jsxElement and insert the i18n hook function
  JSXElement(path) {
    const functionParent = path.getFunctionParent();
    const functionBody = functionParent.node.body.body;
    if (!this.hasInsertUseI18n) {
      functionBody.unshift(
        buildDestructFunction({
          VALUE: t.identifier(I18_FUNC),
          SOURCE: t.callExpression(t.identifier(I18_HOOK), []),
        })
      );
      this.hasInsertUseI18n = true; }},// The text in JSX is replaced directly with {t(key)}
  JSXText(path) {
    const { node } = path;
    const i18nKey = findI18nKey(node.value);
    if (i18nKey) {
      node.value = ` {${I18_FUNC}("${i18nKey}` ")}; }},// Literal found could be the text of a function call parameter or a JSX property
  Literal(path) {
    const { node } = path;
    const i18nKey = findI18nKey(node.value);
    if (i18nKey) {
      if (path.parent.type === 'JSXAttribute') {
        path.replaceWith(
          t.jsxExpressionContainer(makeCallExpression(I18_FUNC, i18nKey))
        );
      } else {
        if(t.isStringLiteral(node)) { path.replaceWith(makeCallExpression(I18_FUNC, i18nKey)); }}}},});Copy the code

Of course, the actual project also needs to consider the copywriting translation part, how to set up the platform, how to cooperate with the operation or translation specialist.

And the AST handles a wide variety of boundary cases, which are certainly much more complex, so this is just a simplified version of the idea.

conclusion

Moved into the giant brick has 3 months, the feeling here is the density of talents is really high, can see a lot of bosses of community cutting-edge issues in the internal front group discussion, even if you and him in a floor, you can also ran the reality and his face base, ask questions, this kind of feeling really good. Once I met a problem on TS, so I went directly to a famous guy on Zhihu to discuss and solve it (cheeky).

In the future work, I will further summarize the knowledge points I have learned and send some valuable articles. If you are interested, please pay attention to me