An introduction to React we were missing

React is the world's most popular JavaScript library. But this library is not good because it is popular, but because it is popular because it is good. Most of the existing React introductory tutorials start with examples of how to use the library. But these guides don't say anything about why React is the right choice.



This approach has its strengths. If someone strives to get to practice right away while mastering React, they just need to look into the official documentation and get down to business . This material ( here , if you're interested, its video version) is written for those who want to find an answer to the following questions: “Why React? Why does React work this way? Why are React APIs designed the way they are? "











Why React?



Life becomes easier if the components are unaware of network communication, the business logic of the application, or its state. Such components, receiving the same input parameters, always form the same visual elements.



When the React library came along, it fundamentally changed the way JavaScript frameworks and libraries work. While other similar projects promoted the ideas of MVC, MVVM and the like, in React a different approach was taken. Namely, here the rendering of the visual component of the application was isolated from the presentation of the model. Thanks to React, a completely new architecture has appeared in the JavaScript frontend ecosystem - Flux.



Why did the React team do this? Why is this approach better than those that came before it, like the MVC architecture and the spaghetti code written in jQuery? If you are interested in these questions, you can watch this 2013 talk on JavaScript Application Development on Facebook.



In 2013, Facebook just completed some serious integration work into its chat platform. This new feature was built into almost every page of the project, chat influenced the usual scenarios of working with the platform. It was a complex application embedded in another application that had not been easy before. The Facebook team had to deal with nontrivial challenges, dealing with uncontrolled DOM mutation and the need to provide concurrent asynchronous user experience in the new environment.



For example, how do you know in advance what will be displayed on the screen in a situation where anything, at any time and for any reason, can access the DOM and make changes there? How to ensure that what the user sees is correctly plotted?



Using popular front-end tools that existed before React, nothing like that could be guaranteed. In early web applications, "race condition" in the DOM was one of the most common problems.



Lack of determinism = parallel computing + mutable state.



Martin Oderski




The main task of the React development team was to solve this problem. They dealt with it with two main innovative approaches:



  • Unidirectional data binding using the Flux architecture.
  • Component state immutability. Once the state of a component is set, it can no longer be changed. State changes do not affect the rendered components. Instead, such changes lead to the output of a new view with a new state.


The simplest way we found to structure and render components, from a conceptual point of view, was to simply aim for zero mutation at all.



Tom Ochchino, JSConfUS 2013




The React library was able to dramatically reduce the problem of uncontrolled mutations by using the Flux architecture. Instead of attaching event handlers to trigger DOM updates to an arbitrary number of arbitrary objects (models), the React library gave developers a single way to manage component state. This is the dispatching of actions that affect the data warehouse. When the state of the store changes, the system prompts the component to be rendered.





The Flux Architecture



When I am asked why I should pay attention to React, I give a simple answer: “The point is, we need deterministic rendering of views, and React makes this task much easier.”



Note that reading data from the DOM in order to implement some logic is an anti-pattern. Whoever does this goes against the purpose of using React. Instead, the data must be read from the store, and decisions based on that data must be made before the corresponding components are rendered.



If deterministic rendering of components were the only thing about React, then that alone would be a great innovation. But the React development team didn't stop there. This team presented the world with a library that has other interesting, unique features. And as the project evolved, React added even more useful things.



JSX



JSX is a JavaScript extension that allows you to declaratively create user interface components. JSX has the following notable features:





If, before the advent of JSX, it was necessary to declaratively describe interfaces, then it was impossible to do without using HTML templates. In those days, there was no generally accepted standard for creating such templates. Each framework used its own syntax. This syntax had to be learned by someone who, for example, needed to loop through some data, embed values ​​from variables into a text template, or decide which interface component to display and which not.



Nowadays, if you look at different frontend tools, you will find that you *ngForcannot do without special syntax, like a directive from Angular. But, since JSX can be called a superset of JavaScript, creating JSX markup can take advantage of existing JS capabilities.



For example, you can iterate over a set of elements using the method Array.prototype.map. You can use logical operators, organize conditional rendering using the ternary operator. You can use pure functions , you can construct strings using template literals . In general, all JavaScript features are available to those who describe interfaces in JSX. I think this is a huge advantage of React over other frameworks and libraries.



Here's a sample JSX code:



const ItemList = ({ items }) => (
  <ul>
    {items.map((item) => (
      <li key={item.id}>
        <div>{item.name}</div>
      </li>
    ))}
  </ul>
);


True, when working with JSX, you need to take into account some features that, at first, may seem unusual.



  • , , HTML. , class className. camelCase.
  • , , , JSX- key. . id, key.


React does not impose on the developer the only correct way to work with CSS. For example, you can pass a JavaScript object with styles to a component by writing it to a property style. With this approach, most of the familiar style names will be replaced with their camelCase equivalents. But the possibilities for working with styles are not limited to this. In practice, I simultaneously use different approaches to styling React applications. Which approach you choose depends on what you want to style. For example, I use global styles to style app themes and page layouts, and local styles to customize the look of a specific component.



Here are my favorite React styling features:



  • CSS-, . , . — , .
  • CSS- — CSS- . JavaScript-. CSS-, . Next.js, , .
  • The styled-jsx package , which allows you to declare styles right in your React component code. This is similar to using a tag <style>in HTML. The scope of such styles can be called "hyperlocal". The point is that styles only affect the elements to which they are applied and their children. When using Next.js, the styled-jsx package can be used without the need to connect and configure something yourself.


Synthetic events



React provides us with a cross-browser wrapper SyntheticEventsthat represents synthetic events and is designed to unify the work with DOM events. Synthetic events are useful for several reasons:



  1. , . .
  2. . , , , JavaScript HTML, . . , , React- .
  3. . . , , . , , , . . , . , JavaScript, .


Note that due to the use of the event pool, the properties of a synthetic event cannot be accessed from an asynchronous function. To implement such a scheme of work, you need to take data from the event object and write it to a variable accessible to the asynchronous function.



Component life cycle



The lifecycle concept of React components focuses on protecting the state of the component. The state of the component should not change while it is being displayed. This is achieved due to the following scheme of work: the component is in a certain state and rendered. Then, thanks to the life cycle events, it becomes possible to apply effects to it, you can influence its state, work with events.



Understanding the life cycle of React components is extremely important in order to develop interfaces and at the same time not fight with React, but use this library as intended by its developers. "Battles" with React, such as incorrectly changing the state of components or reading data from the DOM, negate the strengths of this library.



In React, starting with version 0.14, there was a class-based component description syntax that allows you to handle component lifecycle events. There are three critical stages in the lifecycle of React components: Mount, Update, and Unmount.





Component lifecycle



The Update stage can be divided into three parts: Render (rendering), Precommit (preparing for making changes to the DOM tree), Commit (making changes to the DOM tree).





The structure of the Update stage



Let us dwell on these stages of the component life cycle in more detail:



  • Render — . render() , . , JSX.
  • Precommit — DOM, getSnapShotBeforeUpdate. , , .
  • Commit - During this phase of the component's lifecycle, React updates the DOM and refs . Here you can use a method componentDidUpdateor hook useEffect. This is where you can perform effects, schedule updates, use the DOM, and other similar tasks.


Dan Abramov has prepared an excellent diagram that illustrates how the life cycle mechanisms of components work.





The Life Cycle of React Components



I think that representing components as long-lived classes is not the best React mental model. Remember that the state of React components must not mutate. The obsolete state must be replaced with a new one. Each such replacement causes the component to be re-rendered. This gives React what is arguably its most important and most valuable feature: support for a deterministic approach to rendering component visuals.



This behavior is best thought of as this: each time the component renders, the library calls a deterministic function that returns JSX. This function should not invoke its own side effects on its own. But she, if she needs it, can pass requests to React to perform such effects.



In other words, it makes sense to think of most React components as pure functions that take input parameters and return JSX. Pure functions have the following features:



  • When given the same input, they always return the same output (they are deterministic).
  • They have no side effects (that is, they do not work with network resources, do not output anything to the console, do not write anything to, localStorageand so on).


Note that if side effects are needed for a component to work, you can execute them by using useEffector referring to the action creator passed to the component through props and allowing side effects to be handled outside the component .



React Hooks



React 16.8 introduces a new concept called React hooks. These are functions that allow you to connect to component lifecycle events without using class syntax and without relying on component lifecycle methods. As a result, it became possible to create components not in the form of classes, but in the form of functions.



Calling a hook, in general, means a side effect - one that allows the component to work with its state and with the I / O subsystem. A side effect is any change in state that is visible outside of the function, except for a change in the value returned by the function.



Hook useEffectallows you to queue side effects for later execution. They will be called at the appropriate time in the component's lifecycle. This time can come immediately after the component is mounted (for example, when the componentDidMount lifecycle method is called ), during the Commit phase ( componentDidUpdate method ), just before the component is unmounted ( componentWillUnmount ).



Notice that there are three component lifecycle methods associated with one hook? The point here is that hooks allow you to combine related logic, and not "lay out" it, as it was before them, according to different methods of the component life cycle.



Many components need to perform some action while they are mounted, something needs to be updated every time the component is redrawn, and resources must be freed immediately before unmounting the component to prevent memory leaks. Thanks to the use, useEffectall these tasks can be solved in one function, without dividing their solution into 3 different methods, without mixing their code with the code of other tasks that are not related to them, but also need these methods.



Here's what React hooks give us:



  • They allow you to create components that are represented as functions rather than as classes.
  • They help you organize your code better.
  • They make it easier to share the same logic across different components.
  • New hooks can be created by composing existing hooks (calling them from other hooks).


In general, we recommend using functional components and hooks rather than class-based components. Functional components are usually more compact than class-based components. Their code is better organized, more readable, more reusable, and easier to test.



Container Components and Presentation Components



In an effort to improve the modularity of components and their reusability, I focus on developing two types of components:



  • Container components are components that are connected to data sources and can have side effects.
  • Presentation beans are, for the most part, pure beans that, given the same props and context, always return the same JSX.


Pure components should not be confused with the base class React.PureComponent , which is so named because it is unsafe to use to create components that are not pure.



▍Presentation Components



Consider the features of presentation components:



  • They do not interact with network resources.
  • They do not save data to localStorageor load from there.
  • They do not give out some unpredictable data.
  • They do not refer directly to the current system time (for example, by calling a method Date.now()).
  • They do not interact directly with the application state store.
  • - , , , .


It is because of the last item on this list that I mentioned, when talking about presentation components, that these are mostly pure components. These components read their state from the global React state. Therefore, like the hooks useStateand useReducerprovide them with a certain implicit data (ie - data that is not described in the signature function) that, from a technical point of view, can not call such components are "clean". If you need them to be truly clean, you can delegate all the tasks of managing the state to the container component, but I suppose that you should not do this, at least until the correct operation of the component can be verified using modular tests.



Best the enemy of the good.



Voltaire




▍Container Components



Container components are those components that are responsible for managing state, performing I / O operations, or any other task that might be a side effect. They don't have to render any markup on their own. Instead, they delegate the rendering task to presentation components, and they themselves serve as a wrapper for such components. Typically, a container component in a React + Redux application simply calls mapStateToProps()and mapDispatchToProps()then passes the appropriate data to the presentation components. Containers can also be used for some general tasks, which we will discuss below.



Higher-order components



A Higher Order Component (HOC) is a component that takes other components and returns a new component that implements new functionality based on the original components.



Higher-order components function by wrapping some components with others. A wrapper component can implement some logic and create DOM elements. It may or may not pass additional props to the wrapped component.



Unlike React hooks and render props, higher-order components lend themselves to composition using the standard approach to function composition. This allows you to declaratively describe the results of the composition of capabilities intended for use in different places of the application. At the same time, ready-made components should not be aware of the existence of certain possibilities. Here's an example HOC from EricElliottJS.com :



import { compose } from 'lodash/fp';
import withFeatures from './with-features';
import withEnv from './with-env';
import withLoader from './with-loader';
import withCoupon from './with-coupon';
import withLayout from './with-layout';
import withAuth from './with-auth';
import { withRouter } from 'next/router';
import withMagicLink from '../features/ethereum-authentication/with-magic-link';

export default compose(
  withEnv,
  withAuth,
  withLoader,
  withLayout({ showFooter: true }),
  withFeatures,
  withRouter,
  withCoupon,
  withMagicLink,
);


Shown here is a mixture of many features shared across all pages on the site. Namely, it withEnvreads settings from environment variables, withAuthimplements the GitHub authentication mechanism, withLoadershows an animation while loading user data, , withLayout({ showFooter: true })displays a standard layout with a footer, withFeatureshows settings, withRouterloads the router, withCouponis responsible for working with coupons, and withMagicLingsupports user authentication without a password using Magic .



By the way, given that password authentication is outdated and a dangerous practice, it is worth using other methods of user authentication these days.



Almost all of the pages on the aforementioned site take advantage of all these features. Considering that they are composed by means of a higher-order component, you can include them all in a container component with just one line of code. For example, here's how it would look for the tutorial page:



import LessonPage from '../features/lesson-pages/lesson-page.js';
import pageHOC from '../hocs/page-hoc.js';
export default pageHOC(LessonPage);


These higher-order components have an alternative, but this is a dubious construction called the "pyramid of doom" and is best not used. This is how it looks:



import FeatureProvider from '../providers/feature-provider';
import EnvProvider from '../providers/env-provider';
import LoaderProvider from '../providers/loader-provider';
import CouponProvider from '../providers/coupon-provider';
import LayoutProvider from '../providers/layout-provider';
import AuthProvider from '../providers/auth-provider';
import RouterProvider from '../providers/RouterProvider';
import MagicLinkProvider from '../providers/magic-link-provider';
import PageComponent from './page-container';

const WrappedComponent = (...props) => (
  <EnvProvider { ...props }>
    <AuthProvider>
      <LoaderProvider>
        <LayoutProvider showFooter={ true }>
          <FeatureProvider>
            <RouterProvider>
              <CouponProvider>
                <MagicLinkProvider>
                  <YourPageComponent />
                </MagicLinkProvider>
              </CouponProvider>
            </RouterProvider>
          </FeatureProvider>
        </LayoutProvider>
      </LoaderProvider>
    </AuthProvider>
  </EnvProvider>
);


And this will have to be repeated on every page. And if something needs to be changed in this structure, then changes will have to be made to it wherever it is present. I think the disadvantages of this approach are quite obvious.



Using composition to solve general problems is one of the best ways to reduce the complexity of your application code. Composition is so important that I even wrote a book about it .



Outcome



  • Why React? React gives us a deterministic rendering of visual representations of components based on unidirectional data binding and component immutable state.
  • JSX allows us to easily declaratively describe interfaces in JavaScript code.
  • - .
  • . , . , DOM DOM.
  • React , . , , .
  • - . - .
  • , . ( , ).


?



In this React article, we've covered a lot of functional programming concepts. If you are striving for a deep understanding of the principles of React application development, it will be useful for you to brush up on your knowledge about pure functions , about immutability , about currying and partial application of functions, about function composition. You can find related materials at EricElliottJS.com .



I recommend using React in conjunction with Redux , Redux-Saga, and RITEway . Redux is recommended for use with Autodux and Immer... To organize complex state schemes, you can try using Redux-DSM .



When you get the basics down and are ready to build real React apps, take a look at Next.js and Vercel . These tools will help automate the configuration of the project build system and the CI / CD pipeline, with their help you can prepare the project for an optimized deployment on the server. They have the same effect as an entire DevOps team, but they're completely free to use.



What auxiliary tools do you use when developing React apps?










All Articles