Больше JS, чем React: как фреймворк использует возможности языка

React практически не добавляет к нативному JS внешней абстракции. Поэтому разработчику необходимо хорошо разбираться в основах языка.

В этой статье разберем самые нужные для React концепции JavaScript.

Вхождение в React

Первое что мы видим после создания проекта с create-react-app – это классы компонентов:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  getGreeting() { 
    return 'Добро пожаловать в React!'; 
  } 
  render() {
    return (
      <div className="App">
        <img src={logo} className="App-logo" alt="logo" />
        <h1 className="App-title">{this.getGreeting()}</h1>
      </div>
    );
  }
}

export default App;

Здесь много концепций, которые не связаны с фреймворком напрямую: ключевое слово class, наследование, методы, интерполяция, импорт и экспорт. Кажется, без понимания нативного JavaScript в React делать нечего. Начнем с самых простых вещей и увидим, что в React больше JS, чем мы думали.

Стрелочные функции

Стрелочные функции – одно из недавних приобретений JavaScript. Они делают код намного короче и проще.

// ES6 стрелочная функция с телом
const getGreeting = () => {
  return 'Welcome to JavaScript';
}

// ES6 стрелочная функция без тела
const getGreeting = () => 'Welcome to JavaScript';

Строковые литералы

Мы привыкли к такому синтаксису конкатенации строк в JS:

const framework = 'React';
const greeting = 'Добро пожаловать в ' + framework + '!';

А шаблоны строк позволяют использовать интерполяцию с помощью обратных кавычек и нотации ${}:

const framework = 'React'; 
const greeting = `Добро пожаловать в ${framework}!`;

Их можно использовать также для реализации многострочности:

const framework = 'React'; 
const greeting = `
    Добро пожаловать 
    в 
    ${framework}!
`;

Компоненты

React и JS-классы

Классы появились в языке относительно недавно, заменив собой цепочки прототипов. Один из способов определения компонентов в React основан именно на них.

class Developer {
  constructor(firstname, lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }

  getName() {
    return this.firstname + ' ' + this.lastname;
  }
}

var me = new Developer('Robin', 'Wieruch');

console.log(me.getName());

Оператор new вызывает конструктор класса, который создает новый объект, обладающий некоторыми свойствами и методами.

Оператор extends позволяет одному классу наследовать от другого. Класс-наследник может расширять функциональность родителя собственными методами.

class ReactDeveloper extends Developer {
  getJob() {
    return 'React Developer';
  }
}

var me = new ReactDeveloper('Robin', 'Wieruch');

console.log(me.getName());
console.log(me.getJob());

Все компоненты React наследуют об базового класса Component, импортированного из пакета React.

import React, { Component } from 'react';

class App extends Component {
  render() {
    // ...
  }
}

export default App;

Метод render является обязательным, так как класс Component вызывает его для отображения чего-либо в браузере. Без расширения базового компонента не получится использовать методы жизненного цикла, например, componentDidMount, а также метод setState, управляющий локальным состоянием.

Таким образом, использование JS-классов позволяет расширить существующий функционал базового компонента и получить доступ к API React.

Сокращенный синтаксис

Если компонент имеет собственные методы, их необходимо привязать в конструкторе. Там же создается объект состояния:

class Counter extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0,
    };

    this.onIncrement = this.onIncrement.bind(this);
    this.onDecrement = this.onDecrement.bind(this);
  }

  onIncrement() {
    // ...
  }

  onDecrement() {
    // ...
  }

  render() {
    // ...
  }
}

Если таких компонентов много, то привязка методов и вообще создание конструктора становится весьма утомительной задачей. К счастью, существует сокращенный синтаксис:

class Counter extends Component {
  state = {
    counter: 0,
  };

  onIncrement = () => { ... }

  onDecrement = () => { ... }

  render() {
    // ...
  }
}

Использование стрелочных функций JavaScript позволяет не биндить методы. А если конструктор не использует props, его вообще можно убрать, определив состояние непосредственно как свойство класса. (Свойства класса еще не входят в стандарт JS.)

Компоненты-функции

Некоторые компоненты должны получать входные данные и просто возвращать отображаемые HTML-элементы без управления состоянием. Такие компоненты называются функциональными, они менее сложны и более удобны.

function Greeting(props) {
  return <h1>{props.greeting}</h1>;
}

Для их создания можно использовать стрелочные функции JS.

const Greeting = (props) => <h1>{props.greeting}</h1>

Выражения импорта и экспорта

Любой create-react-app проект начинается с инструкций import и export:

import React, { Component } from 'react'; 
import './App.css';

class App extends Component {
  render() {
    // ...
  }
}

export default App;

Переменные, компоненты или функции, объявленные в одном файле:

const firstname = 'Robin';
const lastname = 'Wieruch';
export { firstname, lastname };

можно импортировать в другой:

import { firstname, lastname } from './file1.js';

Весь экспорт можно получить в виде единого объекта:

import * as person from './file1.js';

Чтобы избежать совпадений имен при импорте используются псевдонимы.

import { firstname as username } from './file1.js';

Есть также вариант импорта/экспорта по умолчанию. Он используется, если экспортируется всего одна переменная или если требуется выделить главную функциональность модуля.

const robin = {
  firstname: 'Robin',
  lastname: 'Wieruch',
};
export default robin;

Импортировать дефолтный объект можно без фигурных скобок и с другим именем.

import developer from './file1.js';
console.log(developer);  // { firstname: 'Robin', lastname: 'Wieruch' }

Можно сочетать дефолтный и именованный экспорт:

const firstname = 'Robin';
const lastname = 'Wieruch';

const person = {
  firstname,
  lastname,
};

export {
  firstname,
  lastname,
};

export default person;

