React

Немного истории

Эволюция задач перед JS

Простые задачи

Любые изменения на странице чаще всего вызывали полную перезагрузку страницы

Более сложные задачи

Single Page Application  (SPA)

Веб-приложение или сайт, который загружает только одну страницу и все последующие запросы обрабатываются без полной перезагрузки страницы

В SPA сервер отвечает только данными, а все изменения верстки происходят из клиентского JavaScript

React

React — JavaScript-библиотека для создания пользовательских интерфейсов, которая позволяет довольно просто реализовывать и SPA в том числе

Концепции React

  1. Компонентный подход
  2. Эффективная абстракция над DOM
  3. Реактивный рендеринг

Как пользоваться?

Как пользоваться?


        // Импортируем необходимые библиотеки
        import React from 'react';
        import ReactDOM from 'react-dom';

        // Выбираем элемент, внутри которого мы хотим нарисовать форму
        const root = document.getElementById('root');

        // Создаем элемент формы
        const form = React.createElement(
            'div',
            { className: 'editor' },
            // Элемент состоит из 3ех частей: имя тега, атрибуты, дочерние элементы
            // HTML эквивалент: <input placeholder="Ключ заметки" />
            React.createElement('input', { placeholder: 'Ключ заметки' }),
            React.createElement('textarea', { placeholder: 'Текст заметки' }),
            React.createElement('button', null, 'Отменить'),
            // HTML эквивалент: <button>Сохранить</button>
            React.createElement('button', null, 'Сохранить')
        );

        // Отображаем форму на экране
        ReactDOM.render(form, root);
    

Как пользоваться?


        // Импортируем необходимые библиотеки
        import React from 'react';
        import ReactDOM from 'react-dom';

        // Выбираем элемент, внутри которого мы хотим нарисовать форму
        const root = document.getElementById('root');

        // Создаем элемент формы
        const form = React.createElement(
            'div',
            { className: 'editor' },
            // Элемент состоит из 3ех частей: имя тега, атрибуты, дочерние элементы
            // HTML эквивалент: <input placeholder="Ключ заметки" />
            React.createElement('input', { placeholder: 'Ключ заметки' }),
            React.createElement('textarea', { placeholder: 'Текст заметки' }),
            React.createElement('button', null, 'Отменить'),
            // HTML эквивалент: <button>Сохранить</button>
            React.createElement('button', null, 'Сохранить')
        );

        // Отображаем форму на экране
        ReactDOM.render(form, root);
    

Как пользоваться?


        // Импортируем необходимые библиотеки
        import React from 'react';
        import ReactDOM from 'react-dom';

        // Выбираем элемент, внутри которого мы хотим нарисовать форму
        const root = document.getElementById('root');

        // Создаем элемент формы
        const form = React.createElement(
            'div',
            { className: 'editor' },
            // Элемент состоит из 3ех частей: имя тега, атрибуты, дочерние элементы
            // HTML эквивалент: <input placeholder="Ключ заметки" />
            React.createElement('input', { placeholder: 'Ключ заметки' }),
            React.createElement('textarea', { placeholder: 'Текст заметки' }),
            React.createElement('button', null, 'Отменить'),
            // HTML эквивалент: <button>Сохранить</button>
            React.createElement('button', null, 'Сохранить')
        );

        // Отображаем форму на экране
        ReactDOM.render(form, root);
    

Как пользоваться?


        // Импортируем необходимые библиотеки
        import React from 'react';
        import ReactDOM from 'react-dom';

        // Выбираем элемент, внутри которого мы хотим нарисовать форму
        const root = document.getElementById('root');

        // Создаем элемент формы целиком
        const form = React.createElement(
            'div',
            { className: 'editor' },
            // Элемент состоит из 3ех частей: имя тега, атрибуты, дочерние элементы
            // HTML эквивалент: <input placeholder="Ключ заметки" />
            React.createElement('input', { placeholder: 'Ключ заметки' }),
            React.createElement('textarea', { placeholder: 'Текст заметки' }),
            React.createElement('button', null, 'Отменить'),
            // HTML эквивалент: <button>Сохранить</button>
            React.createElement('button', null, 'Сохранить')
        );

        // Отображаем форму на экране
        ReactDOM.render(form, root);
    

