網站搜尋

javascript - 組件因上下文中未使用的屬性而重新渲染


我有一個 AsyncContext,它允許我啟動/停止任何類型的異步計算。在幕後,它管理一個全局加載器和一個 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)

這裡有一個消費者:

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>
  )
}

如你所見,MyChild 並不在意 loading。但它被包含在上下文中,所以這個元件無緣無故重渲染了兩次。

為了防止這種情況,我的第一個嘗試是將我的組件分成兩個,並使用 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>
  )
})

它可以運作,但我不想拆分所有使用 AsyncContext 的子項目。

第二次嘗試是直接在 JSX 上使用 useMemo:

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>
  ), [])
}

它也可行,內容更精簡,但我不確定這是不是一個好的做法。

我的兩種方法有哪一種是正確的嗎?如果都不正確,你會建議什麼?

答案#1

我認為我找到了最佳的方法,多虧了 https://kentcdodds.com/blog/how-to-use-react-context-effectively:將 context 分成兩個 context。 一個用於 state,另一個用於 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})

如果消費者不需要這個狀態,我只需添加 const {start, stop}=useContext(AsyncDispatchContext),就不會有重新渲染。