React Style Guide
This is a list of patterns that I follow when writing React code. It’s intended to be a living document and is always subject to change and reconsideration.
Hooks
useState: Name the array destructure variables with the pattern state
and setState
.
Why?
- To know which function affects which variable, especially when multiple
useState
s exist.
Do
const [name, setName] = useState('')
Don’t
const [firstName, setName] = useState('')
The exception to this rule is booleans, which follow a slightly different naming approach, outlined below.
useState: Use is
before a boolean state variable
Why?
- To identify a boolean type in the state variable
- Naming becomes
[isState, setState]
- No need for “is” in the function name, since
setState
will always be called with a boolean
Do
const [isSelected, setSelected] = useState(false)
Don’t
const [selected, setSelected] = useState(false)
// or
const [isSelected, setIsSelected] = useState(false)
useState: Set a default state where possible
Do
const [count, setCount] = useState(0)
Don’t
const [isSelected, setSelected] = useState()
// or when a default would be inaccurate if persisted
const [age, setAge] = useState(0)
useState: Prefer functional setState
when relying on current state
Why?
- React may batch multiple
setState
calls into a single update for performance - It may be unsafe to rely on current state values when updating state
- Pass a function into
setState
to get the current state
Do
const [count, setCount] = useState(0);
// ...
<p>{count}</p>
<button onClick={() => setCount(prevCount => prevCount + 1)}>
Increment
</button>
Don’t
const [count, setCount] = useState(0);
// ...
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
useEffect: Separate useEffect
s based on dependency array
Why?
- To prevent unneccesary rerenders
- To group effects based on “what function(s) should run when the variable(s) in the array change”
Do
useEffect(() => {
runOnlyOnMount()
}, [])
useEffect(() => {
runWhenCountChanges()
}, [count])
Don’t
useEffect(() => {
runOnlyOnMount() // will rerun when count changes
runWhenCountChanges()
}, [count])
useEffect: Comment on effect’s purpose
Why?
- To reduce the time it takes to understand what the effect does
Do
// set adding state to false if a todo is selected
useEffect(() => {
if (selectedTodo) {
setAdding(false)
}
}, [selectedTodo])
Don’t
useEffect(() => {
if (selectedTodo) {
setAdding(false)
}
}, [selectedTodo])
While the above example is simplified, the helpfulness of a comment grows proportionally to the complexity of the effect.
useReducer: Prefer this over useState
when pieces of state need to know about one another
Why?
- To keep states that need to know about each other together
Do
const initialState = {
length: 0,
width: 0,
area: 0,
}
const reducer = (state, action) => {
switch (action.dimension) {
case 'length':
return {
...state,
length: action.value,
area: action.value * state.width,
}
case 'width':
return {
...state,
width: action.value,
area: action.value * state.length,
}
default:
throw new Error('Invalid action in reducer')
}
}
// implementation
const [state, dispatch] = useReducer(reducer, initialState)
<button onClick={() => dispatch({ dimension: 'length', value: state.length + 1 })}>
Increment Length
</button>
The above example shows that area
needs to update when length
or width
change. Though it is a bit contrived and could be refactored for simplicity.
Don’t
const [length, setLength] = useState(0)
const [width, setWidth] = useState(0)
const [area, setArea] = useState(0)
const updateDimension = dimension => {
// update length or width
// update area with updated length/width
}
Context
Export custom useContext
hook
Why?
- To reduce imports in the context implementation
Do
import React, { createContext, useContext } from 'react'
const TodosContext = createContext({})
export const TodosContextProvider = ({ children }) => {
// context logic
}
export const useTodosContext = () => {
const context = useContext(TodosContext)
if (context === undefined) {
throw new Error(
'useTodosContext must be used within the TodosContextProvider'
)
}
return context
}
// use case
import { useTodosContext } from '.'
//...
const { todos, setTodos } = useTodosContext()
Don’t
import React, { createContext, useContext } from 'react'
export const TodosContext = createContext({})
export const TodosContextProvider = ({ children }) => {
// context logic
}
// use case - two imports
import React, { useContext } from 'react'
import { TodosContext } from '.'
// ...
const { todos, setTodos } = useContext(TodosContext)
Shout out to Kent C. Dodds for this pattern.
useMemo
return value and useCallback
functions in context
Why
- To avoid unneccesary rerenders of context and its children
Do
export const TodosContextProvider = ({ children }) => {
const [todos, setTodos] = useState([])
const toggleTodo = useCallback(
id => {
// toggle the todo, sorting, side effects, etc.
// setTodos here
setTodos(newTodos)
},
[todos]
)
// same thing with addTodo and deleteTodo functions
const value = useMemo(() => {
return {
todos,
addTodo,
toggleTodo,
deleteTodo,
}
}, [todos, addTodo, toggleTodo, deleteTodo])
return <TodosContext.Provider value={value}>{children}</TodosContext.Provider>
}
TypeScript
Prefer interface Props
to type props on the main exported component
Why?
- To differentiate the main component being exported from co-located components
Do
// Modal.tsx
import styled from 'styled-components'
interface ButtonProps {
// button props
}
const Button =
styled.button <
ButtonProps >
`
// button styles
`
interface Props {
// Modal props
isVisible: boolean;
}
export function Modal({ isVisible }: Props) {
return (
<>
{/* Modal component JSX */}
<Button />
</>
)
}
Don’t
// Modal.tsx
import styled from 'styled-components'
interface ButtonProps {
// button props
}
const Button =
styled.button <
ButtonProps >
`
// button styles
`
interface ModalProps {
// Modal props
isVisible: boolean;
}
export function Modal({ isVisible }: ModalProps) {
return (
<>
{/* Modal component JSX */}
<Button />
</>
)
}
Prefer to type arrays over array method callback arguments
Why?
- TypeScript will be more useful if it knows the array type
- If TS says that an array method callback must be typed, it’s a sign that the array is not typed and TS infers it an
any
Do
const fruits: Array<String> = ['apple', 'banana', 'watermelon', 'tomato']
const fruitSalad = fruits.filter(fruit => fruit !== 'tomato')
Don’t
const fruits = ['apple', 'banana', 'watermelon', 'tomato']
const fruitSalad = fruits.filter((fruit: string) => fruit !== 'tomato')
Styled Components
Co-locate Styled Components with React component and abstract as needed
Why?
- To indicate which directories might be affected by a style change
Start with the Styled Component in the same file as the React component. Move the styles up to a styled.js
file only as high up in the directory as needed.
Conditionally Rendering Components
Guard before render
Why?
- Readability - less mental context to hold onto while reading the component
- Component flow becomes:
- Declaration, props accepted
- Exception/error handling
- Business logic
Do
const UserAvatar = ({ user }) => {
if (!user) return false
// return the component
}
Don’t
const UserAvatar = ({ user }) => {
if (user) return // the component
if (!user) return false
}
// or
const UserAvatar = ({ user }) => {
if (user) return {
// the component
} else {
return false
}
}
It’s also ok to have the conditional logic guard against the component call, where applicable.
Do
const UserProfile = () => {
const { user } = useContext(UserContext)
return (
<>
{user && <UserAvatar user={user} />}
<RenderAnyway />
</>
)
}
Import/Export
Destructure imports when possible
Why?
- Cleaner implementation code
- See all imported functions at a glance
Example
Do
import React, { useState } from 'react'
// ...
const [state, setState] = useState()
Don’t
import React from 'react'
// ...
const [state, setState] = React.useState()
Prefer named exports
Why?
- Easier to trace usage of components with string search when debugging
Example
Do
const Component = () => {
// component
}
export { Component }
Don’t
const Component = () => {
// component
}
export default Component
Named exports ensure that all declared and imported components have the same spelling. Exporting a default component allows a typo or a differently-named component to be imported.
React Router
If <Link>
needs an onClick
, use history.push
Why?
- Issues with
<Link onClick={} />
in Firefox
Example
<button
onClick={() => {
history.push('/path')
handleSideEffect()
}}
as="a"
>
Navigate
</button>
Remember to add as="a"
as a prop so that screen readers know that the button is a link.
Conclusion
That’s it for now. If you have any thoughts or comments you’d like to share, I’d love to hear them! Let me know via Twitter or GitHub.