React 2

Лена Рашкован

Хуки (hooks) —

специальные функции, которые позволяют функциональным компонентам «подцепиться» к возможностям React.

Стандартные хуки

useState
useRef
useContext
useEffect
useMemo
useCallback
...

useState

Добавляет состояние


          import React, { useState } from 'react';

          const Example = () => {
              const [state, setState] = useState(initialState);
          }
          

useState

Сохраняет состояние между рендерами

Изменение состояния вызывает ререндер

В отличие от this.setState
состояние при обновлении заменяется целиком

useRef

Добавляет контейнер { current: ... }


          import React, { useRef } from 'react';

          const Example = () => {
              const refContainer = useRef(null);
              // refContainer.current = null;

              return <div ref={refContainer} />;
          }
          

useRef

Отдаёт один и тот же объект между рендерами

Изменение .current не вызывает ререндер

Может хранить ссылку на DOM-узел
или любое мутируемое значение

useContext

Добавляет контекст


          import React, { useContext } from 'react';

          const Example = () => {
              const value = useContext(MyContext);
          }
          

useContext

Добавляет возможность читать контекст
и подписываться на его изменения

Изменение значения контекста всегда вызывает ререндер

Мемоизация (memoization) —

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

useMemo


          import React, { useMemo } from 'react';

          const Example = ({ a, b }) => {
              const memoizedValue = useMemo(
                  () => computeExpensiveValue(a, b), // фабрика
                  [a, b]
              );
          }
          

useMemo

Значение перевычисляется
только при изменении зависимостей

Полезно для оптимизации

Фабрика запускается во время рендера,
в ней не должно быть сайд-эффектов

useCallback


          import React, { useCallback } from 'react';

          const Example = ({ a, b }) => {
              const memoizedCallback = useCallback(
                  () => {
                      doSomething(a, b);
                  },
                  [a, b],
              );
          }
          

useCallback


          import React, { useCallback } from 'react';

          const Example = () => {
              const handleChange = useCallback(
                  e => {
                      console.log(e.target.value);
                  },
                  [],
              );
          }
          

useCallback

Коллбэк сохраняется между рендерами

Функция пересоздаётся при изменении зависимостей

Оборачивание коллбэков в хук нужно
для передачи их дочерним компонентам
для предотвращения ненужных рендеров

useEffect

Добавляет выполнение сайд-эффектов

componentDidMount + componentDidUpdate + componentWillUnmount

useEffect


          import React, { useState, useEffect } from 'react';

          const Example = () => {
              const [count, setCount] = useState(0);

              // Эффект запускается после каждого рендера
              useEffect(() => {
                  document.title = `Вы нажали ${count} раз`;
              });
          }
          

useEffect


          import React, { useState, useEffect } from 'react';

          const Example = () => {
              const [count, setCount] = useState(0);

              // Эффект запускается только после первого рендера
              useEffect(() => {
                  document.title = `Вы нажали ${count} раз`;
              }, []);
          }
          

useEffect


          import React, { useState, useEffect } from 'react';

          const Example = ({ friend }) => {
              const [isOnline, setIsOnline] = useState(null);

              useEffect(() => {
                  function handleStatusChange(status) {
                      setIsOnline(status.isOnline);
                  }

                  subscribeToFriendStatus(friend.id, handleStatusChange);

                  // Сброс эффекта
                  return () => {
                      unsubscribeFromFriendStatus(friend.id, handleStatusChange);
                  };
              });
          }
          

useEffect


          import React, { useState, useEffect } from 'react';

          const Example = ({ friend }) => {
              const [isOnline, setIsOnline] = useState(null);

              useEffect(() => {
                  function handleStatusChange(status) {
                      setIsOnline(status.isOnline);
                  }

                  subscribeToFriendStatus(friend.id, handleStatusChange);

                  // Сброс эффекта
                  return () => {
                      unsubscribeFromFriendStatus(friend.id, handleStatusChange);
                  };
              });
          }