Как пользоваться?


        // Импортируем необходимые библиотеки
        import React from 'react';
        import ReactDOM from 'react-dom';

        // Выбираем элемент, внутри которого мы хотим нарисовать форму
        const root = document.getElementById('root');

        // Создаем элемент формы
        const form = React.createElement(
            'div',
            { className: 'editor' },
            // Элемент состоит из 3ех частей: имя тега, атрибуты, дочерние элементы
            // HTML эквивалент: <input placeholder="Ключ заметки" />
            React.createElement('input', { placeholder: 'Ключ заметки' }),
            React.createElement('textarea', { placeholder: 'Текст заметки' }),
            React.createElement('button', null, 'Отменить'),
            // HTML эквивалент: <button>Сохранить</button>
            React.createElement('button', null, 'Сохранить')
        );

        // Отображаем форму на экране
        ReactDOM.render(form, root);
    

Как пользоваться?


        // Импортируем необходимые библиотеки
        import React from 'react';
        import ReactDOM from 'react-dom';

        // Выбираем элемент, внутри которого мы хотим нарисовать форму
        const root = document.getElementById('root');

        // Создаем элемент формы
        const form = React.createElement(
            'div',
            { className: 'editor' },
            // Элемент состоит из 3ех частей: имя тега, атрибуты, дочерние элементы
            // HTML эквивалент: <input placeholder="Ключ заметки" />
            React.createElement('input', { placeholder: 'Ключ заметки' }),
            React.createElement('textarea', { placeholder: 'Текст заметки' }),
            React.createElement('button', null, 'Отменить'),
            // HTML эквивалент: <button>Сохранить</button>
            React.createElement('button', null, 'Сохранить')
        );

        // Отображаем форму на экране
        ReactDOM.render(form, root);
    

При изменениях React перерисовывает только нужные части интерфейса


            const element = 

Что такое JSX?

;

JSX — это расширение языка JavaScript.
Позволяет нагляднее создавать элементы.


        <select multiple>
            <option value="Пункт 1">Пункт 1</option>
            <option selected value="Пункт 2">Пункт 2</option>
        </select>
    

VS


        React.createElement(
            'select',
            { multiple: true },
            React.createElement(
                'option',
                { value: 'Пункт 1' },
                'Пункт 1'
            ),
            React.createElement(
                'option',
                { selected: true, value: 'Пункт 2' },
                'Пункт 2'
            )
        );
    

Отличия JSX от HTML

  • Все атрибуты именуются в camelCase
  • Все атрибуты должны быть закрыты
  • Имена пользовательских компонентов с заглавной буквы

Так как JSX превратится в валидный JavaScript, то в нем можно использовать любые JavaScript выражения

JavaScript выражения должны быть заключены в { }


        const user = { name: 'Студент' };

        const element = <div>Привет, {user.name}</div>;
    

Всё содержимое JSX экранируется


        const html = '<strong>Мир</strong>';

        const element = <div>Привет, {html}</div>;
    

Привет, <strong>Мир</strong>

Однако, есть выход


        const html = 'Привет, <strong>Мир</strong>!';

        const element = (
            <div dangerouslySetInnerHTML={{ __html: html }} />
        );
    

Привет, Мир!

Компоненты

Components

Компоненты предоставляют механизм разбиения интерфейса на небольшие независимые части, которые реализуются по отдельности

Пример


        const root = document.getElementById('root');

        ReactDOM.render(
            // Весь код, отвечающий за форму, можно вынести в компонент
            <div className="editor">
                <input placeholder="Ключ заметки" />
                <textarea placeholder="Текст заметки" />
                <button>Отменить</button>
                <button>Сохранить</button>
            </div>,
            root
        );
    

