[Front-end tutorial] How to use the "golden rule" of React components to make the code more elegant and play the role of hooks

Original: https://www.freecodecamp.org/news/how-the-golden-rule-of-react-components-can-help-you-write-better-code-127046b478eb/

Author: Rico Kahler

Translator: ZhichengChen

Proofreader: Chengjun.L

Tip: The blue font in the text can be accessed by clicking "Read Original"

I recently learned a new concept that changed the way I create components. It is not only a new idea, but also a new perspective.

The Golden Rule of Components

Create and define components in a more natural way. Components only contain the code they need.

This is a short sentence, you may think it is understood, but it is easy to violate this principle.

For example, there are the following components:

image

PersonCard

If you define this component naturally, you might write:

PersonCard.propTypes = {  name: PropTypes.string.isRequired,  jobTitle: PropTypes.string.isRequired,  pictureUrl: PropTypes.string.isRequired,};

The code is very simple, each attribute is necessary for it, such as name, job title and picturel URL.

Suppose now that you need to add another more formal picture that the user can change, perhaps the easiest to think of is:

PersonCard.propTypes = {  name: PropTypes.string.isRequired,  jobTitle: PropTypes.string.isRequired,  officialPictureUrl: PropTypes.string.isRequired,  pictureUrl: PropTypes.string.isRequired,  preferOfficial: PropTypes.boolean.isRequired,};

It seems that these props are required by the component. In fact, the component will not be affected without these props. But added the preferOfficialseemingly increased flexibility, in fact, logic should not have to add here, considering the time multiplexing will find this very elegant.

How to improve

So the logic for converting the image URL does not belong to the component itself, so where does it belong?

On the indexinside how?