useEffect


          import React, { useState, useEffect } from 'react';

          const Example = ({ friend }) => {
              const [isOnline, setIsOnline] = useState(null);
              
              useEffect(() => {
                  function handleStatusChange(status) {
                      setIsOnline(status.isOnline);
                  }

                  subscribeToFriendStatus(friend.id, handleStatusChange);

                  // Сброс эффекта
                  return () => {
                      unsubscribeFromFriendStatus(friend.id, handleStatusChange);
                  };
              });
          }
          

useEffect


          import React, { useState, useEffect } from 'react';

          const Example = ({ friend }) => {
              const [isOnline, setIsOnline] = useState(null);

              useEffect(() => {
                  function handleStatusChange(status) {
                      setIsOnline(status.isOnline);
                  }

                  subscribeToFriendStatus(friend.id, handleStatusChange);

                  // Сброс эффекта
                  return () => {
                      unsubscribeFromFriendStatus(friend.id, handleStatusChange);
                  };
              });
          }
          

useEffect


          import React, { useState, useEffect } from 'react';

          const Example = ({ friend }) => {
              const [isOnline, setIsOnline] = useState(null);

              useEffect(() => {
                  function handleStatusChange(status) {
                      setIsOnline(status.isOnline);
                  }

                  subscribeToFriendStatus(friend.id, handleStatusChange);

                  // Сброс эффекта
                  return () => {
                      unsubscribeFromFriendStatus(friend.id, handleStatusChange);
                  };
              });
          }
          

useEffect


          import React, { useState, useEffect } from 'react';

          const Example = ({ friend }) => {
              const [isOnline, setIsOnline] = useState(null);

              useEffect(() => {
                  function handleStatusChange(status) {
                      setIsOnline(status.isOnline);
                  }

                  subscribeToFriendStatus(friend.id, handleStatusChange);


                  return () => {
                      unsubscribeFromFriendStatus(friend.id, handleStatusChange);
                  };
              }, [friend.id]); // Перезапускать эффект только если изменился friend.id
          }
          

useEffect

Эффект и его очистка запускаются
после каждого рендера (есть оптимизации)

Запуск отложенный:
после текущей отрисовки, но до следующей

Для синхронного выполнения есть useLayoutEffect

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


        import React, { useState, useEffect } from 'react';

        function useFriendStatus(friendID) {
          const [isOnline, setIsOnline] = useState(null);

          useEffect(() => {
              function handleStatusChange(status) {
                  setIsOnline(status.isOnline);
              }

              subscribeToFriendStatus(friendID, handleStatusChange);

              return () => {
                  unsubscribeFromFriendStatus(friendID, handleStatusChange);
              };
          });

          return isOnline;
        }
          

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


        import React, { useState, useEffect } from 'react';

        function useFriendStatus(friendID) {
          ...

          return isOnline;
        }

        const isFriendOnline = useFriendStatus('some id');
          

Хуки

Название начинается с use

Можно вызывать
из компонентов в функциональном стиле
или других хуков

Можно вызывать только на верхнем уровне
(нельзя вызывать условно или в цикле)

Single Page Application  (SPA)

Статическая маршрутизация

Маршруты объявлены заранее

Express.js


        import express from 'express';

        const app = express();

        app.get('/notes', (req, res) => res.render('notes'));
        app.get('/notes/:id', (req, res) => res.render('note'));

        app.all('*', (req, res) => res.sendStatus(404));

        app.listen(8080);
    

Динамическая маршрутизация

Маршрутизация происходит во время рендера

React Router

v5

React Router


        $ npm install react-router-dom @types/react-router-dom
    

        import { BrowserRouter, Route } from 'react-router-dom';

        const NotesApp = () => (
            <BrowserRouter>
                <>
                    <Route path="/">
                        <HomePage />
                    </Route>
                    <Route path="/notes">
                        <NotesPage />
                    </Route>
                </>
            </BrowserRouter>
        );
    

