Basic Checklist for Creating a React Component

RM
11 min readDec 17, 2021

--

Three developers about to argue on something. Image by Ekaterina79 Getty Images Signature.

Where do you even begin?

I would start with figuring out the basics, such as:

  1. The state needed and which component stores it (or should the state be stored in Redux?)
  2. Should the component be Class based or functional?
  3. 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?
  4. For functional component, use Hooks for lifecycle methods
  5. 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 selected option 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 setStates 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
});

useStatesetter 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

useEffectwith 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)

useEffectcan 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 dependenciesare 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 ofProvidershould 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:

  1. form elements uselabel (for becomes htmlFor in JSX). Provide autocomplete on input element as appropriate. Usearia-required="true" if a user input is mandatory on the element.
  2. 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-labelif 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 the href attribute is present), audio and video (when the controls attribute is present), input (unless they are of type="hidden"), select, button, and textarea
  • elements that have a landmark role — either implicit (header, footer, main, nav, aside, section, and form) or explicitly set via the role attribute
  • elements that have an explicit widget role applied using the role attribute – there are 27 widget roles in ARIA 1.1, including dialog, slider, progressbar, and tooltip
  • iframe and img 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.

--

--

RM
RM

Written by RM

Software engineer with master’s degrees in Computer Science and Politics. Passionate about animals, software accessibility and UI/UX design.