3

I have an AsyncContext that allows me to start / stop any kind of async computation. Behind the scene, it manages a global loader and a snackbar.

export type Context = {
  loading: boolean
  start: () => void
  stop: (message?: string) => void
}

const defaultContext: Context = {
  loading: false,
  start: noop,
  stop: noop,
}

export const AsyncContext = createContext(defaultContext)

Here a consumer:

const MyChild: FC = () => {
  const {start, stop} = useContext(AsyncContext)

  async function fetchUser() {
    try {
      start()
      const res = await axios.get('/user')
      console.log(res.data)
      stop()
    } catch (e) {
      stop('Error: ' + e.message)
    }
  }

  return (
    <button onClick={fetchData}>
      Fetch data
    </button>
  )
}

As you can see, MyChild does not care about loading. But it is included in the context, so the component re-render 2 times for nothing.

To prevent this, my first attempt is to split my component in two, and use a memo:

type Props = {
  start: AsyncContext['start']
  stop: AsyncContext['stop']
}

const MyChild: FC = () => {
  const {start, stop} = useContext(AsyncContext)
  return <MyChildMemo start={start} stop={stop} />
}

const MyChildMemo: FC<Props> = memo(props => {
  const {start, stop} = props

  async function fetchUser() {
    try {
      start()
      const res = await axios.get('/user')
      console.log(res.data)
      stop()
    } catch (e) {
      stop('Error: ' + e.message)
    }
  }

  return (
    <button onClick={fetchData}>
      Fetch data
    </button>
  )
})

It works, but I do not want to split all children that use AsyncContext.

The second attempt is to use useMemo directly on the JSX:

const MyChild: FC = () => {
  const {start, stop} = useContext(AsyncContext)

  async function fetchUser() {
    try {
      start()
      const res = await axios.get('/user')
      console.log(res.data)
      stop()
    } catch (e) {
      stop('Error: ' + e.message)
    }
  }

  return useMemo(() => (
    <button onClick={fetchData}>
      Fetch data
    </button>
  ), [])
}

It works also, it is more condensed, but I'm not sure if this is a good practice.

Is any of my two approaches correct? If not, what would you advise?

soywod
  • 4,377
  • 3
  • 26
  • 47

1 Answers1

0

I think I found the best approach, thanks to https://kentcdodds.com/blog/how-to-use-react-context-effectively: to separate the context in two contexts. One for the state, one for the dispatch:

type StateContext = boolean
type DispatchContext = {
  start: () => void
  stop: (message?: string | void) => void
}

export const AsyncStateContext = createContext(false)
export const AsyncDispatchContext = createContext({start: noop, stop: noop})

If a consumer does not need the state, I just add const {start, stop} = useContext(AsyncDispatchContext) and I don't have re-renders.

soywod
  • 4,377
  • 3
  • 26
  • 47