React Router


        $ npm install react-router-dom @types/react-router-dom
    

        import { BrowserRouter, Route } from 'react-router-dom';

        const NotesApp = () => (
            <BrowserRouter>
                <>
                    <Route exact path="/">
                        <HomePage />
                    </Route>
                    <Route exact path="/notes">
                        <NotesPage />
                    </Route>
                </>
            </BrowserRouter>
        );
    

React Router


        import { BrowserRouter, Route, Link } from 'react-router-dom';

        const NotesApp = () => (
            <BrowserRouter>
                <>
                    <nav>
                        <Link to="/">Home</Link>
                        <Link to="/notes">Notes</Link>
                    </nav>
                    <Route exact path="/">
                        <HomePage />
                    </Route>
                    <Route exact path="/notes">
                        <NotesPage />
                    </Route>
                </>
            </BrowserRouter>
        );
    

React Router


        import { BrowserRouter, Route, Link } from 'react-router-dom';

        const NotesApp = () => (
            <BrowserRouter>
                <>
                    <nav>
                        <Link to="/">Home</Link>
                        <Link to="/notes">Notes</Link>
                    </nav>
                    <Route exact path="/">
                        <HomePage />
                    </Route>
                    <Route exact path="/notes">
                        <NotesPage />
                    </Route>
                </>
            </BrowserRouter>
        );
    

React Router


        import { BrowserRouter, Route, NavLink } from 'react-router-dom';

        const NotesApp = () => (
            <BrowserRouter>
                <>
                    <nav>
                        <NavLink activeClassName="active" to="/">Home</NavLink>
                        <NavLink activeClassName="active" to="/notes">Notes</NavLink>
                    </nav>
                    <Route exact path="/">
                        <HomePage />
                    </Route>
                    <Route exact path="/notes">
                        <NotesPage />
                    </Route>
                </>
            </BrowserRouter>
        );
    

React Router


        import { BrowserRouter, Route } from 'react-router-dom';

        const NotesApp = () => (
            <BrowserRouter>
                <>
                    {/* other routes */}
                    <Route exact path="/notes/:id">
                        <SingleNotePage />
                    </Route>
                </>
            </BrowserRouter>
        );
    

React Router


        import { useParams } from 'react-router-dom';

        type TParams = { id: string; };

        const SingleNotePage = () => {
            const { id } = useParams<TParams>()

            return <h1>Note id: {id}</h1>;
        }
    

React Router


        import { useParams } from 'react-router-dom';

        type TParams = { id: string; };

        const SingleNotePage = () => {
            const { id } = useParams<TParams>()

            return <h1>Note id: {id}</h1>;
        }
    

React Router


        import { useParams } from 'react-router-dom';

        type TParams = { id: string; };

        const SingleNotePage = () => {
            const { id } = useParams<TParams>()

            return <h1>Note id: {id}</h1>;
        }
    

React Router


        import { useParams } from 'react-router-dom';

        type TParams = { id: string; };

        const SingleNotePage = () => {
            const { id } = useParams<TParams>()

            return <h1>Note id: {id}</h1>;
        }
    

React Router


        import { BrowserRouter, Route } from 'react-router-dom';

        const NotesApp = () => (
            <BrowserRouter>
                <>
                    <Route exact path="/">
                        <HomePage />
                    </Route>
                    <Route exact path="/notes">
                        <NotesPage />
                    </Route>
                    <Route exact path="/notes/:id">
                        <SingleNotePage />
                    </Route>
                    <Route>No Match (404)</Route>
                </>
            </BrowserRouter>
        );
    

React Router


        import { BrowserRouter, Route, Switch } from 'react-router-dom';

        const NotesApp = () => (
            <BrowserRouter>
                <Switch>
                    <Route exact path="/">
                        <HomePage />
                    </Route>
                    <Route exact path="/notes">
                        <NotesPage />
                    </Route>
                    <Route exact path="/notes/:id">
                        <SingleNotePage />
                    </Route>
                    <Route>No Match (404)</Route>
                </Switch>
            </BrowserRouter>
        );
    

Работа с API

