JavaScript, який потрібно знати для React
Vadym Grabovyi 🇺🇦
Posted on July 18, 2022
Це переклад оригінальної статті JavaScript to Know for React від Kent C. Dodds.
Одна з речей, які мені найбільше подобаються в React порівняно з іншими фреймворками, які я використовував, це те, наскільки ви не виходите за рамки звичайного JavaScript, коли використовуєте його. Немає шаблону DSL (JSX компілюється у JavaScript), API компонентів став простішим із додаванням React Hooks, а фреймворк пропонує вам лише декілька абстракцій за межами основних проблем UI, які він призначений вирішувати.
Через це вивчення функцій JavaScript справді є доцільним для ефективного створення застосунків за допомогою React. Отже, ось декілька фіч JavaScript, на вивчення яких я рекомендую вам витратити деякий час, щоб ви могли максимально ефективно працювати з React.
Перш ніж ми перейдемо до деяких синтаксичних речей, ще одна річ, яку дуже корисно зрозуміти для React, це концепція замикання функції. Чудовий опис цієї концепції є тут: mdn.io/closure.
Гаразд, давайте перейдемо до фіч JS, які ви захочете знати для React.
Шаблонні літерали
Шаблонні строки схожі на звичайні, але із суперсилами:
const greeting = 'Hello'
const subject = 'World'
console.log(`${greeting} ${subject}!`) // Hello World!
// this is the same as:
console.log(greeting + ' ' + subject + '!')
// in React:
function Box({className, ...props}) {
return <div className={`box ${className}`} {...props} />
}
Скорочені назви властивостей
Це настільки звично і корисно, що я роблю це, не замислюючись.
const a = 'hello'
const b = 42
const c = {d: [true, false]}
console.log({a, b, c})
// this is the same as:
console.log({a: a, b: b, c: c})
// in React:
function Counter({initialCount, step}) {
const [count, setCount] = useCounter({initialCount, step})
return <button onClick={setCount}>{count}</button>
}
MDN: Object initializer New notations in ECMAScript 2015
Стрілкові функції
Функції зі стрілками — ще один спосіб написання функцій у JavaScript, хоч вони і мають декілька семантичних відмінностей. На щастя для нас, у світі React нам не потрібно так сильно турбуватися про this
, оскільки ми використовуємо в нашому проекті хуки (а не класи). Але стрілкова функція дозволяє використовувати коротші анонімні функції та неявні повернення, тож ви подивитесь і захочете використовувати їх більше в своєму коді.
const getFive = () => 5
const addFive = a => a + 5
const divide = (a, b) => a / b
// this is the same as:
function getFive() {
return 5
}
function addFive(a) {
return a + 5
}
function divide(a, b) {
return a / b
}
// in React:
function TeddyBearList({teddyBears}) {
return (
<ul>
{teddyBears.map(teddyBear => (
<li key={teddyBear.id}>
<span>{teddyBear.name}</span>
</li>
))}
</ul>
)
}
Щодо наведеного вище прикладу слід звернути увагу на відкриття та закриття круглих дужок
(
. Це поширений спосіб використання можливостей неявного повернення стрілочної функції під час роботи з JSX.
Деструктуризація
Декструкрутизація це, мабуть, моя найулюбленіша фіча в JavaScript. Я деструктуризую об'єкти та масиви постійно (і якщо ви використовуєте useState
, то ви теж, ось так). Мені дуже подобається їх декларативність.
// const obj = {x: 3.6, y: 7.8}
// makeCalculation(obj)
function makeCalculation({x, y: d, z = 4}) {
return Math.floor((x + d + z) / 3)
}
// this is the same as
function makeCalculation(obj) {
const {x, y: d, z = 4} = obj
return Math.floor((x + d + z) / 3)
}
// which is the same as
function makeCalculation(obj) {
const x = obj.x
const d = obj.y
const z = obj.z === undefined ? 4 : obj.z
return Math.floor((x + d + z) / 3)
}
// in React:
function UserGitHubImg({username = 'ghost', ...props}) {
return <img src={`https://github.com/${username}.png`} {...props} />
}
Обов'язково прочитайте ту статтю на MDN. Ви точно вивчите щось нове для себе. Коли закінчите, спробуйте відрефакторити цей код використавши лише одну строчку з деструктуризацією:
function nestedArrayAndObject() {
// refactor this to a single line of destructuring...
const info = {
title: 'Once Upon a Time',
protagonist: {
name: 'Emma Swan',
enemies: [
{name: 'Regina Mills', title: 'Evil Queen'},
{name: 'Cora Mills', title: 'Queen of Hearts'},
{name: 'Peter Pan', title: `The boy who wouldn't grow up`},
{name: 'Zelena', title: 'The Wicked Witch'},
],
},
}
// const {} = info // <-- replace the next few `const` lines with this
const title = info.title
const protagonistName = info.protagonist.name
const enemy = info.protagonist.enemies[3]
const enemyTitle = enemy.title
const enemyName = enemy.name
return `${enemyName} (${enemyTitle}) is an enemy to ${protagonistName} in "${title}"`
}
Параметри за замовчуванням
Це ще одна фіча, яку я використовую постійно. Це дійсно потужний спосіб декларативного вираження значень параметрів за замовчуванням для ваших функцій.
// add(1)
// add(1, 2)
function add(a, b = 0) {
return a + b
}
// is the same as
const add = (a, b = 0) => a + b
// is the same as
function add(a, b) {
b = b === undefined ? 0 : b
return a + b
}
// in React:
function useLocalStorageState({
key,
initialValue,
serialize = v => v,
deserialize = v => v,
}) {
const [state, setState] = React.useState(
() => deserialize(window.localStorage.getItem(key)) || initialValue,
)
const serializedState = serialize(state)
React.useEffect(() => {
window.localStorage.setItem(key, serializedState)
}, [key, serializedState])
return [state, setState]
}
Rest/Spread
Синтаксис ...
можна сприймати як свого роду синтаксис "колекцій", що працює з колекціями значень. Я постійно його використовую і вам також наполегливо рекомендую вивчити як і де його застосовувати. Насправді, він має різні значення в залежності від контексту, тож вивчення нюансів буде вам корисним.
const arr = [5, 6, 8, 4, 9]
Math.max(...arr)
// is the same as
Math.max.apply(null, arr)
const obj1 = {
a: 'a from obj1',
b: 'b from obj1',
c: 'c from obj1',
d: {
e: 'e from obj1',
f: 'f from obj1',
},
}
const obj2 = {
b: 'b from obj2',
c: 'c from obj2',
d: {
g: 'g from obj2',
h: 'g from obj2',
},
}
console.log({...obj1, ...obj2})
// is the same as
console.log(Object.assign({}, obj1, obj2))
function add(first, ...rest) {
return rest.reduce((sum, next) => sum + next, first)
}
// is the same as
function add() {
const first = arguments[0]
const rest = Array.from(arguments).slice(1)
return rest.reduce((sum, next) => sum + next, first)
}
// in React:
function Box({className, ...restOfTheProps}) {
const defaultProps = {
className: `box ${className}`,
children: 'Empty box',
}
return <div {...defaultProps} {...restOfTheProps} />
}
ES Модулі
Якщо ви створюєте програму за допомогою сучасних інструментів, ймовірно, вона підтримує модулі. Тож варто дізнатися, як працює синтаксис, оскільки будь-яка програма навіть незначного розміру, вірогідно, потребуватиме використання модулів для повторного використання та організації коду.
export default function add(a, b) {
return a + b
}
/*
* import add from './add'
* console.assert(add(3, 2) === 5)
*/
export const foo = 'bar'
/*
* import {foo} from './foo'
* console.assert(foo === 'bar')
*/
export function subtract(a, b) {
return a - b
}
export const now = new Date()
/*
* import {subtract, now} from './stuff'
* console.assert(subtract(4, 2) === 2)
* console.assert(now instanceof Date)
*/
// dynamic imports
import('./some-module').then(
allModuleExports => {
// the allModuleExports object will be the same object you get if you had
// used: import * as allModuleExports from './some-module'
// the only difference is this will be loaded asynchronously which can
// have performance benefits in some cases
},
error => {
// handle the error
// this will happen if there's an error loading or running the module
},
)
// in React:
import React, {Suspense, Fragment} from 'react'
// dynamic import of a React component
const BigComponent = React.lazy(() => import('./big-component'))
// big-component.js would need to "export default BigComponent" for this to work
В якості альтернативи - я виступав із доповіддю про цей синтаксис і ви можете подивитись її тут
Тернарний оператор
Обожнюю тернарні оператори. Вони неймовірно декларативні. Особливо в JSX.
const message = bottle.fullOfSoda
? 'The bottle has soda!'
: 'The bottle may not have soda :-('
// is the same as
let message
if (bottle.fullOfSoda) {
message = 'The bottle has soda!'
} else {
message = 'The bottle may not have soda :-('
}
// in React:
function TeddyBearList({teddyBears}) {
return (
<React.Fragment>
{teddyBears.length ? (
<ul>
{teddyBears.map(teddyBear => (
<li key={teddyBear.id}>
<span>{teddyBear.name}</span>
</li>
))}
</ul>
) : (
<div>There are no teddy bears. The sadness.</div>
)}
</React.Fragment>
)
}
Я розумію, що тернарні оператори можуть викликати огиду в деяких людей, яким довелося терпіти спроби зрозуміти їх, перш ніж з’явився prettier і відформатував наш код. Якщо ви ще не використовуєте prettier, я наполегливо раджу вам це зробити. Prettier зробить ваші тернарні оператори набагато легшими для читання.
MDN: Conditional (ternary) operator
Методи масивів
Масиви фантастичні і я часто використовую їх методи, зокрема такі:
- find
- some
- every
- includes
- map
- filter
- reduce
Ось декілька прикладів:
const dogs = [
{
id: 'dog-1',
name: 'Poodle',
temperament: [
'Intelligent',
'Active',
'Alert',
'Faithful',
'Trainable',
'Instinctual',
],
},
{
id: 'dog-2',
name: 'Bernese Mountain Dog',
temperament: ['Affectionate', 'Intelligent', 'Loyal', 'Faithful'],
},
{
id: 'dog-3',
name: 'Labrador Retriever',
temperament: [
'Intelligent',
'Even Tempered',
'Kind',
'Agile',
'Outgoing',
'Trusting',
'Gentle',
],
},
]
dogs.find(dog => dog.name === 'Bernese Mountain Dog')
// {id: 'dog-2', name: 'Bernese Mountain Dog', ...etc}
dogs.some(dog => dog.temperament.includes('Aggressive'))
// false
dogs.some(dog => dog.temperament.includes('Trusting'))
// true
dogs.every(dog => dog.temperament.includes('Trusting'))
// false
dogs.every(dog => dog.temperament.includes('Intelligent'))
// true
dogs.map(dog => dog.name)
// ['Poodle', 'Bernese Mountain Dog', 'Labrador Retriever']
dogs.filter(dog => dog.temperament.includes('Faithful'))
// [{id: 'dog-1', ..etc}, {id: 'dog-2', ...etc}]
dogs.reduce((allTemperaments, dog) => {
return [...allTemperaments, ...dog.temperament]
}, [])
// [ 'Intelligent', 'Active', 'Alert', ...etc ]
// in React:
function RepositoryList({repositories, owner}) {
return (
<ul>
{repositories
.filter(repo => repo.owner === owner)
.map(repo => (
<li key={repo.id}>{repo.name}</li>
))}
</ul>
)
}
Оператор нульового злиття (??)
Якщо значення дорівнює null
чи undefined
, ймовірно, ви захочете задати дефолтне значення:
// here's what we often did for this:
x = x || 'some default'
// but this was problematic for numbers or booleans where "0" or "false" are valid values
// So, if we wanted to support this:
add(null, 3)
// here's what we had to do before:
function add(a, b) {
a = a == null ? 0 : a
b = b == null ? 0 : b
return a + b
}
// here's what we can do now
function add(a, b) {
a = a ?? 0
b = b ?? 0
return a + b
}
// in React:
function DisplayContactName({contact}) {
return <div>{contact.name ?? 'Unknown'}</div>
}
MDN: Nullish coalescing operator
Оператор опціональної послідовності
Також відомий як «Оператор Елвіса», він дозволяє безпечно отримувати доступ до властивостей і викликати функції, які можуть існувати або не існувати. Перед цим оператором ми використовували хаки, що покладались на логіку true/false
.
// what we did before optional chaining:
const streetName = user && user.address && user.address.street.name
// what we can do now:
const streetName = user?.address?.street?.name
// this will run even if options is undefined (in which case, onSuccess would be undefined as well)
// however, it will still fail if options was never declared,
// since optional chaining cannot be used on a non-existent root object.
// optional chaining does not replace checks like if (typeof options == "undefined")
const onSuccess = options?.onSuccess
// this will run without error even if onSuccess is undefined (in which case, no function will be called)
onSuccess?.({data: 'yay'})
// and we can combine those things into a single line:
options?.onSuccess?.({data: 'yay'})
// and if you are 100% certain that onSuccess is a function if options exists
// then you don't need the extra ?. before calling it. Only use ?. in situations
// where the thing on the left might not exist.
options?.onSuccess({data: 'yay'})
// in React:
function UserProfile({user}) {
return (
<div>
<h1>{user.name}</h1>
<strong>{user.bio?.short ?? 'No bio provided'}</strong>
</div>
)
}
Застереження щодо цього: якщо ви виявите, що використовуєте ?.
занадто часто у вашому коді, перегляньте місце, звідки ці значення походять, і переконайтеся, що вони щоразу повертають значення, які повинні.
Проміси та async/await
Це велика тема і вам може знадобитися трохи практики та часу, щоб попрацювати з промісами і добре розібратись. Вони є скрізь в екосистемі JavaScript і завдяки тому, що React укорінився в ній, там вони також скрізь (насправді, сам React використовує проміси всередині бібліотеки).
Проміси допомагають керувати асинхронним кодом, їх повертають багато DOM API, а також сторонні бібліотеки. Синтаксис Async/await — це спеціальний синтаксис для роботи з промісами. Вони йдуть рука об руку.
function promises() {
const successfulPromise = timeout(100).then(result => `success: ${result}`)
const failingPromise = timeout(200, true).then(null, error =>
Promise.reject(`failure: ${error}`),
)
const recoveredPromise = timeout(300, true).then(null, error =>
Promise.resolve(`failed and recovered: ${error}`),
)
successfulPromise.then(log, logError)
failingPromise.then(log, logError)
recoveredPromise.then(log, logError)
}
function asyncAwaits() {
async function successfulAsyncAwait() {
const result = await timeout(100)
return `success: ${result}`
}
async function failedAsyncAwait() {
const result = await timeout(200, true)
return `failed: ${result}` // this would not be executed
}
async function recoveredAsyncAwait() {
try {
const result = await timeout(300, true)
return `failed: ${result}` // this would not be executed
} catch (error) {
return `failed and recovered: ${error}`
}
}
successfulAsyncAwait().then(log, logError)
failedAsyncAwait().then(log, logError)
recoveredAsyncAwait().then(log, logError)
}
function log(...args) {
console.log(...args)
}
function logError(...args) {
console.error(...args)
}
// This is the mothership of all things asynchronous
function timeout(duration = 0, shouldReject = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldReject) {
reject(`rejected after ${duration}ms`)
} else {
resolve(`resolved after ${duration}ms`)
}
}, duration)
})
}
// in React:
function GetGreetingForSubject({subject}) {
const [isLoading, setIsLoading] = React.useState(false)
const [error, setError] = React.useState(null)
const [greeting, setGreeting] = React.useState(null)
React.useEffect(() => {
async function fetchGreeting() {
try {
const response = await window.fetch('https://example.com/api/greeting')
const data = await response.json()
setGreeting(data.greeting)
} catch (error) {
setError(error)
} finally {
setIsLoading(false)
}
}
setIsLoading(true)
fetchGreeting()
}, [])
return isLoading ? (
'loading...'
) : error ? (
'ERROR!'
) : greeting ? (
<div>
{greeting} {subject}
</div>
) : null
}
Підсумки
Звісно, існує багато корисних фіч в JS для створення застосунків на React, але ці одні із моїх улюблених і я помічаю, що використовую їх дуже часто. Сподіваюсь, вони стануть вам у пригоді.
Якщо у вас буде бажання заглибитись у цю тему, у мене є воркшоп, який я проводив і записав під час роботи у PayPal. Можливо, він буде вам корисним: ES6 and Beyond Workshop at PayPal
Успіхів!
Posted on July 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.