当数据类属性发生变化时,如何使React组件重新渲染

4
在我的TypeScript应用程序中,有一个表示一些数据的类。这个类被端到端共享(前端和后端都使用它来组织数据)。它有一个名为items的属性,它是一个数字数组。
class Data {
  constructor() {
    this.items = [0];
  }

  addItem() {
    this.items = [...this.items, this.items.length];
  }
}


我试图在我的组件中呈现这些数字,但是由于修改类实例不会导致重新渲染,所以我必须"强制重新渲染"来使新的items值被呈现出来。
const INSTANCE = new Data();

function ItemsDisplay() {
  const forceUpdate = useUpdate(); // from react-use

  useEffect(() => {
    const interval = setInterval(() => {
      INSTANCE.addItem();
      forceUpdate(); // make it work
    }, 2000);

    return () => clearInterval(interval);
  }, []);

  return (
    <div>
      <h1>with class:</h1>
      <div>{INSTANCE.items.map(item => <span>{item}</span>)}</div>
    </div>
  );
}

虽然这种方法可行,但存在一个主要缺点: addItem() 不是对INSTANCE 做的唯一修改;该类实际上有大约10到15个属性,代表了不同的数据部分。因此,在每次修改发生时都执行 forceUpdate() 是一场噩梦。更不用说,如果该实例将在组件外部修改,我将无法使用forceUpdate() 将更改与组件同步。
使用useState([]) 代表items将解决此问题,但是如我所说,Data 有很多属性,还有一些函数。这是另一个噩梦。
我想知道从类实例中呈现数据的最佳方法,而不需要重新渲染或将整个实例拆分为本地组件状态。
谢谢! 这里有一个 Codesandbox演示 ,展示了使用类和本地状态之间的差异。

看起来这个Data的items是ItemsDisplay想要用useState来管理或拥有的。 - HMR
@HMR 这是我试图避免的事情;我不想将整个实例解包到本地状态中,因为它会变得非常庞大并破坏单一真相来源。 - Itay Ganor
这句话的意思是什么:INSTANCE && INSTANCE. 是否有可能 INSTANCE 是 null?根据提供的代码,这种情况不可能发生。 - HMR
@HMR 哎呀,那是代码的另一个版本中遗留下来的。我现在已经把它删除了。 - Itay Ganor
如果可以观察数据实例的更改(添加更改侦听器),会怎样呢?这样,您就可以在组件中创建一个效果,当数据发生更改时得到通知。 - HMR
@HMR 这个怎么实现? - Itay Ganor
1个回答

2
以下是如何使Data实例可观察并在组件中使用Effect来观察Data实例项更改的示例:

const { useState, useEffect } = React;
class Data {
  constructor() {
    this.data = {
      users: [],
      products: [],
    };
    this.listeners = [];
  }

  addItem(type, newItem) {
    this.data[type] = [...this.data[type], newItem];
    //notify all listeners that something has been changed
    this.notify();
  }
  addUser(user) {
    this.addItem('users', user);
  }
  addProduct(product) {
    this.addItem('products', product);
  }
  reset = () => {
    this.data.users = [];
    this.data.products = [];
    this.notify();
  };
  notify() {
    this.listeners.forEach((l) => l(this.data));
  }
  addListener = (fn) => {
    this.listeners.push(fn);
    //return the remove listener function
    return () =>
      (this.listeners = this.listeners.filter(
        (l) => l !== fn
      ));
  };
}
const instance = new Data();
let counter = 0;
setInterval(() => {
  if (counter < 10) {
    if (counter % 2) {
      instance.addUser({ userName: counter });
    } else {
      instance.addProduct({ productId: counter });
    }
    counter++;
  }
}, 500);
//custom hook to use instance
const useInstance = (instance, fn = (id) => id) => {
  const [items, setItems] = useState(fn(instance.data));
  useEffect(
    () =>
      instance.addListener((items) => setItems(fn(items))),
    [instance, fn]
  );
  return items;
};
const getUsers = (data) => data.users;
const getProducts = (data) => data.products;
const Users = () => {
  const users = useInstance(instance, getUsers);
  return <pre>{JSON.stringify(users)}</pre>;
};
const Products = () => {
  const products = useInstance(instance, getProducts);
  return <pre>{JSON.stringify(products)}</pre>;
};
const App = () => {
  const reset = () => {
    instance.reset();
    counter = 0;
  };
  return (
    <div>
      <button onClick={reset}>Reset</button>
      <div>
        users:
        <Users />
      </div>
      <div>
        products:
        <Products />
      </div>
    </div>
  );
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


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


那么这意味着我必须将Data中的所有属性解包到useInstance作为本地状态吗? - Itay Ganor
@ItayGanor 目前你只有一些项目,但如果你愿意,我可以提供一个包含多种数据类型的示例。 - HMR
我不想打扰你太多 ;) - Itay Ganor
@ItayGanor 不用担心,这只是一个小改变。 - HMR
如果你能做到,那将非常有帮助。谢谢伙计! - Itay Ganor
@ItayGanor 更新了答案,创建一个数据上下文可能会更好,将其用作数据抽象,除非您有某种原因需要在组件外部更改数据。此外,创建数据实例不是一个好主意,除非您有充分的理由创建多个实例,否则应该只有一个数据对象。 - HMR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接