Работа с API


        class Notes extends Component {
            ...

            render() {
                if (this.state.loading) {
                    return 'Загрузка...';
                }

                if (this.state.error) {
                    return 'Что-то пошло не так';
                }

                return (
                    <div className="notes">
                        {this.state.notes.map(note => (
                            <Note key={note.id} name={note.name} text={note.text} />
                        ))}
                    </div>
                );
            }
        }
    

Работа с API


        class Notes extends Component<TNotesProps, TNotesState> {
            state: TNotesState = {
                loading: true,
                error: false,
                notes: null
            }

            componentDidMount() {
                fetch('/api/notes')
                    .then(response => response.json())
                    .then(notes => {
                        this.setState({ notes, loading: false });
                    })
                    .catch(() => {
                        this.setState({ error: true, loading: false });
                    });
            }

            ...
        }
    

Работа с API


        class Notes extends Component<TNotesProps, TNotesState> {
            state: TNotesState = {
                loading: true,
                error: false,
                notes: null
            }

            componentDidMount() {
                fetch('/api/notes')
                    .then(response => response.json())
                    .then(notes => {
                        this.setState({ notes, loading: false });
                    })
                    .catch(() => {
                        this.setState({ error: true, loading: false });
                    });
            }

            ...
        }
    

Работа с API


        class Notes extends Component<TNotesProps, TNotesState> {
            state: TNotesState = {
                loading: true,
                error: false,
                notes: null
            }

            componentDidMount() {
                fetch('/api/notes')
                    .then(response => response.json())
                    .then(notes => {
                        this.setState({ loading: false, notes });
                    })
                    .catch(() => {
                        this.setState({ error: true, loading: false });
                    });
            }

            ...
        }
    

Компоненты высшего порядка

Higher-Order Components (HOC)

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

Higher-Order Functions

Функция, которая принимает функцию
в качестве аргумента или
возвращает функцию в качестве результата

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


        function greaterThan(x) {
            return function (y) {
                return x > y;
            }
        }

        const greaterThan10 = greaterThan(10);

        greaterThan10(42); // true
    

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


        function logArguments(f) {
            return function (...args) {
                const result = f(...args);

                console.log('called with', args, 'result', result);

                return result;
            }
        }

        logArguments(Math.max)(1, 2, 3);
        // called with [1, 2, 3] result 3
    

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


        [...].map(n => ...);
    

        [...].filter(n => ...);
    

        [...].reduce((acc, n) => ...);
    

Компоненты высшего порядка

Принимают компонент в качестве аргумента
Возвращают новый компонент
Могут отрендерить переданный компонент

