Больше 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:
- Пособие по React: всестороннее изучение
- 25 понятных туториалов для изучения React Native
- Основные концепции React, о которых стоит знать