React для Vue разрабочиков

Последние несколько лет я использую как React, так и Vue в различных проектах, начиная от небольших веб-сайтов и заканчивая крупномасштабными приложениями.
Недавно я писал статью о том, почему я предпочитаю React, а не Vue для некоторых своих проектов. И сегодня я решил выпустить статью на тему того, в чём похожи, и чем отличаются React и Vue. Какие похожие концепции используют, и какие аналоги имеют для той, или иной функциональности.
Этот пост - краткое описание большинства функций Vue и того, как я буду писать их с помощью React, используя хуки.
- Шаблоны
- Условный рендеринг
- Условный рендеринг списка элементов
- Работа с CSS
- Props
- Data
- V-model
- Вычисленные свойства (computed)
- Методы
- События
- Методы жизненного цикла
- Наблюдатели (watchers)
- Слоты и scoped слоты
- Provide / inject
- Кастомные директивы
- Анимации
Шаблоны
React альтернативно: JSX
Vue в своих шаблонах использует чистую HTML-разметку с некоторыми пользовательскими директивами. Рекомендуется использовать однофайловые .vue
компоненты для разделения шаблонов и скриптов (и опционально стилей).
Ранее я писал, как в разметку шаблонов Vue можно добавить поддержку pug, или JSX. Но сегодня речь пойдёт именно о дефолтных настройках фреймворка.
<!-- Greeter.vue -->
<template>
<p>Hello, {{ name }}!</p>
</template>
<script>
export default {
props: ['name']
};
</script>
React использует JSX, который является расширением ECMAScript.
export default function Greeter({ name }) {
return <p>Hello, {name}!</p>;
}
Условный рендеринг
React альтернатива: логический оператор &&
, тернарные выражения, или ранние возвраты
Vue использует директивы v-if
, v-else
и v-else-if
для условного отображения частей шаблона.
<!-- Awesome.vue -->
<template>
<article>
<h1 v-if="awesome">Vue is awesome!</h1>
</article>
</template>
<script>
export default {
props: ['awesome']
};
</script>
React не поддерживает директивы, аналогичные Vue, поэтому необходимо использовать нативные средства JavaScript для условного рендеринга частей шаблона.
Оператор &&
предоставляет краткий способ написания оператора if
.
export default function Awesome({ awesome }) {
return (
<article>
{awesome && <h1>React is awesome!</h1>};
</article>
);
}
Если вам нужно выполнение кода на else
, тогда, вместо него, используйте тернарное выражение.
export default function Awesome({ awesome }) {
return (
<article>
{awesome ? (
<h1>React is awesome!</h1>
) : (
<h1>Oh no 😢</h1>
)};
</article>
}
Вы также можете полностью разделить эти две ветви и использовать подход с ранним возвратом.
export default function Awesome({ awesome }) {
if (!awesome) {
return (
<article>
<h1>Oh no 😢</h1>
</article>
);
}
return (
<article>
<h1>React is awesome!</h1>
</article>
);
}
Рендеринг списка элементов
React альтернатива: Array.map
Vue использует директиву v-for
для циклического обхода по массивам и объектам.
<!-- Recipe.vue -->
<template>
<ul>
<li v-for="(ingredient, index) in ingredients" :key="index">
{{ ingredient }}
</li>
</ul>
</template>
<script>
export default {
props: ['ingredients']
};
</script>
А в React можно "пройтись" по массиву с помощью встроенной функции Array.map
.
export default function Recipe({ ingredients }) {
return (
<ul>
{ingredients.map((ingredient, index) => (
<li key={index}>{ingredient}</li>
))}
</ul>
);
}
С итерационными объектами дела обстоят немного сложнее. Vue позволяет использовать одну и ту же директиву v-for
для ключей и значений.
<!-- KeyValueList.vue -->
<template>
<ul>
<li v-for="(value, key) in object" :key="key">
{{ key }}: {{ value }}
</li>
</ul>
</template>
<script>
export default {
props: ['object'] // E.g. { a: 'Foo', b: 'Bar' }
};
</script>
Мне нравится использовать встроенную функцию Object.entries
для итераций по объектам в React.
export default function KeyValueList({ object }) {
return (
<ul>
{Object.entries(object).map(([key, value]) => (
<li key={key}>{value}</li>
))}
</ul>
);
}
Привязка CSS классов и стилей
React альтернатива: Передача вручную через props
Vue автоматически привязывает class
и style
теги к HTML-элементу компонента.
<!-- Post.vue -->
<template>
<article>
<h1>{{ title }}</h1>
</article>
</template>
<script>
export default {
props: ['title'],
};
</script>
<!--
<post
:title="About CSS"
class="margin-bottom"
style="color: red"
/>
-->
С помощью React вам нужно вручную передать параметры ClassName
и style
элемента.
Обратите внимание, что style
должен быть объектом, строки не поддерживаются.
export default function Post({ title, className, style }) {
return (
<article className={className} style={style}>
{title}
</article>
);
}
{/* <Post
title="About CSS"
className="margin-bottom"
style={{ color: 'red' }}
/> */}
Если вы хотите передать все оставшиеся props
-параметры дочернему компоненту, то вам пригодится spread оператор.
export default function Post({ title, ...props }) {
{/* В результате, будет передано все параметры, кроме title */}
return (
<article {...props}>
{title}
</article>
);
}
Если вы скучаете по отличному API классов Vue, взгляните на библиотеку classnames.
Props
React альтернатива: Props
Props в React ведут себя так же, как и Vue. Одна небольшая разница: компоненты React не наследуют неизвестные атрибуты.
<!-- Post.vue -->
<template>
<h1>{{ title }}</h1>
</template>
<script>
export default {
props: ['title'],
};
</script>
export default function Post({ title }) {
return <h3>{title}</h3>;
}
Использование выражений в качестве props в Vue возможно с префиксом :
, который является псевдонимом для директивы v-bind
. React использует фигурные скобки для динамических значений.
<!-- Post.vue -->
<template>
<post-title :title="title" />
</template>
<script>
export default {
props: ['title'],
};
</script>
export default function Post({ title }) {
return <PostTitle title={title} />;
}
Data
React альтернатива: хук useState
В Vue опция data
используется для хранения локального состояния компонентов.
<!-- ButtonCounter.vue -->
<template>
<button @click="count++">
You clicked me {{ count }} times.
</button>
</template>
<script>
export default {
data() {
return {
count: 0
}
}
};
</script>
React предоставляет хук useState
, который возвращает массив из двух элементов: содержащий текущее значение состояния и функцию-сеттер.
import { useState } from 'react';
export default function ButtonCounter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Вы можете выбрать, желаете ли вы распределить состояние между несколькими вызовами useState
, или оставить его в одном объекте.
import { useState } from 'react';
export default function ProfileForm() {
const [name, setName] = useState('Sebastian');
const [email, setEmail] = useState('sebastian@spatie.be');
// ...
}
import { useState } from 'react';
export default function ProfileForm() {
const [values, setValues] = useState({
name: 'Sebastian',
email: 'sebastian@spatie.be'
});
// ...
}
v-model
v-model
- это удобная директива Vue, которая сочетает передачу props значения с прослушиванием события input
. Эта директива похожа на двустороннюю привязку данных Vue, в то время как она всё еще просто является "props вниз, события вверх" под капотом.
<!-- Profile.vue -->
<template>
<input type="text" v-model="name" />
</template>
<script>
export default {
data() {
return {
name: 'Sebastian'
}
}
};
</script>
Хотя, по факту, Vue под капотом v-model делаем следующее:
<template>
<input
type="text"
:value="name"
@input="name = $event.target.value"
/>
</template>
React не имеет аналога m-model, тут нужно указывать связь явно:
import { useState } from 'react';
export default function Profile() {
const [name, setName] = useState('Sebastian');
return (
<input
type="text"
value={name}
onChange={event => setName(event.target.name)}
/>
);
}
Вычисляемые свойства (computed)
React альтернатива: переменные, опционально обёрнутые в useMemo
Vue имеет вычисляемые свойства по двум причинам: чтобы избежать смешивания логики и разметки в шаблонах, и чтобы кэшировать сложные вычисления в экземпляре компонента.
Без вычисляемых свойств:
<!-- ReversedMessage.vue -->
<template>
<p>{{ message.split('').reverse().join('') }}</p>
</template>
<script>
export default {
props: ['message']
};
</script>
export default function ReversedMessage({ message }) {
return <p>{message.split('').reverse().join('')}</p>;
}
С помощью React можно извлечь расчёт вычисленного свойства из шаблона, присвоив результат переменной.
<!-- ReversedMessage.vue -->
<template>
<p>{{ reversedMessage }}</p>
</template>
<script>
export default {
props: ['message'],
computed: {
reversedMessage() {
return this.message.split('').reverse().join('');
}
}
};
</script>
export default function ReversedMessage({ message }) {
const reversedMessage = message.split('').reverse().join('');
return <p>{reversedMessage}</p>;
}
Если речь идет о производительности, вычисления могут быть завернуты в useMemo
хук. useMemo
нужно передать функцию обратного вызова, которая возвращает вычисленный результат, и массив зависимостей.
В следующем примере reverseMessage
будет пересчитано только в случае изменения зависящих параметров.
import { useMemo } from 'react';
export default function ReversedMessage({ message }) {
const reversedMessage = useMemo(() => {
return message.split('').reverse().join('');
}, [message]);
return <p>{reversedMessage}</p>;
}
Methods
React альтернатива: функции
Vue имеет опцию methods
для объявления функций, доступных во всём компоненте.
<!-- ImportantButton.vue -->
<template>
<button onClick="doSomething">
Do something!
</button>
</template>
<script>
export default {
methods: {
doSomething() {
// ...
}
}
};
</script>
В React вы можете объявить нативные функции внутри компонента, к которым можно обратиться.
export default function ImportantButton() {
function doSomething() {
// ...
}
return (
<button onClick={doSomething}>
Do something!
</button>
);
}
События (Events)
React альтернатива: props коллбеки
События, по сути, являются callback-ами, которые вызываются, когда что-то происходит в дочернем компоненте. Vue предоставляет возможность "прослушивать" события с помощью аннотации @
, что является сокращением для директивы v-on
.
<!-- PostForm.vue -->
<template>
<form>
<button type="button" @click="$emit('save')">
Save
</button>
<button type="button" @click="$emit('publish')">
Publish
</button>
</form>
</template>
События в React не особенного значения, они являются обычными props-функциями, которые будут вызываться дочерним компонентом при определённом событии.
export default function PostForm({ onSave, onPublish }) {
return (
<form>
<button type="button" onClick={onSave}>
Save
</button>
<button type="button" onClick={onPublish}>
Publish
</button>
</form>
);
}
Модификаторы событий
React альтернатива: функции высшего порядка, если вам этой действительно нужно
Vue имеет несколько модификаторов, например, prevent
и stop
, которые изменяют способ обработки события без изменения кода его обработчика.
<!-- AjaxForm.vue -->
<template>
<form @submit.prevent="submitWithAjax">
<!-- ... -->
</form>
</template>
<script>
export default {
methods: {
submitWithAjax() {
// ...
}
}
};
</script>
В React нет синтаксиса модификаторов. Потому, вызов preventDefault()
и stopPropagation()
, в основном, вызываются в функции обработчика событий.
export default function AjaxForm() {
function submitWithAjax(event) {
event.preventDefault();
// ...
}
return (
<form onSubmit={submitWithAjax}>
{/* ... */}
</form>
);
}
Если вы действительно хотите использовать что-то похожее на модификатор, вы можете использовать функцию более высокого порядка.
function prevent(callback) {
return (event) => {
event.preventDefault();
callback(event);
};
}
export default function AjaxForm() {
function submitWithAjax(event) {
// ...
}
return (
<form onSubmit={prevent(submitWithAjax)}>
{/* ... */}
</form>
);
}
Методы жизненного цикла
React альтернатива: хук useEffect
React имеет очень похожий API на Vue, когда речь идёт о жизненном цикле компонентов. Большинство проблем, связанных с жизненным циклом, могут быть решены с помощью использования хука
useEffect
. Эффекты и методы жизненного цикла - это совершенно разные парадигмы, поэтому их сложно сравнивать. В свою очередь, этот раздел ограничивается несколькими практическими примерами, так как эффекты заслуживают отдельной статьи.
<template>
<input type="text" ref="input" />
</template>
<script>
import DateTimePicker from 'awesome-date-time-picker';
export default {
mounted() {
this.dateTimePickerInstance = new DateTimePicker(this.$refs.input);
},
beforeDestroy() {
this.dateTimePickerInstance.destroy();
}
};
</script>
С помощью useEffect
вы можете объявить "побочный эффект", который должен быть запущен после рендера компонента. Когда вы возвращаете callback
из useEffect
, он будет вызван, когда эффект будет очищен. В текущем случае, когда компонент будет уничтожен.
import { useEffect, useRef } from 'react';
import DateTimePicker from 'awesome-date-time-picker';
export default function Component() {
const dateTimePickerRef = useRef();
useEffect(() => {
const dateTimePickerInstance = new DateTimePicker(dateTimePickerRef.current);
return () => {
dateTimePickerInstance.destroy();
};
}, []);
return <input type="text" ref={dateTimePickerRef} />;
}
Это похоже на регистрацию слушателя beforeDestroy
в функции mounted
компонента Vue.
<script>
export default {
mounted() {
const dateTimePicker = new DateTimePicker(this.$refs.input);
this.$once('hook:beforeDestroy', () => {
dateTimePicker.destroy();
});
}
};
</script>
Подобно useMemo
, useEffect
принимает массив зависимостей в качестве второго параметра.
Без заданных зависимостей, эффект будет запускаться после каждого рендеринга, и будет очищаться перед каждым следующим рендерингом. Эта функциональность похожа на комбинацию mounted, updated, beforeUpdate и beforeDestroy.
useEffect(() => {
// выполняется каждый раз при рендеринге
return () => {
// Опционально: выполняется перед новым рендерингом
};
});
Если вы укажете, что эффект не имеет зависимостей, то он будет запущен только при первом отображении компонента, так как у него нет причин для обновления. Эта функциональность похожа на комбинацию mounted
, и beforeDestroyed
.
useEffect(() => {
// Выполнится при монтировании (mount аналог)
return () => {
// Опционально: выполняется перед демонтированием
};
}, []);
Если вы укажете зависимость, то эффект будет работать только при изменении определённой зависимости. К этому мы вернемся в разделе "Наблюдатели".
const [count, setCount] = useState(0);
useEffect(() => {
// Выполнится при изменении `count`
return () => {
// Опционально: выполнится когда `count` изменится
};
}, [count]);
Попытка прямого трактования жизненного цикла хуков как вызовов useEffect
, как правило - плохая идея. Лучше всего - переосмыслить вещи, и думать об этом как о наборе декларативных побочных эффектов. Когда вызывается эффект, выполняется какая-то деталь реализации.
Как сказал Райан Флоренс:
Вопрос не в том, "когда запускается этот эффект", а в том, "с каким состоянием синхронизируется этот эффект".
useEffect(fn)
// все состояние
useEffect(fn, [])
// нет состояния
useEffect(fn, [перечисленные, состояния])
Наблюдатели (Watchers)
React альтернатива: хук useEffect
Наблюдатели идейно похожи на хуки на протяжении всего жизненного цикла: "Когда Х случается, делай Y". Наблюдателей концептуально не существует в React, но вы можете достичь того же самого эффекта с помощью useEffect
.
<!-- AjaxToggle.vue -->
<template>
<input type="checkbox" v-model="checked" />
</template>
<script>
export default {
data() {
return {
checked: false
}
},
watch: {
checked(checked) {
syncWithServer(checked);
}
},
methods: {
syncWithServer(checked) {
// ...
}
}
};
</script>
import { useEffect, useState } from 'react';
export default function AjaxToggle() {
const [checked, setChecked] = useState(false);
function syncWithServer(checked) {
// ...
}
useEffect(() => {
syncWithServer(checked);
}, [checked]);
return (
<input
type="checkbox"
checked={checked}
onChange={() => setChecked(!checked)}
/>
);
}
Обратите внимание, что useEffect
также запустится при первом рендеринге. Это то же самое, что и использование параметра immediate
в Vue watcher.
Если вы не хотите, чтобы эффект работал при первом рендере, вам нужно будет создать ref
, чтобы определить, случился первый рендеринг или нет.
import { useEffect, useRef, useState } from 'react';
export default function AjaxToggle() {
const [checked, setChecked] = useState(false);
const firstRender = useRef(true);
function syncWithServer(checked) {
// ...
}
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
syncWithServer(checked);
}, [checked]);
return (
<input
type="checkbox"
checked={checked}
onChange={() => setChecked(!checked)}
/>
);
}
Слоты (Slots & scoped slots)
React альтернатива: JSX props или render props
Если вы отображаете шаблон внутри открывающегося и закрывающегося тега компонента, React передает его как дочерний prop
.
С помощью Vue вам нужно объявить тег prop
-элемент.
<!-- RedParagraph.vue -->
<template>
<p style="color: red">
<slot />
</p>
</template>
export default function RedParagraph({ children }) {
return (
<p style={{ color: 'red' }}>
{children}
</p>
);
}
Так как "слоты" - это просто prop в React, нам не нужно ничего объявлять в наших шаблонах. Мы можем просто принимать prop в JSX и рендерить его где и когда захотим.
<!-- Layout.vue -->
<template>
<div class="flex">
<section class="w-1/3">
<slot name="sidebar" />
</section>
<main class="flex-1">
<slot />
</main>
</div>
</template>
<!-- Как используем: -->
<layout>
<template #sidebar>
<nav>...</nav>
</template>
<template #default>
<post>...</post>
</template>
</layout>
export default function RedParagraph({ sidebar, children }) {
return (
<div className="flex">
<section className="w-1/3">
{sidebar}
</section>
<main className="flex-1">
{children}
</main>
</div>
);
}
// Как используем:
return (
<Layout sidebar={<nav>...</nav>}>
<Post>...</Post>
</Layout>
);
Vue имеет возможность задавать слоты с ограниченной областью видимости. При каждой итерации Vue передаёт данные для конкретного элемента.
Обычные слоты отрисовываются до того, как они будут переданы родительскому компоненту. Родительский компонент затем решает, что делать с отрисовываемым фрагментом.
Scoped слоты не могут быть выведены до отрисовки родительским компонентом, потому что они полагаются на данные, которые они получат от родительского компонента. В некотором роде, scoped слоты - это лениво обрабатываемые слоты.
Лениво выполнять что-либо в JavaScript довольно просто: оберните это в функцию и вызывайте при необходимости. Если вам нужен scoped слот в React, просто передайте функцию, которая будет вызвана при рендеринге в шаблоне.
Для scoped слотов, мы можем так же использовать дочерние слоты, или любой другой prop для именованных scoped слотов. Однако, мы передадим функцию вместо объявления шаблона.
<!-- CurrentUser.vue -->
<template>
<span>
<slot :user="user" />
</span>
</template>
<script>
export default {
inject: ['user']
};
</script>
<!-- Как используем: -->
<template>
<current-user>
<template #default="{ user }">
{{ user.firstName }}
</template>
</current-user>
</template>
import { useContext } from 'react';
import UserContext from './UserContext';
export default function CurrentUser({ children }) {
const { user } = useContext(UserContext);
return (
<span>
{children(user)}
</span>
);
}
// Как используем:
return (
<CurrentUser>
{user => user.firstName}
</CurrentUser>
);
Provide / inject
React альтернатива: хуки createContext
и useContext
Provide/inject позволяет компоненту разделить состояние со своим поддеревом дочерних компонентов. React имеет похожую опцию, называемую контекстом.
<!-- MyProvider.vue -->
<template>
<div><slot /></div>
</template>
<script>
export default {
provide: {
foo: 'bar'
},
};
</script>
<!-- Должно быть срендерено в экземпляре MyProvider: -->
<template>
<p>{{ foo }}</p>
</template>
<script>
export default {
inject: ['foo']
};
</script>
import { createContext, useContext } from 'react';
const fooContext = createContext('foo');
function MyProvider({ children }) {
return (
<FooContext.Provider value="foo">
{children}
</FooContext.Provider>
);
}
// Должно быть срендерено в экземпляре MyProvider:
function MyConsumer() {
const foo = useContext(FooContext);
return <p>{foo}</p>;
}
Кастомные директивы
React альтернатива: компоненты
В React директив не существует. Однако большинство проблем, которые решаются с помощью директив, могут быть решены с помощью компонентов.
<div v-tooltip="Hello!">
<p>...</p>
</div>
return (
<Tooltip text="Hello">
<div>
<p>...</p>
</div>
</Tooltip>
);
Анимация (Transitions)
React альтернатива: сторонние библиотеки
React не имеет встроенных утилит для анимации. Если вы ищете библиотеку для React чем-то похожую на Vue transition, которая на самом деле ничего не анимирует, а только организует анимацию с помощью классов, обратите внимание на react-transition-group.
Если вы предпочитаете библиотеку, которая делает для вас более тяжелую работу, посмотрите в сторону Pose.
Резюме
В этой статье мы рассмотрели, отличие React и Vue на аналогичных примерах и задачах. Было приведено пример и аналоги реализаций функционала Vue на синтаксисе React. Подробное сравнение React x Vue, и идеальное пособие по изучению React после Vue. Так же, эта статья отлично подойдёт в качестве шпаргалки тем, кто хорошо ориентируется во Vue, и изучает React. Надеюсь, что эта статья поможет быстрее выучить React тем, кто уже изучил Vue.