loading...
Cover image for 14 Beneficial Tips to Write Cleaner Code in React Apps

14 Beneficial Tips to Write Cleaner Code in React Apps

jsmanifest profile image jsmanifest Originally published at jsmanifest.com ・17 min read

Find me on medium

Writing clean code is something that becomes mandatory at some point in your career especially while you're trying to obtain your first developer job. It's essentially what makes you a team player and can either break or make your success from a job interview. How you write code is one of the things they want to look at before making the hiring decision. Your code should be understandable by humans and not just by a machine.

The things listed in this article should apply more importantly the bigger your project becomes and might not be necessary for smaller ones. Just use your best judgment :)

Here are 14 Beneficial Tips to Write Cleaner Code in React Apps:

1. Destructure Your Props

Destructuring your props is a good way to help make your coder cleaner and more maintainable. That's because you clearly define or declare what something (like a component) is using and it doesn't force developers to read through the implementation of the component to find out all the props that are tied to the component.

It also gives you the ability to declare default values for them which you've probably seen plenty of times:

import React from 'react'
import Button from 'components/Button'

const MyComponent = ({ placeholder = '', style, ...otherProps }) => {
  return (
    <Button
      type="button"
      style={{
        border: `1px solid ${placeholder ? 'salmon' : '#333'}`,
        ...style,
      }}
      {...otherProps}
    >
      Click Me
    </Button>
  )
}

export default MyComponent

One of the coolest things I find about destructuring in JavaScript is that it lets you support different variations of parameters.

For example, if you had an authenticate function that used to taken in a token as a parameter to authenticate users and now wish to take in jwt_token because of a new server response structure, you can easily support both parameters without changing much of your code:

// before refactoring
async function authenticate({ user_id, token }) {
  try {
    const response = await axios.post('https://someapi.com/v1/auth/', {
      user_id,
      token,
    })
    console.log(response)
    return response.data
  } catch (error) {
    console.error(error)
    throw error
  }
}

// after refactoring
async function authenticate({ user_id, jwt_token, token = jwt_token }) {
  try {
    const response = await axios.post('https://someapi.com/v1/auth/', {
      user_id,
      token,
    })
    console.log(response)
    return response.data
  } catch (error) {
    console.error(error)
    throw error
  }
}

jwt_token will be evaluated by the time the code gets to token, so if jwt_token is a valid token and token is undefined, then the value of token will become the value of jwt_token. If the token was already some truthy value (a real token), it will just keep itself.

2. Folderize Your Components

Lets take a look at this directory structure below:

  • src
    • components
    • Breadcrumb.js
    • CollapsedSeparator.js
    • Input
      • index.js
      • Input.js
      • utils.js
      • focusManager.js
    • Card
      • index.js
      • Card.js
      • CardDivider.js
    • Button.js
    • Typography.js

Breadcrumbs are commonly known to be associated with some sort of separator as one of their core functionalities. The CollapsedSeparator component is imported inside Breadcrumb.js, so we know that they are both related in implementation. However, someone who doesn't know this information might assume that Breadcrumb and CollapsedSeparator are two completely separate components that are not related to each other at all--especially if CollapsedSeparator does not have any clear indications of it being related to a breadcrumb like having the prefix Breadcrumb (BreadcrumbCollapsedSeparator.js) for example.

Since we know that they are related we'd probably question why they aren't in a folder like Input and Card is doing and start to make weird possible assumptions like "I wonder if someone put it there to see if I would take it out like a good samaritan...". The effects of clean code practices should be the opposite--developers should be able to read your code and understand the situation in a snap!

Folderizing the breadcrumb looks something like this:

  • src
    • components
    • Breadcrumb
      • index.js
      • Breadcrumb.js
      • CollapsedSeparator.js
    • Input
      • index.js
      • Input.js
      • utils.js
      • focusManager.js
    • Card
      • index.js
      • Card.js
      • CardDivider.js
    • Button.js
    • Typography.js