Пример


        const root = document.getElementById('root');

        ReactDOM.render(
            // Весь код, отвечающий за форму, можно вынести в компонент
            <div className="editor">
                <input placeholder="Ключ заметки" />
                <textarea placeholder="Текст заметки" />
                <button>Отменить</button>
                <button>Сохранить</button>
            </div>,
            root
        );
    

Почти всё в приложении, написанном на React, будет компонентом. Неважно что это — форма, компонент заметки или целая страница

Это становится возможным благодаря механизму композиции или объединения компонентов. Любой компонент в своей реализации может использовать любой другой компонент

Объединение компонентов

Объединение компонентов


        import Editor from './Editor';
        import Notes from './Notes';

        function NotesApp() {
            return (
                <div className="notes-app">
                    <Editor />
                    <Notes />
                </div>
            );
        }
    

Атрибуты

Props

Атрибуты (Props)

Любой компонент может получать на вход пропсы, подобно тому как функции принимают аргументы, а html-элементы атрибуты

Именно просы позволяют делать компоненты универсальными

Атрибуты (Props)


            function Notes() {
                return (
                    <div className="notes">
                        <Note name="Books" text="Books to read" />
                        <Note name="Music" text="Music to listen" />
                        <Note name="Films" text="Films to watch" />
                    </div>
                );
            }
        

Атрибуты (Props)

В компонентах пропсы доступны в объекте, который будет передан в первый аргумент функции-компонента

Атрибуты (Props)


        function Note(props) {
            return (
                <div className="note">
                    <h1>{props.name}</h1>
                    <p>{props.text}</p>
                </div>
            );
        }
    

Потомки

Children

Потомки (Children)

children - это зарезервированное название одного из пропсов

Этот механизм позволяет передавать дочерние элементы более наглядно и явно, подобно тому как это делается в HTML

Потомки (Children)


        function Notes() {
            return (
                <div className="notes">
                    <Note name="Books">
                        Books to read
                    </Note>

                    <Note name="Films">
                        <p>Films to read</p>
                        <button>Like</button>
                    </Note>

                    ...
                </div>
            );
        }
    

Потомки (Children)


        import React from 'react';

        function Note(props) {
            return (
                <div className="note">
                    <h1 className="note__title">
                        {props.name}
                    </h1>
                    <div className="note__content">
                        {props.children}
                    </div>
                </div>
            );
        }
    

Потомки (Children)


        import React from 'react';

        function Note(props) {
            return (
                <div className="note">
                    <h1 className="note__title">
                        {props.name}
                    </h1>
                    <div className="note__content">
                        {props.children}
                    </div>
                </div>
            );
        }
    

Условный рендеринг

Условный рендеринг

Так как JSX позволяет использовать произвольные JavaScript выражения, то проблемы необходимости рендеринга в зависимости от тех или иных условий решаются стандартными средствами языка

Условный рендеринг


        function Note(props) {
            return (
                <div className="note">
                    <h2 className="note__title">
                        {props.type === 'warning'
                            ? 'Warning'
                            : props.name
                        }
                    </h2>
                    ...
                </div>
            );
        }
    

Условный рендеринг


        function Note(props) {
            return (
                <div className="note">
                    <h2 className="note__title">
                        {props.type === 'warning'
                            ? 'Warning'
                            : props.name
                        }
                    </h2>
                    ...
                </div>
            );
        }
    

Работа со списками

Работа со списками

Проблемы со списками однотипных элементов решаются аналогично

Работа со списками


        function NotesList() {
            return (
                <div className="notes-list">
                    <Note name="Books" text="Books to read" />
                    <Note name="Films" text="Films to watch" />
                    <Note name="Music" text="Music to listen" />
                </div>
            );
        }
    

Работа со списками


        function NotesList({ notes }) {
            return (
                <div className="notes-list">
                    {notes.map(note => (
                        <Note name={note.name} text={note.text} />
                    ))}
                </div>
            );
        }
    

Однако, при рендеринге, нас ожидает ошибка в консоли браузера

Each child in an array or iterator should have a unique "key" prop.

При изменениях React перерисовывает только нужные части интерфейса

