React hooks - win or lose?

    image


    With the release of the new React 16.6.0, HOOKS (PROPOSAL) has appeared in the documentation . They are now available at react 17.0.0-alpha and are discussed in the open RFC: React Hooks . Let's see what it is and why it is needed under the cut.


    Yes, this is the RFC and you can influence the final implementation by discussing with the creators of react why they chose this or that approach.


    Let's take a look at what a standard hook looks like:


    import { useState } from'react';
    functionExample() {
      // Declare a new state variable, which we'll call "count"const [count, setCount] = useState(0);
      return (
        <div><p>You clicked {count} times</p><buttononClick={() => setCount(count + 1)}>
            Click me
          </button></div>
      );
    }

    Try to think about this code, this is a teaser and by the end of the article you will already understand what it means. The first thing to know is that it does not break backward compatibility and may be added to 16.7 after collecting feedback and suggestions in the RFC.


    As the guys assure, this is not a plan for cutting out classes from the reactor.


    Also, hooks do not replace current reactor concepts, everything is in place of props / state / context / refs. This is just another way to use their power.


    Motivation


    At first glance, hooks solve not coherent problems that appeared with the support of tens of thousands of components within 5 years of facebook.


    The most difficult thing is to reuse logic in stateful components, the reactor has no way to attach reusable behavior to the component (for example, to connect it to the storage). If you worked with React you know the notion of HOC (high-order-component) or render props. These are fairly good patterns, but sometimes they are used excessively, they require restructuring of the components so that they can be used, which usually makes the code more cumbersome. It is worth looking at a typical application and it will become clear what is at stake.


    image


    This is called wrapped-hell - hell wrappers.


    An application from one HOC is normal in the current realities, the component was connected to the store / theme / localization / custom hocks, I think everyone is familiar with it.


    It becomes clear that the reactor needs another primitive mechanism for the separation of logic.


    With the help of hooks, we can retrieve the state of the component so that it can be tested and reused. Hooks allow you to reuse state logic without changing the component hierarchy. This makes it easy to exchange links between many components or the entire system. Also, the class components look pretty scary, we describe the life-cycle methods componentDidMount/ shouldComponentUpdate/ componentDidUpdate, the state of the component, create methods for working with the state / stor, bindim the methods for the component instance, and so it can be continued indefinitely. Typically, such components go beyond x lines, where x is quite difficult to understand.


    Hooks allow you to do the same by breaking the logic between components into small functions and using them inside components.


    Classes are hard for people and for cars.


    Watching facebook classes is a big obstacle when learning React. You need to understand how it works this, but it does not work as in other programming languages, you should also remember to bind event handlers. Without stable syntax sentences, the code looks very verbose. People perfectly understand the props / state patterns and the so-called top-down data flow, but it’s quite difficult to understand the classes.


    Especially if you do not limit yourself to templates, not so long ago, the guys from the reactor experimented with the layout of components with Prepack and saw promising results, but nevertheless the components of the class allow you to create unintended bad patterns that make these optimizations disappear. hot-booting classes make it unreliable. First of all, the guys wanted to provide an API that supports all optimizations and works great with a hot reboot.


    Look at the hooks


    State hook


    The code below renders the paragraph and the button, and if we press the button, the value in the paragraph will be incremented.


    import { useState } from'react';
    functionExample() {
      // Declare a new state variable, which we'll call "count"const [count, setCount] = useState(0);
      return (
        <div><p>You clicked {count} times</p><buttononClick={() => setCount(count + 1)}>
            Click me
          </button></div>
      );
    }

    From this we can conclude that this hook seems to work with such a concept as state.
    A little more detailed method useStatetakes one argument, this is the default value and returns a tuple in which there is a value itself and a method for changing it, unlike setState, setCount will not do merge values, but simply update it. We can also use multiple state declarations, for example:


    functionExampleWithManyStates() {
      // Declare multiple state variables!const [age, setAge] = useState(42);
      const [fruit, setFruit] = useState('banana');
      const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
      // ...
    }

    this way we create several states at once and we don’t need to think about how to decompose them somehow. Thus, it can be emphasized that hooks are functions that allow you to "connect" to the chips of class components, just as hooks do not work inside classes, it is important to remember.


    Effect hook


    Often in class components, we do side effect functions, for example, subscribe to events or make requests for data, usually we use the methods componentDidMount/componentDidUpdate


    import { useState, useEffect } from'react';
    functionExample() {
      const [count, setCount] = useState(0);
      // Similar to componentDidMount and componentDidUpdate:
      useEffect(() => {
        // Update the document title using the browser APIdocument.title = `You clicked ${count} times`;
      });
      return (
        <div><p>You clicked {count} times</p><buttononClick={() => setCount(count + 1)}>
            Click me
          </button></div>
      );
    }

    When we call useEffectwe tell the reactant to make a 'side effect' after updating the changes in the DOM tree. Effects are declared inside the component, so they have access to props / state. And we can create them as much as you want.


    functionFriendStatusWithCounter(props) {
      const [count, setCount] = useState(0);
      useEffect(() => {
        document.title = `You clicked ${count} times`;
      });
      const [isOnline, setIsOnline] = useState(null);
      useEffect(() => {
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return() => {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
      });
      functionhandleStatusChange(status) {
        setIsOnline(status.isOnline);
      }
      // ...

    Immediately you should pay attention to the second side effect in it, we return the function, we do it in order to perform some actions after the component performs unmount, in the new api this is called cleaning effects. The rest of the effects can return anything.


    Hook rules


    Hooks are just javascript functions, but they only require two rules:


    • Hooks should be executed at the very top of the function hierarchy (this means that hooks should not be called in conditions and cycles, otherwise the reactor cannot guarantee the order of the hooks)
    • Only call hooks in React functions or functional components, or call hooks from custom hooks (see below).
      To follow these rules, the guys from the reactor team created a linter plugin that will generate an error if you call hooks in class components or in cycles and conditions.

    Custom hooks


    At the same time, we want to reuse the logic of stateful components, usually for this purpose either HOC or render props patterns are used, but they create an additional amount of our application.
    For example, we describe the following function:


    import { useState, useEffect } from'react';
    functionuseFriendStatus(friendID) {
      const [isOnline, setIsOnline] = useState(null);
      functionhandleStatusChange(status) {
        setIsOnline(status.isOnline);
      }
      useEffect(() => {
        ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
        return() => {
          ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
        };
      });
      return isOnline;
    }

    Realize this code, it will be a custom hook that we can call in various components. For example:


    functionFriendStatus(props) {
      const isOnline = useFriendStatus(props.friend.id);
      if (isOnline === null) {
        return'Loading...';
      }
      return isOnline ? 'Online' : 'Offline';
    }

    or so


    functionFriendListItem(props) {
      const isOnline = useFriendStatus(props.friend.id);
      return (
        <listyle={{color:isOnline ? 'green' : 'black' }}>
          {props.friend.name}
        </li>
      );
    }

    In any case, we reuse the state of the component, each function call useFriendStatuscreates an isolated state. It is also worth noting that the beginning of this function begins with the word use , which means that it is a hook. We advise to follow this format. You can write custom hooks for anything, animations / subscriptions / timers and much more.


    There are a couple of hooks.


    useContext


    useContext allows us to use the usual return value instead of renderProps, we should transfer to it the context we want to extract and it will return it to us, so we can get rid of all the HOCs that passed the context to props.


    functionExample() {
      const locale = useContext(LocaleContext);
      const theme = useContext(ThemeContext);
      // ...
    }

    And now the context object we can simply use in the return value.


    useCallback


    const memoizedCallback = useCallback(
      () => {
        doSomething(a, b);
      },
      [a, b],
    );

    How often did you have to create a class component only to save a reference to a method? You no longer need to do this, we can use useCallback and our components will not redraw because a new link to onClick has come.


    useMemo


    We return a memoized value, memoized means it is calculated only when one of the arguments has changed, the second time the same thing will not be calculated either.


    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

    Why, you have to duplicate the values ​​in the array so that the hook understands that they have not changed.


    useRef


    useRefreturns a mutable value, where the field .currentwill be initialized with the first argument, the object will exist as long as the component exists.
    The most common example with input focus


    functionTextInputWithFocusButton() {
      const inputEl = useRef(null);
      const onButtonClick = () => {
        // `current` points to the mounted text input element
        inputEl.current.focus();
      };
      return (
        <>
          <input ref={inputEl} type="text" />
          <button onClick={onButtonClick}>Focus the input</button>
        </>
      );
    }

    useImperativeMethods


    useImperativeMethodscustomizes the value of the instance that is passed from the parent and uses ref directly. As always, you should avoid referring to direct links and should useforwardRef


    functionFancyInput(props, ref) {
      const inputRef = useRef();
      useImperativeMethods(ref, () => ({
        focus: () => {
          inputRef.current.focus();
        }
      }));
      return<inputref={inputRef}... />;
    }
    FancyInput = forwardRef(FancyInput);

    In this example, the component that renders FancyInputcan cause fancyInputRef.current.focus().


    useMutationEffect


    useMutationEffectvery similar to useEffectexcept that it runs synchronously at the stage when the reactant changes the DOM values, before neighboring components are updated, this hook should be used to perform DOM mutations.
    It is better to prefer useEffect to prevent blocking of visual changes.


    useLayoutEffect


    useLayoutEffectit is also similar to useEffectthe exception that it runs synchronously after all updates of the DOM and synchronous re-renderer. Updates scheduled useLayoutEffectare applied synchronously before the browser is able to draw the elements. You should also try to use the standard useEffectso as not to block visual changes.


    useReducer


    useReducer - this is a hook for creating a reduser that returns a state and the ability to dispute changes:


    const [state, dispatch] = useReducer(reducer, initialState);

    If you understand how Redux works, then you understand how it works useReducer. The same example that was with the counter on top only through useReducer:


    const initialState = {count: 0};
    functionreducer(state, action) {
      switch (action.type) {
        case'reset':
          return initialState;
        case'increment':
          return {count: state.count + 1};
        case'decrement':
          return {count: state.count - 1};
      }
    }
    functionCounter({initialCount}) {
      const [state, dispatch] = useReducer(reducer, initialState);
      return (
        <>
          Count: {state.count}
          <buttononClick={() => dispatch({type: 'reset'})}>
            Reset
          </button><buttononClick={() => dispatch({type: 'increment'})}>+</button><buttononClick={() => dispatch({type: 'decrement'})}>-</button></>
      );
    }

    Also, useReducer takes 3 arguments, actionwhich is to be executed when initializing the reducer:


    const initialState = {count: 0};
    functionreducer(state, action) {
      switch (action.type) {
        case'reset':
          return {count: action.payload};
        case'increment':
          return {count: state.count + 1};
        case'decrement':
          return {count: state.count - 1};
      }
    }
    functionCounter({initialCount}) {
      const [state, dispatch] = useReducer(
        reducer,
        initialState,
        {type: 'reset', payload: initialCount},
      );
      return (
        <>
          Count: {state.count}
          <buttononClick={() => dispatch({type: 'reset', payload: initialCount})}>
            Reset
          </button><buttononClick={() => dispatch({type: 'increment'})}>+</button><buttononClick={() => dispatch({type: 'decrement'})}>-</button></>
      );
    }

    We can also create a context in the given reducer and useContextuse it in the entire application through the hook , it remains for homework.


    Summarizing


    Hooks are quite a powerful approach to solving wrapper-hell and solve several problems, but all of them can be solved with the same definition of referral links . Already begin to appear collections of hooks on the use of or this collection . More details about the hooks can be found in the documentation .


    Also popular now: