Источник 👀
import React from 'react';
import ReactDOM from 'react-dom';
class Hello extends React.Component {
render () {
return (
<div className='message-box'>
Привет, {this.props.name}
</div>
)
}
}
const el = document.body
ReactDOM.render(<Hello name='Иван' />, el)
// Компонент без состояния
const Headline = () => {
return <h1>Шапргалка по React</h1>
}
// Компонент, получающий пропы
const Greetings = (props) => {
return <p>Тебе это понравится, {props.name}.</p>
}
// Компонент должен возвращать единственный элемент
const Intro = () => {
return (
<div>
<Headline />
<p>Добро пожаловать в React!</p>
<Greetings name="Иван" />
</div>
)
}
ReactDOM.render(
<Intro />,
document.getElementById('root')
);
<Video fullscreen={true} autoplay={false} />
render () {
this.props.fullscreen;
// Используем `this.props` для доступа к пропам, переданным в компонент
const { fullscreen, autoplay } = this.props;
}
constructor(props) {
super(props)
this.state = { username: undefined }
}
this.setState({ username: 'Иван' })
render () {
this.state.username;
// Используем `this.state` для управления динамическими данными
const { username } = this.state
}
<AlertBox>
<h1>У вас имеются непрочитанные сообщения</h1>
</AlertBox>
class AlertBox extends Component {
render () {
return (
// Потомки передаются в виде пропа `children`
<div className='alert-box'>
{this.props.children}
</div>
)
}
}
import React, { Component, Fragment } from 'react';
class Info extends Component {
render () {
const { avatar, username } = this.props
return (
<Fragment>
<UserAvatar src={avatar} />
<UserProfile username={username} />
</Fragment>
)
}
}
До появления хуков функциональные компоненты не могли иметь состояния. Они получали пропы от родительского компонента в качестве первого параметра.
function MyComponent ({ name }) {
return (
<div className='message-box'>
Привет, {name}
</div>
)
}
Оптимизированная с точки зрения производительности версия React.Component
.
import React, { PureComponent } from 'react';
class MessageBox extends PureComponent {
···
}
Устанавливаем начальное состосние в constructor()
. Добавляем обработчики событий, таймеры и т.п. в componentDidMount()
, затем удаляем их в componentWillUnmount()
.
constructor (props) // Перед рендерингом
componentWillMount() // Не рекомендуется использовать
render() // Рендеринг
componentDidMount() // После рендеринга (DOM доступен)
componentWillUnmount() // Перед удалением из DOM
componentDidCatch() // Перехват ошибок
Вызывается при изменении состояния или пропов. Не вызывается при первом рендеринге.
componentDidUpdate (prevProps, prevState, snapshot) // При использовании `setState()` не забывайте сравнивать пропы
shouldComponentUpdate (newProps, newState) // Если возвращается `false`, повторный рандеринг не выполняется
render() // Рендеринг
componentDidUpdate (prevProps, prevState) // Выполнение операций с DOM
import React, { useState } from 'react';
function Example() {
// Хук `useState()` возвращает начальное состояние (`count`) и функцию для его обновления (`setCount`) - геттер и сеттер
const [count, setCount] = useState(0);
return (
<div>
<p>Вы нажали {count} раз</p>
<button onClick={() => setCount(count + 1)}>
Нажми на меня
</button>
</div>
);
}
Хук useEffect()
представлет собой сочетание методов жизненного цикла componentDidMount()
, componentDidUpdate()
и componentWillUnmount()
.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Аналогично `componentDidMount()` и `componentDidUpdate()`
useEffect(() => {
// Обновляем заголовок документа с помощью браузерного API
document.title = `Вы нажали ${count} раз`;
}, [count]);
return (
<div>
<p>Вы нажали {count} раз</p>
<button onClick={() => setCount(count + 1)}>
Нажми на меня
</button>
</div>
);
}
Позволяют получить доступ к узлам DOM.
class MyComponent extends Component {
render () {
return (
<div>
<input ref={el => this.input = el} />
</div>
)
}
componentDidMount () {
this.input.focus()
}
}
Передаем функции в атрибуты вроде onChange()
.
class MyComponent extends Component {
render () {
<input type="text"
value={this.state.value}
onChange={event => this.onChange(event)} />
}
onChange (event) {
this.setState({ value: event.target.value })
}
}
Передаем src="..."
в субкомпонент.
<VideoPlayer src="video.mp4" />
class VideoPlayer extends Component {
render () {
return <VideoEmbed {...this.props} />
}
}
Встроенные стили
const style = { height: 10 }
return <div style={style}></div>
return <div style={{ margin: 0, padding: 0 }}></div>
<Fragment>
{showMyComponent
? <MyComponent />
: <OtherComponent />}
</Fragment>
class TodoList extends Component {
render () {
const { items } = this.props
return <ul>
{items.map(item =>
<TodoItem item={item} key={item.key} />)}
</ul>
}
}
<Fragment>
{showPopup && <Popup />}
...
</Fragment>
// Массивы
render () {
// Не забывайте про ключи
return [
<li key="A">Первый элемент</li>,
<li key="B">Второй элемент</li>
]
}
// Фрагменты
render () {
// Фрагментам не нужны ключи
return (
<Fragment>
<li>Первый элемент</li>
<li>Второй элемент</li>
</Fragment>
)
}
Перехватываем ошибки в componentDidCatch()
.
class MyComponent extends Component {
···
componentDidCatch (error, info) {
this.setState({ error })
}
}
Позволяют рендерить this.props.children
в любом узле DOM.
render () {
return React.createPortal(
this.props.children,
document.getElementById('menu')
)
}
Используем ReactDOM.hydrate()
вместо ReactDOM.render()
, если рендерим статическую разметку, полученную от сервера.
const el = document.getElementById('app')
ReactDOM.hydrate(<App />, el)
Проверка типов.
import PropTypes from 'prop-types';
Свойство | Описание |
---|---|
any | Что угодно |
string | Строка |
number | Число |
func | Функция |
bool | True или false |
oneOf(any) | Тип Enum |
oneOfType(type array) | Тип Union |
array | Массив |
arrayOf(…) | Массив типов |
object | Объект |
objectOf(…) | Объект типов |
instanceOf(…) | Экземлпяр |
shape(…) | Форма |
element | React-элемент |
node | Узел DOM |
(···).isRequired | Обязательный |
MyComponent.propTypes = {
email: PropTypes.string,
seats: PropTypes.number,
callback: PropTypes.func,
isClosed: PropTypes.bool,
any: PropTypes.any
}
MyCo.propTypes = {
name: PropTypes.string.isRequired
}
MyCo.propTypes = {
// React-элемент
element: PropTypes.element,
// Число, строка, элемент или массив
node: PropTypes.node
}
MyCo.propTypes = {
direction: PropTypes.oneOf([
'левый', 'правый'
])
}
MyCo.propTypes = {
customProp: (props, key, componentName) => {
if (!/matchme/.test(props[key])) {
return new Error('Валидация провалена!')
}
}
}
Используем .arrayOf()
, .objectOf()
, .instanceOf()
, .shape()
.
MyCo.propTypes = {
list: PropTypes.array,
ages: PropTypes.arrayOf(PropTypes.number),
user: PropTypes.object,
user: PropTypes.objectOf(PropTypes.number),
message: PropTypes.instanceOf(Message)
}
MyCo.propTypes = {
user: PropTypes.shape({
name: PropTypes.string,
age: PropTypes.number
})
}
describe('Выбор цвета', () => {
beforeAll(() => {
/* Запускается перед всеми тестами */
})
afterAll(() => {
/* Запускается после всех тестов */
})
beforeEach(() => {
/* Запускается перед каждым тестом */
})
afterEach(() => {
/* Запускается после каждого теста */
})
test('Выбираем цвет', () => {
const actual = fn(['Alice', 'Bob', 'John'])
expect(actual).toEqual(['Pink Alice', 'Pink Bob', 'Pink John'])
})
})
Использование поиска совпадений, официальная документация
expect(42).toBe(42) // Строгое равенство (===)
expect(42).not.toBe(3) // Строгое неравенство (!==)
expect([1, 2]).toEqual([1, 2]) // Глубокое сравнение
expect({ a: undefined, b: 2 }).toEqual({ b: 2 }) // Глубокое сравнение
expect({ a: undefined, b: 2 }).not.toStrictEqual({ b: 2 }) // Строгое сравнение
// Совпадает с истинными значениями
expect('foo').toBeTruthy()
// Совпадает с ложными значениями
expect('').toBeFalsy()
// Совпадает только с `null`
expect(null).toBeNull()
// Совпадает только с `undefined`
expect(undefined).toBeUndefined()
// Значение должно быть определено
expect(7).toBeDefined()
// Совпадает с `true` или `false`
expect(true).toEqual(expect.any(Boolean))
expect(2).toBeGreaterThan(1)
expect(1).toBeGreaterThanOrEqual(1)
expect(1).toBeLessThan(2)
expect(1).toBeLessThanOrEqual(1)
expect(0.2 + 0.1).toBeCloseTo(0.3, 5)
expect(NaN).toEqual(expect.any(Number))
expect('длинная строка').toMatch('стр')
expect('строка').toEqual(expect.any(String))
expect('кофе').toMatch(/ф/)
expect('пицца').not.toMatch('кофе')
expect(['пицца', 'кофе']).toEqual([expect.stringContaining('цц'), expect.stringMatching(/ф/)])
expect([]).toEqual(expect.any(Array))
expect(['Alice', 'Bob', 'John']).toHaveLength(3)
expect(['Alice', 'Bob', 'John']).toContain('Alice')
expect([{ a: 1 }, { a: 2 }]).toContainEqual({ a: 1 })
expect(['Alice', 'Bob', 'John']).toEqual(expect.arrayContaining(['Alice', 'Bob']))
expect({ a: 1 }).toHaveProperty('a')
expect({ a: 1 }).toHaveProperty('a', 1)
expect({ a: { b: 1 } }).toHaveProperty('a.b')
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 })
expect({ a: 1, b: 2 }).toMatchObject({
a: expect.any(Number),
b: expect.any(Number)
})
expect([{ a: 1 }, { b: 2 }]).toEqual([
expect.objectContaining({ a: expect.any(Number) }),
expect.anything()
])
// const fn = () => { throw new Error('Упс!') }
expect(fn).toThrow()
expect(fn).toThrow('Упс')
expect(fn).toThrowErrorMatchingSnapshot()
expect(node).toMatchSnapshot()
expect(user).toMatchSnapshot({
date: expect.any(Date)
})
expect(user).toMatchInlineSnapshot()
// const fn = jest.fn()
// const fn = jest.fn().mockName('Единорог') - именованная фикция
expect(fn).toBeCalled() // Функция была вызвана
expect(fn).not.toBeCalled() // Функция *не была* вызвана
expect(fn).toHaveBeenCalledTimes(1) // Функция была вызвана один раз
expect(fn).toBeCalledWith(arg1, arg2) // Любой вызов функции сопровождался указанными аргументами
expect(fn).toHaveBeenLastCalledWith(arg1, arg2) // При последнем вызове функции, ей были переданы указанные аргументы
expect(fn).toHaveBeenNthCalledWith(args) // Определенный вызов функции сопровождался указанными аргументами
expect(fn).toHaveReturnedTimes(2) // Функция возвращает значения без ошибок
expect(fn).toHaveReturnedWith(value) // Функция возвращает указанное значение
expect(fn).toHaveLastReturnedWith(value) // Последний вызов функции вернул указанное значение
expect(fn).toHaveNthReturnedWith(value) // Определенный вызов функции вернул указанное значение
expect(fn.mock.calls).toEqual([['first', 'call', 'args'], ['second', 'call', 'args']]) // Несколько вызовов
expect(fn.mock.calls[0][0]).toBe(2) // fn.mock.calls[0][0] — первый аргумент первого вызова
"Алиасы" (синонимы)
toBeCalled
→toHaveBeenCalled
toBeCalledWith
→toHaveBeenCalledWith
lastCalledWith
→toHaveBeenLastCalledWith
nthCalledWith
→toHaveBeenNthCalledWith
toReturnTimes
→toHaveReturnedTimes
toReturnWith
→toHaveReturnedWith
lastReturnedWith
→toHaveLastReturnedWith
nthReturnedWith
→toHaveNthReturnedWith
expect(new A()).toBeInstanceOf(A)
expect(() => {}).toEqual(expect.any(Function))
expect('пицца').toEqual(expect.anything())
test('Разрешенным значением должен быть "лимон"', () => {
expect.assertions(1)
// Не забудьте добавить оператор `return`
return expect(Promise.resolve('лимон')).resolves.toBe('лимон')
return expect(Promise.reject('осьминог')).rejects.toBeDefined()
return expect(Promise.reject(Error('пицца'))).rejects.toThrow()
})
Или с помощью async/await
:
test('Разрешенным значением должен быть "лимон"', async () => {
expect.assertions(2)
await expect(Promise.resolve('лимон')).resolves.toBe('лимон')
await expect(Promise.resolve('лимон')).resolves.not.toBe('осьминог')
})
Смотрите больше примеров в официальной документации Jest.
Хорошей практикой считается определение количества ожидаемых утверждений (assertions) в асинхронных тестах, тест провалится, если утверждения не будут вызваны.
test('Асинхронный тест', () => {
expect.assertions(3) // В процессе тестирования вызывается ровно три утверждения
// или
expect.hasAssertions() // В процессе тестирования вызывается по крайней мере одно утверждение
// Далее следуют асинхронные тесты
})
Обратите внимание, что вы также можете делать это в файле, за пределами любых describe
и test
:
beforeEach(expect.hasAssertions)
Это обеспечит присутствие хотя бы одного утверждения в процессе тестирования. Это также подходит для случаев, когда ожидается конкретное число утверждений - expect.assertions(3)
.
test('Асинхронный тест', async () => {
expect.assertions(1)
const result = await runAsyncOperation()
expect(result).toBe(true)
})
test('Асинхронный тест', () => {
expect.assertions(1)
return runAsyncOperation().then(result => {
expect(result).toBe(true)
})
})
Утверждение должно быть обернуто в блок try/catch
, иначе Jest будет игнорировать ошибки:
test('Асинхронный тест', done => {
expect.assertions(1)
runAsyncOperation()
setTimeout(() => {
try {
const result = getAsyncOperationResult()
expect(result).toBe(true)
done()
} catch (err) {
done.fail(err)
}
})
})
test('Вызов коллбека', () => {
const callback = jest.fn()
fn(callback)
expect(callback).toBeCalled()
expect(callback.mock.calls[0][1].baz).toBe('пицца') // Второй аргумент первого вызова
// Отслеживаем первый и последний аргументы, но игнорируем второй
expect(callback).toHaveBeenLastCalledWith('мясо', expect.anything(), 'маргарита');
})
Вы также можете использовать снимки:
test('Вызов коллбека', () => {
const callback = jest.fn().mockName('Единорог')
fn(callback)
expect(callback).toMatchSnapshot()
// ->
// [MockFunction Единорог] {
// "calls": Array [
// ...
})
И передавать реализацию в функцию jest.fn()
:
const callback = jest.fn(() => true)
Ваши фикции могут возвращать значения:
const callback = jest.fn().mockReturnValue(true);
const callbackOnce = jest.fn().mockReturnValueOnce(true);
Или разрешать значения:
const promise = jest.fn().mockResolvedValue(true);
const promiseOnce = jest.fn().mockResolvedValueOnce(true);
Они даже могут отклонять значения:
const failedPromise = jest.fn().mockRejectedValue("Роскосмос, у нас случилась оказия");
const failedPromiseOnce = jest.fn().mockRejectedValueOnce("Роскосмос, у нас случилась оказия");
Вы можете комбинировать названные подходы:
const callback = jest.fn()
.mockReturnValueOnce(false)
.mockReturnValue(true);
// ->
// вызов 1: false
// вызов 2+: true
jest.mock('lodash/memoize', () => a => a) // Должна присутствовать реальная функция lodash/memoize
jest.mock('lodash/memoize', () => a => a, { virtual: true }) // Реальная функция lodash/memoize может отсутствовать
Обратите внимание, при использовании babel-jest
вызовы jest.mock()
будут подниматься в начало блока кода. Используйте jest.doMock()
для предотвращения подобного поведения.
-
Создаем файл, например,
__mocks__/lodash/memoize.js
:module.exports = a => a
-
Добавлем его в тест:
jest.mock('lodash/memoize')
const spy = jest.spyOn(console, 'log').mockImplementation(() => {})
expect(console.log.mock.calls).toEqual([['dope'], ['nope']])
spy.mockRestore()
const spy = jest.spyOn(ajax, 'request').mockImplementation(() => Promise.resolve({ success: true }))
expect(spy).toHaveBeenCalled()
spy.mockRestore()
Новая версия:
const location = {}
const getTitle = jest.spyOn(location, 'title', 'get').mockImplementation(() => 'пицца')
const setTitle = jest.spyOn(location, 'title', 'set').mockImplementation(() => {})
Старая версия:
const getTitle = jest.fn(() => 'пицца')
const setTitle = jest.fn()
const location = {}
Object.defineProperty(location, 'title', {
get: getTitle,
set: setTitle
})
Для одной фикции:
fn.mockClear() // Удаляет дату использования фикции (fn.mock.calls, fn.mock.instances)
fn.mockReset() // Удаляет любые возвращенные значения или реализации фикции
fn.mockRestore() // Сбрасывает и восстанавливает первоначальную реализацию
Обратите внимание: mockRestore()
работает только применительно к фикциям, созданным с помощью jest.spyOn()
.
Для всех фикций:
jest.clearAllMocks()
jest.resetAllMocks()
jest.restoreAllMocks()
jest.mock('fs')
const fs = require('fs') // Модуль с "моком"
const fs = require.requireActual('fs') // Исходный модуль
Позволяет писать синхронные тесты для кода, в котором используются нативные таймеры (setTimeout
, setInterval
, clearTimeout
, clearInterval
).
// Разрешаем использование фиктивных таймеров
jest.useFakeTimers()
test('Убить время', () => {
const callback = jest.fn()
// Запускаем код, в котором используются `setTimeout()` или `setInterval()`
const actual = someFunctionThatUseTimers(callback)
// Перематываем до выполнения всех таймеров
jest.runAllTimers()
// Синхронно проверяем результаты
expect(callback).toHaveBeenCalledTimes(1)
})
Или настраиваем таймеры по времени с помощью advanceTimersByTime():
// Разрешаем использование фиктивных таймеров
jest.useFakeTimers()
test('Убить время', () => {
const callback = jest.fn()
// Запускаем код, в котором используются `setTimeout()` или `setInterval()`
const actual = someFunctionThatUseTimers(callback)
// Перематываем на 250 мс
jest.advanceTimersByTime(250)
// Синхронно проверяем результаты
expect(callback).toHaveBeenCalledTimes(1)
})
В особых случаях используется jest.runOnlyPendingTimers().
Обратите внимание: jest.useFakeTimers()
следует вызывать только для использования других методов фиктивных таймеров.
Запускаем одни и те же тесты с разными данными:
test.each([[3, 2, 1], [1, 2, 3], [2, 1, 3]])('.add(%s, %s)', (a, b, expected) => {
expect(a + b).toBe(expected)
})
Или с помощью шаблонных литералов:
test.each`
a | b | expected
${3} | ${2} | ${1}
${1} | ${2} | ${3}
${2} | ${1} | ${3}
`('Возвращает $expected при сложении $a и $b', ({ a, b, expected }) => {
expect(a + b).toBe(expected)
})
Или на уровне describe
:
describe.each([['mobile'], ['tablet'], ['desktop']])('проверка выполнения за %s', (viewport) => {
test('отображение загруженной страницы', () => {
//
})
})
Не запускать указанные тесты:
describe.skip('makePoniesPink'...
tests.skip('сделать каждого пони розовым'...
Запускать только указанные тесты:
describe.only('makePoniesPink'...
tests.only('сделать каждого пони розовым'...
Node.js и Jest will кэшируют запрашиваемые (require
) модули. Для тестирования модулей с побочными эффектами необходимо очищать реестр модулей между тестами:
const modulePath = '../module-to-test'
afterEach(() => {
jest.resetModules()
})
test('первый тест', () => {
// Подготовка условия для первого теста
const result = require(modulePath)
expect(result).toMatchSnapshot()
})
test('второй тест', () => {
// Подготовка условия для первого теста
const fn = () => require(modulePath)
expect(fn).toThrow()
})
Хранилище создается с помощью редуктора, принимающего текущее состояние и возвращающего новое состояние на основе полученной операции.
import { createStore } from 'redux'
// Редуктор
function counter (state = { value: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { value: state.value + 1 }
case 'DECREMENT':
return { value: state.value - 1 }
default:
return state
}
}
const store = createStore(counter)
// Опционально, в качестве второго аргумента можно передать начальное состояние - `initialState`
const store = createStore(counter, { value: 0 })
Для того, чтобы измененить состояние хранилища, необходимо отправить операцию в редуктор:
let store = createStore(counter)
// Отправляем операции; это приводит к изменению состояния
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })
// Получаем текущее состояние
store.getState()
// Регистрируем изменения
store.subscribe(() => { ... })
Компонент <Provider>
делает хранилище доступным для компонентов. Компонент подключается к хранилищу с помощью метода connect()
:
import { Provider } from 'react-redux'
React.render(
<Provider store={store}>
<App />
</Provider>, mountNode)
import { connect } from 'react-redux'
// Функциональный компонент
function App ({ message, onMessageClick }) {
return (
<div onClick={() => onMessageClick('Привет!')}>
{message}
</div>
)
}
// Привязываем `state` к `props`:
function mapState (state) {
return { message: state.message }
}
// Привязываем `dispatch` к `props`:
function mapDispatch (dispatch) {
return {
onMessageClick (message) {
dispatch({ type: 'click', message })
}
}
}
// Подключаем их
export default connect(mapState, mapDispatch)(App)
const reducer = combineReducers({
counter, user, store
})
Посредники (middlewares) - это декораторы для dispatch()
, позволяющие принимать операции и выполнять определяемые ими задачи:
// Бесполезный посредник
const logger = store => dispatch => action { dispatch(action) }
const logger = store => {
// Данная функция запускается в `createStore()`
// и возвращает декоратор для `dispatch()`
return dispatch => {
// Также запускается в `createStore()`
// и возвращает новую функцию `dispatch()`
return action => {
// Запускается при каждом выполнении `dispatch()`
}
}
}
const enhancer = applyMiddleware(logger, thunk, ...)
const store = createStore(reducer, {}, enhancer)
-
Названия компонентов должны начинаться с большой буквы.
-
Компоненты должны быть маленькими и отвечать за выполнение одной задачи.
-
Компоненты должны иметь небольшое описание.
/**
*
* Author: {...}
* Description: {...}
* Dependencies: {...}
*
**/
const SampleComponent = () => {
return (
<div>
Пример компонента
</div>
);
}
export default SampleComponent;
-
В коде должен использоваться синтаксис ES6.
-
Названия переменных и функций, которые не являются константами и конструкторами, соответственно, и которые состоят из нескольких слов, должны быть в стиле lowerCamelCased.
-
Предопределенные константы именуются в верхнем регистре, слова разделяются нижним подчеркиванием - UPPER_UNDERSCORED.
-
При проверке типа переменной, название типа указывается в кавычках (не оборачивается в фигурные скобки), а для сравнения используется оператор строго равенства:
if (typeof myVariable === 'string') {
// ...
}
- В простых случаях вместо оператора
if/else
должен использоваться тернарный оператор:
// if/else
if (condition) {
//...
} else {
//...
}
// тернарный оператор
const myVariable = condition ? exprIfTrue : exprIfFalse
- Вместо контейнеров следует использовать фрагменты:
//...
render() {
return (
<Fragment>
<p>Какой-то текст.</p>
<h2>Заголовок</h2>
<p>Еще текст.</p>
<h2>Другой заголовок</h2>
<p>И снова текст.</p>
</Fragment>
);
}
Сокращенный вариант:
//...
render() {
return (
<>
<p>Какой-то текст.</p>
<h2>Заголовок</h2>
<p>Еще текст.</p>
<h2>Другой заголовок</h2>
<p>И снова текст.</p>
</>
);
}
-
Все файлы, относящиеся к одному компоненту, должны находиться в одной директории
-
Следует отдавать предпочтение функциональным компонентам.
-
В качестве обработчиков событий не следует использовать анонимные функции.
-
Следует избегать использования встроенных стилей.
-
Чтобы скрыть компонент, нужно вернуть
null
при его рендеринге. -
Компоненты высшего порядка должны использоваться только для решения проблем взаимодействия компонентов между собой.
-
Индексы элементов массива не должны использоваться в качестве ключей (
keys
). -
В JSX вместо тернарного оператора могут использоваться короткие вычисления:
const sampleComponent = () => {
return isTrue ? <p>Истина</p> : null
};
const sampleComponent = () => {
return isTrue && <p>Истина</p>
};