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:
- Imports and exports
- 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:
- React import
- Library imports (Alphabetical order)
- Absolute imports from project (Alphabetical order)
- Relative imports (Alphabetical order)
import * as
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
Top comments (11)
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:
You can also put a package.json in place of
index.js
as below:src/components/Tooltip/package.json
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 :)
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 in6. 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.
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!
It's crucial to prioritize writing clean code, especially when aiming to land your first developer job. Clean code demonstrates your ability to be a team player fnaf world and can significantly impact your success in a job interview.
Great post! Thank u!
I think you might be interested in sort-imports plugin for eslint, it's configurable and could potentially do the import sorting for you.
Nice article 🙂