0
Follow
0
View

How to sync a JS class to a component's state in React?

W75728882 注册会员
2023-01-26 00:55

You can make a Cart class that allows for observers to be notified when something important happens. To make it available for the react components, provide an instance of it with a context, and use a stateful hook to notify components by setting the state through the observer function.

Here we go: First, we need a Cart class that notifies observers when something happens

export class Cart {
  constructor() {
    this.products = [];
    this.subscribers = new Set();
  }
  subscribe = (notifyMe) => {
    this.subscribers.add(notifyMe);
  };
  unSubscribe = (notifyMe) => {
    this.subscribers.delete(notifyMe);
  };
  addToCart = (product) => {
    this.products = [...this.products, product];
    this.notify();
  };
  removeFromCart = (product) => {
    this.products = this.products.filter(product);
    this.notify();
  };
  notify = () => {
    this.subscribers.forEach((n) => n(this.products));
  };
}

We will expose this through the react tree with a context, so lets make one

const CartContext = React.createContext();

export const CartProvider = ({ children, cart }) => {
  return {children};
};

Now for the trick! A hook that will update its state using the carts observer pattern, thereby notifying the component that uses it.

export const useCart = () => {
  const cart = React.useContext(CartContext);
  const [content, r] = React.useState();
  React.useEffect(() => {
    const notify = (productsInCart) => r(productsInCart);
    cart.subscribe(notify);
    cart.notify();
    return () => cart.unSubscribe(notify);
  }, [cart, r]);
  return {
    addToCart: cart.addToCart,
    removeFromCart: cart.removeFromCart,
    content
  };
};

Note that it can be worth to update after subscribing.

Now we have our library set up, we can make some components. So here's where we instantiate the Cart class. We make a new Cart, and let the provider provide that instance

const cart = new Cart();

export default function App() {
  return (
    

Welcome to the shop

start putting stuff in the cart!

); }

Here are the other components

const Catalog = () => {
  const getProducts = async () =>
    await fetch(
      "https://random-data-api.com/api/commerce/random_commerce?size=6"
    ).then((r) => r.json());
  const [products, setProducts] = React.useState();
  React.useEffect(() => {
    getProducts().then(setProducts);
  }, []);

  if (!products) {
    return null;
  }

  return (
    
    {products.map((product) => ( ))}
); }; const Item = ({ product }) => { const { addToCart } = useCart(); const addProductToCart = () => addToCart(product); return (
  • {product.product_name}

    $ {product.price}
  • ); }; const CartCounter = () => { const { content } = useCart(); return
    items in cart: {content?.length || 0}
    ; };

    This can be a pretty handy pattern, and can be taken pretty far (e.g. React Query works like this).

    CodeSandbox link

    dsy_8110 注册会员
    2023-01-26 00:55

    Using OOP instances with methods that mutate internal state will prevent observation of a state change -

    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
    

    React is a functional-oriented pattern and uses complimentary ideas like immutability. Immutable object methods will create new data instead of mutating existing state -

    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
    

    In this way, a == b is false which effectively sends the signal to redraw this component. So we need a immutable Checkout class, where methods return new state instead of mutating existing state -

    // 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
    

    Let's make a quick app. You can click the ???? and ???? buttons to add items to the cart. The app will show the correct count and total as well as the individual items -

    App component preview

    Now "syncing" the class to the component is just using ordinary React pattern. Use your class and methods directly in your componenets -

    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 
    {products.map(p => )} {checkout.count} items for {money(checkout.total)}
    } const data = [{name: "????", price: 5}, {name: "????", price: 3}] const money = f => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(f)

    A simple Cart component uses JSON.stringify to quickly visualize each item -

    // Cart.js
    
    function Cart({ checkout }) {
      return 
    {JSON.stringify(checkout, null, 2)}
    } export default Cart

    Run the demo below to verify the result in your browser -

    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 
    {products.map(p => )} {checkout.count} items for {money(checkout.total)}
    } const money = f => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(f) function Cart({ checkout }) { return
    {JSON.stringify(checkout, null, 2)}
    } const data = [{name: "????", price: 5}, {name: "????", price: 3}] ReactDOM.render(, document.body)
    
    

    du7775 注册会员
    2023-01-26 00:55

    Hmm, looks like you need to share the state. The first solution that came to my mind is just to use the Class component. You can use force rerender while you need and write more custom logic without useEffect hacks.

    The second solution is more clear IMO. It uses an Observer pattern. You need to add a subscription to your Checkout class. So basically.

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

    Since setState is immutable, this hook will be run only once.

    chengxianqing 注册会员
    2023-01-26 00:55

    Your idea is correct, you need somehow to start re-render to sync state of checkout object and state of a component.

    E.g. you may do it by context and force update (in case if you do not want to duplicate data in object and state):

    const CheckoutContext = React.createContext();
    
    const checkout = new Checkout();
    
    const CheckoutProvider = ({ children }) => {
      // init force update, just to start re-render
      const [ignored, forceUpdate] = React.useReducer((x) => x + 1, 0);
      
      const add = (a) => {
        checkout.add(a);
        forceUpdate();
      };
    
      const total = checkout.total();
    
      const value = { add, total };
    
      return (
        
          {children}
        
      );
    };
    
    
    const Child = () => {
      const v = React.useContext(CheckoutContext);
      console.log(v.total);
      return ;
    };
    
    export default function App() {
      return (
        
    ); }

    About the Author

    Question Info

    Publish Time
    2023-01-26 00:55
    Update Time
    2023-01-26 00:55