We use the following directory structure, each component has its own named folder, and the indexfile is a bridge between the elegant component and the outside world. We call this file a "container" (refer to React Redux's "container" component concept).

/PersonCard  -PersonCard.js ------ the "natural" component  -index.js ----------- the "container"

We define a container as a bridge connecting elegant components with the outside world. Because of this, we sometimes call it "injectors".

A natural component means that your code contains only the necessary parts (no details such as how to get data or location-all code is required).

The outside world can transform data into props that meet the requirements of elegant components.

This article will discuss how to protect components from the outside world and the benefits of doing so.

Note: Although inspired by Dan's Abramov and React Redux's ideas, our containers are slightly different from them.

The difference between Dan Abramov's container and us is at the conceptual level. Dan believes that there are two kinds of components: display components and container components. We take this one step further and think that there are components first and then containers.

Although we use components to implement containers, we do not consider containers to be components in the traditional sense. That is why we recommend that you put the container on the indexfile - as it is elegant and bridges component of the outside world does not exist independently.

So this article will have a lot of components and containers.

why?

Creating an elegant component-it's easy, even fun.

Connecting components to the outside world-a little difficult.

In my opinion, the external world's pollution of elegant components is mainly in these three ways:

  • Weird data structure

  • Requirements outside the scope of the component (just like the code above)

  • Trigger event when update or mount

The next few sections will explain these situations, and use examples to show container implementations in different situations.

Handle weird data structures

Sometimes in order to present the required information, the data needs to be connected together and then converted into a specific format. With no better design patterns, "quirky" data structures are the easiest to think of and the least elegant.

It is tempting to pass weird data structures directly into the component and then convert it inside the component, but this makes the component more complex and difficult to test.

I recently fell into this pit, I created a component, get data from a special data structure, and then let it support special forms.

image

ChipField.propTypes = {  field: PropTypes.object.isRequired,      // <-- the "weird" data structure  onEditField: PropTypes.func.isRequired,  // <-- and a weird event too};

Then I was born this stuff, weird fielddata structure as a prop. In addition, it's okay if you don't need to deal with it later, but when we want to reuse it in another completely unrelated data structure, the nightmare comes.

Since this component requires a complex data structure, reuse is almost impossible, and reconstruction is also very large. The previously written tests will also be difficult to understand, because they mock up a weird data structure. Test logic is difficult to understand and rewrite during continuous refactoring.

Unfortunately, weird data structures are hard to avoid, but using containers can tame it well. One of the benefits is that you can reuse components very well, but if you directly pass weird data structures into components before, it will be difficult to reuse.

Note: I ’m not saying that all components should be universal at the beginning of the creation of the component. My suggestion is to consider the basic function of the component and then start coding, so that you can write some highly reusable with little work s component.

Use functional components to implement containers

If your mapping props are very strict, a simple implementation of the container is to use another function component:

import React from 'react';import PropTypes from 'prop-types';import getValuesFromField from './helpers/getValuesFromField';import transformValuesToField from './helpers/transformValuesToField';import ChipField from './ChipField';export default function ChipFieldContainer({ field, onEditField }) {  const values = getValuesFromField(field);  function handleOnChange(values) {    onEditField(transformValuesToField(values));  }  return <ChipField values={values} onChange={handleOnChange} />;}// external propsChipFieldContainer.propTypes = {  field: PropTypes.object.isRequired,  onEditField: PropTypes.func.isRequired,};

The directory structure of the component is as follows:

/ChipField  -ChipField.js ------------------ the "natural" chip field  -ChipField.test.js  -index.js ---------------------- the "container"  -index.test.js  /helpers ----------------------- a folder for the helpers/utils    -getValuesFromField.js    -getValuesFromField.test.js    -transformValuesToField.js    -transformValuesToField.test.js

You might say that this is too much trouble, it seems that there are more files and more bends, but do n’t forget:

Converting data outside the component is consistent with the workload inside the component. The difference is that when you convert data outside the component, you give yourself a clearer point to test whether the conversion is correct and separate concerns.

Meet the needs outside the component scope

Like the Person Card above, when you think about it with the "golden rule", it is likely that you will realize that the requirements are beyond the actual scope of the component. How to achieve it?

That's right, it's the container.

You can create containers to keep components elegant with a little work. When you do this, you will unlock a more professional component, which is also much simpler, and the container is easier to test.

Let's write a PersonCard container to illustrate.

Implement containers using higher-order components

React Redux uses high-level components to implement containers for push and map props from the Redux store. Since we borrowed this idea from React Redux, there is no doubt that React Redux's connect is this container.

Whether you use functional components to map props, or use higher-order components to connect to Redux strore, the golden rule of components remains the same. First, write elegant components, and then connect the two with higher-order components.

import { connect } from 'react-redux';import getPictureUrl from './helpers/getPictureUrl';import PersonCard from './PersonCard';const mapStateToProps = (state, ownProps) => {  const { person } = ownProps;  const { name, jobTitle, customPictureUrl, officialPictureUrl } = person;  const { preferOfficial } = state.settings;  const pictureUrl = getPictureUrl(preferOfficial, customPictureUrl, officialPictureUrl);  return { name, jobTitle, pictureUrl };};const mapDispatchToProps = null;export default connect(  mapStateToProps,  mapDispatchToProps,)(PersonCard);

The file structure is as follows:

/PersonCard  -PersonCard.js ----------------- natural component  -PersonCard.test.js  -index.js ---------------------- container  -index.test.js  /helpers    -getPictureUrl.js ------------ helper    -getPictureUrl.test.js

Note: Here, to getPictureUrlprovide an assistant. The logic is simple. You may have noticed that no matter how the container is implemented, the file structure hardly changes.

If you have used Redux before, you must be familiar with the above example. Again, this golden rule is not just an idea, it also provides a new idea.

In addition, when using higher-order functions to implement containers, you can also link them together-pass a higher-order component as props to the next. I used to connect multiple higher-order components together to form a single container.

2019 Note: The React community seems to be standardizing high-end components into design patterns.

I suggest the same. My experience is that for those who do not understand functional composition, writing code can easily lead to "wrapper hell", and components are nested too many layers to cause serious performance problems.

Here are some related articles: Hooks talk (2018), Recompose talk (2016), Use a Render Prop! (2017), When to NOT use Render Props (2018)

The good hook is here

Use hooks to implement containers

Why are hooks discussed here? Because using hooks to implement containers is really simple.

If you are new to React hooks, I suggest you take a look at the conversation between Dan Abramov and Ryan Florence at 2018 React Conf.

The point is that the hook was introduced by the React team to solve the problem of higher-order components and similar patterns. React wants to replace them with hooks in similar scenarios.

This means that the container can be implemented using both functional components and hooks.

In the following example, we use useRouteand useReduxhooks to represent the "outside world", using tools like getvaluethe outside world map as an elegant assembly props. We also used transformValuesto convert assembly to the outside world dispatch.

import React from 'react';import PropTypes from 'prop-types';import { useRouter } from 'react-router';import { useRedux } from 'react-redux';import actionCreator from 'your-redux-stuff';import getValues from './helpers/getVaules';import transformValues from './helpers/transformValues';import FooComponent from './FooComponent';export default function FooComponentContainer(props) {  // hooks  const { match } = useRouter({ path: /* ... */ });  // NOTE: `useRedux` does not exist yet and probably won't look like this  const { state, dispatch } = useRedux();  // mapping  const props = getValues(state, match);  function handleChange(e) {    const transformed = transformValues(e);    dispatch(actionCreator(transformed));  }  // natural component  return <FooComponent {...props} onChange={handleChange} />;}FooComponentContainer.propTypes = { /* ... */ };

The following is the corresponding directory structure:

/FooComponent ----------- the whole component for others to import  -FooComponent.js ------ the "natural" part of the component  -FooComponent.test.js  -index.js ------------- the "container" that bridges the gap  -index.js.test.js         and provides dependencies  /helpers -------------- isolated helpers that you can test easily    -getValues.js    -getValues.test.js    -transformValues.js    -transformValues.test.js

Trigger an event in the container

The last category that makes it difficult to reuse components is when the props change and the component is mounted.

Taking the dashboard as an example, the design team gave you prototype drawings that require you to convert them into React components. The problem now is how to fill the dashboard with data.

You may have realized that you can call a function (eg:) dispatch(fetchAction)to trigger an event when the component is mounted .

In such a scenario similar to the common practice is to add componentDidMountand compoentDidUpdatelife-cycle approach, as well as onMountand onDashboardIdChangedprops, because I need to trigger external events, in order to establish a connection between the components and the outside world.

According to the golden rule, these onMountand onDashboardIdChangedprops very elegant, they should be placed in the container.

Hook powerful is that it allows onMountdispatch event or props change becomes easier to achieve!

Trigger an event in mount

Passing in an empty array call useEffectto trigger the event when the mount.

import React, { useEffect } from 'react';import PropTypes from 'prop-types';import { useRedux } from 'react-redux';import fetchSomething_reduxAction from 'your-redux-stuff';import getValues from './helpers/getVaules';import FooComponent from './FooComponent';export default function FooComponentContainer(props) {  // hooks  // NOTE: `useRedux` does not exist yet and probably won't look like this  const { state, dispatch } = useRedux();  // dispatch action onMount  useEffect(() => {    dispatch(fetchSomething_reduxAction);  }, []); // the empty array tells react to only fire on mount  // https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects  // mapping  const props = getValues(state, match);  // natural component  return <FooComponent {...props} />;}FooComponentContainer.propTypes = { /* ... */ };

Trigger event when prop changes

useEffect You can monitor property changes during re-rendering and function calls.

With useEffectbefore I found myself adding a redundant function method and life-cycle onPropertyChangedattributes, because I do not know how to extend the property outside of the component.

import React from 'react';import PropTypes from 'prop-types';/** * Before `useEffect`, I found myself adding "unnatural" props * to my components that only fired events when the props diffed. * * I'd find that the component's `render` didn't even use `id` * most of the time */export default class BeforeUseEffect extends React.Component {  static propTypes = {    id: PropTypes.string.isRequired,    onIdChange: PropTypes.func.isRequired,  };  componentDidMount() {    this.props.onIdChange(this.props.id);  }  componentDidUpdate(prevProps) {    if (prevProps.id !== this.props.id) {      this.props.onIdChange(this.props.id);    }  }  render() {    return // ...  }}

With useEffectmore lightweight way to change the prop, the components do not have to add the extra props.

import React, { useEffect } from 'react';import PropTypes from 'prop-types';import { useRedux } from 'react-redux';import fetchSomething_reduxAction from 'your-redux-stuff';import getValues from './helpers/getVaules';import FooComponent from './FooComponent';export default function FooComponentContainer({ id }) {  // hooks  // NOTE: `useRedux` does not exist yet and probably won't look like this  const { state, dispatch } = useRedux();  // dispatch action onMount  useEffect(() => {    dispatch(fetchSomething_reduxAction);  }, [id]); // `useEffect` will watch this `id` prop and fire the effect when it differs  // https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects  // mapping  const props = getValues(state, match);  // natural component  return <FooComponent {...props} />;}FooComponentContainer.propTypes = {  id: PropTypes.string.isRequired,};

Disclaimer: The useEffectformer call will compare the similarities and differences prop containers, can also be used in other ways, such as higher-order components (such as recompose the life cycle), or created as a component within the life cycle like react router, but these methods or is it The trouble is either hard to understand.

What are the benefits

Components stay interesting

For me, creating components is an interesting part of front-end development. It feels great to be able to implement the team ’s ideas. This feeling is worth sharing.

Don't let the outside world mess up the component API anymore, the component should have no extra props as expected-this is what I learned from the golden rule.

More opportunities to test and reuse

When you adopt a pattern like this, a new data-y layer is introduced, in which you can transform the data into the form required by the component as needed.

No matter if you don't care, this layer already exists in your application, but this may also increase the logic of the code. My experience is that when I focus on this layer, I can do a lot of code optimization and can reuse a lot of logic. Now when I know that there are commonalities between components, I will not reinvent the wheel.

I think this is especially obvious on custom hooks. Custom hooks give us a simpler way to extract logic and monitor external changes-more often it is impossible to do with helper functions.

Maximize team output

In teamwork, you can separate components and containers. If you communicate the API well in advance, you can start the following tasks at the same time:

  • Web API (eg backend)

  • Get data (or other means) from Web API, and then convert the data to conform to the component's API

  • Components

Are there any exceptions?

Just like the real golden rule, there are exceptions to this golden rule. In some scenarios, it is necessary to write redundant APIs in components to reduce complexity.

A simple example is the naming of props. If you do not rename the data key under the elegant component, it will make things more complicated.

Superstition golden rules may be more standardized, but at the same time also blocked creativity.

Service recommendation

Published 0 original articles · liked 0 · visits 359

Guess you like

Origin blog.csdn.net/weixin_47143210/article/details/105611667