Now no matter how many Breadcrumb related components are created after that, we will always know that they are related to Breadcrumb as long as they reside in the same directory:

  • src
    • components
    • Breadcrumb
      • index.js
      • Breadcrumb.js
      • CollapsedSeparator.js
      • Expander.js
      • BreadcrumbText.js
      • BreadcrumbHotdog.js
      • BreadcrumbFishes.js
      • BreadcrumbLeftOvers.js
      • BreadcrumbHead.js
      • BreadcrumbAddict.js
      • BreadcrumbDragon0814.js
      • BreadcrumbContext.js
    • Input
      • index.js
      • Input.js
      • utils.js
      • focusManager.js
    • Card
      • index.js
      • Card.js
      • CardDivider.js
    • Button.js
    • Typography.js
import React from 'react'
import Breadcrumb, {
  CollapsedSeparator,
  Expander,
  BreadcrumbText,
  BreadcrumbHotdog,
  BreadcrumbFishes,
  BreadcrumbLeftOvers,
  BreadcrumbHead,
  BreadcrumbAddict,
  BreadcrumbDragon0814,
} from '../../../../../../../../../../components/Breadcrumb'

const withBreadcrumbHotdog = (WrappedComponent) => (props) => (
  <WrappedComponent BreadcrumbHotdog={BreadcrumbHotdog} {...props} />
)

const WorldOfBreadcrumbs = ({
  BreadcrumbHotdog: BreadcrumbHotdogComponent,
}) => {
  const [hasFishes, setHasFishes] = React.useState(false)

  return (
    <BreadcrumbDragon0814
      hasFishes={hasFishes}
      render={(results) => (
        <BreadcrumbFishes>
          {({ breadcrumbFishes }) => (
            <BreadcrumbLeftOvers.Provider>
              <BreadcrumbHotdogComponent>
                <Expander>
                  <BreadcrumbText>
                    <BreadcrumbAddict>
                      <pre>
                        <code>{JSON.stringify(results, null, 2)}</code>
                      </pre>
                    </BreadcrumbAddict>
                  </BreadcrumbText>
                </Expander>
                {hasFishes
                  ? breadcrumbFishes.map((fish) => (
                      <>
                        {fish}
                        <CollapsedSeparator />
                      </>
                    ))
                  : null}
              </BreadcrumbHotdogComponent>
            </BreadcrumbLeftOvers.Provider>
          )}
        </BreadcrumbFishes>
      )}
    />
  )
}

export default withBreadcrumbHotdog(WorldOfBreadcrumbs)

3. Name Your Components Using Standard Naming Conventions

Naming your components using standard conventions makes it easier for other developers to read your code.

For example, higher order components usually becomes prefixed with with which most people are used to:

import React from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'
import getDisplayName from 'utils/getDisplayName'

const withFreeMoney = (WrappedComponent) => {
  class WithFreeMoney extends React.Component {
    giveFreeMoney() {
      return 50000
    }

    render() {
      return (
        <WrappedComponent
          additionalMoney={[
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
          ]}
          {...this.props}
        />
      )
    }
  }

  WithFreeMoney.displayName = `withFreeMoney(${getDisplayName(
    WrappedComponent,
  )}$)`
  hoistNonReactStatics(WithFreeMoney, WrappedComponent)

  return WithFreeMoney
}

export default withFreeMoney

If you decide to do something different like this:

import React from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'
import getDisplayName from 'utils/getDisplayName'

const useFreeMoney = (WrappedComponent) => {
  class WithFreeMoney extends React.Component {
    giveFreeMoney() {
      return 50000
    }

    render() {
      return (
        <WrappedComponent
          additionalMoney={[
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
          ]}
          {...this.props}
        />
      )
    }
  }

  WithFreeMoney.displayName = `useFreeMoney(${getDisplayName(
    WrappedComponent,
  )}$)`
  hoistNonReactStatics(WithFreeMoney, WrappedComponent)

  return WithFreeMoney
}

export default useFreeMoney

It's perfectly valid JavaScript and there's nothing wrong with naming it this way. But there's already a standard naming convention for use which have already reached the scene with react hooks. Just be careful when you're sharing your code especially when you're asking for help because people might already be adapted to seeing common established conventions every day.

4. Avoid the Boolean Trap

