This article was first published on the public account “Xiao Li’s Front-end cabin”, welcome to follow ~

preface

To make it easier for developers to build UX in line with the UI = F (State) philosophy, React introduces functional components and a set of logic reuse solutions called Hooks.

But the introduction of Hooks also brings some problems:

  • useCallback.useMmeo.useRefSuch use makes the code difficult to read.
  • The need to care about the reference changes of JS complex types, there is a certain mental burden, and even affect the correctness of business logic.

Problems caused by reference changes

Reference type is a complex JS data type, collectively known as object type, including objects, arrays, functions and so on. When comparing object types, you are actually comparing references to them. Using == / === = cannot determine whether two objects have equal “values”.

const a = {};
const b = {};
console.log(a === b); // false
Copy the code

The React function component calls its own function every time it renders, and all local variables defined within the function are recreated. If there is an object variable of reference type in it, recreating it will cause the reference to change, which can be risky in the following scenarios:

  • This object is used as a useEffect, useMemo, or useCallback dependency, causing logic and calculations to be executed frequently.

  • This object is passed as a prop to downstream components inherited by the react. Memo component or the react. PureComponent, causing unintended rerendering of downstream components, which can cause performance problems if rendering of downstream components is expensive.

explore

To keep references stable, React provides the Hook API:

  1. useuseCallbackuseMemoPackage the reference type
  2. Hangs the reference type inRef

Using them, can we produce best practices?

Memo all objects

Let’s discuss it in different scenarios:

For business code

Business code functions are complex, and the DOM tree is very deep and large, so the React component tree is also complex. If you have useMemo/useCallback in every component, it takes longer to render the component and consumes more memory. Together, hundreds of components can hurt performance more than they do.

Therefore, useMemo and useCallback in business code need to be used in moderation. Discussion about their use scenarios has always been a hot topic of React. There are a lot of articles online, but there is no widely accepted best practice so far. One thing I do agree with is that useEffect, useMemo and useCallback dependencies and component props should all be basic types to minimize the impact of reference changes.

For third-party libraries

As a third party library, stability is more important, should ensure that there is no downstream dependent party problems caused by its own reasons, “Memo all objects” is the method of no way. React Hook Forms and Ahooks, for example, all exposed objects are memoized for reference purposes.

// react-hook-form

return {
  swap: React.useCallback(swap, [updateValues, name, control, keyName]),
  move: React.useCallback(move, [updateValues, name, control, keyName]),
  prepend: React.useCallback(prepend, [updateValues, name, control, keyName]),
  append: React.useCallback(append, [updateValues, name, control, keyName]),
  remove: React.useCallback(remove, [updateValues, name, control, keyName]),
  insert: React.useCallback(insert, [updateValues, name, control, keyName]),
  update: React.useCallback(update, [updateValues, name, control, keyName]),
  replace: React.useCallback(replace, [updateValues, name, control, keyName]),
  fields: React.useMemo(
  () = >
    fields.map((field, index) = > ({
      ...field,
      [keyName]: ids.current[index] || generateId(),
    })) as FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>[],
  [fields, keyName],
  )
};
Copy the code

useMemoOne

In fact, even if the value is cached by useMemo and useCallback, the cache may be lost.

This is also explained in the React documentation:

You can use useMemo as a means of performance optimization, but don’t use it as a semantic guarantee. In the future, React might choose to “forget” some of the previous Memoized values and recalculate them at the next rendering, such as freeing memory for off-screen components. Write code that can run without useMemo first — then add useMemo to your code to optimize performance.

(However, I haven’t heard of any problems with this mechanic so far).

To address the reference changes that “forgetting” can cause, there is a useMemo design in the community that will never be “forgotten”, and it is also safe in concurrent mode.

// before
const value = useMemo(() = > ({ hello: name }), [name]);

// after
const value = useMemoOne(() = > ({ hello: name }), [name]);
Copy the code

However, useMemoOne takes up more memory than useMemo in order to permanently stabilize references and ensure that the cache is not released until garbage collection.