Компоненты высшего порядка


        import React, { Component } from 'react';

        function hoc(WrappedComponent) {
            return class extends Component {
                render() {
                    return <WrappedComponent {...this.props} />;
                }
            }
        }

        function hoc(WrappedComponent) {
            return props => {
                return <WrappedComponent {...props} />;
            }
        }
    

        import React, { Component } from 'react';

        function withData(url, WrappedComponent) {
            return class extends Component {
                state = { ... }

                componentDidMount() { // Запрашиваем данные, используя полученный url
                    ...               // См. «Работа с API»
                }

                render() {
                    if (this.state.loading) {
                        return 'Загрузка...';
                    }

                    if (this.state.error) {
                        return 'Что-то пошло не так';
                    }

                    return <WrappedComponent data={this.state.data} {...this.props} />;
                }
            }
        }
    

        import React, { Component } from 'react';

        function withData(url, WrappedComponent) {
            return class extends Component {
                state = { ... }

                componentDidMount() { // Получаем данные, используя полученный url
                    ...               // См. «Работа с API»
                }

                render() {
                    if (this.state.loading) {
                        return 'Загрузка...';
                    }

                    if (this.state.error) {
                        return 'Что-то пошло не так';
                    }

                    return <WrappedComponent data={this.state.data} {...this.props} />;
                }
            }
        }
    

        import React, { Component } from 'react';

        function withData(url, WrappedComponent) {
            return class extends Component {
                state = { ... }

                componentDidMount() { // Получаем данные, используя полученный url
                    ...               // См. «Работа с API»
                }

                render() {
                    if (this.state.loading) {
                        return 'Загрузка...';
                    }

                    if (this.state.error) {
                        return 'Что-то пошло не так';
                    }

                    return <WrappedComponent data={this.state.data} {...this.props} />;
                }
            }
        }
    

        import React, { Component } from 'react';

        function withData(url, WrappedComponent) {
            return class extends Component {
                state = { ... }

                componentDidMount() { // Получаем данные, используя полученный url
                    ...               // См. «Работа с API»
                }

                render() {
                    if (this.state.loading) {
                        return 'Загрузка...';
                    }

                    if (this.state.error) {
                        return 'Что-то пошло не так';
                    }

                    return <WrappedComponent data={this.state.data} {...this.props} />;
                }
            }
        }
    

        import withData from './hoc/with-data';

        // Данные просто приходят в пропсах
        function Notes({ data }: NotesProps)  {
            return (
                <div className="notes">
                    {data.map(note => (
                        <Note key={note.id} name={note.name} text={note.text} />
                    ))}
                <div>
            );
        }

        // Оборачиваем Notes, чтобы получить данные
        export default withData('/api/notes', Notes);
    

        import Notes from './components/notes';

        function NotesApp() {
            // Используем как самый обычный компонент
            return (
                <div className="notes-app">
                    <Notes />
                    ...
                </div>
            )
        }
    

        withData('/api/notes', Notes);
    

        withData('/api/notes/???', Note);
    

        withData('/api/notes', Notes);
    

        withData(props => `/api/notes/${props.id}`, Note);
    

        withData({
            url: props => `/api/notes/${props.id}`,
            propName: 'note',
            headers: {
                Authorization: 'OAuth ...'
            }
        }, Note);
    

Композиция


        withSomething(withData('/api/notes', Notes));
    

        withSomethingElse(
            withSomething(
                withData('/api/notes', Notes)
            )
        );
    

Композиция


        // lodash, underscore, ramda, @bem-react/core ...
        import { compose } from 'redux';

        compose(f, g, h)(x) === f(g(h(x)));
    

Композиция


        withData('/api/notes')(Notes);
    

        function withData(url) {
            return function (WrappedComponent) {
                return class extends Component {
                    ...
                }
            }
        }
    

        compose(
            withSomethingElse,
            withSomething,
            withData('/api/notes')
        )(Notes);
    

Компоненты высшего порядка

Решают проблему повторного использования кода
Позволяют наращивать функциональность
Возможны конфликты имен
Не понятно откуда пришли данные
Можно перепутать порядок
Сложно типизовать

Render Props

Render Props


        function Notes() {
            return (
                <DataProvider
                    url="/api/notes"
                    render={notes => notes.map(note => (
                        <Note
                            key={note.id}
                            name={note.name}
                            text={note.text}
                        />
                    ))}
                />
            );
        }
    

Render Props


        function renderNotes(notes) {
            return notes.map(note => (
                <Note key={note.id} name={note.name} text={note.text} />
            ));
        }

        function Notes() {
            return <DataProvider url="/api/notes" render={renderNotes} />;
        }
    

Render Props


        class DataProvider extends Component<Props, State> {
            state = { ... }

            componentDidMount() { // Запрашиваем данные, используя полученный url
                ...               // См. «Работа с API»
            }

            render() {
                if (this.state.loading) {
                    return 'Загрузка...';
                }

                if (this.state.error) {
                    return 'Что-то пошло не так';
                }

                return this.props.render(this.state.data);
            }
        }
    

Render Props


        function Notes() {
            return (
                <DataProvider url="/api/notes">
                    {notes => notes.map(note => (
                        <Note
                            key={note.id}
                            name={note.name}
                            text={note.text}
                        />
                    ))}
                </DataProvider>
            );
        }
    

