It’s been a long time since React Hooks were officially released, after choosing to accept the fact that “this is the future, you’re going to have to run with it” in the first place, gradually trying to reconfigure some of the existing code in my head based on Hooks, and reading a lot of community discussion.

In the community, most evangelists cite advantages such as “overly verbose nesting of components,” “more intimate contact with internal principles,” and “more fine-grained organization and reuse of logic than before.” In addition, based on some of my own experience, I also have some insights on the more academic dimension, which should be supplemented.

The overall feeling

First of all, by Margaret and Hooks, it was an established fact that the function component was no longer a purely data-to-view mapping, and the React team decided to go that route, there had to be trade-offs and there would be no going back. It is more important for users to think about what new definitions will be given to function components in the present.

In my opinion, aside from the fact that ‘suspect’ is something that clearly affects the component of the function, with the help of Hooks, the component will gradually add ‘preconditions on which the logic depends’ to the strict definition of’ input to output mapping logic ‘.

const WordCount = () => {
    const [text, onChange] = useState('');

    return (
        <div>
            <TextBox value={text} onChange={onChange} />
            <span>{text.length} characters</span>
        </div>
    );
};Copy the code

If you remove the useState line and use text and onChange as props for the component, the component is a perfect input-output mapping logic, in which two properties are mapped to a set of React elements.

Under the Hooks implementation, the useState line essentially states the fact that the second half (return) “depends on a state”.

In addition, calls to useContext, useEffect, etc., are also preconditions for declaring the entire rendering process in a function component, requiring a Context, or requiring side effects.

With this in mind, I would now expect future function components that use Hooks to begin with the Hooks call, followed by the rendering logic of a pure function component that splits the logic into:

Const FunctionComponent props of = = > {/ / calls to all Hooks, declare pre-conditions / / on the props and Hooks content processing, data processing / / to turn data into JSX and return};Copy the code

I want to follow this order as closely as possible to guide the design and implementation of highly maintainable components. When there is a scenario where Hooks need to be called after step 2, immediately consider whether the component split and granularity is reasonable, which is often a signal that the component should be further split into more fine-grained components.

State management

The most classic built-in function is useState, which returns a Tuple of the structure [value, setValue].

In practice, the react-Kiss framework we currently use (to be covered in a follow-up article) is pretty much the same:

import {withTransientRegion} from 'react-kiss'; const initialState = { text: '' }; const workflow = { onChange(text) { return {text}; }}; const WordCount = ({text, onChange}) => ( <div> <TextBox value={text} onChange={onChange} /> <span>{text.length} characters</span> </div> ); export default withTransientRegion(initialState, workflow)(WordCount);Copy the code

Apart from the fact that we can’t make the state itself directly a string, and the API is a bit cumbersome to use, the pattern isn’t much different. Even components based on withTransientRegion that do not use the asynchrony capabilities of React-Kiss can be easily refactored with withState or withReducer at no additional cost.

I think that under the impact of useState, most “normal” React State developers should reflect on the following issues:

  1. Why not pair “state” with “logic for changing state” and organize them with a better code structure.
  2. Why not have a more fine-grained breakdown of state, with states that are not linked managed separately in different components, instead of habitually using one large state and multiple placessetStatePerform partial status updates.
  3. Why not separate the management of state from the rendering of views, with a complexrenderThe implemented class component is split into a “pure state management class component” and a “pure function component that implements rendering logic”, and the formerrenderMethod returns the latter directly.

UseState is a classic design that combines the above three theories into a very concise API:

  1. valueandsetValuePair, the latter must affect the former, the former is only affected by the latter, as a whole they are completely unaffected by the outside world.
  2. In almost all cases, it is recommendedvalueIs a very fine-grained value, and can even be an atomic value like a string. (Using non-namespace objects as state was discouraged in the original React.) Multiple uses within a function component are encourageduseStateTo get states in different dimensions.
  3. In the calluseStateBesides, the function component will still be a pure component that implements the render logic, with state management already implemented internally by Hooks.

In our team, one principle is stressed to each other from time to time: stateful components have no render, and rendered components have no state. In retrospect, this principle will greatly facilitate subsequent migrations to Hooks.

The life cycle

UseEffect is also one of the most talked-about Hooks of the moment. The React team will tend to make no distinction between componentDidMount and componentDidUpdate.

In fact, the React team have had the tendency, and through the previous version of the API to developers communicate this signal, that is to use alternative componentWillReceiveProps getDerivedStateFromProps.

In componentWillReceiveProps era, the component of the state in fact according to the different life cycle stage, there are two different calculation methods:

  1. Before mounting, it is recommended to pass in the constructorpropsParameter to calculate state and assign it directly tothis.state.
  2. After mount, usecomponentWillReceivePropsTo calculate and usesetStateUpdate.

GetDerivedStateFromProps, as a static method, makes no distinction between the two. It is the only entry to “calculate state by props”. Even for the unification, the React team ref = “https://github.com/reactjs/rfcs/pull/6#discussion_r162865372” > abandoned prevProps as a method of parameter, caused a lot of discussion in the community. While DISCUSSING rules with ESlint-plugin-React, I had a point:

