Where do you even begin?
I would start with figuring out the basics, such as:
- The state needed and which component stores it (or should the state be stored in Redux?)
- Should the component be Class based or functional?
- Regardless how the component is created (Class/function), does the state depend on the previous state or will it be changed in a short interval?
- For functional component, use Hooks for lifecycle methods
- Accessibility implementation (and how to test them)
Now, let’s talk about some details…
Determine the state and which component stores it
Component state is used to store data that will be referenced inside the render function of the said component. For example:
Data caused by user interaction:
- User-entered input (typed values of form fields, e.g. a number of order quantity added to the cart)
- Current or selected item (the current
radio
button, the selectedoption
tag from a<select>
element) - Open/closed state of a modal or expanded/hidden state of a slide panel
Data from the server (e.g. list of products and their details from an API):
- When the parent component determines the “initial/seed data”, then the props may be assigned into the child component’s state. It may be helpful to leave a comment (at the top of the file) informing that the prop being assigned into the state is seed data.
Use destructuring
Array and object can be used to return multiple values in one single statement, the values can be unpacked using destructuring syntax.
Gotcha: when using array destructuring, the order of the unpacked values has to match the order of the returned values of the function.
function withArray() {
const [counter, setCounter] = useState(0); function increment() {} function decrement() {} return [counter, increment, decrement];
}// Unpack the values belowconst [counter, increment, decrement] = withArray(); // 👍 GOODconst [increment, decrement, counter] = withArray(); // 🔴 BAD
Object destructuring is more recommended because the order of the values doesn’t matter, making the code less error-prone.
Using the same function definition that we name withObject
that returns an object instead of an array:
return { counter, increment, decrement }; // ES6 object shorthand
Unpack the values using object destructuring like so:
const { counter, increment, decrement } = withObject();// orconst { increment, decrement, counter } = withObject();// the order doesn't matter as long as the names of the values match the returned values of the function
Use object destructuring to unpack props:
const Child = ({ name, onNameChange }) => {};// Orconst Child = props => {
const { name, onNameChange } = props;
};
Use short-circuit evaluation for conditionals in JSX
{ return someBool && <Example/> }// Instead of
{ return someBool ? <Example/> : null }
Use property initializers for a class component
With transform-class-properties
plugin, you can use property initializers in the class component, which gets rid of the constructor and binding boilerplates. However, the method (that is now turned into an arrow function) will still be an instance property and get created in the memory after each instantiation.
class Parent extends React.Component {static propTypes = { }static defaultProps = { }// property initializer can use this.props toostate = {
cost: 0
};// no this binding!handleCost = () => {
...
}render() {
return (
<div>
<Child onAction={this.handleCost}/>
</div>
);
}
}
Gotcha: If you need to perform a step-by-step calculation for the state, the class component will still need a constructor.
constructor(props){
super(props); const cost = props.price + props.tax;
const total = cost - props.giftCard;
const shippable = total > 35; this.state = {
shippable
}
}
Pass a function to setState — instead of an object
For a class component whose state is calculated based on the previous state and or when multiple setState
s may be triggered in a short interval of time.
Reason: Passing an object to setState()
may lead to bugs. For example, states that are not being updated in the setState()
call could end up disappearing. That’s because react may batch multiple setState()
calls into a single update for performance. So when there are multiple setState()
calls with the same object key, the value that gets set to the state may just be the one from the last object.
Functional setState()
on the other hand is guaranteed to update the state correctly, every time.
// 🔴 UNPREDICTABLEthis.setState({ balance: prevState.balance + 1 });// 👍 RELIABLEthis.setState(prevState => (
{ balance: prevState.balance + 1 }
));
The function that gets passed into the setState()
may be declared separately from the class component. It can also receive an extra argument to calculate the new state (see updateGiftCard
function below).
function updateGiftCard(cash) {
return state => ({
giftCard: state.giftCard + cash
});
}class Parent extends React.Component {
state = {
cost: 0,
giftCard: 0
};handleGiftCard = () => {
this.setState(updateGiftCard(100));
}
}
setState
shallow-merge the state, meaning it automatically copies the top-level properties. Changing nested properties will still require copying the old object. For this, we can use the ...
spread operator.
Gotcha: ...
spread operator only copies one tier of the object properties. The...
spread operator has to be written first — before the property (state) that needs to be updated (see example below), otherwise the new property will be overwritten by the old one.
this.state = {
item: "Jello",
cost: {
costA: 0,
costB: 0}
};// updating the top-tier properties doesn't require ... operatorthis.setState({
item: "Bean"
});The output looks like this:{
item: "Bean",
cost: {
costA: 0,
costB: 0
}
}---// updating the nested propertiesthis.setState(prevState => ({
// first tier is automatically copied
cost: {
...prevState.cost, // spread the object first
costA: 123 // property update comes second
}
}));The output looks like this:{
item: "Jello",
cost: {
costA: 123,
costB: 0
}
}
Use a functional component instead of a class
const Example = props => {
return <div />;
}// Orfunction Example(props) {
return <div />;
}
Reason: It’s less code as it has no constructor
, no this
binding, no render()
.
Use Hooks in a functional component
Gotcha: Only call Hooks at the top level of your function (do not put them in nested functions). Do not use Hooks in a loop or a condition because React relies on the order in which Hooks are called. If the condition is falsy, the Hook will be skipped and every next Hook calls will shift by one.
// 🔴 BADif (status !== '') {
useEffect(function doStuff() {
...
});
}// 👍 GOODuseEffect(function doStuff() {
if (status !== '') {
...
}
});
Use useState Hook
To initialize and update states in a functional component.
import React, { useState } from 'react';const [cost, setCost] = useState({
costA: 0,
costB: 0
});
useState
setter replaces the entire value of the array/object. Use the ...
spread operator to copy the entire array/object and followed by the property that needs to override the old property.
// 🔴 BADconst badSetter = () => (
setCost({
costA: cost.costA + 1
// costB disappears
})
);// 👍 GOODconst goodSetter = () => (
setCost({
...cost, // spread first, so costB gets copied
costA: cost.costA + 1
})
);// 👍 GOOD for event handlersconst goodSetter = () => {
setCost(cost => ({
...cost, // spread first, so costB gets copied
costA: cost.costA + 1
}))
};
Lazy initial state
If the initial value for the state is computationally expensive and or that you want the state to be initialized just on the first render, then you can pass a function to the useState
.
// before
const [stuff, setStuff] = useState(localStorage.getItem("some-stuff"));
With lazy initial state:
// option 1
const [stuff, setStuff] = useState(() => localStorage.getItem("some-stuff"));// option 2 - move the function to its own variable
const getInitialState = () => localStorage.getItem("some-stuff");
const [stuff, setStuff] = useState(getInitialState);
More on ‘spread’ operator
It can be used in JSX as an alternative syntax to pass down props in child components. By using spread in your JSX, you can make use of the object shorthand syntax if the key and value of the prop are the same.
// Two ways to pass in propsconst WithSpread = () => {
return (
<Child
{...{
name, // object shorthand
onNameChange: setName
}}
/>
);
};const WithSpread2 = () => {
const props = { name: name, onNameChange: setName };return (
<Child {...props} />
);
};const WithoutSpread = () => {
return (
<Child
name={name}
onNameChange={setName}
/>
);
};
useEffect to mimic lifecycle methods
useEffect
without a second argument runs the effect after initial render and every rerender.
Gotcha: Avoid infinite loop! Running an effect that causes a state to change without a second argument will trigger re-render and the effect over and over again.
useEffect(() => {
fetch("someURL.json")
.then(res => res.json())
.then(json =>
setPosts(json.data.map(post => post.data))
)
}); // endless rendering caused by the missing second argument
componentDidMount
useEffect
with an empty array means “this effect does not depends on any prop or state”, so the effect will only run once, which is when the component is mounted/created.
useEffect(() => {
subscribe()},
[] // second argument
);
componentWillUnmount (cleanup)
useEffect
can also be used to do a cleanup job by returning a function from within the effect function itself.
When does the cleanup function get run?
It will be run when the component is unmounted, and after every render but before the next effect fires.
useEffect(() => {
subscribe();
return () => unsubscribe(); // clean up
},
[] // second argument can also be an array of dependencies
);
componentDidUpdate
useEffect
with an array containing a list of dependencies as the second argument will cause the effect to run whenever anything in the list changes (which could be props or states). The example below shows that whenever all of the listed dependencies change, notifyUser
function will be invoked.
useEffect(() => {
notifyUser()
}, [dependencies]
);
To ensure that all the necessary dependencies
are included, we can follow the tip documented here which is to use exhaustive-deps
rule. It is part of eslint-plugin-react-hooks
package that shoots warning when the dependencies are specified incorrectly in our useEffect
and suggests a fix.
useContext to pass props to deeply nested components
After creating the context using createContext()
and assign value to the Provider
(just like you would with the context API before Hooks), the context can just be consumed by calling useContext
.
Reason: Unlike the context API, you don’t need to wrap the JSX with aConsumer
in order to access the context, because useContext
does it automatically.
import React, {useContext} from 'react';
import CostContext from 'somefilesomewhere';// consume the current value attribute of the CostContext.Provider
const cost = useContext(CostContext);
Gotcha: Thevalue
prop ofProvider
should not be assigned a non-primitive data type (e.g. array or object). Because those two data types will get a new reference identity each time the Provider
‘s parent renders. When the reference identity of Provider
‘s value
changes, the consumer components will also re-render.
If that is not a desired behavior, then the array or object value has to be stored in the Provider
‘s parent’s state.
// 👍 GOODconst [item, setItem] = useState(["Jello"]); // defined in Provider's parent<Provider value={this.state.item}>// 🔴 BAD<Provider value={["Jello"]}>
Wrap the context-using component with Provider
Gotcha: Accessing Context in the same component where the Provider is defined is not possible. In order to use the context, the component needs to be wrapped with the Provider.
function Home() {
const context = useContext(SomeContext);
console.log(context); // undefined (this does NOT work) 👎 return <SomeProvider><NavBar /></SomeProvider>
}
The code below will work:
function Home() {
const context = useContext(SomeContext);
console.log(context); // this will work 👍 return <NavBar />
}function HomeWrapper() {
return (<SomeProvider>
<Home />
</SomeProvider>);
}
useRef to reference a DOM node in a functional component
const Example = () => {
const inputEl = useRef(null);
const handleInputFocus = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />;
<button onClick={handleInputFocus}>Focus the input!</button>
</>
)
}
Use optional chaining (?.) and nullish coalescing operator (??
)
Note: As of this writing, these two features have not yet reached cross-browser stability. In order to use them, you’d need to include two babel plugins (this and this) .
Instead of short-circuiting based on the obj.first
object like this:
let nestedProp = obj.first && obj.first.second;
We can just put the optional chaining syntax ?.
on the potentially null/undefined property like so:
let nestedProp = obj.first?.second;
In plain English, that means “Does first
exist in the obj, if it does, does it have second
as its property?”
We can also use this syntax for an array. Before accessing an item at certain index in an array, check if the array exists first like so:
let arrayItem = arr?.[42];
Optionally calling a function callMe
:
let callMeIfYouCan = obj.first?.callMe?.();
Reason: The code with missing variables will return null
or undefined
, but with ?.
, it won’t terminate your application/program.
If you want to add a default value, you can use the nullish coalescing operator syntax: ??
let nestedProp = obj.first?.second ?? 'default stuff';
Use Fragment
To group together multiple elements without adding extra nodes to the DOM.
Gotcha: Only the explicit syntax:React.Fragment
allows for key
attribute, which is recommended if you want to build a list of Fragment
.
return (
<> // or <React.Fragment>
<td>Item Name</td>
<td>Cost</td>
</> // or </React.Fragment>
);
Make sure the component is accessible
This checklist from a11y is quite easy to read through. But I would personally start with:
form
elements uselabel
(for
becomeshtmlFor
in JSX). Provideautocomplete
on input element as appropriate. Usearia-required="true"
if a user input is mandatory on the element.- Announce an error message using
aria-errormessage
that is used in conjunction witharia-invalid
(which can be dynamically changed with Javascript). From MDN:
.errormessage {
visibility: hidden;
}[aria-invalid="true"] {
outline: 2px solid red;
}[aria-invalid="true"] ~ .errormessage {
visibility: visible;
}When an object is invalid, we use JavaScript to add `aria-invalid="true"`. The above CSS makes the `.errormessage` following an invalid object become visible.// HTML
<p>
<label for="email">Email address:</label>
<input type="email" name="email" id="email"
aria-invalid="true" aria-errormessage="err1" />
<span id="err1" class="errormessage">Error: Enter a valid email address</span>
</p>
Alternatively, you can also use aria-live
like this example or alert
role as shown in this example.
4. Group related controls and labels in the forms with fieldset
and define its caption withlegend
.
<form>
<fieldset>
<legend>Choose your favorite food</legend><input type="radio" id="pizza" name="pizza">
<label for="pizza">Pizza</label><br/><input type="radio" id="sushi" name="sushi">
<label for="sushi">Sushi</label><br/></fieldset>
</form>
5. Use proper headings and semantic elements when possible (header
, footer
, nav
, article
, section
, main
, and aside
). Ensure that all of the semantics of HTML elements are expressed correctly. If we have to design custom elements that need to have the same behavior as their native counterparts (for example a button that is created using <div> tags), we would need to also manually incorporate the necessary functionalities, such as adding tabIndex to make the elements focusable.
6. Make sure all interactive elements are focusable usingtabIndex="0"
or programmatically made to focus by calling focus()
on a referenced element. Do not reset the focus outline: 0
, unless using a custom focus outline implementation is intended.
7. Use aria-label
if an element does not have a text label (e.g. burger menu), otherwise use aria-labelledby
. For description, use aria-describedby
. From this source, these aria attributes are only for the following elements:
- interactive elements like
a
(when thehref
attribute is present),audio
andvideo
(when thecontrols
attribute is present),input
(unless they are oftype="hidden"
),select
,button
, andtextarea
- elements that have a landmark role — either implicit (
header
,footer
,main
,nav
,aside
,section
, andform
) or explicitly set via therole
attribute - elements that have an explicit widget role applied using the
role
attribute – there are 27 widget roles in ARIA 1.1, includingdialog
,slider
,progressbar
, andtooltip
iframe
andimg
elements
8. Use aria-hidden="true"
if the element should be hidden from a screen reader, while still being rendered visually. Use this guide for an element that should be visually hidden but still being read out by the assistive technologies.
9. Add succinct descriptions into the alt
attributes of all images, if the image should be skipped by a screen reader, add alt=""
.
10. Because DOM order determines the focus and screen reader order, it is imperative to make sure that it is structured correctly when observed on visual devices and screen readers. Otherwise, you could potentially break this rule and experienced this kind of conundrum that I had while working at Walmart Labs.