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

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

Последние несколько лет я использую как React, так и Vue в различных проектах, начиная от небольших веб-сайтов и заканчивая крупномасштабными приложениями.

Недавно я писал статью о том, почему я предпочитаю React, а не Vue для некоторых своих проектов. И сегодня я решил выпустить статью на тему того, в чём похожи, и чем отличаются React и Vue. Какие похожие концепции используют, и какие аналоги имеют для той, или иной функциональности.

Этот пост - краткое описание большинства функций Vue и того, как я буду писать их с помощью React, используя хуки.

Шаблоны

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 вам нужно объявить тег , где находится внутреннее содержимое. С помощью React вы визуализируете дочерний 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.