Работа со списками


        function NotesList({ notes }) {
            return (
                <div className="notes-list">
                    {notes.map(note => (
                        <Note
                            name={note.name}
                            text={note.text}
                            key={note.id} // Избавляемся от ошибки
                        />
                    ))}
                </div>
            );
        }
    

Нужно добавить уникальный пропс key к каждому из элементов списка

Работа со списками

Ключи (keys) помогают React определять, какие элементы были изменены, добавлены или удалены. Их необходимо указывать, чтобы React мог сопоставлять элементы массива с течением времени

Работа со списками

  • Key должен однозначно определять элемент списка и быть стабильным
  • Повторяющиеся key в рамках одного списка недопустимы
  • Использовать индекс элемента в массиве лучше только тогда, когда другого выхода нет
Почему использовать индекс в качестве key это плохо

Состояние компонента (State)

Состояние компонента (State)

Состояние компонента — это механизм, который позволяет сделать его «живым»

Основное отличие состояния от пропсов в том, что состояние доступно только самому компоненту

Состояние компонента (State)

Любой компонент автоматически отреагирует на все изменения собственного состояния и будет перерисован

Состояние компонента (State)


        import { useState } from 'react';

        function Note(props) {
            const [isReadMoreClicked, setIsReadMoreClicked] = useState(false);

            return (
                <div className="note">
                    <div className="note__name">Books</div>
                    <div className="note__text">Books to read</div>
                    {isReadMoreClicked
                        ? <div className="note__additional-text">Additional text</div>
                        : <button onClick={() => setIsReadMoreClicked(true)}>Read more</button>
                    }
                </div>
            );
        }
    

Состояние компонента (State)


        import { useState } from 'react';
        
        function Note(props) {
            const [isReadMoreClicked, setIsReadMoreClicked] = useState(false);

            return (
                <div className="note">
                    <div className="note__name">Books</div>
                    <div className="note__text">Books to read</div>
                    {isReadMoreClicked
                        ? <div className="note__additional-text">Additional text</div>
                        : <button onClick={() => setIsReadMoreClicked(true)}>Read more</button>
                    }
                </div>
            );
        }
    

Состояние компонента (State)


        import { useState } from 'react';

        function Note(props) {
            const [isReadMoreClicked, setIsReadMoreClicked] = useState(false);

            return (
                <div className="note">
                    <div className="note__name">Books</div>
                    <div className="note__text">Books to read</div>
                    {isReadMoreClicked
                        ? <div className="note__additional-text">Additional text</div>
                        : <button onClick={() => setIsReadMoreClicked(true)}>Read more</button>
                    }
                </div>
            );
        }
    

Состояние компонента (State)


        import { useState } from 'react';

        function Note(props) {
            const [isReadMoreClicked, setIsReadMoreClicked] = useState(false);

            return (
                <div className="note">  
                    <div className="note__name">Books</div>
                    <div className="note__text">Books to read</div>        
                    {isReadMoreClicked
                        ? <div className="note__additional-text">Additional text</div>
                        : <button onClick={() => setIsReadMoreClicked(true)}>Read more</button>
                    }
                </div>
            );
        }
    

Правила работы с состоянием

Для повышения производительности React может групировать обновления состояния


        /**
         * Неправильно, так как обновления будут сгруппированы и каждое
         * из них обратится к старому значению counter.
         * Как результат значение counter увеличится на 1, вместо 2
         */
        setCounter(counter + 1);
        setCounter(counter + 1);

        /**
         * Правильно. Передавая функцию в качестве аргумента, мы делаем
         * изменения атомарными. Каждое из них получит актуальное значение состояния
         */
        setCounter(counter => counter + 1);
        setCounter(counter => counter + 1); 
    

Лучше делать более атомарными (чтобы избежать ненужных перерисовок)

React-хуки

Хуки — специальные функции в React

  • Добавляют возможность управлять состоянием компонента
  • Предоставляют возможность выполнить некоторые интерактивные действия
  • Позволяют “прицепить“ выполнение некоторого кода к жизненному циклу компонента
  • Обязательно прочитайте документацию, она небольшая, но крутая (ссылка в конце)

