React useContext 不会在状态更改时重新渲染

时间:2021-03-27 23:15:33

标签: reactjs react-hooks use-context

我是新手,所以如果这是一个非常基本的问题,我深表歉意。我无法在任何地方找到答案。

我有一个 ExtensionsContext 组件,用于存储扩展类对象数组作为状态。

import React, { useContext, useState } from 'react'

const ExtensionsContext = React.createContext()

export function useExtensions() {
    return useContext(ExtensionsContext)
}

export function ExtensionsProvider ({ children }) {
    const [exts, setExtensions] = useState(startingExtensions) // startingExtentions is an array of Extention class objects

    AppExtensions = {
        extensions: exts,
        update: function () { setExtensions(this.extensions) }
    }

    return (
        <ExtensionsContext.Provider value={AppExtensions}>
            {children}
        </ExtensionsContext.Provider>
    )
}
class Extension { 
    constructor (name, index) {
        this.name = name
        this.isOpen = false
        this.id = index
    }
    open () {
        this.isOpen = true
        AppExtensions.update()
    }
    close () {
        this.isOpen = false
        AppExtensions.update()
    }
}

这些扩展正在一个组件中使用,该组件在打开和关闭之间列出和分组它们。目前,如果我在其中一个扩展上调用 open 方法,它将更改扩展值,但不会重新渲染任何组件。我目前已将其设置为当单击按钮时,它会在组件中的布尔状态在 true 和 1 之间切换以强制重新渲染,但我知道我做错了什么,并且有更好的解决方案。如果我在多个地方使用这些扩展,这也不起作用。我想问题与我传入 AppExtensions 对象和 exts 的副本而不是直接传入 exts 的事实有关。我希望能够调用extension.open() 单击并处理 ExtensionsProvider 中的所有逻辑。

1 个答案:

答案 0 :(得分:0)

React 通过浅层比较当前值和先前值来检测变化。如果它是一个原始值(例如一个数字),它会比较实际值。如果它是一个对象或数组,React 会比较对该对象的引用,而不是实际内容,即使它发生了变化。

这意味着设置相同的扩展名数组不会有任何作用,因为它是同一个数组。这也意味着更改单个属性也无济于事。您需要重新创建数组,并且您需要重新创建更改的对象。

在我下面的示例中,每个 Extension 对象都有 toggle 方法,该方法返回一个基于前一个对象的新对象,并切换了 isOpen 属性。 update 方法通过映射扩展并切换对象之一来重新创建 extensions 数组。

const { useContext, useState, useCallback } = React

const ExtensionsContext = React.createContext()

function useExtensions() {
  return useContext(ExtensionsContext)
}

class Extension {
  constructor (name, index, isOpen = false) {
    this.name = name
    this.isOpen = isOpen
    this.id = index
  }
  toggle() {
    return new Extension(this.name, this.id, !this.isOpen);
  }
}

const createStartingExtensions = () =>
  ['X', 'Y', 'Z'].map((name, i) => new Extension(name, i))

function ExtensionsProvider ({ children }) {
  const [extensions, setExtensions] = useState(createStartingExtensions) // startingExtentions is an array of Extention class objects
  
  const update = useCallback((target) => { 
    setExtensions(extensions => 
      extensions.map(o => 
        o === target ? o.toggle() : o
      ))
  }, []) // recreate the array of extensions
  
  const appExtensions = {
    extensions,
    update
  }

  return (
    <ExtensionsContext.Provider value={appExtensions}>
      {children}
    </ExtensionsContext.Provider>
  )
}

const List = () => {
  const { extensions, update } = useExtensions()

  return (
    <ul>
    {extensions.map(ext => (
      <li key={ext.id} onClick={() => update(ext)}>{ext.name} {ext.isOpen ? 'open' : 'close' }</li>
    ))}
    </ul>
  )
}

const Demo = () => (
  <ExtensionsProvider>
    <List />
  </ExtensionsProvider>
)

ReactDOM.render(
  <Demo />,
  root
)
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

<div id="root"></div>