Tutorial: Building a Shopping Cart
This tutorial guides you through the process of implementing a simple yet practical shopping cart feature using the caro-kann library in a React environment. You will learn how to efficiently manage state and easily integrate various additional features through caro-kann's intuitive state management approach and powerful middleware system.
This tutorial is aimed at developers with a basic understanding of React and aims to help you grasp the core concepts and practical usage of caro-kann so that you can apply it to real projects.
Create a store#
Before starting application state management, it's a good idea to first define the structure of the data we'll be working with. Here, we create a TypeScript type to clearly specify the attributes of a 'Product' that will be placed in the shopping cart. Each product should have a required id, name, and price, and may optionally include imageUrl and description. Defining the type upfront helps reduce data-related errors during development and improves code readability and maintainability.
Next, we import the create function from the caro-kann library to create a global state store. The create function takes an initial state value as an argument and returns a custom hook that can manage that state. In this example, we set an empty array [] as the initial state, indicating that the shopping cart is initially empty. The useCart hook, returned by the create<Array<Product>>([]) call, will now be the core interface used by all components in our application to access and modify the shopping cart state.
Changing Global State with useCart#
Now let's look at how to change the shopping cart state in actual components using the useCart hook we created.
First, the ProductBinder component receives a list of products (productList) from the server as props and is responsible for rendering a ProductCard component for each product. It acts as a list renderer, serving as a container to display multiple product information on the screen.
Calling the useCart() hook inside this component returns a tuple containing the current cart state (cart) and a function to change the state (setCart), similar to React's useState hook.
In the onClick handler of the 'Add to Cart' button, the setCart function is used. It employs a functional update approach (setCart(prev => [...prev, product])), taking the previous state (prev) and returning a new array with the new product (product) added to the end. This is a good pattern for maintaining state immutability and helps React detect changes and update the UI efficiently.
However, looking at the current implementation of the ProductBinder component, it doesn't display the contents of the cart. That is, it doesn't need to read the cart state value directly; it only performs the function of adding items to the cart via the setCart function. In such cases, optimization can be considered to prevent the component from unnecessarily re-rendering whenever the cart state changes.
caro-kann provides the writeOnly method built into the useCart hook for such situations. Using useCart.writeOnly() retrieves only the state-modifying function (corresponding to setCart) without fetching the state value itself.
This allows the ProductBinder component to not subscribe to changes in the current state of the cart, enabling it to solely perform the role of adding products while avoiding unnecessary re-renders. This is a useful optimization technique that can contribute to performance improvements, especially when state changes frequently or many components share the same state.
Managing Derived State with readOnly method#
The application's Header component plays an important role in visually showing the user the total number of products currently in the shopping cart. To implement this feature, we utilize the readOnly method of the useCart hook.
The readOnly method is a powerful feature that allows access to the current value of the state store to extract or calculate desired data. This method takes a callback function as an argument, and this callback function receives the current state (store) as a parameter and returns a derived value.
In the example below, useCart.readOnly(store => store.length) is used to calculate the total number of products in the cart (cartLength) through the length property of the cart array (store).
The main advantages of using readOnly are as follows:
- Selective Subscription: The component re-renders only in response to changes in the specific value returned by the callback function, not the entire state object. For example, the
Headercomponent will only re-render when products are added or removed from the cart, changingstore.length. If only an attribute of a specific item in the cart changes but the total count remains the same, this component will not re-render, reducing unnecessary operations. - Derived State Calculation: It's not just about fetching a part of the state; you can create calculated values (derived state) based on the state. For example, logic to calculate the total cart amount or to filter and show the count of items meeting specific criteria can be handled within the
readOnlycallback. - Performance Optimization: By subscribing precisely to only the necessary data, it prevents unnecessary re-renders that occur when other parts of the state change. This significantly contributes to performance optimization, especially in applications with complex state structures or frequent state updates.
Consequently, useCart.readOnly is very useful for creating components that depend only on specific parts of the state or derived values, and it helps improve the reactivity and performance of the application.
Creating the Cart Page#
The cart page displays a list of products selected by the user and provides functionality to remove individual products or clear the entire cart. It also calculates and displays the total amount for all products in the cart. This page is primarily composed of a Cart component, and each cart item can be rendered via ProductCard (or a separate CartItemCard).
In the Cart component, a selector function is passed to the useCart hook to efficiently retrieve only the necessary data. This selector function takes the current cart state (store) as an argument and returns an object containing the list of cart items (items) and the total price of all items (priceTotal). This way, the Cart component only re-renders when the cart items or the total amount changes.
items: An array of product objects currently in the cart.priceTotal: The sum ofproduct.pricefor each item in theitemsarray.
Extending Cart Functionality with Middleware Composition: Backup and Action Verification#
One of caro-kann's powerful features is the ability to easily compose middleware to extend store functionality. Here, we'll apply persist and logger middleware to keep the cart state in the browser and easily track state changes.
1. Persisting cart state with persist middleware#
By default, an web application's state is reset when the page is refreshed or the browser is closed. To prevent users from losing the products they've added to their cart, the state needs to be stored somewhere. The persist middleware makes managing this state persistence very simple.
The persist middleware automatically saves data to a specified storage (e.g., localStorage) whenever the state changes, and restores the state by loading this data when the application reloads. Here's how to apply the persist middleware to the useCart store:
2. Tracking state changes with logger middleware#
During development, understanding how the state changes and what actions caused those changes is crucial for debugging. The logger middleware helps track this process easily by printing state change-related information to the console.
The logger middleware can be composed with the persist middleware. Middleware is applied by wrapping functions, so you can think of it as executing from the inside out. That is, when a state change occurs, logger acts first, then persist acts.
Now, whenever a product is added to or removed from the cart, relevant logs will be printed to the browser console, allowing clear verification of the state change process. You can also adjust logger options to selectively view only the necessary information.
Merging stores with merge#
As an application grows, it can be more efficient to manage related states in separate stores. For example, cart state can be managed in a useCart store, and user profile information in a useProfile store.
caro-kann provides a merge utility to conveniently use and manage these separated stores as a single, unified hook. merge takes multiple store hooks as input and returns a new hook that can access the state and setter functions of each store.
First, define the individual stores. Here, we'll use cart (useCart) and user profile (useProfile) stores as examples. A User type also needs to be defined.
Now, use the merge utility to combine the useCart and useProfile stores. Pass an object to the merge function, where each key will be the name used in the merged store, and the value will be the respective store hook.
Using the useProfileAndCart hook created this way, you can access the state and setter functions of each store as if dealing with one large store.
For example, within a component, you can use it as follows:
Wrapping up: Efficient State Management with caro-kann#
So far, we've explored how to implement basic shopping cart functionality using the caro-kann library. Through this tutorial, you've learned how to create a store with the create function, read and update state via the useCart hook, optimize with writeOnly and readOnly methods, and manage derived state using selectors. We also looked at how to easily extend store functionality by composing persist and logger middleware, and how to manage multiple stores integratively with the merge utility.
The topics covered here are just a part of the diverse features caro-kann offers. In actual application development, you'll encounter more complex state logic, asynchronous operations, advanced middleware usage, and various other scenarios. caro-kann provides powerful and flexible features to meet these advanced requirements, helping developers handle state management more efficiently and intuitively.
For deeper learning and to explore the full potential of caro-kann, we highly recommend referring to the official documentation. The official documentation provides detailed guidance on various APIs not covered in this tutorial, advanced usage patterns, and best practices for real-world production environments. We hope you have a more enjoyable and productive state management experience with caro-kann.