Рассмотрим реализацию запроса данных к API c помощью нового друга React Hooks и старых добрых товарищей Render Prop и HOC (Higher Order Component). Выясним, действительно ли новый друг лучше старых двух.
Жизнь не стоит на месте, React меняется в лучшую сторону. В феврале 2019 года в React 16.8.0 появились React Hooks. Теперь в функциональных компонентах можно работать с локальным состоянием и выполнять сайд-эффекты. Никто не верил, что это возможно, но все всегда это хотели. Если вы еще не в курсе деталей, за подробностями сюда.
React Hooks дают возможность наконец-то отказаться от таких паттернов как HOC и Render Prop. Потому что за время использования к ним накопилось ряд претензий:
RProp | HOC | |
---|---|---|
1. Много компонентов-оберток, в которых сложно разобраться в React DevTools и в коде. | (???) | (???) |
2. Сложно типизировать (Flow, TypeScript). | (???) | |
3. Не очевидно, от какого HOC какие props компонент получает, что усложняет процесс дебаггинга и понимание как работает компонент. | (???) | |
4. Render Prop чаще всего не добавляет верстки, хотя используется внутри JSX. | (???) | |
5. Коллизия ключей props. При передаче props от родителей одинаковые ключи могут быть перезаписаны значениями из HOC. | (???) | |
6. Сложно читать git diff, так как смещаются все отступы в JSX при оборачивании JSX в Render Prop. | (???) | |
7. Если несколько HOC, то можно ошибиться с последовательностью композиции. Правильный порядок не всегда очевиден, так как логика спрятана внутри HOC. Например, когда сначала проверяем авторизован ли пользователь, и только потом запрашиваем личные данные. | (???) |
Чтобы не быть голословной, давайте рассмотрим на примере чем React Hooks лучше (а может все-таки хуже) Render Prop. Будем рассматривать Render Prop, а не HOC, так как в реализации они очень похожи и у HOC больше недостатков. Попробуем написать утилиту, которая обрабатывает запрос данных к API. Я уверена, что многие писали это в своей жизни сотни раз, ну что же посмотрим можно ли еще лучше и проще.
Для этого будем использовать популярную библиотеку axios. В самом простом сценарии нужно обработать следующие состояния:
- процесс получения данных (isFetching)
- данные успешно получены (responseData)
- ошибка получения данных (error)
- отмена запроса, если в процессе его выполнения поменялись параметры запроса, и нужно отправить новый
- отмена запроса, если данного компонента больше нет в DOM
1. Простой сценарий
Напишем дефолтный state и функцию (reducer), которая меняет state в зависимости от результата запроса: success / error.
Справочно. Reducer к нам пришел из функционального программирования, а для большинства JS разработчиков из Redux. Это функция, которая принимает предыдущее состояние и действие (action) и возвращает следующее состояние.
const defaultState = {
responseData: null,
isFetching: true,
error: null
};
function reducer1(state, action) {
switch (action.type) {
case "fetched":
return {
...state,
isFetching: false,
responseData: action.payload
};
case "error":
return {
...state,
isFetching: false,
error: action.payload
};
default:
return state;
}
}
Эту функцию мы переиспользуем в двух подходах.
Render Prop
class RenderProp1 extends React.Component {
state = defaultState;
axiosSource = null;
tryToCancel() {
if (this.axiosSource) {
this.axiosSource.cancel();
}
}
dispatch(action) {
this.setState(prevState => reducer(prevState, action));
}
fetch = () => {
this.tryToCancel();
this.axiosSource = axios.CancelToken.source();
axios
.get(this.props.url, {
cancelToken: this.axiosSource.token
})
.then(response => {
this.dispatch({ type: "fetched", payload: response.data });
})
.catch(error => {
this.dispatch({ type: "error", payload: error });
});
};
componentDidMount() {
this.fetch();
}
componentDidUpdate(prevProps) {
if (prevProps.url !== this.props.url) {
this.fetch();
}
}
componentWillUnmount() {
this.tryToCancel();
}
render() {
return this.props.children(this.state);
}
React Hooks
const useRequest1 = url => {
const [state, dispatch] = React.useReducer(reducer, defaultState);
React.useEffect(() => {
const source = axios.CancelToken.source();
axios
.get(url, {
cancelToken: source.token
})
.then(response => {
dispatch({ type: "fetched", payload: response.data });
})
.catch(error => {
dispatch({ type: "error", payload: error });
});
return source.cancel;
}, [url]);
return [state];
};
По url из используемого компонента получаем данные — axios.get(). Обрабатываем success и error, меняя state через dispatch(action). Возвращаем state в компонент. И не забываем отменить запрос в случае изменения url или если компонент удалился из DOM. Все просто, но написать можно по-разному. Выделим плюсы и минусы у двух подходов:
Hooks | RProp | |
---|---|---|
1. Меньше кода. | (???) | |
2. Вызов сайд-эффекта (запрос данных в API) читается проще, так как написан линейно, не размазан по жизненным циклам компонента. | (???) | |
3. Отмена запроса написана сразу после вызова запроса. Все в одном месте. | (???) | |
4. Простой код, описывающий отслеживание параметров для вызова сайд-эффектов. | (???) | |
5. Очевидно, в какой цикл жизни компонента будет выполнен наш код. | (???) |
React Hooks позволяют писать меньше кода, и это неоспоримый факт. Значит, эффективность вас как разработчика растет. Но придется освоить новую парадигму.
Когда есть названия циклов жизни компонента все очень понятно. Сначала мы получаем данные после того, как компонент появился на экране (componentDidMount), потом повторно получаем, если поменялся props.url и перед этим руками не забыть отменить предыдущий запрос (componentDidUpdate), если компонент удалился из DOM, то отменяем запрос (componentWillUnmount).
Но теперь мы вызываем сайд-эффект прям в рендере, нас же учили, что так нельзя. Хотя стоп, не совсем в рендере. А внутри функции useEffect, которая будет выполнять асинхронно что-то после каждого рендера, а точнее коммита и отрисовки нового DOM.
Но нам не надо после каждого рендера, а надо только на первый рендер и в случае изменения url, что мы указываем вторым аргументом в useEffect.
Понимание как работают React Hooks требует осознание новых вещей. Например, разницу между фазами: коммит и рендер. В фазе рендера React вычисляет, какие изменения надо применить в DOM, путем сравнения с результатом предыдущего рендера. А в фазе коммита React применяет данные изменения в DOM. Именно в фазе коммита вызываются методы: componentDidMount и componentDidUpdate. А вот то, что написано в useEffect, будет вызвано после коммита асинхронно и, таким образом, не будет блокировать отрисовку DOM, если вы вдруг случайно решили что-то синхронно много посчитать в сайд-эффекте.
Вывод — используйте useEffect. Писать меньше и безопаснее.
И еще одна прекрасная фича: useEffect умеет подчищать за предыдущим эффектом и после удаления компонента из DOM. Спасибо Rx, которые вдохновили команду React на такой подход.
Использование нашей утилиты с React Hooks тоже намного удобнее.
const AvatarRenderProp1 = ({ username }) => (
<RenderProp url={`https://api.github.com/users/${username}`}>
{state => {
if (state.isFetching) {
return "Loading";
}
if (state.error) {
return "Error";
}
return <img src={state.responseData.avatar_url} alt="avatar" />;
}}
</RenderProp>
);
const AvatarWithHook1 = ({ username }) => {
const [state] = useRequest(`https://api.github.com/users/${username}`);
if (state.isFetching) {
return "Loading";
}
if (state.error) {
return "Error";
}
return <img src={state.responseData.avatar_url} alt="avatar" />;
};
Вариант с React Hooks опять выглядит более компактным и очевидным.
Минусы Render Prop:
1) непонятно добавляется ли верстка или только логика
2) если надо будет состояние из Render Prop обработать в локальном state или жизненных циклах дочернего компонента придется создать новый компонент
Добавим новый функционал — получение данных с новыми параметрами по действию пользователя. Захотелось, например, кнопку, которая получает аватарку вашего любимого разработчика.
2) Обновлению данных по действию пользователя
Добавим кнопку, которая отправляет запрос с новым username. Самое простое решение — это хранить username в локальном state компонента и передавать новый username из state, а не props как сейчас. Но тогда нам придется copy-paste везде, где понадобится похожий функционал. Так что вынесем этот функционал в нашу утилиту.
Использовать будем так:
const Avatar2 = ({ username }) => {
...
<button
onClick={() => update("https://api.github.com/users/NewUsername")}
>
Update avatar for New Username
</button>
...
};
Давайте писать реализацию. Ниже написаны только изменения по сравнению с первоначальным вариантом.
function reducer2(state, action) {
switch (action.type) {
...
case "update url":
return {
...state,
isFetching: true,
url: action.payload,
defaultUrl: action.payload
};
case "update url manually":
return {
...state,
isFetching: true,
url: action.payload,
defaultUrl: state.defaultUrl
};
...
}
}
Render Prop
class RenderProp2 extends React.Component {
state = {
responseData: null,
url: this.props.url,
defaultUrl: this.props.url,
isFetching: true,
error: null
};
static getDerivedStateFromProps(props, state) {
if (state.defaultUrl !== props.url) {
return reducer(state, { type: "update url", payload: props.url });
}
return null;
}
...
componentDidUpdate(prevProps, prevState) {
if (prevState.url !== this.state.url) {
this.fetch();
}
}
...
update = url => {
this.dispatch({ type: "update url manually", payload: url });
};
render() {
return this.props.children(this.state, this.update);
}
}
React Hooks
const useRequest2 = url => {
const [state, dispatch] = React.useReducer(reducer, {
url,
defaultUrl: url,
responseData: null,
isFetching: true,
error: null
});
if (url !== state.defaultUrl) {
dispatch({ type: "update url", payload: url });
}
React.useEffect(() => {
…(fetch data);
}, [state.url]);
const update = React.useCallback(
url => {
dispatch({ type: "update url manually", payload: url });
},
[dispatch]
);
return [state, update];
};
Если вы внимательно посмотрели код, то заметили:
- url стали сохранять внутри нашей утилиты;
- появился defaultUrl для идентификации, что url обновился через props. Нам нужно следить за изменением props.url, иначе новый запрос не отправится;
- добавили функцию update, которую возвращаем в компонент для отправки нового запроса по клику на кнопку.
Обратите внимание с Render Prop нам пришлось воспользоваться getDerivedStateFromProps для обновления локального state в случае изменения props.url. А с React Hooks никаких новых абстракций, можно сразу в рендере вызывать обновление state — ура, товарищи, наконец!
Единственно усложнение с React Hooks — пришлось мемоизировать функцию update, чтобы она не изменялась между обновлениями компонента. Когда как в Render Prop функция update является методом класса.
3) Опрос API через одинаковый промежуток времени или Polling
Давайте добавим еще один популярный функционал. Иногда нужно постоянно опрашивать API. Мало ли ваш любимый разработчик поменял аватарку, а вы не в курсе. Добавляем параметр интервал.
Использование:
const AvatarRenderProp3 = ({ username }) => (
<RenderProp url={`https://api.github.com/users/${username}`} pollInterval={1000}>
...
const AvatarWithHook3 = ({ username }) => {
const [state, update] = useRequest(
`https://api.github.com/users/${username}`, 1000
);
...
Реализация:
function reducer3(state, action) {
switch (action.type) {
...
case "poll":
return {
...state,
requestId: state.requestId + 1,
isFetching: true
};
...
}
}
Render Prop
class RenderProp3 extends React.Component {
state = {
...
requestId: 1,
}
...
timeoutId = null;
...
tryToClearTimeout() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
poll = () => {
this.tryToClearTimeout();
this.timeoutId = setTimeout(() => {
this.dispatch({ type: 'poll' });
}, this.props.pollInterval);
};
...
componentDidUpdate(prevProps, prevState) {
...
if (this.props.pollInterval) {
if (
prevState.isFetching !== this.state.isFetching &&
!this.state.isFetching
) {
this.poll();
}
if (prevState.requestId !== this.state.requestId) {
this.fetch();
}
}
}
componentWillUnmount() {
...
this.tryToClearTimeout();
}
...
React Hooks
const useRequest3 = (url, pollInterval) => {
const [state, dispatch] = React.useReducer(reducer, {
...
requestId: 1,
});
React.useEffect(() => {
…(fetch data)
}, [state.url, state.requestId]);
React.useEffect(() => {
if (!pollInterval || state.isFetching) return;
const timeoutId = setTimeout(() => {
dispatch({ type: "poll" });
}, pollInterval);
return () => {
clearTimeout(timeoutId);
};
}, [pollInterval, state.isFetching]);
...
}
Появился новый prop — pollInterval. При завершении предыдущего запроса через setTimeout мы инкрементируем requestId. С хуками у нас появился еще один useEffect, в котором мы вызываем setTimeout. А старый наш useEffect, который отправляет запрос стал следить еще за одной переменной — requestId, которая говорит нам, что setTimeout отработал, и пора уже запрос отправлять за новой аватаркой.
В Render Prop пришлось написать:
- сравнение предыдущего и нового значения requestId и isFetching
- очистить timeoutId в двух местах
- добавить классу свойство timeoutId
React Hooks позволяют писать коротко и понятно то, что мы привыкли описывать подробнее и не всегда понятно.
4) Что дальше?
Мы можем продолжить расширять функционал нашей утилиты: принимать разную конфигурацию параметров запроса, кеширование данных, преобразование ответа и ошибки, принудительное обновление данных с теми же параметрами — рутинные операции в любом большом веб-приложении. На нашем проекте мы давно это вынесли в отдельный (внимание!) компонент. Да, потому что это был Render Prop. Но с выходом Hooks мы переписали на функцию (useAxiosRequest) и даже нашли некоторые баги в старой реализации. Посмотреть и попробовать можно тут.
Комментарии (38)
Spunreal
28.05.2019 21:53Хуки очень круты, но почему все сравнивают с HOC? HOC же аналог декоратора для компонента. Он имеет очень классную особенность — позволяет менять поведение в рантайме и не трогать код основного компонента, тем самым не нарушив работу компонента во всём проекте, а может даже и нескольких. Иногда это жизненно необходимо.
Почему нельзя совмещать хуки и хоки? Просто нужно без фанатизма. Это не противостояние за власть, а разные инструменты для разных задач. Я как бы понимаю, что и микроскопом можно гвозди забивать, но лучше забивать гвозди молотком, а с микроскопом делать другое.JustDont
28.05.2019 23:09+1Я как бы понимаю, что и микроскопом можно гвозди забивать, но лучше забивать гвозди молотком, а с микроскопом делать другое.
Забавно, что вся история с хуками начинается как раз с забивания гвоздей чем угодно кроме молотка — если при проектировании интерфейса не смешивать модель с представлением, то хуки особо и не сдались (потому что представляют собой всего лишь более сильный инструмент для отделения модели и её логики от представленческих вещей типа цикла жизни компонент). Но поскольку модель и логика у фронтэндщиков частенько очень простая и вырожденная — оные фронтэндщики очень любят вписать её сразу в компоненты, а уже потом начать думать, что же с этим делать. И вот тут-то и хуки становятся полезными.Spunreal
28.05.2019 23:43Балом правят паттерны (в прочем как и везде).
Захотел разделения бизнес логики и представления — используешь паттерн Container Component. Захотел изменить что-то в одном месте не меняя поведение в других — HOC. Захотел всё смешать в кучу — написал через хуки.
Только везде учат, что паттерны должны быть к месту, а не ради использования паттернов, но в React какой-то хайп идёт по каждому новому паттерну (или функционалу, который именуют очередным убийцей). То HOC самый лучший паттерн, который заменяет почти всё, потом появился убийца HOC'а — Render Props, а теперь уже и хуки, которые опять же сравнивают на полном серьёзе с HOC и Render Props как их замену.
Такое ощущение, что все просто поклоняются тому, о чём пишет Дэн Абрамов. Он написал, что HOC лучший — все это подхватили. Он написал, что HOC ужасен и Render Props теперь лучший — всё форсят Render Props.
Видимо молодость фронтенда даёт о себе знать. Когда-нибудь большая часть паттернов устаканится и их правильное применение осядет в головах, как это происходит в бэкенде. А сейчас идёт поиск серебряной пули.JustDont
28.05.2019 23:55+2Захотел разделения бизнес логики и представления — используешь паттерн Container Component.
Это шаг один. На шаге два обычно приходит понимание, что модель и логика — она вообще не обязана относиться к компонентам и уж тем более реакту: это просто код, ему пофиг на эти ваши фреймворки и либы по большей части, он и в реакте, и в ангуляре, и в чем угодно вообще будет выглядеть примерно одинаково с поправками на интеграционные рюшечки с представлением.
Во фронтэнде слишком часто встречается проталкивание инструмента везде, где только можно, даже если он не нужен. Есть реакт и его компоненты? А давайте у нас всё-всё будет «компонентом», даже если объективно это вообще ни разу не компонент? А давайте!
Это вот вообще классическое «когда у вас в руках молоток, все проблемы выглядят незабитыми гвоздями».
strannik_k
29.05.2019 02:59JustDont меня опередил)
В общем, хочу сказать следующее. Прихожу к выводу, что нынешние фронтенд фреймворки допустили общую архитектурную ошибку и предлагаются разные решения вокруг этой ошибки.
Я о разделении ответственности. Компонент занимается и логикой и отображением, что нарушает принцип единственной ответственности.
Сейчас в классе/функции пишется template/метод render. Там же пишется обработка логики.
Либо часть логики выносится в миксины, HOC, куда-то еще.
Но суть от этого не меняется. Вся эта дополнительная логика привязывается к одной сущности — компоненту, который занимается еще и отображением.
Получается, что связь между отображением и логикой — один к одному.
В итоге методы, данные, либо props так или иначе смешаны в одном компоненте.
Так же быть не должно!
Можно сделать по другому:
- Компонент занимается только отображением.
- В нем нет методов жизненного цикла.
- К компоненту можно подключать сколько угодно сущностей с логикой (не знаю, как лучше назвать, поэтому пусть будет сущность с логикой), где у каждой сущности есть свое состояние и прочая логика, включая методы жизненного цикла.
И проблема с пересечением данных была бы решена и для компонентов не были бы нужны всякие миксины, HOC, Container Component и прочее. Плюс легко в рантайме можно было бы добавлять/удалять логику.
При этом аналог HOC с логикой может быть полезен, но не для обертки над компонентом, а для обертки над сущностью с логикой.Odrin
29.05.2019 12:31Можете привести пример построения UI и связывания с ним логики на какой-то другой платформе (другом фреймворке/языке), где, по Вашему мнению, такой архитектурной ошибки нет?
JustDont
29.05.2019 12:46Swing, например. Удобством там часто и не пахнет, потому что очень строго MVC везде, и пропустить или упростить бойлерплейт почти нигде нельзя. Зато можно учиться тому, как вообще нормальный MVC должен выглядеть. Главное не тащить его обязаловкой во всё, в том числе и вырожденные случаи.
Odrin
29.05.2019 13:25В чем фундаментальное отличие между
и react'овскимJButton button = new JButton("Button"); ActionListener actionListener = new MyActionListener(); button.addActionListener(actionListener);
или angular'овским<button onClick={handleClick} />
<button (click)="onClick()">Button</button>
?JustDont
29.05.2019 13:33Вы намеренно приводите кусок кода, в котором нет M, да?
Odrin
29.05.2019 15:39Я никогда не работал с Swing и привожу первый кусок кода по запросу «Java Swing». Ну хорошо, допустим что кнопка из предыдущего куска когда по клику делает что-то с данными в таблице, а сама таблица связана с моделью:
а в реактеJTable table = new JTable(); Model model = new Model(); table.setModel(model); Controller controller = new Controller(model); button.addActionListener(controller);
<Table model={model} />
Вопрос остается тем же — в чем фундаментальное отличие от реакта и, особенно, от angular?JustDont
29.05.2019 15:47Вооот, это уже честнее. А теперь смотрите: реализовать в реакте или еще там каком ангуляре MVC — да никаких проблем (потому что ангуляры и прочие реакты это почти полностью V, остальное их не касается). Но очень часто этого не делают, и потом начинают мужественно бороться с проблемами.
Возвращаясь к вашим примерам, в реакте очень редко можно увидеть таблицу, как написано у вас, и очень часто — вот так:
<Table data={data} />
Думаю, вам понятно, чем это отличается от свинга, в котором так сделать попросту нельзя.
strannik_k
29.05.2019 14:29UI в unity3d.
Но привести прям пример UI мне сложно, искать надо.
Скажу, что UI там строится по тому же принципу, что и все остальное в проекте. Про это можно здесь почитать:
docs.unity3d.com/ru/current/Manual/GameObjects.html
docs.unity3d.com/ru/current/Manual/UsingComponents.html
Чтобы понятней было, можно еще тут про ECS почитать:
habr.com/ru/post/358108 (пункт «Что такое ECS»)
kiteno Автор
29.05.2019 11:58Почему нельзя, можно, только зачем? ) HOC (как и Render Prop) — это способ переиспользования логики/верстки в нескольких местах. Композиция UI представления (компоненты) — всегда была сильной стороной React. Просто выносишь в компонент и переиспользуешь. А вот чтобы шерить логику, не было простого инструмента. Для этого как раз все использовали HOC/Render Prop. Как раз hook — это новый правильный, простой «молоток» для переиспользования логики.
Spunreal
29.05.2019 12:27Но HOC и Render Props больше, чем просто способ для повторного использования кода. Во всех статьях их показывают только с одной стороны, как они ужасны по сравнению с хуками в некоторых кейсах. А потом обязательно сделают вывод, что хуки крутые, а хоки и рендер-пропсы уже прошлый век.
Покажите мне простой пример паттерна декоратор или адаптер на хуках. Покажите мне аналог scoped-slots на хуках. Окажется ли это проще, чем с HOC или Render Props?
Ну и главный вопрос. Какой из этих молотков хуки:
виды молотковkiteno Автор
29.05.2019 12:58хук — это молоток с крючком, как картинка в статье )) я еще раз повторюсь хуки — это удобный инструмент. Появились хуки — многие вещи стало писать проще. Про scoped-slots c React это можно написать так:
const Template = ({ col1, col2 }) => ( <div> {col1} {col2} </div> ); const TemplateWithData = ({ data }) => ( <Template col1={<div>{data.first}</div>} col2={<div>{data.second}</div>} /> );
Spunreal
30.05.2019 10:50Что-то я вчера разошёлся, видимо накипело из-за других статей по хукам.
В вашей говорится о конкретном случае использования. Для этого я бы тоже не стал использовать HOC или Render Props. Но в данном кейсе мне больше симпатизирует паттерн Container Component. Всё-таки если захотим использовать тот же компонент, но с другой логикой получения данных, то в вашем случае придётся создавать ещё один компонент, в который прокинем новый хук. В случае контейнера мы просто создаём новый контейнер, а сам компонент не трогаем. А вообще хуки — это конструкция фреймворка. Их можно совмещать со всеми паттернами, будь то HOC, Render Props, Container Component или любой другой из известных и не очень, которые мы используем для разделения зон ответственности и связанности кода.
Но касательно вашего примера — вы написали обычные слоты. Для слотов с ограниченной областью видимости как раз надо передать функцию, т.к. параметры в неё должен передать дочерний компонент. А это уже Render Props.
Это может понадобиться при создании универсальных компонентов, например, автокомплита, когда элемент списка в разных местах может иметь разный шаблон. Т.е. мы говорим, что родитель может передать кусок шаблона в дочерний компонент, но в нём можно использовать только определённый набор параметров (к примеру {id, name, title}).kiteno Автор
31.05.2019 10:58+1я так понимаю, вы про такой вариант говорите.
const Component1 = ({ getData }) => <div>{getData(1)}</div>; const Component2 = ({ getData }) => <div>{getData(2)}</div>; const WrapperRenderProp = ({ children }) => { const DATA = [1, 2, 3, 4]; const getData = i => DATA[i]; return <div>{children(getData)}</div>; }; const ResultWithRenderProp = () => ( <WrapperRenderProp> {getData => ( <div> <Component1 getData={getData} /> <Component2 getData={getData} /> </div> )} </WrapperRenderProp> );
но это можно и так написать без renderProp
const Component1 = ({ getData }) => <div>{getData(1)}</div>; const Component2 = ({ getData }) => <div>{getData(2)}</div>; const WrapperWithProps = ({ Template1, Template2 }) => { const DATA = [1, 2, 3, 4]; const getData = i => DATA[i]; return ( <div> <Template1 getData={getData} /> <Template2 getData={getData} /> </div> ); }; const ResultWithProps = () => ( <Wrapper Template1={Component1} Template2={Component2} /> );
Какой подход выбрать — нужно смотреть на детали. Если, например, есть лэйаут (WrapperWithProps), который в зависимости от темы рендерит разный Template1/Template2 и передает одни и те же пропсы я бы выбрала подход WrapperWithProps.
А если надо общий JSX но при этом супер нетривиальная логика с Template внутри я бы выбрала WrapperRenderProp. Но в любом случае это имеет смысл, когда есть общая верстка-раппер (общий JSX).
Посыл в статье с хуками про то, что удобно переиспользовать логику. Если нужно переиспользовать верстку, то композиция компонентов всегда была сильной стороной React — можно по-разному.
Druu
30.05.2019 09:07А кто-нибудь может пояснить, какую все-таки конкретно задачу решают хуки?
Ну то есть, если нужен компонент со стейтом — мы просто делаем компонент со стейтом, и хуки не нужны (пример с useRequest на обычном стейте без хуков реализуется точно так же, допустим). Если нужен компонент без стейта — мы делаем компонент без стейта, и хуки тоже не нужны.
В каком случае они нужны? Если хотя бы теоретически представить.faiwer
30.05.2019 11:51Хуки не делают ничего такого, чего нельзя сделать с классами. Это просто другой путь, который команда react-а взяла как за основной. В некотором роде классы теперь legacy.
Т.е. нужны они везде где у вас stateful компоненты и вы пишете код, в поддержке и развитии которого вы заинтересованы в будущем.
Команда react-a героически победила "проблему с this", заменив её на не менее сложную проблему с оркестрированием замыканий (не очень опытным разработчикам это страшно "выносит мозг", ибо код быстро становится неочевидным). Да и опытным ребусы гадать не всегда хочется. Самое простое это setState + async-useCallback. Всякие фокусы с useRef не для domElement-ов.
Я с декабря пишу используя хуки, очень нравится. Но не могу не отметить что это уже какой-то другой react. На мой взгляд писать приложение правильно с хуками сложнее, чем с классами. Требует большей квалификации. Как-то это дисгармонирует с их политикой партии. Хотели упростить, ведь "классы это сложнааа"...
Druu
30.05.2019 13:35уки не делают ничего такого, чего нельзя сделать с классами. Это просто другой путь, который команда react-а взяла как за основной. В некотором роде классы теперь legacy.
Как-то стало еще менее понятно. Зачем вообще этот другой путь тогда нужен, если он не поддерживается нативно языком, требует делать закат солнца вручную и при этом ничего нового не дает?
Команда react-a героически победила "проблему с this"
Это какая именно проблема имеется в виду?
Т.е. нужны они везде где у вас stateful компоненты и вы пишете код, в поддержке и развитии которого вы заинтересованы в будущем.
То есть, фактически, разработчики реакта взяли в заложники свое комьюнити? "Используйте наш кривой костыль вместо полноценного решения, иначе поддерживать не будем"?
Фигасе.JustDont
30.05.2019 13:40То есть, фактически, разработчики реакта взяли в заложники свое комьюнити? «Используйте наш кривой костыль вместо полноценного решения, иначе поддерживать не будем»?
Про поддержку классов пока что везде очень осторожные высказывания, что их поддержка никуда не денется и никак не поменяется. Не думаю, что им хочется повторять историю angularJS в исполнении реакта. Никто не побежит переписывать свой код с объектной парадигмы на функциональную просто потому, что разработчикам реакта так нравится больше.Druu
30.05.2019 13:52Никто не побежит переписывать свой код с объектной парадигмы на функциональную
Хуки же это наоборот объектная парадигма. С-но, к ним основная претензия как раз и состоит в том, что непонятно, зачем нужна наколеночная реализация ООП, когда она уже есть нативная. Другое дело, если бы хуки появились лет 5+ назад, когда классы были еще совсем не в моде. Тогда свои костыльные ООП-подсистемы были еще актуальны.
Spunreal
30.05.2019 14:17Не скажу за реакт, но у vue решили отказаться от реализации нативной поддержки классов для компонентов (сейчас у них через декораторы и не совсем красиво) в 3-й версии в пользу своего аналога хуков. Всё больше людей во фронте отдают предпочтение композиции вместо наследования.
Druu
30.05.2019 14:44Всё больше людей во фронте отдают предпочтение композиции вместо наследования.
Так с классами точно такая же композиция. Composition over inheritance — это же вообще ООП-шная придумка. Тут проблема в том, что вместо нативных, полноценных классов, которые поддерживаются на уровне языка, люди зачем-то делают свои, на коленке. Никаких разумных причин, очевидно, для этого быть не может, обычный NIH-синдром.
В результате вместо одной нативной реализации ООП, сейчас будет минимум три распространенных — нативная, реакт-хуки и вуе-хуки. И у каждой из них будет свое собственное темное прошлое, туманное будущее и мрачное настоящее.
faiwer
30.05.2019 15:09Это какая именно проблема имеется в виду?
"this и ООП в JS это так сложно, что начинающие разработчики и не только путаются и пишут говно-код". Что-то в таком духе. ИМХО, мне было бы легче новичку объяснить что у него там с this сломалось, нежели объяснить почему в его async-callback-е не видно обновлённый state.
Фигасе
А то. Правда выпиливать классы пока никто вроде не собирается. Но каких-то заметных улучшений в этой области ждать не приходится, они хорошо дали понять, что right react way это хуки и хоки. react-team вообще тяготеет к таким вещам. Хотят везде pure-function, а где не получается, то хотя бы что-то издали похожее на pure-function :) Хотят композицию, иммутабельность, дробление и всё такое.
В целом мало-мальски сложное приложение сейчас состоит из такого разнообразного бульона подходов. И render-methods, и контекст, и классовые компоненты (error-boundaries), и слоты, и хоки, и хуки. Может ещё чего забыл. Приправьте к этому какой-нибудь redux (особенно если с proxy, селекторами, викмап-мемоизацией) или mobx и получиться того ещё монстра. Но мне нравится :)
Druu
30.05.2019 15:35"this и ООП в JS это так сложно, что начинающие разработчики и не только путаются и пишут говно-код"
Не понял ничего. Какая проблема с this-то? То, что он ведет себя не так как в других ООП-языках? Так это стрелочными ф-ми решается. Или что-то другое?
Хотят везде pure-function, а где не получается, то хотя бы что-то издали похожее на pure-function
Ну "казаться а не быть" — это вообще проблема реакт-экосистемы. Везде какие-то нелепые рассуждения про ФП и чистоту, хотя по факту реакторедаксы — чистой воды процедурщина и никакого ФП с чистотой там рядом даже не пробегало.
В целом мало-мальски сложное приложение сейчас состоит из такого разнообразного бульона подходов. И render-methods, и контекст, и классовые компоненты (error-boundaries), и слоты, и хоки, и хуки.
А потом говорят про простой порог вхождения, ага. Он, может, и был простой — в 2015.
И главное-то что это бесполезная сложность — т.к. в данном случае знать надо не как делается Х, Y и Z (что расширяет возможности), а 100 разных способов сделать Х.faiwer
30.05.2019 17:26Рекомендую прочесть и посмотреть это. Тут они попытались объяснить свою мотивацию. Мои пересказы этого текста вторичны :)
faiwer
30.05.2019 17:29Пока писал ответ на меня снизошло озарение. Такое ощущение что экосистема React бодро так шагает к тому как писали PHP сайты 15 лет назад: помесь html, php, js, css & sql в одном файле. Ныне это JSX, JS || TS, cssInJS (и graphQL? :D).
Druu
30.05.2019 17:40Такое ощущение что экосистема React бодро так шагает к тому как писали PHP сайты 15 лет назад
https://en.wikipedia.org/wiki/XHP
С-но, да, "как в былые времена" — это основа реакта :)
Classes confuse both people and machines
Кек. Я не первый раз от команды реакта вижу это "confuse", оно нередко встречается в мотивации по всяким issues.
Но надо ли говорить, что у ребят какое-то свое странное мнение о том, что конфьюзит людей, а что — нет? :)
Conceptually, React components have always been closer to functions.
По-этому, чтобы не писать классы, давайте сделаем, чтобы эта функция от класса ничем не отличалась :)
kiteno Автор
30.05.2019 18:57Хуки — это способ использовать локальный стейт и вызывать сайд-эффекты (например запрос к API) в функциональных компонентах. До этого это было возможно только в компонентах-классах. В доках можно детали посмотре ь https://reactjs.org/docs/hooks-intro.html
Druu
30.05.2019 19:33Хуки — это способ использовать локальный стейт и вызывать сайд-эффекты (например запрос к API) в функциональных компонентах.
Так а зачем кому-то может захотеться использовать функциональный компонент, если в нем есть стейт? В этом и есть вопрос. Нафига это надо? Для чего? Какие проблемы в том, чтобы сделать компонент со стейтом классом?
kiteno Автор
31.05.2019 09:05Потому что это проще писать и меньше строчек кода. Если говорить про сайд-эффекты — useEffect — это ещё и безопаснее. В useEffect переданная функция будет вызван асинхронно в отличие от componentDidMount.
Druu
31.05.2019 10:09Потому что это проще писать и меньше строчек кода. Если говорить про сайд-эффекты — useEffect — это ещё и безопаснее.
Но это ведь просто не правда. Строчек кода столько же, "безопаснее" быть не может в силу полной эквивалентности семантики.
В useEffect переданная функция будет вызван асинхронно в отличие от componentDidMount.
В js любую синхронную ф-ю всегда можно вызвать асинхронно. А вот наоборот — нельзя. Так что синхронный интерфейс в подобных случаях более предпочтителен (тем более, что в 9 случаях из 10 хочется именно синхронной работы componentDidMount).
kiteno Автор
31.05.2019 10:28Но это ведь просто не правда. Строчек кода столько же, «безопаснее» быть не может в силу полной эквивалентности семантики.
строчек кода меньше.
В js любую синхронную ф-ю всегда можно вызвать асинхронно. А вот наоборот — нельзя.
в componentDidMount синхронный вызов будет откладывать отрисовку DOM. Иногда это правда надо, но из моего опыта как раз наоборот — 1 случай из 10.
но если нужен синхронный вызов то есть хук — useLayoutEffect
reactjs.org/docs/hooks-reference.html#uselayouteffect
kleinmaximus
Хоть я сейчас плотно сижу на Vue, но с появлением Hooks начал опять поглядывать в сторону React :)
Spunreal
У vue тоже это будет, только скорее всего с особенностями для vue.
github.com/vuejs/rfcs/blob/dynamic-lifecycle/active-rfcs/0000-dynamic-lifecycle-injection.md
kleinmaximus
Понятно, что у Vue это все еще только в наметках и вариациях, но судя по тому, что есть, в Реакте это более логичное и законченное решение. Пока что ;)