Best Practices
obviously opinionated, apply as appropriate
Tools
typescript
- typed templates are awesome
- 1000x better than
propTypes
eslint
- automated code review
prettier
- never worry about about whitespace again
jest
- unit testing never felt this good
husky
&lint-staged
- enforce linting rules on everyone
- less failed builds
create-react-app
- baked in with most of these
Components
- prefer
FC
- compare
- smaller the better
- prefer primitive state/props over objects/arrays
- React can optimize away updates
import React, { FC } from 'react'
export const BadFlexBox: FC<{
place?: { align?: string; justify?: string }
}> = () => <></>
export const badFlexBox = (
<BadFlexBox place={{ align: 'center' }}>children</BadFlexBox>
)
export const GoodFlexBox: FC<{
align?: string
justify?: string
}> = () => <></>
export const goodFlexBox = <GoodFlexBox align="center">children</GoodFlexBox>
export const Select: FC<{ options: string[] }> = () => <></>
const constantOptions = ['hello', 'world']
export const goodSelect = <Select options={constantOptions} />
export const badSelect = <Select options={['hello', 'world']} />
- custom hook all the things
- even if it’s not (yet) shared
- easier testing, can be mocked
- props
- use destructuring to set defaults
- spread unused props (contentious)
import React, { FC, InputHTMLAttributes } from 'react'
export const ClosedInput: FC<{ type?: 'text' | 'tel' }> = ({
type = 'text',
}) => <input type={type} />
export const typeText = <ClosedInput />
export const typeTel = <ClosedInput type="tel" />
// export const cannotAddEvents = <ClosedInput onKeyDown={() => {}} />
export const OpenInput: FC<InputHTMLAttributes<HTMLInputElement>> = ({
type = 'text',
...props
}) => <input type={type} {...props} />
export const canAddEvents = <OpenInput onKeyDown={() => {}} />
- parent-child communication & global state via context
- don’t
cloneElement
- don’t
import React, {
createContext,
CSSProperties,
FC,
ReactNode,
useContext,
useMemo,
} from 'react'
// #region global
export interface GlobalContextType {
theme: 'dark' | 'light'
}
export const GlobalContext = createContext<GlobalContextType>({ theme: 'dark' })
export const WithGlobal: FC = () => {
const { theme } = useContext(GlobalContext)
const style = useMemo(
(): CSSProperties => ({ color: theme === 'dark' ? '#f8f8f2' : '#282a36' }),
[theme],
)
return <div style={style}>children</div>
}
// #endregion
//
// #region local
export const LocalContext = createContext<{ index: number }>(null!)
export const List: FC<{ items: ReactNode[] }> = ({ items }) => {
return (
<ul>
{items.map((item, index) => (
<LocalContext.Provider key={Math.random()} value={{ index }}>
{item}
</LocalContext.Provider>
))}
</ul>
)
}
export const NumberedItem: FC = ({ children }) => {
const { index } = useContext(LocalContext)
return (
<li>
({index + 1}) {children}
</li>
)
}
export const UnnumberedItem: FC = ({ children }) => {
return <li>{children}</li>
}
const items = [
<UnnumberedItem>hello</UnnumberedItem>,
<UnnumberedItem>world</UnnumberedItem>,
<NumberedItem>hello</NumberedItem>,
<NumberedItem>world</NumberedItem>,
]
export default <List items={items} />
// #endregion
- hello
- world
- (3) hello
- (4) world
Performance
useCallback
every event handleruseMemo
expensive computationsmemo
“don’t render this component unless props changed since the last render”PureComponent
ismemo
for class components- test with DevTools and/or why-did-you-render
- respect the change detector (
Object.is()
)
import React, {
ChangeEventHandler,
FC,
memo,
useCallback,
useMemo,
useState,
} from 'react'
const words = [
'Lorem',
'ipsum',
'dolor',
'sit',
'amet',
'consectetur',
'adipiscing',
'elit',
]
export const Slower: FC = () => {
const [filter, setFilter] = useState('')
return (
<>
<input
aria-label="filter"
placeholder="filter"
value={filter}
onChange={(event) => setFilter(event.target.value)}
/>
<ul>
{words
.filter((word) => word.toLowerCase().includes(filter.toLowerCase()))
.map((word) => (
<li key={word}>{word}</li>
))}
</ul>
</>
)
}
export const ProbablyFaster: FC = memo(() => {
const [filter, setFilter] = useState('')
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
(event) => setFilter(event.target.value),
[],
)
const filteredWords = useMemo(
() =>
words.filter((word) => word.toLowerCase().includes(filter.toLowerCase())),
[filter],
)
return (
<>
<input
aria-label="filter"
placeholder="filter"
value={filter}
onChange={onChange}
/>
<ul>
{filteredWords.map((word) => (
<li key={word}>{word}</li>
))}
</ul>
</>
)
})
export default <ProbablyFaster />
- Lorem
- ipsum
- dolor
- sit
- amet
- consectetur
- adipiscing
- elit
Organization
- 1 feature per directory
- 1 component per file
- helper & tightly coupled components / hooks can be in same directory
- keep business logic out of components
import React, { FC, useEffect, useState } from 'react'
// #region tangled business logic
export const WithBusiness: FC = () => {
const [result, setResult] = useState(null)
useEffect(() => {
fetch('my-business/api/resource.json', {
headers: { that: 'a', component: 'should', not: 'worry', about: '!' },
})
.then((r) => r.json()) // untyped interface
.then(setResult)
// no error handling
}, [])
return <>display {result} here</>
}
/**
* @example tests
* ```
* describe('WithBusiness', () => {
* beforeEach(() => {
* // no no
* jest.spyOn(window, 'fetch').mockRejectedValue(new Error())
* })
* })
* ```
*/
// #endregion
//
// #region separate business logic
// #region my-api.ts
export interface Resource {
defined: 'interface'
}
class MyAPIImpl {
getResource(): Promise<Resource> {
throw new Error('not implemented')
}
}
export const MyAPI = new MyAPIImpl()
/**
* @example tests
* ```
* describe('MyApi', () => {
* beforeEach(() => {
* // the only place where this has to be done
* jest.spyOn(window, 'fetch').mockRejectedValue(new Error())
* })
* })
* ```
*/
// #endregion
// #region use-my-resource.ts
export const useMyAPIResource = (): [null | unknown, Resource | null] => {
const [result, setResult] = useState<Resource | null>(null) // typed
const [error, setError] = useState<null | unknown>(null)
useEffect(() => {
let shouldUpdate = true
MyAPI.getResource()
.then((r) => shouldUpdate && setResult(r))
.catch((e) => shouldUpdate && setError(e)) // unified error handling
return () => {
shouldUpdate = false
}
}, [])
return [error, result] // clear interface that forces error handling
}
// #endregion
// #region my-resource.tsx
export const WithoutBusiness: FC = () => {
const [error, resource] = useMyAPIResource()
if (error) return <>oh no</>
if (resource) return <>here it is {resource.defined}</>
return <>loading</>
}
// it is much easier to mock either hook or api (don't mock both)
/**
* @example tests
* ```
* jest.mock('./use-my-api-resource')
* const useMyAPIResourceMock = useMyAPIResource as jest.MockedFunction<
* typeof useMyAPIResource
* >
* describe('WithoutBusiness', () => {
* beforeEach(() => {
* useMyAPIResourceMock.mockReturnValue([null, { defined: 'interface' }])
* })
* })
* ```
*/
// #endregion
// #endregion
Forms
- see Events & Forms
- pick and use a form library
- when implementing form controls use
value
&onChange