Therefore, useMemoOne is only an alternative for individual scenarios.

The states are all attached to Ref

React selective forgetting is not a big problem either, just hang these values on Ref’s.

Because the root cause of the problem with complex references is that references to objects change with re-rendering, and values saved in refs are not destroyed and created every time they are rendered.

Think of useRef as useState({current: initialValue})[0]

To do this, create a component instance instanceRef using useRef and store all the states used by the component in that instanceRef.

const myComponent = () = > {
  const instanceRef = useRef({
    state1:...state2:...value1:...value2:...func1:...func2:... });// ...
}
Copy the code

When a view needs to be updated, call forceUpdate() manually.

const forceUpdate = React.useReducer(() = >({}), {})1]
Copy the code

This is a less popular solution, but there are also practices in the community. For example, the useTable API in React-Table stores table-related attributes and methods in instanceRef and manually controls view updates using rerender (forceUpdate).

function useTable(options: Options<TData, TValue, TFilterFns, TSortingFns, TAggregationFns>) {
  const instanceRef = React.useRef(undefined!).const rerender = React.useReducer(() = >({}), {})1]

  if(! instanceRef.current) { instanceRef.current = createTableInstance< TData, TValue, TFilterFns, TSortingFns, TAggregationFns >(options, rerender) }return instanceRef.current
}
Copy the code

This does solve the problem of reference changes, but the code is not maintainable:

  1. The value of state needs to be changed from someState to ref.someState. Once a component is written like this, then any new state has to be put in refinstanceRefThe complexity of the.
  2. If there is any state mismatch on the view, troubleshooting is difficult and the only way to synchronize the state is to use forceUpdate.
  3. Each update to the view requires a manual callforceUpdate“, which is not quite in line with the idea of functional programming and is officially not recommended.

Looking forward to

All of these schemes are a bit opportunistic, not best practice. Is there a better plan for the future?

Record and Tuple types

In JS, object comparisons are not values, but references. This is determined by the JS language itself. Is it possible to solve this problem from the JS language?

In the recent proposal-record-tuple proposal, JS added two new raw data types: Record and tuple. It allows JS to natively support immutable data types, allowing JS to drive a native immutable track.

Record and Tuple are read-only objects and arrays. You can define them by adding a # in front of the original objects and arrays.

// Record
const myRecords = #{ x: 1.y: 2 };

// Tuple
const myTuplee = #[1.2.3];
Copy the code

Crucially, records and tuples are compared by value, not by reference:

const a = #{};
const b = #{};
console.log(a === b); // true

const c = #[]
const d = #[];
console.log(c === d); // true
Copy the code

With this mechanism, we no longer need to see all the memo objects, no longer need to see the useMemo code flying around! Unfortunately, this time only Object and Array are proposed, Function is still not supported, so useCallback will continue to be used.

If you want to learn more about how the proposal can help solve React problems, I recommend the intensive reading of Records & Tuples for React by Huang Ziyi

React Forget

At React Conf 2021, Huang xuan shared a compiler called React Forget.

In short, the compiler detects the need for useMemo and useCallback at code compile time and automatically adds them to optimize the re-rendering process for components.

Not only useMemo and useCallback, React nodes require a memo as well. Therefore, the React. Memo may no longer be required.

The full video can be found here.

conclusion

The JS reference type feature imposes a mental burden and cost on using the React function component.

At present, React’s high degree of freedom allows us to choose solutions that fit business scenarios.

In the future, it is possible to solve the reference type problem fundamentally from the JS language itself and React.

Thanks for your support ❤️

If this article has been helpful to you, please give me a like.

My public account “Xiao Li’s front-end cabin” has just been created, and is still improving. Pay attention to me and become stronger together! 🚀

reference

  • Giacomocerquone.com/whats-an-ob…
  • www.zhenghao.io/posts/memo-…
  • Kentcdodds.com/blog/usemem…
  • Royi – codes. Vercel. App/thousand – us…
  • zhuanlan.zhihu.com/p/443807113
  • Javascript. Plainenglish. IO/react — the conf…