React Hooks best practices

brief introduction

React 16.8 was officially released on February 2019. It is a feature that can improve code quality and development efficiency. The author will first list some practical points for further discussion.

However, it should be understood that there is no perfect best practice specification. For an efficient team, stable specifications are more important than reasonable specifications, so this scheme is only one of the best practices.

intensive reading

Environmental requirements

Component definition

Function Component is defined by const + arrow function:

const App: React.FC<{ title: string }> = ({ title }) => {
  return React.useMemo(() => <div>{title}</div>, [title]);
};

App.defaultProps = {
  title: 'Function Component'
}

The above examples include:

  1. Use react FC declares the Function Component component type and defines the Props parameter type.
  2. Use react Usememo optimizes rendering performance.
  3. Use app Defaultprops defines the default value of Props.

FAQ

Why not use react memo?

It is recommended to use react Usememo instead of react Memo, because react exists during component communication The usage of usecontext, which will re render all the components used, only react Usememo can handle on-demand rendering of such scenes.

Should useMemo also be used for components without performance problems?

Consider that when maintaining this component in the future, some data may be injected through useContext at any time. At this time, who will think of adding useMemo?

Why not use deconstruction instead of defaultProps?

Although writing defaultProps by deconstruction is more elegant, there is a hard injury: for object types, the reference will change every time you Rerender, which will cause performance problems, so don't do so.

Local state

There are three kinds of local states, which are arranged according to the degree of common use: useState useRef useReducer.

useState

const [hide, setHide] = React.useState(false);
const [name, setName] = React.useState('BI');

The name of the state function shall be ideographic and shall be declared together as far as possible for easy reference.

useRef

const dom = React.useRef(null);

useRef should be used as little as possible. A large amount of Mutable data will affect the maintainability of the code.

However, for objects that do not need repeated initialization, useRef storage is recommended, such as new G2().

useReducer

useReducer is not recommended for local state, which will make the internal state of the function too complex and difficult to read. useReducer is recommended to be used together with useContext when communicating among multiple components.

FAQ

Can I declare ordinary constants or ordinary functions directly in a function?

No, the Function Component will be re executed every time it is rendered. It is recommended to put constants on the outer layer of the function to avoid performance problems. It is recommended to use useCallback declaration for functions.

function

All functions in Function Component must use react Usecallback package to ensure accuracy and performance.

const [hide, setHide] = React.useState(false);
  
const handleClick = React.useCallback(() => {
  setHide(isHide => !isHide)
}, [])

The second parameter of useCallback must be written, eslint-plugin-react-hooks The plug-in automatically fills in dependencies.

Send request

Sending requests are divided into operational sending requests and rendering sending requests.

Operational send request

An operation type sends a request as a callback function:

return React.useMemo(() => {
  return (
    <div onClick={requestService.addList} />
  )
}, [requestService.addList])

Render send request

Rendering requests made in useAsync, such as refreshing the list page, obtaining basic information, or searching, can be abstracted as relying on some variables. When these variables change, the data should be retrieved again:

const { loading, error, value } = useAsync(async () => {
  return requestService.freshList(id);
}, [requestService.freshList, id]);

Communication between components

Simple inter component communication uses transparent propagation of Props variables, while frequent inter component communication uses react useContext .

Take a large complex component as an example. If many modules are split inside the component, but many internal states need to be shared, the best practices are as follows:

Define sharing status within components - store ts

export const StoreContext = React.createContext<{
  state: State;
  dispatch: React.Dispatch<Action>;
}>(null)

export interface State {};

export interface Action { type: 'xxx' } | { type: 'yyy' };

export const initState: State = {};