You have to be extra careful when deciding your output when it comes to the primitive booleans to determine output value of something. It's known to be a code smell and it forces the developer to look at the source code / implementation of the component to be able to make an accurate assumption of the end result.

For example, if we declared a Typography component that takes these available options: 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'title', 'subheading'

How would you figure out how they'll be applied when they're passed in like this?

const App = () => (
  <Typography color="primary" align="center" subheading title>
    Welcome to my bio
  </Typography>
)

Those who are more experienced with React (or more appropriately, JavaScript) might already guess that title will proceed over subheading because by the way the ordering works, the last one will overwrite the previous.

But the problem is that we won't be able to truly tell how far title or subheading will be applied without looking at the source code.

For example:

.title {
  font-size: 1.2rem;
  font-weight: 500;
  text-transform: uppercase;
}

.subheading {
  font-size: 1.1rem;
  font-weight: 400;
  text-transform: none !important;
}

Even though title "wins", the text-transform: uppercase CSS line still won't be applied because subheading declares higher specificity with text-transform: none !important; in its implementation. If we aren't careful enough it might become really difficult to debug a styling issue especially when it won't show any warnings/errors to the console. This can complicate the component's signature.

Here's just one example of a cleaner alternative to re-implement the Typography component that solves the issue:

const App = () => <Typography variant="title">Welcome to my bio</Typography>

Typography

import React from 'react'
import cx from 'classnames'
import styles from './styles.css'

const Typography = ({
  children,
  color = '#333',
  align = 'left',
  variant,
  ...otherProps
}) => {
  return (
    <div
      className={cx({
        [styles.h1]: variant === 'h1',
        [styles.h2]: variant === 'h2',
        [styles.h3]: variant === 'h3',
        [styles.h4]: variant === 'h4',
        [styles.h5]: variant === 'h5',
        [styles.h6]: variant === 'h6',
        [styles.title]: variant === 'title',
        [styles.subheading]: variant === 'subheading',
      })}
    >
      {children}
    </div>
  )
}

Now when we pass variant="title" in the App component, we will be assured that only title will be applied and it saves us the trouble of having to look at the source code to determine the outcome.

You can also just do a simple if/else to compute the prop:

let result
if (variant === 'h1') result = styles.h1
else if (variant === 'h2') result = styles.h2
else if (variant === 'h3') result = styles.h3
else if (variant === 'h4') result = styles.h4
else if (variant === 'h5') result = styles.h5
else if (variant === 'h6') result = styles.h6
else if (variant === 'title') result = styles.title
else if (variant === 'subheading') result = styles.subheading

But the best benefit from this is that you can just do this simple, clean one-liner and call it a day:

const result = styles[variant]

5. Use Fat Arrow Functions

Using fat arrow functions is a shorter and concise way of declaring functions in JavaScript (which is more appropriately named a function expression in this case).

However, there are certain times when you don't want to use fat arrow functions over function expressions, like when you need the hoisting.

In React, the same concept applies similarly. However, if you don't need hoisting it's a nicer alternative (in my opinion) to use the arrow syntax:

// Function declaration version
function Gallery({ title, images = [], ...otherProps }) {
  return (
    <CarouselContext.Provider>
      <Carousel>
        {images.map((src, index) => (
          <img src={src} key={`img_${index}`} />
        ))}
      </Carousel>
    </CarouselContext.Provider>
  )
}

// Arrow / Function expression version
const Gallery = ({ title, images = [], ...otherProps }) => (
  <CarouselContext.Provider>
    <Carousel>
      {images.map((src, index) => (
        <img src={src} key={`img_${index}`} />
      ))}
    </Carousel>
  </CarouselContext.Provider>
)

But you can hardly tell the benefits in this example... The beauty of arrow functions shine when you do simple one-liners:

// Function declaration version
function GalleryPage(props) {
  return <Gallery {...props} />
}

// Arrow / Function expression version
const GalleryPage = (props) => <Gallery {...props} />

And one-liners makes everyone happy! :)

6. Put Independent Functions Outside of Your Custom Hooks

I see some people declaring functions inside their custom hooks when they aren't really needed by them. This makes the custom hook a little bloated and harder to read as it gets longer because some developers might begin to question if the hook actually does depend on the function being inside the hook. If it's not, its better to move it outside so that there's a clear understanding of what the dependencies of the hook are and which ones aren't.

