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
useCallbackevery event handleruseMemoexpensive computationsmemo“don’t render this component unless props changed since the last render”PureComponentismemofor 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
// #endregionForms
- see Events & Forms
 - pick and use a form library
 - when implementing form controls use 
value&onChange