Best Practices

Favor Functional Components

Favor functional components - they have a simpler syntax. No lifecycle methods, constructors or boilerplate. You can express the same logic with less characters without losing readability.

Unless you need an error boundary they should be your go-to approach. The mental model you need to keep in your head is a lot smaller.

// 👎 Class components are verbose
class Counter extends React.Component {
  state = {
    counter: 0,
  }
 
  constructor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }
 
  handleClick() {
    this.setState({ counter: this.state.counter + 1 })
  }
 
  render() {
    return (
      <div>
        <p>counter: {this.state.counter}</p>
        <button onClick={this.handleClick}>Increment</button>
      </div>
    )
  }
}
 
// 👍 Functional components are easier to read and maintain
function Counter() {
  const [counter, setCounter] = useState(0)
 
  handleClick = () => setCounter(counter + 1)
 
  return (
    <div>
      <p>counter: {counter}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  )
}

Write Consistent Components

Stick to the same style for your components. Put helper functions in the same place, export the same way and follow the same naming patterns.

There isn’t a real benefit of one approach over the other

No matter if you’re exporting at the bottom of the file or directly in the definition of the component, pick one and stick to it.

Name Components

Always name your components. It helps when you’re reading an error stack trace and using the React Dev Tools.

It’s also easier to find where you are when developing if the component’s name is inside the file.

// 👎 Avoid this
export default () => <form>...</form>
 
// 👍 Name your functions
export default function Form() {
  return <form>...</form>
}

Don't Hardcode Repetitive Markup

Don’t hardcode markup for navigation, filters or lists. Use a configuration object and loop through the items instead.

This means you only have to change the markup and items in a single place.

// 👎 Hardcoded markup is harder to manage.
function Filters({ onFilterClick }) {
  return (
    <>
      <p>Book Genres</p>
      <ul>
        <li>
          <div onClick={() => onFilterClick('fiction')}>Fiction</div>
        </li>
        <li>
          <div onClick={() => onFilterClick('classics')}>
            Classics
          </div>
        </li>
        <li>
          <div onClick={() => onFilterClick('fantasy')}>Fantasy</div>
        </li>
        <li>
          <div onClick={() => onFilterClick('romance')}>Romance</div>
        </li>
      </ul>
    </>
  )
}
 
// 👍 Use loops and configuration objects
const GENRES = [
  {
    identifier: 'fiction',
    name: Fiction,
  },
  {
    identifier: 'classics',
    name: Classics,
  },
  {
    identifier: 'fantasy',
    name: Fantasy,
  },
  {
    identifier: 'romance',
    name: Romance,
  },
]
 
function Filters({ onFilterClick }) {
  return (
    <>
      <p>Book Genres</p>
      <ul>
        {GENRES.map((genre) => (
          <li>
            <div onClick={() => onFilterClick(genre.identifier)}>
              {genre.name}
            </div>
          </li>
        ))}
      </ul>
    </>
  )
}

Manage Component Size

A React component is just a function that gets props and returns markup. They adhere to the same software design principles.

If a function is doing too many things, extract some of the logic and call another function. It’s the same with components - if you have too much functionality, split it in smaller components and call them instead.

If a part of the markup is complex, requires loops and conditionals - extract it.

Rely on props and callbacks for communication and data. Lines of code are not an objective measure. Think about responsibilities and abstractions instead.

Write Comments in JSX

When something needs more clarity open a code block and provide the additional information just like you would in a regular function. The markup is a part of the logic so when you feel that something needs more clarity - provide it.

Business logic is always coupled to the markup, at least a little. So we should provide any context about the domain that may not be obvious.

function Component(props) {
  return (
    <>
      {/* Subscribers should not see any ads. */}
      {user.subscribed ? null : <Advert />}
    </>
  )
}

Pass Objects Instead of Primitives

One way to limit the amount of props is to pass an object instead of primitive values. Rather than passing down the user name, email and settings one by one you can group them together. This also reduces the changes that need to be done if the user gets an extra field for example.

Using TypeScript makes this even easier.

// 👎 Don't pass values on by one if they're related
<UserProfile
  bio={user.bio}
  name={user.name}
  email={user.email}
  subscription={user.subscription}
/>
 
// 👍 Use an object that holds all of them instead
<UserProfile user={user} />

Conditional Rendering

In some situations using short circuit operators for conditional rendering may backfire and you may end up with an unwanted 0 in your UI. To avoid this default to using ternary operators. The only caveat is that they’re more verbose.

The short-circuit operator reduces the amount of code which is always nice. Ternaries are more verbose but there is no chance to get it wrong. Plus, adding the alternative condition is less of a change.

// 👎 Try to avoid short-circuit operators
function Component() {
  const count = 0
 
  return <div>{count && <h1>Messages: {count}</h1>}</div>
}
 
