React's exposure to JavaScript when using it is one of the features I appreciate most about it when compared to other frameworks I've tried. The component API has only gotten simpler with the advent of React Hooks, there is no template DSL (JSX compiles to sensible JavaScript), and the framework offers very little abstraction outside the basic UI issues it is meant to address.
Because of this, mastering JavaScript features is highly recommended if you want to be successful creating React applications. In order to work with React as efficiently as possible, I'd advise you to spend some time studying the following JavaScript features.
Another thing that's incredibly helpful to understand for React is the idea of a function "closure," so let's talk about that before we go into syntax. A fantastic explanation of this idea may be found at mdn.io/closure.
Let's move on to the JS features you need to be aware of for React.
Template Literals
Regular strings with superpowers are what template literals resemble:
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} />
}
Shorthand property names
Now, since it's so frequent and practical, I do it without thinking.
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>
}
Arrow functions
Although there are some semantic differences, arrow functions offer an additional approach to construct functions in JavaScript. Fortunately for us in React land, we don't have to worry about "this" as much if we're utilizing hooks in our project (rather than classes), but you'll see and want to utilize arrow functions a lot because they allow for terser anonymous functions and implicit returns.
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>
)
}
The beginning and closing parentheses "(" in the example above are something to take note of. When working with JSX, this is a typical technique to take advantage of the arrow function's implicit return features.
Destructuring
I think my favourite JavaScript feature is destructuring. I frequently destructure objects and arrays (and you probably do too, assuming you're using useState). It's so declarative, which I adore.
// 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} />
}
Parameter defaults
This is yet another feature that I frequently use. It's a highly effective approach to define the default values for your functions declaratively.
// 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
The "..." syntax operates on a collection of values and can be compared to a "collection" syntax. I frequently utilize it, and I strongly advise that you find out how and when you can use it as well. It truly has varied connotations in other circumstances, so understanding the subtleties there will be beneficial.
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: 'h 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} />
}
ESModules
It's a good idea to grasp how the syntax works if you're building an app with modern technologies because it's likely that it supports modules. Any application, no matter how little, will probably need to use modules for code reuse and structure.
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
Ternaries
I enjoy ternaries. They have lovely declarative language. Particularly in JSX.
? '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>
)
}
Array Methods
I frequently utilize array methods since they are great! I probably employ these techniques the most frequently:
- find
- some
- every
- includes
- map
- filter
- reduce
Here are some examples:
{
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>
)
}
Nullish coalescing operator
You might wish to fallback to a default value if a variable is null or undefined:
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>
}
Optional chaining
This permits you to securely access attributes and call functions that may or may not exist. It is also referred to as the "Elvis Operator." Prior to optional chaining, we employed a crude workaround based on falsy/truthy-ness.
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>
)
}
A word of caution in this regard: if you notice that you frequently use the operator?. in your code, you may want to take into account the source of those values and ensure that it consistently returns the appropriate values.
Promises and async/await
It can take some time and effort to become proficient with them because this is a huge subject. Promises are prevalent throughout the JavaScript ecosystem, and given how well-established React is in that environment, they are also prevalent there (in fact, React itself uses promises internally).
Many DOM APIs and external libraries return promises to assist you in managing asynchronous code. For handling promises, a particular syntax known as async/await is used. They complement one another.
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
}
Conclusion
Although there are many language features that are helpful when developing React apps, here are some of my favourites that I frequently use. I sincerely hope you can use this.
I sincerely hope that the majority of you find the approach covered here to be helpful. Thank you for reading, and please feel free to leave any comments or questions in the comments section below.
0 Comments