export const reducer: React.Reducer<State, Action> = (state, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

Root component injection shared state - main ts

import { StoreContext, reducer, initState } from './store'

const AppProvider: React.FC = props => {
  const [state, dispatch] = React.useReducer(reducer, initState);

  return React.useMemo(() => (
    <StoreContext.Provider value={{ state, dispatch }}>
      <App />
    </StoreContext.Provider>
  ), [state, dispatch])
};

Any subcomponent accessing / modifying shared state - child ts

import { StoreContext } from './store'

const app: React.FC = () => {
  const { state, dispatch } = React.useContext(StoreContext);
  
  return React.useMemo(() => (
    <div>{state.name}</div>
  ), [state.name])
};

The above has solved the problem of convenient sharing of state among multiple closely related component modules, but sometimes the problem of sharing root component Props is encountered. This unmodifiable state is not suitable to be stuffed into StoreContext. We create a PropsContext to inject Props into the root component:

const PropsContext = React.createContext<Props>(null)

const AppProvider: React.FC<Props> = props => {
  return React.useMemo(() => (
    <PropsContext.Provider value={props}>
      <App />
    </PropsContext.Provider>
  ), [props])
};

Combined with project data flow

reference resources react-redux hooks.

debounce optimization

For example, when the input box is frequently entered, we will choose to debounce when onChange in order to ensure the smoothness of the page. However, in the field of Function Component, we have a more elegant way to implement it.

In fact, there is a problem with using debounce in onChange of the Input component, that is, when the Input component is controlled, the debounce value cannot be backfilled in time, resulting in the problem that it can not even be Input.

We think about this problem in the thinking mode of Function Component:

  1. React scheduling By optimizing the rendering priority through the intelligent scheduling system, we don't have to worry about performance problems caused by frequent state changes.
  2. Do you still feel slow if you link a text? onChange is not slow, and most components that use values are not slow. There is no need to debounce from the source of onChange.
  3. Find the component with the slowest rendering performance (such as iframe component), and use debounce some input parameters that frequently cause its rendering.

The following is a component with poor performance. It refers to text that changes frequently (this text may be triggered by onChange). We can use useDebounce to slow down the change frequency:

const App: React.FC = ({ text }) => {
  // No matter how fast text changes, textDebounce can be modified once a second at most
  const textDebounce = useDebounce(text, 1000)
  
  return useMemo(() => {
    // A pile of code that uses textDebounce but renders slowly
  }, [textDebounce])
};

Using textDebounce instead of text can control the rendering frequency within the range we specify.

useEffect considerations

In fact, useEffect is the weirdest Hook and the most difficult to use. For example, the following code:

useEffect(() => {
  props.onChange(props.id)
}, [props.onChange, props.id])

If the id changes, onChange is called. However, if the upper layer code does not reasonably encapsulate onChange, resulting in changes every time the reference is refreshed, serious consequences will occur. Let's assume that the parent code is written as follows:

class App {
  render() {
    return <Child id={this.state.id} onChange={id => this.setState({ id })} />
  }
}

This can lead to an endless cycle. Although it seems that < app > only gives the time to update the id to the child element < child >, because the onChange function will be regenerated every time it is rendered, the reference is always changing, and an infinite loop will appear:

New onChange - > useeffect dependency update - > props onChange - > parent re render - > New onChange

To prevent this cycle, just change to onChange={this.handleChange}. useEffect has strict requirements for external dependence. It can take effect only when the whole project pays attention to maintaining correct references.

However, how to write the code at the called place is not under our control, which leads to non-standard parent elements, which may lead to an endless loop in React Hooks.

Therefore, when using useEffect, pay attention to the debugging context and whether the parameter reference passed by the parent is correct. If the reference is not passed correctly, there are two methods:

  1. use useDeepCompareEffect Deep comparison of dependencies.
  2. Wrap props whose references always change with useCurrentValue:
function useCurrentValue<T>(value: T): React.RefObject<T> {
  const ref = React.useRef(null);
  ref.current = value;
  return ref;
}

const App: React.FC = ({ onChange }) => {
  const onChangeCurrent = useCurrentValue(onChange)
};

The reference to onChangeCurrent remains unchanged, but it always points to the latest props Onchange, which can avoid this problem.

Keywords: React Native hooks

Added by cyclefiend2000 on Fri, 14 Jan 2022 18:00:09 +0200