ByteGrad / Professional-React-and-Next.js-Course

This repo contains everything you need as a student of the Professional React & Next.js Course by ByteGrad.com
https://bytegrad.com/courses/professional-react-nextjs
140 stars 70 forks source link

A few improvements in TrekBag project #16

Open Rope-a-dope opened 3 months ago

Rope-a-dope commented 3 months ago
  1. Since we use Zustand to manage the state, there is no need to pass props to AddItemForm and Counter, we can just use Zustand in them directly.
  2. It's always a good idea to use custom hooks as explained in https://tkdodo.eu/blog/working-with-zustand and your video https://www.youtube.com/watch?v=I7dwJxGuGYQ&t=69s&ab_channel=ByteGrad. It's the same for other tools as for useContext. But of course, we must only persist the state here, otherwise we will have an error like "addItem is not a function"

itemsStore.js

import { create } from "zustand";
import { persist } from "zustand/middleware";
import { initialItems } from "../lib/constants";

export const useItemsStore = create(
  persist(
    (set) => ({
      items: initialItems,
      // Separate Actions from State
      actions: {
        addItem: (newItemText) => {
          const newItem = {
            id: new Date().getTime(),
            name: newItemText,
            packed: false,
          };

          set((state) => ({ items: [...state.items, newItem] }));
        },
        deleteItem: (id) => {
          set((state) => {
            const newItems = state.items.filter((item) => item.id !== id);
            return { items: newItems };
          });
        },
        toggleItem: (id) => {
          set((state) => {
            const newItems = state.items.map((item) => {
              if (item.id === id) {
                return { ...item, packed: !item.packed };
              }

              return item;
            });
            return { items: newItems };
          });
        },
        removeAllItems: () => {
          set(() => ({ items: [] }));
        },
        resetToInitial: () => {
          set(() => ({ items: initialItems }));
        },
        markAllAsComplete: () => {
          set((state) => {
            const newItems = state.items.map((item) => {
              return { ...item, packed: true };
            });

            return { items: newItems };
          });
        },
        markAllAsIncomplete: () => {
          set((state) => {
            const newItems = state.items.map((item) => {
              return { ...item, packed: false };
            });

            return { items: newItems };
          });
        },
      },
    }),
    {
      name: "items",
      partialize: ({ state }) => ({ state }), // only persist state
    }
  )
);

// exported - consumers don't need to write selectors. Prefer atomic selectors
export const useItems = () => useItemsStore((state) => state.items); 
// 🎉 one selector for all our actions.  As actions never change, it doesn't matter that we subscribe to "all of them". The actions object can be seen as a single atomic piece.
export const useItemsActions = () => useItemsStore((state) => state.actions);

AddItemForm.jsx

import { useRef, useState } from "react";
import { useItemsActions } from "../stores/itemsStore";
import Button from "./Button";

export default function AddItemForm() {
  const [itemText, setItemText] = useState("");
  const inputRef = useRef();

  const { addItem } = useItemsActions();
  const handleSubmit = (e) => {
    e.preventDefault();

    // basic validation
    if (!itemText) {
      alert("Item can't be empty");
      inputRef.current.focus();
      return;
    }

    addItem(itemText);
    setItemText("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>Add an item</h2>
      <input
        ref={inputRef}
        value={itemText}
        onChange={(e) => {
          setItemText(e.target.value);
        }}
        autoFocus
      />
      <Button>Add to list</Button>
    </form>
  );
}

Counter.jsx

import { useItems } from "../stores/itemsStore";

export default function Counter() {
  const items = useItems();
  const numberOfItemsPacked = items.filter((item) => item.packed).length;
  const totalNumberOfItems = items.length;

  return (
    <p>
      <b>{numberOfItemsPacked}</b> / {totalNumberOfItems} items packed
    </p>
  );
}