/how-to-react

Proven principles and rules on writing code with React

MIT LicenseMIT

How to React?

Proven principles and rules on writing code with React

Components

Prefer Functional Components

Functional components gretaly simplify the sytnax, and improve the readability of your React components. Embrace the functional approach everywhere, or almost everywhere possible as it's less code, highly focussed, and is devoid of class component boilerplate.

// 👎 Don't
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// 👍 Do
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

Favor Atomic Components Components encapsulate the presentation logic and behavior. Focussing on components that do one thing(and one thing well), or represent one aspect of the UI leads to more maintenable code. Everytime, you observe a component doing a lot, or more than what it truly represents by its name or position in the component heirarchy, split the logic apart into separate components or hooks to favor compose-ability, reusability, debug/test-ability, readability, maintainabilty, etc. of your App as it evolves.

Name Components

Choose to always name your components, as it helps with their findability and debugability

// 👎 Avoid this
export default (props) => <h1>Hello, {props.name}</h1>

// 👍 Name your functions
export default function Welcome(props) {
  return <h1>Hello, {props.name}</h1>
}

What's a function without a name? An anonymous function.What's a component without a name? An anonymous component.

Think DRY. Co-locate for efficiency

Any logic or state that's generic, not directly related to a component or its rendering should be maintained outside the component -

  • Co-located: Within the same file and before component definition if there's no possibility of re-use
// 👎 Don't
function WelcomeUser(props) {
  function renderUserAvatar(name) {
    ...
  }

  return <div>Hello, {renderUserAvatar(props.name)}</div>
}

// 👍 Do
function renderUserAvatar(name) {
  ...
}

function WelcomeUser(props) {
  return  <div>Hello, {renderUserAvatar(props.name)}</div>
}
  • Globally available: From a common place, if it's pretty re-usable logic
import { renderAvatar } from '../utils'
// 👍 Do
function WelcomeUser(props) {
  return <div>Hello, {renderAvatar(props.name)}</div>
}

A general rule of thumb is that you can always start with co-location, and can split the logic apart if you find yourself repeating the same logic multiple times(more than 3)

Prefer Dynamic, DRY, or Non Hard-coded Mark-up

Never code what you could generate. In React, or more aptly in JSX you encapsulate mark-up(with slight changes) in functions. Though, it looks and feels like mark-up, it's purely Javascript at the end of the day and you have all of the Javascript constructs(for looping, conditionals, etc.) at your disposal. Even if you're not using APIs you should clearly separate and code for what's data(and dynamic) and what's mark-up(and static).

// 👎 Don't
export default function Navigation() {
    return (
      <ul className="navigation">
        <li className="nav-item">
          <a href="/" title="home">Home</a>
        </li>
        <li className="nav-item">
          <a href="/about" title="about">About</a>
        </li>
        <li className="nav-item">
          <a href="/contact" title="contact">Contact</a>
        </li>
      </ul>
    )
}


// 👍 Do

// configuration object.
const NAV_ITEMS = [
  { label: "Home", path: "/" },
  { label: "About", path: "/about" }
  { label: "Contact", path: "/contact" }
]

export default function Header() {
    return (
      <h1>{/* Logo and other stuff */}</h1>
      <ul className="navigation">
      {
        NAV_ITEMS.map(navItem => {
          return (
            <li className="nav-item">
              <a href={navItem.path} title={navItem.label.toLowerCase()} >{navItem.label}</a>
            </li>
          )
        })
      }
      </ul>
    )
}

This, as a side-effect also leads to portability of configuration thus helping with maintainability of component as well as the configuration

Extract listing as a separate responsibility(and Component)

See and treat listing as a separate responsibility. For exmaple, if you're building a Header component with Logo, Navigation(like above), etc. it's better to organise your code as

function NavListItem(props) {
    return (
        <li className="nav-item">
            <a href={props.path} title={props.label.toLowerCase()} >{props.label}</a>
        </li>
    )
}

function NavList() {
    return (
      <ul className="navigation">
      {
        NAV_ITEMS.map(navItem => <NavListItem path={navItem.path} label={navItem.label} />)
      }
      </ul>
    )
}

export default function Header() {
    return (
      <h1>{/* Logo and other stuff */}</h1>
      <NavList/>
    )
}

Conditional Rendering

Though more verbose, ternary operators produce more clear and readable code.

// Don't
import { useState } from 'react'

const FollowButton = () => {
    const [ isFollowed, setIsFollowed ] = useState(false)
    return (
        <button className="follow-btn">
            { !isFollowed && `follow` }
            { isFollowed && `unfollow` }
        </button>
    )
}

export default FollowButton

// Do
import { useState } from 'react'

const FollowButton = () => {
    const [ isFollowed, setIsFollowed ] = useState(false)
    return (
        <button className="follow-btn">
            { isFollowed ? `unfollow` : `follow` }
        </button>
    )
}

export default FollowButton

Add Comments for clarity

Add comments for clarity when logic inside a component is not easily understandable.

import { useState } from 'react'

const FollowButton = ({ }) => {
    const [ isFollowed, setIsFollowed ] = useState(false)
    return (
        <button className="follow-btn">
            {/* Show `unfollow` when the user is followed, or `follow` otherwise */}
            { isFollowed ? `unfollow` : `follow` }
        </button>
    )
}

export default FollowButton

JSX lets you write JS expressions alongside renderable mark-up and your mark-up is mark-up + rendering logic. Just like Javascript, you're free to express what your code is doing.

Use Error Boundaries

Use <ErrorBoundary/> to catch errors that happen during the rendering phase of a component, and not let it cascade beyond its boundary.

More Note: Use something like react-error-boundary to deal with errors even more gracefully.