// 👍 Use a ternary instead
function Component() {
  const count = 0
 
  return <div>{count ? <h1>Messages: {count}</h1> : null}</div>
}

Avoid Nested Ternary Operators

Ternary operators become hard to read after the first level. Even if they seem to save space at the time, it’s better to be explicit and obvious in your intentions.

// 👎 Nested ternaries are hard to read in JSX
isSubscribed ? (
  <ArticleRecommendations />
) : isRegistered ? (
  <SubscribeCallToAction />
) : (
  <RegisterCallToAction />
)
 
// 👍 Place them inside a component on their own
function CallToActionWidget({ subscribed, registered }) {
  if (subscribed) {
    return <ArticleRecommendations />
  }
 
  if (registered) {
    return <SubscribeCallToAction />
  }
 
  return <RegisterCallToAction />
}
 
function Component() {
  return (
    <CallToActionWidget
      subscribed={subscribed}
      registered={registered}
    />
  )
}

Move Lists in a Separate Component

Looping through a list of items is a common occurrence, usually done with the map function. However, in a component that has a lot of markup, the extra indentation and the syntax of map don’t help with readability.

When you need to map over elements, extract them in their own listing component, even if the markup isn’t much. The parent component doesn’t need to know about the details, only that it’s displaying a list.

Only keep a loop in the markup if the component’s main responsibility is to display it. Try to keep a single mapping per component but if the markup is long or complicated, extract the list either way.

// 👎 Don't write loops together with the rest of the markup
function Component({ topic, page, articles, onNextPage }) {
  return (
    <div>
      <h1>{topic}</h1>
      {articles.map((article) => (
        <div>
          <h3>{article.title}</h3>
          <p>{article.teaser}</p>
          <img src={article.image} />
        </div>
      ))}
      <div>You are on page {page}</div>
      <button onClick={onNextPage}>Next</button>
    </div>
  )
}
 
// 👍 Extract the list in its own component
function Component({ topic, page, articles, onNextPage }) {
  return (
    <div>
      <h1>{topic}</h1>
      <ArticlesList articles={articles} />
      <div>You are on page {page}</div>
      <button onClick={onNextPage}>Next</button>
    </div>
  )
}

Assign Default Props When Destructuring

One way to specify default prop values is to attach a defaultProps property to the component. This means that the component function and the values for its arguments are not going to sit together.

Prefer assigning default values directly when you’re destructuring the props. It makes it easier to read the code from top to bottom without jumping and keeps the definitions and values together.

// 👎 Don't define the default props outside of the function
function Component({ title, tags, subscribed }) {
  return <div>...</div>
}
 
Component.defaultProps = {
  title: '',
  tags: [],
  subscribed: false,
}
 
// 👍 Place them in the arguments list
function Component({ title = '', tags = [], subscribed = false }) {
  return <div>...</div>
}

Use Absolute Paths

Making things easier to change is fundamental for your project structure. Absolute paths mean that you will have to change less if you need to move a component. Also it makes it easier to find out where everything is getting pulled from.

// 👎 Don't use relative paths
import Input from '../../../modules/common/components/Input'
 
// 👍 Absolute ones don't change
import Input from '@modules/common/components/Input'

Don't Optimize Prematurely

Before you make any kinds of optimizations, make sure that there is a reason for them. Following e best practice blindly is a waste of effort unless it’s impacting your application in a way.

Yes, it’s important to be aware of certain things but prioritize building readable and maintainable components before performance. Well written code is easier to improve.

When you notice a performance problem in your application - measure and identify the cause of your problem. No point in trying to reduce rerender count if your bundle size is enormous.

Once you know where the performance problems are coming from, fix them in the order of their impact.

Rerenders - Callbacks, Arrays and Objects

It’s good to try and reduce the amount of unnecessary rerenders that your app makes. Keep this in mind but also note that unnecessary rerenders will rarely have the greatest impact on your app.

The most common advice is to avoid passing callback functions as props. Using one means that a new function will be created each time, triggering a rerender. I haven’t faced any performance problems with callbacks and in fact that’s my go to approach.

If you are experiencing performance problems and the closures are the cause then remove them. But don’t make your code less readable ot more verbose unnecessarily.

Passing down arrays or objects directly falls into the same category of problems. They fail the reference check so they will trigger a rerender. If you need to pass a fixed array extract it as a constant before the component definition to make sure the same instance is passed each time.

Test Edge Cases

When you have the basic tests covered, make sure you add some to handle edge cases.

That would mean passing an empty array to make sure you’re not accessing an index without checking. Throw an error in an API call to make sure the component handles it.

Write Integration Tests

Integration tests are meant to validate an entire page or a larger component. It tests whether it works well as an abstraction. They give us the most confident that the application works as expected.

The components on their own could be working well and their unit tests could be passing. The integration between them could have problems, though.