Render Props


        function Notes() {
            return (
                <DataProvider url="/api/notes">
                    {({ data, error, loading }) => {
                        if (loading) {
                            return <LoadingScreen />;
                        }

                        ...

                        return notes.map(note => (
                            <Note
                                key={note.id}
                                name={note.name}
                                text={note.text}
                            />
                        ));
                    }}
                </DataProvider>
            );
        }
    

Render Props


        class DataProvider extends Component<Props, State> {
            state = { ... }

            componentDidMount() { // Запрашиваем данные, используя полученный url
                ...               // См. «Работа с API»
            }

            render() {
                return this.props.children(this.state);
            }
        }
    

Render Props

Решают проблему повторного использования кода
Не имеют проблем HOC
Нет сложностей с типизацией
Увеличивают вложенность
Из jsx нет доступа к локальному состоянию
или жизненному циклу

Render Props Hell


        <DataFetcher>
            {data => (
                <Actions>
                    {actions => (
                        <Translations>
                            {translations => (
                                <Styles>
                                    {styles => ...}
                                </Styles>
                            )}
                        </Translations>
                    )}
                </Actions>
            )}
        </DataFetcher>
    

Хуки

Хуки


          function useDataProvider(url) {
              const [isLoading, setIsLoading] = useState(true);
              const [error, setError] = useState(null);
              const [data, setData] = useState(null);

              useEffect(() => {
                  fetch(url)
                      .then(response => response.json())
                      .then(fetchedData => {
                          setIsLoading(false);
                          setData(fetchedData);
                      })
                      .catch(error => {
                          setIsLoading(false);
                          setError(error);
                      });
                  }, [url]);

              return { isLoading, error, data };
          }
          

Хуки

Меньше кода
Уменьшают вложенность
Проще читается, функциональность не размазана по жизненным циклам компонента
Чтобы добавить функциональность, нужно править код

Обработка ошибок

Error Boundary

Компонент, который отлавливает ошибки в любом месте поддерева компонентов и реагирует на них

Error Boundary


        import React, { Component } from 'react';

        class ErrorBoundary extends Component {
            state = {
                hasError: false
            }

            static getDerivedStateFromError(error) { // Здесь можно обновить state,
                return { hasError: true };           // чтобы отобразить запасной UI
            }

            componentDidCatch(error, info) {
                logError(error, info); // Здесь можно залогировать ошибку
            }

            ...
        }
    

Error Boundary


        import React, { Component } from 'react';

        class ErrorBoundary extends Component {
            state = {
                hasError: false
            }

            static getDerivedStateFromError(error) { // Здесь можно обновить state,
                return { hasError: true };           // чтобы отобразить запасной UI
            }

            componentDidCatch(error, info) {
                logError(error, info); // Здесь можно залогировать ошибку
            }

            ...
        }
    

Error Boundary


        import React, { Component } from 'react';

        class ErrorBoundary extends Component {
            state = {
                hasError: false
            }

            static getDerivedStateFromError(error) { // Здесь можно обновить state,
                return { hasError: true };           // чтобы отобразить запасной UI
            }

            componentDidCatch(error, info) {
                logError(error, info); // Здесь можно залогировать ошибку
            }

            ...
        }
    

Error Boundary


        import React, { Component } from 'react';

        class ErrorBoundary extends Component {
            ...

            render() {
                // В случае ошибки можно отобразить запасной UI
                if (this.state.hasError) {
                    return <h1>Что-то пошло не так</h1>;
                }

                return this.props.children;
            }
        }
    

Error Boundary


        import React, { Component } from 'react';

        import ErrorBoundary from './error-boundary';

        class App extends Component {
            render() {
                return (
                    <ErrorBoundary>
                        ...
                    </ErrorBoundary>
                );
            }
        }
    

Не будут отловлены

Ошибки в обработчиках событий
Ошибки в коде самого Error Boundary компонента
Ошибки в асинхронном коде

React Developer Tools

Yandex Browser, Chrome extension
Firefox extension

Документация React

Hooks Error boundaries Компоненты высшего порядка Render Props

React Router React Higher-Order Components in TypeScript Запрос к API c React Hooks, HOC или Render Prop