Here's an example:

import React from 'react'

const initialState = {
  initiated: false,
  images: [],
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'initiated':
      return { ...state, initiated: true }
    case 'set-images':
      return { ...state, images: action.images }
    default:
      return state
  }
}

const usePhotosList = ({ imagesList = [] }) => {
  const [state, dispatch] = React.useReducer(reducer, initialState)

  const removeFalseyImages = (images = []) =>
    images.reduce((acc, img) => (img ? [...acc, img] : acc), [])

  React.useEffect(() => {
    const images = removeFalseyImages(imagesList)
    dispatch({ type: 'initiated' })
    dispatch({ type: 'set-images', images })
  }, [])

  return {
    ...state,
  }
}

export default usePhotosList

Looking at the example, removeFalseyImages actually doesn't need to be inside the custom hook and can instead be extracted outside and still be used without any problems inside of the hook since it doesn't interact with any of its state.

7. Stay Consistent

Staying consistent is also a commonly recommended approach in JavaScript.

As for React, stay consistent with:

  1. Imports and exports
  2. Naming components, hooks, HOC's, classNames

When importing and exporting components, I sometimes like using this syntax when I want to put exports in between:

import App from './App'

export { default as Breadcrumb } from './Breadcrumb'

export default App

But I equally love this syntax:

export { default } from './App'
export { default as Breadcrumb } from './Breadcrumb'

Whichever one you like doing, just make sure that you're consistent with choosing one for each project so that it stays simple.

Staying consistent with naming conventions is also a very important rule.

When you define a hook like useApp, it's important to name your next hook with the prefix use like useController.

If you don't, what you end up doing is something like this:

// custom hook #1
const useApp = ({ data: dataProp = null }) => {
  const [data, setData] = React.useState(dataProp)

  React.useEffect(() => {
    setData(data)
  }, [])

  return {
    data,
  }
}

// custom hook #2
const basicController = ({ device: deviceProp }) => {
  const [device, setDevice] = React.useState(deviceProp)

  React.useEffect(() => {
    if (!device && deviceProp) {
      setDevice(deviceProp === 'mobile' ? 'mobile' : 'desktop')
    }
  }, [deviceProp])

  return {
    device,
  }
}

Importing the two hooks:

import React from 'react'
import useApp from './useApp'
import basicController from './basicController'

const App = () => {
  const app = useApp()
  const controller = basicController()

  return (
    <div>
      {controller.errors.map((errorMsg) => (
        <div>{errorMsg}</div>
      ))}
    </div>
  )
}

export default App

It's not immediately obvious that basicController is a custom react hook just like useApp is and forces the developer to look and read inside the code to really figure out the truth. If we kept it consistent, it wouldn't have turned out that way because we can make it obvious:

const app = useApp()
const controller = useBasicController()

8. Componentize Duplicate Elements

Componentize is just a fancy way of saying "converting duplicate elements to their own reusable component".

Everybody has their reasons for writing duplicate code in React whether it was intentional or an accident.

What ever the cause, it's a good idea for you to not leave plenty of duplicode code untouched.

For one, you're probably forming a habit of likely doing that again because you didn't care about the previous duplicated code. How are you a team player by doing this? You're putting a burden on your teammates in the future because they're probably going to get frustrated seeing duplicate elements and they might even be confused especially when they're put to the task of editing them.

The worst part is for them to get criticized by their duplicate code when they didn't even write it. When they do, just took one for the team on your behalf. Repay them back by avoiding the duplication in the future!

Lets take a look at this code below and componentize the duplicate parts:

const SomeComponent = () => (
  <Body noBottom>
    <Header center>Title</Header>
    <Divider />
    <Background grey>
      <Section height={500}>
        <Grid spacing={16} container>
          <Grid xs={12} sm={6} item>
            <div className={classes.groupsHeader}>
              <Header center>Groups</Header>
            </div>
          </Grid>
          <Grid xs={12} sm={6} item>
            <div>
              <img src={photos.groups} alt="" className={classes.img} />
            </div>
          </Grid>
        </Grid>
      </Section>
    </Background>
    <div>
      <Section height={500}>
        <Grid spacing={16} container>
          <Grid xs={12} sm={6} item>
            <div className={classes.labsHeader}>
              <Header center>Labs</Header>
            </div>
          </Grid>
          <Grid xs={12} sm={6} item>
            <div>
              <img src={photos.labs} alt="" className={classes.img} />
            </div>
          </Grid>
        </Grid>
      </Section>
    </div>
  </Body>
)