Встроенные в React хуки (10 штук)

  • useState
  • useRef
  • useContext
  • useEffect
  • useMemo
  • useCallback
  • ...

useRef

    Добавляет переменную, которая хранит нереактивную ссылку на узел
    
                import React, { useRef } from 'react';
    
                const AnyComponent = () => {
                    const ref = useRef(null);
    
                    const onSubmit = () => {
                        ref.current.submit();
                    };
    
                    return (
                        <form ref={ref}>...</form>
                    )
                }
            

useMemo

  • Добавляет возможность кешировать какие-то значения
    
                    import React, { useMemo } from 'react';
    
                    const AnyComponent = (props) => {
                        const value = useMemo(
                            () => calculateAnyValue(props.a, state.b),
                            [props.a, state.b]
                        );
                        
                        ...
                    }
                
  • Позволяет явно сказать реакту, когда не требуется пересчитывать значение
  • Запускается во время рендера и не должен содержать side-effect

useMemo

  • Принимает 2 аргумента: функцию и массив зависимостей
    
                    import React, { useMemo } from 'react';
    
                    const AnyComponent = (props) => {
                        const value = useMemo(
                            () => calculateAnyValue(props.a, state.b),
                            [props.a, state.b]
                        );
                        ...
                    }
                

useMemo

  • Принимает 2 аргумента: функцию и массив зависимостей
    
                    import React, { useMemo } from 'react';
    
                    const AnyComponent = (props) => {
                        const value = useMemo(
                            () => calculateAnyValue(props.a, state.b),
                            [props.a, state.b]
                        );
                        ...
                    }
                

useMemo

  • Принимает 2 аргумента: функцию и массив зависимостей
    
                    import React, { useMemo } from 'react';
    
                    const AnyComponent = (props) => {
                        const value = useMemo(
                            () => calculateAnyValue(props.a, state.b),
                            [props.a, state.b]
                        );
                        ...
                    }
                

useCallback

  • Добавляет возможность мемоизировать обработчики
    
                    import React, { useCallback } from 'react';
    
                    const AnyComponent = ({ name }) => {
                        const value = useCallback(
                            e => sendParams(name, e.target.value),
                            [name]
                        );
                        ...
                    }
                

useCallback

  • Добавляет возможность мемоизировать обработчики
    
                    const value = useCallback(
                        e => sendParams(name, e.target.value),
                        [name]
                    );
                
  • Пересчитывается только при изменении зависимостей
  • Все, что используется внутри, нужно передать

useEffect

  • Декларирует способ выполнять что-то в различных стадиях жизненного цикла
    
                    import React, { useEffect } from 'react';
    
                    const AnyComponent = () => {
                        useEffect(
                            () => {
                                ...
                            },
                            []
                        );
                    }
                
  • Основное предназначение: выполнение side-effect

useEffect


        import React, { useEffect } from 'react';

        const AnyComponent = ({ pageName }) => {
            useEffect(
                () => {
                    fetchAnyData(pageName);
                }
            );
        }
    
Если не передать массив зависимостей, будет выполняться при каждом рендере

useEffect


        import React, { useEffect } from 'react';

        const AnyComponent = ({ pageName }) => {
            useEffect(
                () => {
                    fetchAnyData(pageName);
                },
                []
            );
        }
    
Если передать пустой массив зависимостей, выполнится после только первого рендера (componentDidMount)

useEffect


        const AnyComponent = () => {
            useEffect(
                () => {
                    const keyPressHandler = () => {
                        ...
                    };

                    document.addEventListener('keypress', keyPressHandler);

                    return () => document.removeEventListener('keypress', keyPressHandler);
                },
                []
            );
        }
    
Можно вернуть функцию, которая будет выполнена при размонтировании компонента