Components & Props

Check "Prop" Types

To get what you(your component) wants, you need to tell what you want. React provides you with a way to tell the shape of the properties your components recieve, that you should prefer to enforce the prop contract.

import PropTypes from 'prop-types' // external package

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

Welcome.propTypes = {
  name: PropTypes.string
}

export default Welcome

prop-types lets you do a runtime check of the properties a components receive and helps you avoid a lot of bugs. To catch bugs early, and always develop in a type-safe way, you obviously have Typescript.

Destructure Props Since the modern tooling allows for ES6+ syntax, there's no point in not conveniently accessing all of the Props a component receives.

// Don't
const Button = (props) => {
    return (
        <button type={props.type} className="follow-btn" onClick={props.onClick}>
            {props.label}
        </button>
    )
}

// Do
const Button = ({ type, label, onCLick }) => {
    return (
        <button type={type} className="follow-btn" onClick={onClick}>
            {label}
        </button>
    )
}

Save yourself from some Prop-ery and leverage ES6+ syntax wherever possible today, as long as they don't impact the legibility of your code

Use/Apply Default Props the ES6+ way Assign default values directly when you're destructing the Props. Since components are just functions, this is a more natural and efficient way to apply and find the default values a component takes.

// Don't
const Button = ({ type, label, disabled, onCLick }) => {
    return (
        <button type={type} className="follow-btn" onClick={onClick} disabled={disabled}>
            {label}
        </button>
    )
}

Button.defaultProps = {
    type: 'button',
    label: '',
    onCLick: () => {},
    disabled: false
}


// Do
const Button = ({ type = 'button', label = '', disabled = false, onCLick = ()=> {} }) => {
    return (
        <button type={type} className="follow-btn" onClick={onClick} disabled={disabled}>
            {label}
        </button>
    )
}

Prefer Objects for Complex Data Props

Prefer objects as props when it's actually objects you're working with, not primitives.

// Don't
    <Product
        name={product.name}
        description={product.description}
        price={product.price}
        images={product.images}
    />

// Do
    <Product product={product} />

This has the benefit of keeping your Component's Props API simple and easily maintainable.

Components that render the data consumed out of APIs are a good candidate for this, whereas components that abstract/encapsulate browser elements and their styling/functional behavior are not

Limit the number of Props

Try to limit the number of Props a component receives to give it less reasons to change and re-render. Apply the previous pattern to decide what can be clubbed as a single parameter.

Components & state

useState for primitives, useReducer for objects

Start simple, but as you find yourself doing a lot of useState-ery in your components, immediately switch to useReducer. useReducer is naturally good for objects, and even better for stuff that might not look related but actually is like error, loading, data state when calling a API.

State Management

Though React started as view-only library, reaching out state management libraries for external state management like Redux wasn't always desirable. This popular use case(and demand) led to React having useState and useReducer hooks out of the box. As per your preference you can either co-locate and keep the state management code in your components or easily extract them into a custom hooks. When clubbed with Context you can roll your own light-weight, app-wide state management structure for stuff like themes, preferences, user details, carts, etc.

When you do use a community maintained library try to first reach out to lightweight and smart state management approaches/solutions like Zustand, Valtio, Jotai, etc.

Components & types

Don't use FunctionComponent

It's important to properly type your components, so even though React provides types like React.FC or React.FunctionComponent, the benefits are not just worth it if you want to be explicit about all the Props and their types you wan't to accept

// 👍 Do
interface WelcomeProps = {
    name: string,
}

const Welcome = ({ name }: WelcomeProps) => {
  return <h1>Hello, {name}</h1>;
}

// or
interface WelcomeProps = {
    name: string,
    children: React.ReactNode // if you need to accept `children` with type control
}

const Welcome = ({ name, children /* permissible */ }: WelcomeProps) => {
  return (<div>
            <h1>Hello, {name}</h1>
            {children}
        </div>);
}

// 👎 Don't

interface WelcomeProps = {
    name: string
}
// this version
const Welcome: React.FunctionComponent<WelcomeProps> = ({ name }) => {
  return <h1>Hello, {name}</h1>;
}

// or,

// this version
const Welcome: React.FC<WelcomeProps> = ({ name }) => {
  return <h1>Hello, {name}</h1>;
}

// the primary reason folks prefer this is because you get an implicit definition of `children` and autocomplete for static properties (i.e displayName, defaultProps)

// this version
const Welcome: React.FC<WelcomeProps> = ({ name, children /*permissible */ }) =>{
  return <h1>Hello, {name}</h1>;
}

// but this version won't let you control what you want as `children`

Prefer type over enum

Prefer type to reserve acceptable/allowable values to known tokens/strings

// Do
type Size = 'small' | 'medium' | 'large'

// or,
const sizeObj = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large',
} as const;

type Size = typeof sizeObj[keyof typeof sizeObj];
// const teeSize: Size = // only takes 'SMALL' or 'MEDIUM' or 'LARGE'

// Don't
enum Size {
  SMALL = 'small',
  MEDIUM = 'medium',
  LARGE = 'large'
}
// const teeSize: Size = // takes 'Size.SMALL' or 'Size.MEDIUM' or 'Size.LARGE'

Use InferProps when working with prop-types

If you want end-to-end typechecking experience: Compiler and run-time you should use InferProps generic to infer the types

import PropTypes from 'prop-types' // needs @types/prop-types installed as dependencies

const Welcome = ({ name }: InferProps<typeof Welcome.propTypes>) => {
  return <h1>Hello, {name}</h1>;
}

Welcome.propTypes = {
    name: PropTypes.string.isRequired
}

App Structure

Coming Soon