Now if someone were to tell you to change the grid sizes from xs={12} sm={6} to xs={12} sm={4} it would become a hassle because you have to change that four times.

The beauty of compenentizing is that you can just make a single change and it will reflect throughout all of the grids:

const SomeComponent = ({ classes, xs = 12, sm = 6, md, lg }) => {
  const BodySection = ({ header, src }) => {
    const gridSizes = { xs, sm, md, lg }
    return (
      <Section height={500}>
        <Grid spacing={16} container>
          <Grid {...gridSizes} item>
            <div className={classes.groupsHeader}>
              <Header center>{header}</Header>
            </div>
          </Grid>
          <Grid {...gridSizes} item>
            <div>
              <img src={src} alt="" className={classes.img} />
            </div>
          </Grid>
        </Grid>
      </Section>
    )
  }

  return (
    <Body noBottom>
      <Header center>Title</Header>
      <Divider />
      <Background grey>
        <BodySection header="Groups" src={photos.groups} />
      </Background>
      <div>
        <BodySection header="Labs" src={photos.labs} />
      </div>
    </Body>
  )
}

At its most basic level of extraction, this became much easier for humans to read and maintain while still keeping the normal implementation in place!

9. Keep Your Components Simple

Something I've learned while working for a production web app wasn't to keep your components simple, but to avoid making your components complicated.

Here's an example of a component that was unnecessarily complicated:

ConfirmAvailability.js

import React from 'react'
import Grid from '@material-ui/core/Grid'
import Typography from '@material-ui/core/Typography'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import Time from 'util/time'

/**
 * Timezone picker. Automatically detects the timezone from the client's device but also displays
 * a clock using this timezone to make sure it is correct. If not, the user may override it.
 *
 * NOTE: Be careful about Date().getTimezoneOffset(). It does two things differently from standard
 *      1. Time difference is in minutes
 *      2. Time difference is from local to UTC, not UTC to local. This means it will be negative of
 *          the expected UTC format
 */
export default class TimeZonePicker extends React.Component {
  state = {
    time: new Date(),
    offset: -(new Date().getTimezoneOffset() / 60),
  }

  componentDidMount() {
    this.props.setOffset(this.state.offset)
  }

  handleChange = (event) => {
    const d = new Date()
    d.setTime(
      d.getTime() +
        d.getTimezoneOffset() * 60 * 1000 +
        event.target.value * 3600 * 1000,
    )
    this.setState({
      time: d,
      offset: event.target.value,
    })
    this.props.setOffset(event.target.value)
  }

  render() {
    const timezones = []
    for (let i = -12; i <= 14; i++) {
      timezones.push(
        <MenuItem key={i} value={i}>
          {i > 0 ? '+' : null}
          {i}
        </MenuItem>,
      )
    }

    return (
      <React.Fragment>
        <Grid container justify="space-between">
          <div>
            <Typography>Current time</Typography>
            <Typography variant="h6" gutterBottom>
              {Time.formatTime(this.state.time)}
            </Typography>
          </div>
          <div>
            <Typography>Set timezone</Typography>
            <Select value={this.state.offset} onChange={this.handleChange}>
              {timezones}
            </Select>
          </div>
        </Grid>
      </React.Fragment>
    )
  }
}

The component was intended to be a simple component but since the logic was tightly coupled it was responsible for multiple things. At the time this code was written, react hooks was not yet released, but there were still higher order components and render props. So we'll just use one of those patterns to rewrite this to be simpler just to demonstrate how to keep your components simpler (without changing the functionality):

SelectTimeZone.js

import React from 'react'