useEffect


        const AnyComponent = (props) => {
            useEffect(
                () => {
                    const keyPressHandler = () => {
                        ... // использует anyValue
                    };

                    document.addEventListener('keypress', keyPressHandler);

                    return () => document.removeEventListener('keypress', keyPressHandler);
                },
                [props.anyValue]
            );
        }
    
Зависимости работают точно так же, как и в useCallback, useMemo

Собственные хуки

  • Точно так же именуются с "use" в начале (так линтер и реакт понимают, что это хук)
  • Внутри себя используют реакт-хуки или другие кастомные хуки
  • В них можно либо инкапсулировать какую-то бизнес логику; тогда это будет хук, который предназначен для одного конкретного места
  • Либо положить какой-то частый паттерн использования реакт-хуков; тогда это можно вынести в хэлперы и переиспользовать

Напишем собственный хук


        const useToggle = initialValue => {
            const [value, setValue] = useState(initialValue);
            
            const toggle = useCallback(
                () => setValue(value => !value),
                []
            )
            
            return [value, toggle];
        }
    

Напишем собственный хук


        const useToggle = initialValue => {
            const [value, setValue] = useState(initialValue);
            
            const toggle = useCallback(
                () => setValue(value => !value),
                []
            )
            
            return [value, toggle];
        }
    
Возвращаем так же массивом:
  • Во-первых, это интуитивнее по аналогии с useState
  • Во-вторых, можно переименовать по назначению при использовании

Напишем собственный хук


        const useToggle = initialValue => {
            const [value, setValue] = useState(initialValue);
            
            const toggle = useCallback(
                () => setValue(value => !value),
                []
            )
            
            return [value, toggle];
        }
    
Используем вариант через колбэк, потому что ссылаемся на предыдущее значение

В собственные хуки можно засунуть много всего


        const useSearch({ items, searchKey }) => {
            const [searchValue, setSearchValue] = useState('');
        
            const onChangeSearch = debounce(setSearchValue, 300);
        
            const filteredItems = useMemo(
                () => items.filter(
                    item => item[searchKey].includes(searchValue)
                ),
                [items, searchValue, searchKey]
            );
        
            return { searchValue, onChangeSearch, filteredItems };
        }
    

В собственные хуки можно засунуть много всего


        const useSearch({ items, searchKey }) => {
            const [searchValue, setSearchValue] = useState('');
        
            const onChangeSearch = debounce(setSearchValue, 300);
        
            const filteredItems = useMemo(
                () => items.filter(
                    item => item[searchKey].includes(searchValue)
                ),
                [items, searchValue, searchKey]
            );
        
            return { searchValue, onChangeSearch, filteredItems };
        }
    
Здесь уже возвращаем объектом, потому что порядок неочевиден

В собственные хуки можно засунуть много всего


        const useSearch({ items, searchKey }) => {
            const [searchValue, setSearchValue] = useState('');
        
            const onChangeSearch = debounce(setSearchValue, 300);
        
            const filteredItems = useMemo(
                () => items.filter(
                    item => item[searchKey].includes(searchValue)
                ),
                [items, searchValue, searchKey]
            );
        
            return { searchValue, onChangeSearch, filteredItems };
        }
    
Обработчики для нашего input'а

В собственные хуки можно засунуть много всего


        const useSearch({ items, searchKey }) => {
            const [searchValue, setSearchValue] = useState('');
        
            const onChangeSearch = debounce(setSearchValue, 300);
        
            const filteredItems = useMemo(
                () => items.filter(
                    item => item[searchKey].includes(searchValue)
                ),
                [items, searchValue, searchKey]
            );
        
            return { searchValue, onChangeSearch, filteredItems };
        }
    
Фильтруем элементы по значению из input

Хуки

  • По соглашению именуются с "use" в начале
  • Полагаются на порядок своего выполнения (нельзя вызывать условно или в цикле)
  • Сильно сокращают количество кода и вложенность
  • Делают компонент более императивным
  • React имеет несколько встроенных хуков
  • Большинство популярных библиотек для реакта предоставляют наружу, в частности, хуки
  • При необходимости легко написать свой хук, спрятав в него некоторую часть логики

Вопросы?