5 common mistakes when creating React components (with hooks) in 2020

6 min


image

Hello! In this article, you will learn about the most common errors when creating React components, as well as why they are considered errors, how to avoid or fix them.

The original material was written by German developer Lorenz Weiss for a personal blog, and later collected a lot of positive reviews on dev.to. Translated by the team Quarkly especially for a community on Habré.

React

React has long existed in the world of web development, and its position as a tool for agile development has rapidly strengthened in recent years. And after the announcement and release of a new hook api / concept making React components even easier.

Despite the fact that the team that developed React, and the huge community that codifies in this language, tried to give an impressive explanation of the concept of React, I still found some shortcomings and typical mistakes while working with it.

I made a list of all the errors that I discovered in recent years, especially those related to hooks. In this article I will talk about the most common mistakes, try to explain in detail why I consider them to be mistakes, and show how to avoid or fix them.

Disclaimer

Before moving on to the list, I must say that at first glance most of the errors from the list are not really fundamental or even look quite correct. Most of them are unlikely to affect the operation or appearance of the application at all. Most likely, except for developers working on the product, no one will notice that something is wrong here. But I still think that good quality code can help improve your development experience and, therefore, develop a better product.

As for React code, as with any other software framework or library, there are a million different opinions on it. All that you see here is my personal opinion, and not the ultimate truth. If you have a different opinion, I will listen to it with pleasure.

1. Use useStatewhen there is no need for re-rendering

One of React’s core concepts is working with state. You can control the entire data stream and rendering through state. Each time a tree is rendered again, it is most likely tied to a state change.

Using hook useState You can also define state in functional components because it is a really clean and easy way to handle states in React’s. In the example below, you can see how this hook is used for other purposes.

Now more about the example: suppose we have two buttons, one button is a counter, and the other sends a request or starts an action with the current counter. However, the current number is not displayed inside the component. It is only required for a request when a second button is pressed.

Doing so is not good:

function ClickButton(props) {
  const [count, setCount] = useState(0);

  const onClickCount = () => {
    setCount((c) => c + 1);
  };

  const onClickRequest = () => {
    apiCall(count);
  };

  return (
    
); }

Problem:

At first glance, you may ask: What, in fact, is the problem? Isn’t this condition created for this? And you will be right: everything will work perfectly, and problems are unlikely to arise. However, in React’s, each state change affects the component and most likely its child components, that is, forces them to re-render.

In the above example, since we are not using this state in our part of the render, every time we set the counter, it will end up with unnecessary rendering. And this can affect the operation of the application or lead to unexpected side effects.

Decision:

If inside your component you want to use a variable that should retain its value between rendering, but not cause re-rendering, you can use a hook useRef. It will retain the value, but will not lead to re-rendering.

function ClickButton(props) {
  const count = useRef(0);

  const onClickCount = () => {
    count.current++;
  };

  const onClickRequest = () => {
    apiCall(count.current);
  };

  return (
    
); }

2. Use router.push instead of link

This is probably obvious and has nothing to do with React itself, but I still quite often observe this oversight in people who create React components.

Suppose you create a button, and the user, when clicked on it, should be redirected to another page. So how is it SPA, then this action will be a client routing mechanism. So you need some kind of library. The most popular of them in React is react-router, and the following example will use this particular library.

Does this mean that adding a click event listener will correctly redirect the user to the desired page?

It’s not good to do so:

function ClickButton(props) {
  const history = useHistory();

  const onClick = () => {
    history.push('/next-page');
  };

  return ;
}

Problem:

Although for most users this is a completely working solution, there is one big problem: it appears when it comes to implementing web accessibility. The button will not be marked as a link to another page at all, which means that the screen reader will not be able to identify it. Can you open it in a new tab or window? Most likely not either.

Decision:

Links to other pages in any interaction with the user should, as far as possible, be processed by the component or regular tag .

function ClickButton(props) {
  return (
    
      Go to next page
    
  );
}

Bonuses: it also makes the code more readable and concise!

3. Processing actions with useEffect

One of the best and thoughtful hooks introduced in React’s is useEffect. It allows you to handle the actions associated with the change. prop or state. Despite its functionality, this hook is also often used where it is not really needed.

Imagine a component that retrieves a list of elements and displays them in the DOM. In addition, in case of successful execution of the request, we want to call the function onSuccesswhich is passed to the component as a property.