/**
 * Timezone picker. Automatically detects the timezone from the client's device but also displays
 * a clock using this timezone to make sure it is correct. If not, the user may override it.
 *
 * NOTE: Be careful about Date().getTimezoneOffset(). It does two things differently from standard
 *      1. Time difference is in minutes
 *      2. Time difference is from local to UTC, not UTC to local. This means it will be negative of
 *          the expected UTC format
 */

class SelectTimeZone extends React.Component {
  state = {
    time: new Date(),
    offset: -(new Date().getTimezoneOffset() / 60),
  }

  componentDidMount() {
    this.props.setOffset(this.state.offset)
  }

  handleChange = (event) => {
    const d = new Date()
    d.setTime(
      d.getTime() +
        d.getTimezoneOffset() * 60 * 1000 +
        event.target.value * 3600 * 1000,
    )
    this.setState({
      time: d,
      offset: event.target.value,
    })
    this.props.setOffset(event.target.value)
  }

  getTimeZones = () => {
    const timezones = []
    for (let i = -12; i <= 14; i++) {
      timezones.push(
        <MenuItem key={i} value={i}>
          {i > 0 ? '+' : null}
          {i}
        </MenuItem>,
      )
    }
    return timezones
  }

  render() {
    return this.props.render({
      ...this.state,
      getTimeZones: this.getTimeZones,
    })
  }
}

TimeZonePicker.js

import React from 'react'
import Grid from '@material-ui/core/Grid'
import Typography from '@material-ui/core/Typography'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import Time from 'util/time'

const TimeZonePicker = () => (
  <SelectTimeZone
    render={({ time, offset, getTimeZones, handleChange }) => (
      <Grid container justify="space-between">
        <div>
          <Typography>Current time</Typography>
          <Typography variant="h6" gutterBottom>
            {Time.formatTime(time)}
          </Typography>
        </div>
        <div>
          <Typography>Set timezone</Typography>
          <Select value={offset} onChange={handleChange}>
            {getTimeZones()}
          </Select>
        </div>
      </Grid>
    )}
  />
)

export default TimeZonePicker

Now we have a much cleaner approach and extracted out the logic from its presentational counterpart. Unit testing these components now becomes much easier!

10. Use useReducer if useState becomes complex

When you're having multiple states to keep track of, using useState begins to become harder to manage.

This can look something like this:

import React from 'react'
import axios from 'axios'

const useFrogs = () => {
  const [fetching, setFetching] = React.useState(false)
  const [fetched, setFetched] = React.useState(false)
  const [fetchError, setFetchError] = React.useState(null)
  const [timedOut, setTimedOut] = React.useState(false)
  const [frogs, setFrogs] = React.useState(null)
  const [params, setParams] = React.useState({ limit: 50 })
  const timedOutRef = React.useRef()

  function updateParams(newParams) {
    if (newParams != undefined) {
      setParams(newParams)
    } else {
      console.warn(
        'You tried to update state.params but the parameters were null or undefined',
      )
    }
  }

  function formatFrogs(newFrogs) {
    const formattedFrogs = newFrogs.reduce((acc, frog) => {
      const { name, age, size, children } = frog
      if (!(name in acc)) {
        acc[name] = {
          age,
          size,
          children: children.map((child) => ({
            name: child.name,
            age: child.age,
            size: child.size,
          })),
        }
      }
      return acc
    }, {})
    return formattedFrogs
  }

  function addFrog(name, frog) {
    const nextFrogs = {
      ...frogs,
      [name]: frog,
    }
    setFrogs(nextFrogs)
  }

  function removeFrog(name) {
    const nextFrogs = { ...frogs }
    if (name in nextFrogs) delete nextFrogs[name]
    setFrogs(nextFrogs)
  }

  React.useEffect(() => {
    if (frogs === null) {
      if (timedOutRef.current) clearTimeout(timedOutRef.current)

      setFetching(true)

      timedOutRef.current = setTimeout(() => {
        setTimedOut(true)
      }, 20000)

      axios
        .get('https://somefrogsaspi.com/api/v1/frogs_list/', { params })
        .then((response) => {
          if (timedOutRef.current) clearTimeout(timedOutRef.current)
          setFetching(false)
          setFetched(true)
          if (timedOut) setTimedOut(false)
          if (fetchError) setFetchError(null)
          setFrogs(formatFrogs(response.data))
        })
        .catch((error) => {
          if (timedOutRef.current) clearTimeout(timedOutRef.current)
          console.error(error)
          setFetching(false)
          if (timedOut) setTimedOut(false)
          setFetchError(error)
        })
    }
  }, [])

  return {
    fetching,
    fetched,
    fetchError,
    timedOut,
    frogs,
    params,
    addFrog,
    removeFrog,
  }
}

