специальные функции, которые позволяют функциональным компонентам «подцепиться» к возможностям React.
Добавляет состояние
import React, { useState } from 'react';
const Example = () => {
const [state, setState] = useState(initialState);
}
Сохраняет состояние между рендерами
Изменение состояния вызывает ререндер
В отличие от this.setState
состояние при
обновлении заменяется целиком
Добавляет контейнер { current: ... }
import React, { useRef } from 'react';
const Example = () => {
const refContainer = useRef(null);
// refContainer.current = null;
return <div ref={refContainer} />;
}
Отдаёт один и тот же объект между рендерами
Изменение .current не вызывает ререндер
Может хранить ссылку на DOM-узел
или любое мутируемое значение
Добавляет контекст
import React, { useContext } from 'react';
const Example = () => {
const value = useContext(MyContext);
}
Добавляет возможность читать контекст
и подписываться на его изменения
Изменение значения контекста всегда вызывает ререндер
запоминание результатов выполнения функций
для
предотвращения повторных вычислений.
import React, { useMemo } from 'react';
const Example = ({ a, b }) => {
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b), // фабрика
[a, b]
);
}
Значение перевычисляется
только при изменении
зависимостей
Полезно для оптимизации
Фабрика запускается во время рендера,
в ней не должно быть сайд-эффектов
import React, { useCallback } from 'react';
const Example = ({ a, b }) => {
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
}
import React, { useCallback } from 'react';
const Example = () => {
const handleChange = useCallback(
e => {
console.log(e.target.value);
},
[],
);
}
Коллбэк сохраняется между рендерами
Функция пересоздаётся при изменении зависимостей
Оборачивание коллбэков в хук нужно
для передачи их дочерним компонентам
для предотвращения ненужных рендеров
Добавляет выполнение сайд-эффектов
componentDidMount + componentDidUpdate + componentWillUnmount
import React, { useState, useEffect } from 'react';
const Example = () => {
const [count, setCount] = useState(0);
// Эффект запускается после каждого рендера
useEffect(() => {
document.title = `Вы нажали ${count} раз`;
});
}
import React, { useState, useEffect } from 'react';
const Example = () => {
const [count, setCount] = useState(0);
// Эффект запускается только после первого рендера
useEffect(() => {
document.title = `Вы нажали ${count} раз`;
}, []);
}
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);
};
});
}
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);
};
});
}
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);
};
});
}
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);
};
});
}
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);
};
});
}
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
}
Эффект и его очистка запускаются
после каждого рендера
(есть оптимизации)
Запуск отложенный:
после текущей отрисовки, но до
следующей
Для синхронного выполнения есть 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
Можно вызывать
из компонентов в функциональном стиле
или
других хуков
Можно вызывать только на верхнем уровне
(нельзя вызывать
условно или в цикле)
Маршруты объявлены заранее
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);
Маршрутизация происходит во время рендера
$ 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>
);
$ 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>
);
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>
);
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>
);
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>
);
import { BrowserRouter, Route } from 'react-router-dom';
const NotesApp = () => (
<BrowserRouter>
<>
{/* other routes */}
<Route exact path="/notes/:id">
<SingleNotePage />
</Route>
</>
</BrowserRouter>
);
import { useParams } from 'react-router-dom';
type TParams = { id: string; };
const SingleNotePage = () => {
const { id } = useParams<TParams>()
return <h1>Note id: {id}</h1>;
}
import { useParams } from 'react-router-dom';
type TParams = { id: string; };
const SingleNotePage = () => {
const { id } = useParams<TParams>()
return <h1>Note id: {id}</h1>;
}
import { useParams } from 'react-router-dom';
type TParams = { id: string; };
const SingleNotePage = () => {
const { id } = useParams<TParams>()
return <h1>Note id: {id}</h1>;
}
import { useParams } from 'react-router-dom';
type TParams = { id: string; };
const SingleNotePage = () => {
const { id } = useParams<TParams>()
return <h1>Note id: {id}</h1>;
}
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>
);
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>
);
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>
);
}
}
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 });
});
}
...
}
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 });
});
}
...
}
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 });
});
}
...
}
Функция, которая принимает функцию
в качестве аргумента или
возвращает функцию в качестве результата
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);
function Notes() {
return (
<DataProvider
url="/api/notes"
render={notes => notes.map(note => (
<Note
key={note.id}
name={note.name}
text={note.text}
/>
))}
/>
);
}
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} />;
}
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);
}
}
function Notes() {
return (
<DataProvider url="/api/notes">
{notes => notes.map(note => (
<Note
key={note.id}
name={note.name}
text={note.text}
/>
))}
</DataProvider>
);
}
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>
);
}
class DataProvider extends Component<Props, State> {
state = { ... }
componentDidMount() { // Запрашиваем данные, используя полученный url
... // См. «Работа с API»
}
render() {
return this.props.children(this.state);
}
}
<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 };
}
Компонент, который отлавливает ошибки в любом месте поддерева компонентов и реагирует на них
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); // Здесь можно залогировать ошибку
}
...
}
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); // Здесь можно залогировать ошибку
}
...
}
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); // Здесь можно залогировать ошибку
}
...
}
import React, { Component } from 'react';
class ErrorBoundary extends Component {
...
render() {
// В случае ошибки можно отобразить запасной UI
if (this.state.hasError) {
return <h1>Что-то пошло не так</h1>;
}
return this.props.children;
}
}
import React, { Component } from 'react';
import ErrorBoundary from './error-boundary';
class App extends Component {
render() {
return (
<ErrorBoundary>
...
</ErrorBoundary>
);
}
}
Hooks Error boundaries Компоненты высшего порядка Render Props
React Router React Higher-Order Components in TypeScript Запрос к API c React Hooks, HOC или Render Prop