A short note on the way this document is organized
Note:
As I was writing this, I realized that it was actually difficult for me to separate my thoughts into the design
, performance
, and testing
. I noticed that a lot of designs aimed for maintainability also makes your application faster. So apologies in advance if the discussion appears to be cluttered at times.
react-philosophies
is:
- things I think about before I write
React
code. - at the back of my mind whenever I review someone else's code or my own
- just guidelines and NOT rigid rules
- a living document and will evolve overtime as my experience grows
- mostly techniques which are variations of basic refactoring methods, SOLID principles, and extreme programming ideas... just applied to React specifically π
A lot of these things may feel like very basic and common-sense. But surprisingly, I've worked with large complex applications where these things are not taken into consideration. The examples I present here are based on code I have actually seen in production.
react-philosophies
is inspired by various places I've stumbled upon at different points of my coding journey.
Most notably:
- Tim Peters: Zen of Python (Pep 20)
- Dev Cheney: Zen of Go
- Sandi Metz
- Kent C Dodds
- Martin Fowler
- Dan Abramov
- Bob Martin (Not saying I agree with his political views)
- wiki.c2.com
- sapegin/washingcode-book
- trekhleb/state-of-the-art-shitcode
- droogans/unmaintainable-code
Special thanks to the r/reactjs
community for giving very valuable suggestions that helped significantly improve the quality of this document.
Comments, suggestions, violent reactions? I'd love to hear them!
Note: If there's something that you think should be part of my reading list, or if you have great ideas that you think I should include here, don't hesitate to submit a PR or an issue; I'll check it out. Any contributions to improve this document whether big or small is always welcome and appreciated.
Resources recommended to me that I haven't checkout yet
β If this document helped you in some way, consider βββ buying me a few cups of coffee βββ to motivate me to write more about React! β
- Statically analyze your code with
ESLint
. Enable therule-of-hooks
andexhaustive-deps
rule to catch react-specific errors. Typescript
will make your life so much easierNextJS
is an awesome framework- Be honest about your dependencies. Fix
exhaustive-deps
warnings / errors foruseMemo
,useCallback
anduseEffect
. You can also use "The Latest ref pattern" (Yago) - Always add keys whenever you use
map
to display components - Only Call Hooks at the Top Level. Donβt call Hooks inside loops, conditions, or nested functions
- Understand the warning "Can't perform state update on unmounted component" See PR: facebook/react/pull/22114, Reddit/u/free_username17
- If you see a warning or error in the console, it's there for a reason
- Prettier (or an alternative) formats your code for you, giving you consistent formatting every time. You no longer need to think about it!
- I highly recommend Code Climate (or similar) for open-source repositories or if you can afford it. Automatically detecting code smells really helps a lot to motivate you to reduces technical debts of your application
tree-shaking
is your friend!- Prevent the "white screen of death" by adding several error boundaries at different levels of your application. You can also use them to send alerts to an error monitoring service such as Sentry if you need to
"The best code is no code at all. Every new line of code you willingly bring into the world is code that has to be debugged, code that has to be read and understood, code that has to be supported." - Jeff Atwood
"One of my most productive days was throwing away 1000 lines of code." - Eric S. Raymond
"I hate code and I want as little of it as possible in our product" - Jack Diederich
"If I had more time, I would have written a shorter letter" - Blaise Pascal, Mark Twain, among others..
See also: Write Less Code - Richard Hariss (Svelte), Washing Code: Code is evil - Artem Sapegin
- Think first before adding another dependency
- Eliminate code with techniques not unique to
React
- Don't be clever. YAGNI!
Needless to say, the more you add dependencies, the more code you ship to the browser. Ask yourself, are you actually using the features that makes a particular library great?
- Do you really need
Redux
? It's possible. But keep in mind that React is already a state management library. - Do you really need
Apollo client
? Apollo client has many awesome features, like manual normalization. However, it will significantly increase your bundle size. If your application only makes use of features that are not unique to Apollo client , consider using a smaller library such as react-query or SWR (or none at all). Axios
? Axios is a great library with features that are not easily replicable with nativefetch
. But if the only reason for using Axios is that it has a better looking API, then consider just making a wrapper on top of fetch. Determine whether or not your application is actually using Axios's best features.Lodash
/underscoreJS
? you-dont-need/You-Dont-Need-Lodash-UnderscoreMomentJS
? you-dont-need/You-Dont-Need-Momentjs- You might not need
Context
for theming (light
anddark
mode), consider usingcss variables
instead. - You might not even need
Javascript
. CSS is powerful. you-dont-need/You-Dont-Need-JavaScript
React
is just Javascript
and Javascript
is just code
- Simplify complex conditionals and exit early if you can.
- If there is no discernable performance difference, replace traditional loops with chained higher-order functions (
map
,filter
,find
,findIndex
,some
, etc)
"What could happen with my software in the future? Oh yeah, maybe this and that. Letβs implement all these things since we are working on this part anyway. That way itβs future-proof."
You Aren't Gonna Need It! Always implement things when you actually need them, never when you just foresee that you may need them.
See also: Martin Fowler: YAGNI, C2 Wiki: You Arent Gonna Need It!, C2: YAGNI original, Jack Diederich: Stop Writing Classes
Detect code smells and do something about them if you need to.
π Examples of easy-to-catch code smells
- β Methods or functions defined with a high number of arguments
- β Boolean logic that may be hard to understand
- β Excessive lines of code within a single file
- β Duplicate code which is syntactically identical (but may be formatted differently)
- β Functions or methods that may be hard to understand
- β Classes defined with a high number of functions or methods
- β Excessive lines of code within a single function or method
- β Functions or methods with a high number of return statements
- β Duplicate code which is not identical but shares the same structure (e.g. variable names may differ)
TIP: Remember that you may not need to put your state
as a dependency because you can pass a callback function instead.
You don't need to put setState
(from useState
) and dispatch
(from useReducer
) in your dependency array hooks like useEffect
and useCallback
. ESLint will NOT complain because React guarantees their stability.
π Example:
β Not-so-good
const decrement = useCallback(() => setCount(count - 1), [setCount, count])
const decrement = useCallback(() => setCount(count - 1), [count])
β
BETTER
const decrement = useCallback(() => setCount(count => (count - 1)), [])
TIP: If your useMemo
or useCallback
doesn't have a dependency, you might be using it wrong.
π Example
β Not-so-good
const MyComponent = () => {
const functionToCall = useCallback(x: string => `Hello ${x}! I am actually doing more than this`,[])
const iAmAConstant = useMemo(() => { return {x: 5, y: 2} }, [])
/* I will use functionToCall and iAmAConstant */
}
β
BETTER
const I_AM_A_CONSTANT = { x: 5, y: 2 }
const functionToCall = (x: string => `Hello ${x}! I am actually doing more than this`)
const MyComponent = () => {
/* I will use functionToCall and I_AM_A_CONSTANT */
}
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." - Martin Fowler
"The ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. So if you want to go fast, if you want to get done quickly, if you want your code to be easy to write, make it easy to read." β Robert C. Martin
- π Avoid state management complexity by removing redundant states
- π Pass the banana, not the gorilla holding the banana and the entire jungle (prefer passing primitives as props)
- π Keep things small and simple. (The single responsibility principle and separation of concerns)
- Duplication is far cheaper than the wrong abstraction (avoid premature / inappropriate generalization)
- Avoid prop drilling by using composition (KCD: Prop Drilling)
- Split giant
useEffect
s to smaller independent ones (KCD: Myths about useEffect) - Extract logic to hooks and helper functions
- It might be a good idea to have
logical
andpresentational
components - Prefer having mostly primitives as dependencies to
useCallback
,useMemo
anduseEffect
- Do not put too many dependencies in
useCallback
,useMemo
anduseEffect
- Consider using
useReducer
, if some values of your state rely on other values of your state Context
is not the solution for every state sharing problem
When you have redundant states, some states may fall out of sync; you may forget to update it given a complex sequence of interactions.
Aside from avoiding synchronization bugs
, you'd notice that it's also easier to reason about and require less code.
See also: KCD: Don't Sync State. Derive It!, Tic-Tac-Toe
You are tasked to display properties each right triangle (from a list)
- the lengths of each of the three sides
- the perimeter
- the area
A list of two numbers {a: number, b: number}[]
should be fetched from an API. The two numbers represent the two shorter sides of a right triangle.
β Not-so-good Solution
const TriangleInfo = () => {
const [triangleInfo, setTriangleInfo] = useTriangles<{a: number, b: number}>([])
const [hypotenuses, setHypotenuses] = useState<number[]>([])
const [perimeters, setPerimeters] = useState<number[]>([])
const [areas, setAreas] = useState<number[]>([])
useEffect(() => {
fetchTriangles().then(r => {
setTriangleInfo(r)
setHypotenuses(r.map(t => computeHypotenuse(t.a, t.b))
setArea(r.map(t => computeArea(t.a, t.b))
})
}, [])
useEffect(() => {
setHypotenuses(triangleInfo.map(t => computeHypotenuse(t.a, t.b))
setArea(triangleInfo.map(t => computeArea(t.a, t.b))
}, [triangleInfo])
useEffect(() => {
const p = triangleInfo((t, i) => {
return computePerimeter(t.a, t.b, hypotenuse[i])
})
}, [triangleInfo, hypotenuses])
/*** show here info here ****/
}
β Better Solution
const TriangleInfo = () => {
const [triangleInfo, setTriangleInfo] = useTriangles<{a: number, b: number}>([])
useEffect(() => {
fetchTriangles().then(r => setTriangleInfo(r))
}, [])
const areas = triangleInfo.map(t => computeArea(t.a, t.b))
const hypotenuses = triangleInfo.map(t => computeHypotenuse(t.a, t.b))
const perimeters = triangleInfo.map((t, i) => computePerimeters(t.a, t.b, hypotenuses[i]))
/*** show here info here ****/
}
Suppose you are assigned to design a component which:
- Fetches a list of unique points from an api
- Includes a button to either sort by x or y (ascending order)
- Includes a button to change the
maxDistance
(increase + 10) used for filtering displayed points - Only display the points that is NOT farther than the current
maxDistance
from the origin(0, 0)
β Not-so-good Solution
type SortBy = 'x' | 'y'
const toggle = (current: SortBy): SortBy => current === 'x' ? : 'y' : 'x'
const Points = () => {
const [points, setPoints] = useState<{x: number, y: number}[]>([])
const [filteredPoints, setFilteredPoints] = useState<{x: number, y: number}[]>([])
const [sortedPoints, setSortedPoints] = useState<{x: number, y: number}[]>([])
const [maxDistance, setMaxDistance] = useState<number>(Infinity)
const [sortBy, setSortBy] = useState<SortBy>('x')
useEffect(() => {
fetchPoints().then(r => setPoints(r))
}, [])
useEffect(() => {
setSortedPoints(sortPoints(points, sortBy))
}, [sortBy, points])
useEffect(() => {
setFilteredPoints(sortedPoints.filter(p => getDistance(p.x, p.y) < maxDistance))
}, [sortedPoints, maxDistance])
const otherSortBy = toggle(sortBy)
return (
<>
<button onClick={() => setSortBy(otherSortBy)}>Sort by: {otherSortBy}<button>
<button onClick={() => setMaxDistance(maxDistance + 5)}>Increase max distance<button>
Showing only points that are less than {maxDistance} units away from origin `(0, 0)`
Currently sorted by: {sortBy} in increasing order
<ol>{filteredPoints.map(p => <li key={`${p.x}|{p.y}`}>({p.x}, {p.y})</li>}
</>
)
}
β Better Solution
// NOTE: You can also use useReducer instead
type SortBy = 'x' | 'y'
const toggle = (current: SortBy): SortBy => current === 'x' ? : 'y' : 'x'
const Points = () => {
const [points, setPoints] = useState<{x: number, y: number}[]>([])
const [maxDistance, setMaxDistance] = useState<number>(Infinity)
const [sortBy, setSortBy] = useState<SortBy>('x')
useEffect(() => {
fetchPoints().then(r => setPoints(r))
}, [])
const otherSortBy = toggle(sortBy)
return (
<>
<button onClick={() => setSortBy(otherSortBy)}>Sort by: {otherSortBy} <button>
<button onClick={() => setMaxDistance(maxDistance + 10)}>Increase max distance<button>
Showing only points that are less than {maxDistance} units away from origin `(0, 0)`
Currently sorted by: {sortBy} in increasing order
<ol>{
sortPoints(
points.filter(p => getDistance(p.x, p.y) < maxDistance),
sortBy
).map(p => <li key={`${p.x}|{p.y}`}>({p.x}, {p.y})</li>
}
</>
)
}
You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. - Joe Armstrong, creator of Erlang
Try to pass primitives (boolean
, string
, number
etc), instead of passing objects most of the time to avoid falling into this trap. (Passing primitives is also a good idea because if you want to use React.memo
for optimization.)
A component should just know enough to do its job and nothing more. As much as possible, components should be able to collaborate with others without knowing what they are and what they do.
When we do this, the components will be loosely coupled. Loose coupling means that the degree of dependency between two components is low, which will make it easier to change, replace, or remove components without affecting other components. See also stackoverflow:2832017
Create a UserCard
component that displays two components: Summary
and SeeMore
. The SeeMore
component includes presenting the age
and bio
of the user
. There must be button to toggle between showing and hiding the age
and bio
of the user
.
The Summary
component that displays the profile picture of the user and also his title
, firstName
and lastName
(e.g. Mr Vincenzo Cassano
) and a picture. Clicking his name should take you to the user's personal site. The Summary
component may also have other functionalities like randomly changing the font, size of the image, and background color whenever this component is clicked (We can call this the "random styling" feature).
The UserCard
calls the hook useUser
that returns an object with the type below.
type User = {
firstName: string
lastName: string
title: string
imgUrl: string
webUrl: string
age: number
bio: string
/****** 100 or more fields ******/
}
β Not-so-good solution
const Summary = ({ user } : {user: User}) => {
/*** include the "random styling" feature ***/
return (
<>
<img src={user.imgUrl} />
<a href={user.webUrl}>{user.title}. {user.firstName} {user.lastName}</a>
</>
)
}
const SeeMore = ({ user }: {user: User}) => {
const [seeMore, setSeeMore] = useState<boolean>(false)
return (
<>
<button onClick={() => setSeeMore(!seeMore)}>See more</button>
{seeMore && <>AGE: {user.age} | BIO: {user.bio}</>}
</>
)
}
const UserCard = () => {
const user = useUser()
return <><Summary user={user} /><SeeMore user={user} /></>
}
β Better solution
const Summary = ({ imgUrl, webUrl, displayName }: { imgUrl: string, webUrl: string, displayName: string }) => {
/*** include the "random styling" feature ***/
return (
<>
<img src={imgUrl} />
<a href={webUrl}>{displayName}</a>
</>
)
}
const SeeMore = ({ componentToShow }: {componentToShow: ReactNode }) => {
const [seeMore, setSeeMore] = useState<boolean>(false)
return (
<>
<button onClick={() => setSeeMore(!seeMore)}>See more</button>
{seeMore && <>{componentToShow}</>}
</>
)
}
const UserCard = () => {
const { title, firstName, lastName, webUrl, imgUrl, age, bio } = useUser()
return (
<>
<Summary displayName={`${title}. ${firstName} ${lastName}`} {...{imgUrl, webUrl}} />
<SeeMore componentToShow={<>AGE: {age} | BIO: {bio}</>} />
</>
)
}
(The paragraphs below is based on my 2016 article Three things I learned from Sandi Metzβs book as a non-Ruby programmer)
What is the single responsibility principle?
A component should have one and only one job. It should do the smallest possible useful thing. It only has responsibilities that fulfil its purpose.
A component with various responsibilities is difficult to reuse. If you want to reuse some but not all of a its behavior, it's almost always impossible to just get what you need. It is also likely to be entangled with other code. Components that do one thing which isolate that thing from the rest of your application allows change without consequence and reuse without duplication.
How to know if your component has a single responsibility?
Try to describe that component in one sentence. If it is only responsible for one thing then it should be simple to describe. If it uses the word βandβ or βorβ then it is likely that your component fails this test.
Inspect the component's states, the props and hooks it consumes, as well as variables and methods declared inside the component (They shouldn't be too many). Ask yourself: Do these things actually work together to fulfill the component's purpose? If some of them don't, consider moving those somewhere else or breaking down your big component to smaller ones.
The requirement is to display special kinds of buttons you can click to shop for items of a specific category. For example, the user can select bags, chairs, and food.
- Each button for each category has a unique label and an icon
- Each button opens a modal you can use to select and "save" items
- If the user has "saved" selected items in a specific category, that category said to be "booked"
- If it is booked, the button will have a checkmark
- You should be able to edit your booking (add or delete items) even if that category is booked
- If the user is hovering the button it should also display
WavingHand
component - The buttons should also be disabled when no items for a specific category is available
- When a user hovers a disabled button, a tooltip shows "not available"
- If the category has no items available, the button's background should be gray
- If the category is booked, the button's background should be green
- If the category has available items and is not booked, the button's background should be red
β Not-so-good solution
type ShopCategoryTileProps = {
isBooked: boolean
icon: ReactNode
label: string
componentInsideModal?: ReactNode
items?: {name: string, quantity: number}[]
}
const ShopCategoryTile = ({
icon,
label,
items
componentInsideModal,
}: ShopCategoryTileProps ) => {
const [openDialog, setOpenDialog] = useState(false)
const [hover, setHover] = useState(false)
const disabled = !items || items.length === 0
return (
<>
<Tooltip title="Not available" show={disabled }>
<StyledButton
className={disabled ? "grey" : isBooked ? "green" : "red" }
disabled={disabled}
onClick={() => disabled ? null : setOpenDialog(true) }
onMouseEnter={() => disabled ? null : setHover(true)}
onMouseLeave={() => disabled ? null : setHover(false)}
>
{icon}
<StyledLabel>{label}<StyledLabel/>
{!disabled && isBooked && <FaCheckCircle/>}
{!disabled && hover && <WavingHand />}
</StyledButton>
</Tooltip>
{componentInsideModal &&
<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
{componentInsideModal}
</Dialog>
}
</>
)
}
β Better solution
// separate to two components DisabledShopCategoryTile and ShopCategoryTile
const DisabledShopCategoryTile = ({ icon, label }: { icon: ReactNode, label: string }) => {
return (
<Tooltip title="Not available">
<StyledButton disabled={true} className="grey">
{icon} <StyledLabel>{label}<StyledLabel/>
</Button>
</Tooltip>
)
}
type ShopCategoryTileProps = {
isBooked: boolean
icon: ReactNode
label: string
componentInsideModal: ReactNode
}
const ShopCategoryTile = ({
icon,
label,
isBooked,
componentInsideModal,
}: ShopCategoryTileProps ) => {
const [openDialog, setOpenDialog] = useState(false)
const [hover, setHover] = useState(false)
return (
<>
<StyledButton
disabled={false}
className={isBooked ? "green" : "red"}
onClick={() => setOpenDialog(true) }
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{icon}
<StyledLabel>{label}<StyledLabel/>
{isBooked && <FaCheckCircle/>}
{hover && <WavingHand />}
</StyledButton>
<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
{componentInsideModal}
</Dialog>
</>
)
}
Note: This is a simplified version of a component that I've actually seen in production
β Not-so-good solution
const ShopCategoryTile = ({ item, offers }: { item: ItemMap, offers?: Offer}) => {
const dispatch = useDispatch()
const location = useLocation()
const history = useHistory()
const { items } = useContext(OrderingFormContext)
const [openDialog, setOpenDialog] = useState(false)
const [hover, setHover] = useState(false)
const isBooked =
!item.disabled &&
!!items?.some((a: Item) => a.itemGroup === item.group)
const isDisabled = item.disabled || !offers
const RenderComponent = item.component
useEffect(() => {
if (openDialog && !location.pathname.includes('item')) {
setOpenDialog(false)
}
}, [location.pathname])
const handleClose = useCallback(() => {
setOpenDialog(false)
history.goBack()
}, [])
return (
<GridStyled xs={6} sm={3} md={2} item booked={isBooked} disabled={isDisabled}>
<Tooltip
title="Not available"
placement="top"
disableFocusListener={!isDisabled}
disableHoverListener={!isDisabled}
disableTouchListener={!isDisabled}
>
<PaperStyled disabled={isDisabled} elevation={isDisabled ? 0 : hover ? 6 : 2}>
<Wrapper
onClick={() => {
if (isDisabled) {
return
}
dispatch(push(ORDER__PATH))
setOpenDialog(true)
}}
disabled={isDisabled}
onMouseEnter={() => !isDisabled && setHover(true)}
onMouseLeave={() => !isDisabled && setHover(false)}
>
{item.icon}
<Typography variant="button">{item.label}</Typography>
<CheckIconWrapper>{isBooked && <FaCheckCircle size="26" />}</CheckIconWrapper>
</Wrapper>
</PaperStyled>
</Tooltip>
<Dialog
fullScreen
open={openDialog}
onClose={handleClose}
>
{RenderComponent && (
<RenderComponent item={item} offer={offers} onClose={handleClose} />
)}
</Dialog>
</GridStyled>
)
}
See also: Sandi Metz: The Wrong Abstraction, Kent C Dodds: AHA Programming, C2 Wiki: Contrived Interfaces, C2 Wiki: Expensive Setup, C2 Wiki: Premature Generalization, Expensive Set Up Smell
Avoid premature / inappropriate generalization. If your implementation for a simple feature requires a huge overhead, consider other options.
Premature optimization is the root of all evil - Tony Hoare (popularized by Donald Knuth)
- If you think itβs slow, prove it with a benchmark. "In the face of ambiguity, refuse the temptation to guess."
- Use lazy loading
- Use
useMemo
mostly just for expensive calculations - For
React.memo
,useMemo
,useCallback
for reducing re-renders, it's better to pass mostly primitive props, if not all - Make sure your
React.memo
,useCallback
anduseMemo
is doing what you think it's doing (is it really preventing rerendering?) - Window large lists (with tannerlinsley/react-virtual or similar)
- Put
Context
as low as possible in your component tree.Context
does not have to be global to your whole app Context
should be logically separated, do not add to many values in one context provider- You can optimize
context
by separating thestate
and thedispatch
function - Stop punching yourself everytime you blink (fix slow renders before fixing rerenders)
- Putting your state as close as possible to where it's being used will make you app faster
- Consider using
react-hook-forms
Selected KCD articles about performance
- KCD: State Colocation will make your React app faster
- KCD: When to
useMemo
anduseCallback
- KCD: Fix the slow render before you fix the re-render
- KCD: Profile a React App for Performance
- KCD: How to optimize your context value
- KCD: How to use React Context effectively
- KCD: One React Mistake that is slowing you down
Write tests. Not too many. Mostly integration. - Guillermo Rauch, creator of Socket.io (and other awesome things)
- Your tests should always resemble the way your software is used
- Stop testing implementation details
- If your tests don't make you confident that you didn't break anything, then they didn't do their (one and only) job
- For the front-end, you don't need 100% code coverage, about 70% is okay
- You should very rarely have to change tests when you refactor code
- Consider using Jest, React testing library, Cypress, and Mock service worker
Selected KCD articles about testing
- KCD: Testing Implementation Details
- KCD: Stop mocking fetch
- KCD: Common mistakes with React Testing Library
- KCD: Making your UI tests resilient to change
- KCD: Write fewer, longer tests
- KCD: Write tests. Not too many. Mostly integration.
- KCD: How to know what to test
- KCD: Common Testing Mistakes
- KCD: Why you've been bad about testing
- KCD: Demystifying Testing
- KCD: UI Testing Myths
- KCD: Effective Snapshot Testing