The component state is divided into two parts, one is owned, which is generated for itself, and the other is derived, which is calculated by props. When initializing the state, only the autonomous state is initialized and the native state is assigned to
nullAnd, in
getDerivedStateFromProps(even though the derived state can be computed in the constructor).

I still believe that was the idea the React team was trying to convey to developers with the getDerivedStateFromProps API, which was to stop differentiating whether components were already mounted and use a unified API for state management.

With this in mind, the most common “request data during the life cycle” scenario in our product is handled like this:

import {connectOnLifeCycle} from '@baidu/etui-hocs'; import {connect} from 'react-redux'; import {property} from 'lodash'; import {compose} from 'recompose'; import {fetchMessageForUser} from '.. /.. /actions'; const Welcome = ({message, username}) => ( <div> Hellow {uesername}: {message} </div> ); Const connects = [{grab: property('message'), // Ready to load selector: Fetch (username, {onRequestMessage}) {return onRequestMessage(username);}}]; const mapStateToProps = state => ({username: state.currentUser.username}); const mapDispatchToProps = { onRequestMessage: fetchMessageForUser }; const enhance = compose( connect(mapStateToProps, mapDispatchToProps), connectOnLifeCycle(connects) ); export default enhance(Welcome);Copy the code

As you can see, we are handing over the lifecycle logic to connectOnLifeCycle HOC, keeping the component a pure rendering process implemented as a pure functional component. For connectOnLifeCycle we do not define the difference between mount and update, but declare an external dependency with three attributes:

  1. Where is the data (grab) : There is no need to make a request when the data is available.
  2. What does the data relate to (selector) : Think of this as a “parameter to get data” and the request will only be re-initiated if the parameter changes (if the data does not exist, of course).
  3. How to initiate a request (fetch) : Real request logic.

For a biased information management system, we often as long as we pay attention to these three content, can quickly complete the development of functional modules, and mount or update is completely unimportant.

Tool support

This is what WORRIES me most about React Hooks. While the community generally supports Hooks from the point of view that the original React component is too deeply nested, it seems to me that some component nesting actually has a very positive effect.

Take the react- WHETHER library we used to process logical branches, which in the final rendering of the component tree looks like this, as you can see with React Devtools:



We can quickly see from Devtools that the component enters the IfElseMode (with a corresponding SwitchMode) and the condition does not match (matches={false}), resulting in a < A > update structure.

In my opinion, leaving the appropriate information in the component tree and tracking it with Devtools is a good way to debug React development. Just as console.log is often used to locate information, the essence of this process is to solidify short life cycle information that quickly disappears along the function call stack, whether through the console or through the component tree, to enable developers to debug.

For more complex scenes, as long as the logical layering is reasonable, nesting in the form of HOC or Render prop in the component tree allows developers to quickly reduce the problem location to a very small range by checking the props of each layer, and then reproduce and repair the problem based on the high testability of pure functions. Can also solidify into unit test use cases.

With Hooks, there is a clear signal that developers can use a single function component to declare their pre-dependencies using different Hooks, compress this information together and (at least for now) leave no trace in Devtools. As a result, while the development process may be very smooth and pleasant, tracking and debugging of bugs that are relatively difficult to reproduce online can be exponentially more painful.

API design

I’ve always found the React API design to be very strong, and of all the teams I’ve seen, Microsoft is the only one that can reliably beat React in API design. React’s API design is entirely language-level, rather than a simple framework for gameplay. This is mainly reflected in the following aspects:

  1. There is a mature process for deprecating apis. N + apis are not immediately discarded in a large release, but rather through markupUNSAFE_, the use ofStrictModeAnd so on, give the developer a smooth transition. This deprecation mode across large versions of the API I only use in language-level frameworks such as. NET, Java, but not yet in any view layer framework.
  2. An API that shouldn’t be used but has to exist will find ways to disgust the caller. classicdangerouslySetInnerHTMLThis property, except for adding onedangerouslyIn addition to such an obvious prefix, the value must be an object, and there must be an object within the object__htmlThis forced death obsessive-compulsive disorder attribute name. Developers who have a little bit of an eye for code beauty will try to make it disappear.
  3. All apis have very strong best practice guidance. Such asgetDerivedStateFromPropsThis method, simply to make static, so that developers do not usethisAnd naturally it is difficult to make things with side effects in it. Now such assetStateCallback was used instead of Promise, and the conversion to Promise implementation was officially rejected. Now seeuseStateAnd then look backsetStateAlways control how smart it is for developers to avoid using callback as much as possible.
  4. API publishing is very sequential. First,componentDidCatchThe API, which provided the concept of Error Boundary and was accepted by the developers, and then soon, which is a very destructive bomb, is actually done by Error Bounday and Promise type judgment. This gives the developer time to accept and prepare, without the backlash of having too many concepts at once.
  5. An API that completely breaks with the original theory can be released without a major release. ‘From Margaret to Hooks, it really struck me that these two apis were released, and a big part of it was that they were only released on a small version, so you can see how inclusive the React API is because of its simplicity.’

Because of this strong API design capability, React introduced an almost disruptive low-level change like Fiber with very little community backlash. In fact, there are a lot of React misuses that can’t be seamlessly transferred to asynchronous Fiber, and React version 15 has plenty of misuses of its own. Fiber’s push into developer adaptation has rarely been a major failure, thanks to the strong best practices of its API.