export default useFrogs

This would become more manageable if you were to convert this to a useReducer:

import React from 'react'
import axios from 'axios'

const initialFetchState = {
  fetching: false
  fetched: false
  fetchError: null
  timedOut: false
}

const initialState = {
  ...initialFetchState,
  frogs: null
  params: { limit: 50 }
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'fetching':
      return { ...state, ...initialFetchState, fetching: true }
    case 'fetched':
      return { ...state, ...initialFetchState, fetched: true, frogs: action.frogs }
    case 'fetch-error':
      return { ...state, ...initialFetchState, fetchError: action.error }
    case 'set-timed-out':
      return { ...state, ...initialFetchState, timedOut: true }
    case 'set-frogs':
      return { ...state, ...initialFetchState, fetched: true, frogs: action.frogs }
    case 'add-frog':
      return { ...state, frogs: { ...state.frogs, [action.name]: action.frog }}
    case 'remove-frog': {
      const nextFrogs = { ...state.frogs }
      if (action.name in nextFrogs) delete nextFrogs[action.name]
      return { ...state, frogs: nextFrogs }
    }
    case 'set-params':
      return { ...state, params: { ...state.params, ...action.params } }
      default:
        return state
  }
}

const useFrogs = () => {
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const timedOutRef = React.useRef()

  function updateParams(params) {
    if (newParams != undefined) {
      dispatch({ type: 'set-params', params })
    } else {
      console.warn(
        'You tried to update state.params but the parameters were null or undefined',
      )
    }
  }

  function formatFrogs(newFrogs) {
    const formattedFrogs = newFrogs.reduce((acc, frog) => {
      const { name, age, size, children } = frog
      if (!(name in acc)) {
        acc[name] = {
          age,
          size,
          children: children.map((child) => ({
            name: child.name,
            age: child.age,
            size: child.size,
          })),
        }
      }
      return acc
    }, {})
    return formattedFrogs
  }

  function addFrog(name, frog) {
    dispatch({ type: 'add-frog', name, frog })
  }

  function removeFrog(name) {
    dispatch({ type: 'remove-frog', name })
  }

  React.useEffect(() => {
    if (frogs === null) {
      if (timedOutRef.current) clearTimeout(timedOutRef.current)

      timedOutRef.current = setTimeout(() => {
        setTimedOut(true)
      }, 20000)

      axios
        .get('https://somefrogsaspi.com/api/v1/frogs_list/', { params })
        .then((response) => {
          if (timedOutRef.current) clearTimeout(timedOutRef.current)
          const frogs = formatFrogs(response.data)
          dispatch({ type: 'set-frogs', frogs })
        })
        .catch((error) => {
          if (timedOutRef.current) clearTimeout(timedOutRef.current)
          console.error(error)
          dispatch({ type: 'fetch-error', error })
        })
    }
  }, [])

  return {
    fetching,
    fetched,
    fetchError,
    timedOut,
    frogs,
    params,
    addFrog,
    removeFrog,
  }
}

export default useFrogs

Although this may arguably not be cleaner than the useState approach when you look at it, it is easier to manage when you're implementing the custom hook using the useReducer version because you don't have to worry about keeping track of state updates in multiple parts of the hook since you'll have it all defined in one place inside the reducer.

We've also now defined an "official" set of rules of how the manipulation of state.frogs will be manipulated inside the reducer function and have a direct, clearer separation of logic. In other words, if we were to continue using useState for this there won't be a pre-defined entity unlike the useReducer where all the logic was placed inside the reducer.

