How to get rid of complexity in React management in React - report on the results of a trip to React Amsterdam

    In April, I was lucky to attend a very cool event - React Amsterdam . In addition to pleasant organizational moments, there were also many interesting reports. They were mainly of an applied nature. Since the technology stack was basically settled, the speakers talked about ways to solve practical problems, and did not promote something unfamiliar and revolutionary. Here I’ll talk more about the performance of “ setState Machine ” by Michele Bertoli from Facebook.

    The main problem that the report was dedicated to is the difficulty in managing state in React.

    For example, let's implement the familiar functionality - Infinite Scroll, that is, loading data when the user scrolls to the end (or almost to the end) of the page. There are many useful packages that solve this problem, but you often have to write it yourself.

    What we need to do in our component for this:

    1. Add a handler for the event scroll.
    2. Add a check to see if the user has scrolled to the right place from which we will load the data.
    3. Actually, load the data.

    In a first approximation, this is enough, however, let's add a few more requirements for correct operation:

    1. Do not load new data if the previous batch is still loading.
    2. Somehow inform the user that the data is loading - show loading or something like that at the time of loading.
    3. Do not start data loading if everything is already loaded.
    4. Add error display to user.

    Let's imagine what should be stored in our state, in addition to data, so that our functionality works correctly:

    1. Flag isFetching - shows us that data is currently loading.
    2. Field error - should contain information about the error.
    3. Field isEmpty - shows us that there is no data.
    4. If suddenly we want to add functionality for retry, then we need to store information for it.

    What are the main disadvantages of such an implementation:

    1. Great contextual reference. A lot of conditions, for example, we load data only when we scroll to the right place, while previous data is not loaded, etc.
    2. Our code is hard to read and understand.
    3. It is difficult to scale - when adding a new property to a state, you need to go through all our conditions and understand how to change the state in a particular place so as not to break the logic. It can also lead to bugs.

    To correct these shortcomings, we will help the state machine (State Machine).

    In fact, this is the principle of using the Final State Machine, an abstract model containing a finite number of states.

    The model is described using five parameters:

    1. All states in which the machine may be located.
    2. A set of all input received by the machine.
    3. Transition function - accepts the previous state and set of input data, returns a new state.
    4. Initial state.
    5. Final state.

    Only one state can be active at any given time.

    Accordingly, we can determine the conditions for the transition from one state to another.
    As an example, consider the work of a traffic light. This is a machine with three states, we know their order, and we can also conditionally name the initial and final state.

    Let's add the react-automata library to our code - an abstraction of the state machine for React. This is a wrapper over another xstate library - functional stateless JS state machines and state diagrams.

    To understand how this theory applies to our case, let's see how the functionality will look like a statechart :



    To begin, we indicate the initial state - the entry point, which is the addition of a scroll event. When we are ready for further work, we send the READY event to the machine, and the machine enters the boot state. If the download was successful and not all the data has been downloaded yet, we go into the listening state. When, when scrolling, the condition for loading a new piece of data is fulfilled, we enter the load state and can stay in this cycle until we download all the data. Then we no longer listen to the event.

    Schematically, our code may look like this:

    import React from "react";
    import { hot } from "react-hot-loader";
    import { Action, withStatechart } from "react-automata";
    export const statechart = {
      // начальное состояние
      initial: "attach",
      // Список состояний
      states: {
        attach: {
          on: {
            READY: "fetching"
          }
        },
        fetching: {
          on: {
            SUCCESS: {
              listening: {
                //Переходим в состояние listening по событию SUCCESS
                //cond - pure функция, переводящая машину в указанное состояние, если возвращает правдивое значение
                cond: extState => extState.hasMore
              },
              detach: {
                cond: extState => !extState.hasMore
              }
            },
            ERROR: "listening"
          },
          // fetch - событие, которое должно быть выполнено при входе в состояние fetching
          onEntry: "fetch"
        },
        listening: {
          on: {
            SCROLL: "fetching"
          }
        },
        detach: {}
      }
    };
    class InfiniteScroll extends React.Component {
      componentDidMount() {
        // на mount нашего компонента переходим из начального состояния в fetching
        this.attach();
      }
      attach() {
        //навешиваем наш обработчик и переходим в состояние fetching
        //возможна, конечно, и другая реализация этого перехода - зависит от требований к работе фичи
        this.element.addEventListener("scroll", this.handleScroll);
        this.props.transition("READY");
      }
      handleScroll = e => {
        const { scrollTop, scrollHeight, clientHeight } = e.target;
        const isCilentAtBottom = 0.9 * (scrollHeight - scrollTop) === clientHeight;
        if (isCilentAtBottom) {
          // Переход из listening в fetching
          this.props.transition("SCROLL");
        }
      };
      fetch() {
        const { transition } = this.props;
        loadTodos()
          .then(res => res.json())
          .then(data => transition("SUCCESS", { todos: data }))
          .catch(() => transition("ERROR"));
      }
      render() {
        // Action - компонент, который определяет, что должно рендериться для данного события
        return (
          
    { this.element = element; }} > Loading...
      {this.props.todos.map(todo =>
    • {todo.text}
    • )}
    ); } } InfiniteScroll.defaultProps = { todos: [] }; const initialData = { todos: [], devTools: true }; const StateMachine = withStatechart(statechart, initialData)(InfiniteScroll); export default hot(module)(StateMachine);

    We use the hoc withStatechart from react-automata, transfer our initial data, and now in props a method is available transition for changing the state of the machine, and machineState - the current state of the machine.

    A variable statechart is a programmatic description of our drawing.

    Advantages of the approach:

    1. Less bugs.
    2. Easier to read and understand the code.
    3. Separation of what happened and when happened. The first is controlled by the component, the second by state diagrams.

    Useful links:

    1. Michele Bertoli's talk at React Amsterdam 2018: https://www.youtube.com/watch?v=smBND2pwdUE&t=3137s
    2. React Automata: https://github.com/MicheleBertoli/react-automata
    3. Xstate documentation: http://davidkpiano.github.io/xstate/docs/#/
    4. State diagram explanation: http://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/resources/statecharts.pdf

    Also popular now: