如何将 JS 类同步到 React 中组件的状态?

Jua*_*her 7 javascript reactjs

我正在完成一项技术挑战,并遇到了一个我以前从未遇到过的场景。

我被要求编写一个购物车,该购物车的 UI 表示基本结帐数据,例如订单总数、购物车中的当前商品等。

其中一项要求是我需要实现一个可以实例化的Checkout 类:

const checkout = new Checkout();
Run Code Online (Sandbox Code Playgroud)

我应该能够从中获取基本信息,例如:

const total = checkout.total();
Run Code Online (Sandbox Code Playgroud)

并通过它添加商品到购物车:

checkout.add(product.id);
Run Code Online (Sandbox Code Playgroud)

是什么让这个问题变得棘手,因为我想不出一种干净的“DRY”方式将其实现到 UI 中。这主要是因为 checkout 类中的任何更新都不会触发任何重新渲染,因为它不是状态的一部分。我通常会为此使用状态变量。

我尝试将状态变量绑定到结账类中的参数,例如:

const [total, setTotal] = useState();
useEffect(()=>{
   setTotal(checkout.total)
}, [checkout.total])
Run Code Online (Sandbox Code Playgroud)

checkout.total只是对方法的引用,所以它永远不会改变,所以我没有得到我想要的绑定。

尝试其他东西我设法组合出一个“解决方案”,但我怀疑这是否是一个好的模式。

我基本上将回调传递给结帐类,每当购物车更新时都会调用该回调。回调是状态变量的设置器,因此:

const [cart, setCart] = useState<string[]>(checkout.cart);
checkout.callback = setCart;
Run Code Online (Sandbox Code Playgroud)

然后在add方法里面:

add(productId) {
   // Some code...
   this.callback([...this.cart]);
}
Run Code Online (Sandbox Code Playgroud)

这允许每当类的参数发生变化cart时状态变量就会更新。checkout因此,它会在 Cart 组件及其所有具有传递 props 的子组件上触发重新渲染。因此我得到了一个同步的用户界面。

问题是我除了强制重新渲染之外不需要购物车变量。我可以直接从班级获取购物车信息,checkout这就是我所做的。但为了将其反映在 UI 中,我需要更新一些状态变量。它甚至可能是一个计数器,我只是选择cart而不是计数器,以使其更加连贯,我想。

我在这里把事情复杂化了吗?我是否缺少用于此场景的模式?通常如何与实例化的类进行交互并确保 UI 以某种方式根据类的更改进行更新?

编辑(添加缺少的信息):Checkout 类需要实现以下接口:

interface Checkout {
  // ...
  // Some non relevant properties methods
  // ...
  add(id: number): this;
}
Run Code Online (Sandbox Code Playgroud)

因此,明确要求该add方法返回this(以便允许函数链接)。

Tha*_*you 5

图案的混合

将 OOP 实例与改变内部状态的方法一起使用将阻止观察状态变化 -

const a = new Checkout()
const b = a                     // b is *same* state
console.log(a.count)            // 0
a.add(item)
console.log(a.count)            // 1
console.log(a == b)             // true
console.log(a.count == b.count) // true
Run Code Online (Sandbox Code Playgroud)

React 是一种面向功能的模式,并使用诸如不变性之类的互补思想。不可变对象方法将创建数据而不是改变现有状态 -

const a = new Checkout() 
const b = a.add(item)           // b is *new* state
console.log(a.count)            // 0
console.log(b.count)            // 1
console.log(a == b)             // false
console.log(a.count == b.count) // false
Run Code Online (Sandbox Code Playgroud)

这样,a == bfalse有效地发送了重绘该组件的信号。所以我们需要一个不可变的 Checkout类,其中方法返回新状态而不是改变现有状态 -

// Checkout.js

class Checkout {
  constructor(items = []) {
    this.items = items
  }
  add(item) {
    return new Checkout([...this.items, item]) // new state, no mutation
  }
  get count() {
    return this.items.length // computed state, no mutation
  }
  get total() {
    return this.items.reduce((t, i) => t + i.price, 0) // computed, no mutation
  }
}

export default Checkout
Run Code Online (Sandbox Code Playgroud)

演示应用程序

让我们制作一个快速应用程序。您可以单击按钮将商品添加到购物车。该应用程序将显示正确的count以及total单个项目 -

应用程序组件预览
预览

现在将类“同步”到组件只是使用普通的 React 模式。直接在组件中使用您的类和方法 -

import Checkout from "./Checkout.js"
import Cart from "./Cart.js"

function App({ products = [] }) {
  const [checkout, setCheckout] = React.useState(new Checkout)
  const addItem = item => event =>
    setCheckout(checkout.add(item))
  return <div>
    {products.map(p =>
      <button key={p.name} onClick={addItem(p)}>{p.name}</button>
    )}
    <b>{checkout.count} items for {money(checkout.total)}</b>
    <Cart checkout={checkout} />
  </div>
}

const data =
  [{name: "", price: 5}, {name: "", price: 3}]

const money = f =>
  new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(f)
Run Code Online (Sandbox Code Playgroud)

一个简单的Cart组件用于JSON.stringify快速可视化每个项目 -

// Cart.js

function Cart({ checkout }) {
  return <pre>{JSON.stringify(checkout, null, 2)}</pre>
}

export default Cart
Run Code Online (Sandbox Code Playgroud)

Run下面的演示验证浏览器中的结果 -

class Checkout {
  constructor(items = []) {
    this.items = items
  }
  add(item) {
    return new Checkout([...this.items, item])
  }
  get count() {
    return this.items.length
  }
  get total() {
    return this.items.reduce((t, i) => t + i.price, 0)
  }
}

function App({ products = [] }) {
  const [checkout, setCheckout] = React.useState(new Checkout)
  const addItem = item => event =>
    setCheckout(checkout.add(item))
  return <div>
    {products.map(p =>
      <button key={p.name} onClick={addItem(p)}>{p.name}</button>
    )}
    <b>{checkout.count} items for {money(checkout.total)}</b>
    <Cart checkout={checkout} />
  </div>
}

const money = f =>
  new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(f)
  
function Cart({ checkout }) {
  return <pre>{JSON.stringify(checkout, null, 2)}</pre>
}

const data = [{name: "", price: 5}, {name: "", price: 3}]

ReactDOM.render(<App products={data} />, document.body)
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
Run Code Online (Sandbox Code Playgroud)


小智 5

嗯,看来您需要共享状态。我想到的第一个解决方案就是使用 Class 组件。您可以在需要时使用强制重新渲染,并无需useEffect黑客即可编写更多自定义逻辑。

在我看来,第二个解决方案更清晰。它使用观察者模式。您需要向您的 Checkout 类添加订阅。所以基本上。

useEffect(() => {
 const subscription = (newState) => setState(newState)
 const instance = new Checkout()
 instance.subcribe(subscription)
 return () => instance.unsubcribe(subscription)
}, [setState])
Run Code Online (Sandbox Code Playgroud)

由于 setState 是不可变的,因此该钩子只会运行一次。

  • `unsubscribe` 应在空返回函数 `return () =&gt; instance.unsubscribe(subscription)` 中声明,以便它仅在组件卸载时运行 (2认同)

Emi*_*son -2

我认为向对象发送回调然后在需要时调用该回调是完全合理的。如果您不想添加任何不必要的数据,请不要:

add(productId) {
  // Some code...
  this.callback();
}
Run Code Online (Sandbox Code Playgroud)
checkout.callback = () => {
  setTotal(checkout.total);
}
Run Code Online (Sandbox Code Playgroud)