и импорт:

import developer, { firstname, lastname } from './file1.js';

Это основные функции ES6 модулей. Они помогают организовать код, удобно поддерживать его, тестировать и повторно использовать.

JavaScript detected

Деструктуризация и spread-оператор

Если требуется получить доступ к большому количеству свойств из state или props, можно использовать новую возможность JavaScript – деструктуризацию.

// без деструктуризации
const users = this.state.users;
const counter = this.state.counter;

// деструктуризация
const { users, counter } = this.state;

Входящий параметр можно деструктурировать прямо в сигнатуре функции.

function Greeting({ greeting }) {
  return <h1>{greeting}</h1>;
}

Для сохранения незадействованных при деструктурировании свойств объекта существует параметр rest.

const { users, ...rest } = this.state;

С его помощью можно передать неиспользованные данные дальше по цепочке компонентов, используя spread-оператор.

Map, Reduce и Filter

Вывести в JSX-коде одну переменную или свойство объекта нетрудно, нужно просто обернуть их в фигурные скобки. Но как вывести список? В React не существует какого-то специального API или атрибута для рендеринга коллекции элементов, нужно использовать нативные возможности языка.

class App extends Component {
  render() {
    var users = [
      { name: 'Robin' },
      { name: 'Markus' },
    ];

    return (
      <ul>
        {users.map(function (user) {
          return <li>{user.name}</li>;
        })}
      </ul>
    );
  }
}

Можно еще сократить код, используя стрелочные функции:

return (
  <ul>
    {users.map(user => <li>{user.name}</li>)}
  </ul>
);

JS-метод map просто проходит по массиву и возвращает JSX-код для каждого элемента. Иногда вместо map следует применять filter или reduce.

Тернарный оператор

В JSX нельзя напрямую использовать конструкцию if...else, но можно воспользоваться тернарным оператором JS:

import React, { Component } from 'react';

class App extends Component {
  render() {
    const users = [
      { name: 'Robin' },
      { name: 'Markus' },
    ];

    const showUsers = false;

    return (
      <div>
        {
          showUsers ? (
            <ul>
              {users.map(user => <li>{user.name}</li>)}
            </ul>
          ) : (
            null
          )
        }
      </div>
    );
  }
}

export default App;

Вы можете ознакомиться с другими методами условного рендеринга в React.

Функции высшего порядка

Рассмотрим пример фильтрации списка пользователей на основе значения поля ввода.

import React, { Component } from 'react';

class App extends Component {
  state = {
    query: '', // значение поля
  };

  onChange = event => {
    this.setState({ query: event.target.value });
  }

  render() {
    const users = [
      { name: 'Robin' },
      { name: 'Markus' },
    ];

    return (
      <div>
        <ul>
          {users
            .filter(user => this.state.query === user.name)   // фильтрация
            .map(user => <li>{user.name}</li>)                // вывод
          }
        </ul>

        <input
          type="text"
          onChange={this.onChange}
        />
      </div>
    );
  }
}

export default App;

Выделим фильтрующую функцию из компонента, чтобы было удобнее ее тестировать и передадим в метод filter как параметр.

function doFilter(user) {
  return query === user.name;
}
{users
    .filter(doFilter)
    .map(user => <li>{user.name}</li>)
}

Но doFilter ничего не знает о свойстве query. Чтобы все заработало, мы превратим ее в функцию высшего порядка.

function doFilter(query) {
  return function (user) {
    return query === user.name;
  }
}
{users
  .filter(doFilter(this.state.query))
  .map(user => <li>{user.name}</li>)
}

Стрелочные функции сделают запись более краткой.

const doFilter = query => user =>
  query === user.name;

Теперь doFilter можно экспортировать из файла и тестировать изолированно от компонента.

Поняв этот принцип, вы без труда разберетесь в React-компонентах высшего порядка.

Больше JavaScript, чем React

Фреймворк React имеет тонкую прослойку аутентичного API, но все остальное – это чистый JavaScript. Давайте убедимся в этом, отрефакторив компонент высшего порядка, а заодно повторим изученное.

function withLoading(Component) {
  return class WithLoading extends {
    render() {
      const { isLoading, ...props } = this.props;

      if (isLoading) {
        return <p>Loading</p>;
      }

      return <Component { ...props } />;
    }
  }
  };
}

Это компонент-обертка для отображения индикатора загрузки. Вы уже можете видеть деструктурирование и работу spread-оператора, который позволяет передать все неиспользованные свойства из объекта props.

Первым шагом рефакторинга будет превращение компонента в функциональный:

function withLoading(Component) {
  return function ({ isLoading, ...props }) {
    if (isLoading) {
      return <p>Loading</p>;
    }

    return <Component { ...props } />;
  };
}

Переносим деструктуризацию в сигнатуру параметра и используем стрелочную функцию для краткости:

const withLoading = Component => ({ isLoading, ...props }) => {
  // ...
}

Добавим тернарный оператор:

const withLoading = Component => ({ isLoading, ...props }) =>
  isLoading
    ? <p>Loading</p>
    : <Component { ...props } />

Очевидно, что методы, которые использует компонент высшего порядка, принадлежат не фреймворку, а языку. Таким образом, React-приложения базируются на  возможностях JS. Расширить их функциональность можно с помощью различных внешних библиотек.

Перевод статьи JavaScript fundamentals before learning React

Другие статьи по изучению React:

 

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

eFusion
01 марта 2020

ТОП-15 книг по JavaScript: от новичка до профессионала

В этом посте мы собрали переведённые на русский язык книги по JavaScript – ...
admin
10 июня 2018

Лайфхак: в какой последовательности изучать JavaScript

Огромный инструментарий JS и тонны материалов по нему. С чего начать? Расск...