In the useState version, we had to declare functions inside the hook in order to figure out the next part of the state, in addition to writing the logic, where as in the useReducer version we didn't have to do that and instead moved them to the reducer function. We just needed to call the type of action and that's all it needed to worry about :)

11. Use Function Declaration In Dull Areas

A good example of this is the useEffect clean up handler:

React.useEffect(() => {
  setMounted(true)

  return () => {
    setMounted(false)
  }
}, [])

As react developers who know what this does it's not a problem. But if you assume that other people are going to be reading your code it's a good idea to be explicit with code like this using function declarations because we get to name them to our advantage. For example:

React.useEffect(() => {
  setMounted(true)

  return function cleanup() {
    setMounted(false)
  }
}, [])

This more clearly describes what happens when you return the function.

12. Use Prettier

Prettier helps you and your team stay consistent with code formatting. It saves time, energy, and reduces the need to discuss the style in code reviews. It also enforces clean code practices which you can configure based on your opinions on what feels right and what doesn't.

13. Use Small Fragment over Large Fragment

Small fragment

const App = () => (
  <>
    <FrogsTable />
    <FrogsGallery />
  </>
)

Large Fragment

const App = () => (
  <React.Fragment>
    <FrogsTable />
    <FrogsGallery />
  </React.Fragment>
)

14. Put Things in Order

Something I like to do when writing code is to put things in order, like when importing files (except the react import):

import React from 'react'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import FrogsGallery from './FrogsGallery'
import FrogsTable from './FrogsTable'
import Stations from './Stations'
import * as errorHelpers from '../utils/errorHelpers'
import * as utils from '../utils/'

Some of you may think to yourself that this isn't even in alphabetical order. That's only part of what this ordering scheme is.

The way I like to order my imports for a clean approach is using these guidelines, in order of precedence:

  1. React import
  2. Library imports (Alphabetical order)
  3. Absolute imports from project (Alphabetical order)
  4. Relative imports (Alphabetical order)
  5. import * as
  6. import './<some file>.<some ext>'

And I also like to order variables in other ways:

const character = (function() {
  return {
    cry() {
      //
    },
    eat() {
      //
    },
    hop() {
      //
    },
    jump() {
      //
    },
    punch() {
      //
    },
    run() {
      //
    },
    scratch() {
      //
    },
    scream() {
      //
    },
    sleep() {
      //
    },
    walk() {
      //
    },
    yawn() {
      //
    },
  }
})()

Following a guideline helps for a cleaner code base.

Conclusion

And that concludes the end of this post! I hope you found this to be useful and stay tuned for more!

Find me on medium

Posted on by:

jsmanifest profile

jsmanifest

@jsmanifest

Obsessed with JavaScript and its technologies. Join me on my adventures.

Discussion

pic
Editor guide
 

Thank you for the great post as usual.

I agree with 10. Use useReducer if useState becomes complex not just because you can co-locate concerns but also because you can extract that functionality out as you mentioned in 6. Put Independent Functions Outside of Your Custom Hooks.

I had to create a context (using tips on How to use React Context effectively) and had very easy time make it into a context value.

 

Good tips, thanks. In regards to "folderizing your components" - do you ever run into issues with so many files called "index.js"? That's just a little thing - but it always kind of confuses/bugs me when I have a bunch of index.js functions open, and I have to figure out which is which... Any tips for that?

 

No problem! And what I do is to not write anything in the index.js but to export default the actual file in the same directory.

src/components/Tooltip/index.js:

export { default } from './Tooltip'
import Tooltip from '../components/Tooltip'

You can also put a package.json in place of index.js as below:

src/components/Tooltip/package.json

{
  "main": "./Tooltip.js"
}
import Tooltip from '../components/Tooltip'

Either way the end goal is to write code in the actual Tooltip.js file.

 

Ahhhh - got it. Thanks! that's a good idea.

No problem :)

 
const removeFalseyImages = (images = []) => images.reduce((acc, img) => (img ? [...acc, img] : acc), [])

const removeFalseyImages = (images = []) => images.filter(Boolean)

Sorry don't read the whole article after this🤢😱

 

You just showed an amazing list of good practices. Thanks for sharing them here

 

Your very welcome. Glad to hear that, Usman Khalil!

 

Great post! Thank u!