It’s not good to do so:

function DataList({ onSuccess }) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);

  const fetchData = useCallback(() => {
    setLoading(true);
    callApi()
      .then((res) => setData(res))
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, []);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  useEffect(() => {
    if (!loading && !error && data) {
      onSuccess();
    }
  }, [loading, error, data, onSuccess]);

  return 
Data: {data}
; }

Problem:

There are two hooks useEffect: the first handles the data request to the API during the initial rendering, and the second calls the function onSuccess. That is, if there is no download or error in the state, but there is data, then this call will be successful. Sounds logical, right?

Of course, this will almost always work with the first call. But you will also lose the direct connection between the action and the called function. And there is no 100% guarantee that this will happen only if the action fetch will be successful. And this is exactly what we developers do not like so much.

Decision:

The easiest solution is to install the function onSuccess to where the challenge will be successful.

function DataList({ onSuccess }) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);

  const fetchData = useCallback(() => {
    setLoading(true);
    callApi()
      .then((fetchedData) => {
        setData(fetchedData);
        onSuccess();
      })
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [onSuccess]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return 
{data}
; }

Now at a glance it’s clear that onSuccess It is called only if the API call is successful.

4. Single Responsible Components

Composing components can be quite a difficult task. When to divide one component into several smaller components? How to structure a tree of components? These questions are asked by everyone who works with the component framework every day. And the most common mistake when creating components is to combine two use cases into one component.

Take, for example, the title that shows either the burger menu button on the mobile device or the tabs on the desktop (the condition will be processed by a magic function isMobilewhich is not part of this example).

It’s not good to do so:

function Header(props) {
  return (
    
); } function HeaderInner({ menuItems }) { return isMobile() ? : ; }

Problem:

With this approach, the component Headerinner trying to lead a double life. And we know that it’s hard to be Clark Kent and Superman at the same time. It also makes it difficult to test or reuse the component in other places.

Decision:

If we transfer the condition one level higher, it will be easier to see why the components are made and that they are responsible for only one thing: the title, tab or button of the burger menu, and you should not try to answer for several things at the same time.

function Header(props) {
  return (
    
{isMobile() ? : }
); }

five. useEffect with one responsibility

Remember the times when we only had methods componentWillReceiveProps or componentDidUpdate to connect to the rendering process of a React component? Gloomy memories immediately come to mind, and you also realize the beauty of using a hook useEffect and especially the fact that it can be used without restrictions.

But sometimes when you use useEffect for some things, gloomy memories come again. Imagine, for example, that you have a component that retrieves some data from a backend and displays a path (breadcrumbs) depending on the current location (used again react-router to get the current location).

It’s not good to do so:

function Example(props) {
  const location = useLocation();

  const fetchData = useCallback(() => {
    /*  Calling the api */
  }, []);

  const updateBreadcrumbs = useCallback(() => {
    /* Updating the breadcrumbs*/
  }, []);

  useEffect(() => {
    fetchData();
    updateBreadcrumbs();
  }, [location.pathname, fetchData, updateBreadcrumbs]);

  return (
    
); }

Problem:

There are two ways to use a hook: “data collection” (data-fetching) and “path mapping” (displaying breadcrumbs) Both are updated with a hook. useEffect. This hook useEffect will work when fetchData and updateBreadcrumbs function or change location. The main problem is that now we also call the function fetchData when it changes location. This can be a side effect that we have not thought about.

Decision:

If you divide the effect, it becomes clear that the functions are used for only one effect, while the unexpected side effects disappear.

function Example(props) {
  const location = useLocation();

  const updateBreadcrumbs = useCallback(() => {
    /* Updating the breadcrumbs*/
  }, []);

  useEffect(() => {
    updateBreadcrumbs();
  }, [location.pathname, updateBreadcrumbs]);

  const fetchData = useCallback(() => {
    /*  Calling the api */
  }, []);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return (
    
); }

Bonuses: Use cases are now also logically distributed within the component.

Conclusion

When creating React components, you come across many pitfalls. You never manage to thoroughly understand the whole mechanism, and you will certainly come across at least a small, and, most likely, even a large jamb. However, you also need to make mistakes when you are studying some kind of framework or programming language. No one is 100% safe from them.

I think that sharing my experience in this area is very useful for others, this should help other guys not step on the same rake.


0 Comments

Leave a Reply