08 ยท Tailwind CSS + Zustand โ polish and order
A short phase, two quality-of-life upgrades: Tailwind changes how you
write CSS (not what CSS is โ you know CSS), and Zustand gives app state one tidy
home instead of being threaded through props.
1. Tailwind โ utility classes instead of named classes
Phase 1 CSS: invent a name (.low-stock), define rules in style.css, apply the name.
Tailwind ships thousands of tiny single-purpose classes so most styles never need a name:
<!-- your Phase 1 way -->
<tr class="low-stock"> /* + style.css: .low-stock { background: mistyrose; } */
<!-- the Tailwind way -->
<tr className="bg-rose-100"> <!-- style lives ON the element -->
Why anyone wants this (it looks ugly at first โ everyone thinks so)
Three real pains it removes. (1) Naming: half of CSS effort is inventing names for
one-off styles. (2) Fear of deleting: is .form-box still used
anywhere? Who knows โ so dead CSS piles up (your style.css already had a zombie a
rule by week 2). Utilities live on the element: delete the element, its styles die with it.
(3) The over-broad selector trap โ your Task 1.11 bug, color: white
on global a leaking everywhere. Utilities can't leak; they touch only the element
they're written on.
It's still just CSS โ your knowledge maps 1:1
Every utility is a CSS rule you already know. Nothing new is being learned about layout โ only a
new spelling. That's why Tailwind comes AFTER real CSS: you can read what it abbreviates.
| CSS you wrote in Phase 1 | Tailwind spelling |
display: flex; gap: 12px; | flex gap-3 |
flex-direction: column; | flex-col |
padding: 16px; / padding: 8px 16px; | p-4 / py-2 px-4 |
background: mistyrose; | bg-rose-100 |
font-weight: bold; font-size: 18px; | font-bold text-lg |
border: 1px solid #ccc; border-radius: 8px; | border border-gray-300 rounded-lg |
max-width: 900px; margin: 0 auto; | max-w-4xl mx-auto |
The spacing scale โ why p-4 is 16px
Numbers are steps of 0.25rem (4px): p-1=4px, p-2=8px, p-4=16px,
p-8=32px. A fixed scale is the point: every gap in the app comes from the same small
menu, so spacing looks consistent without anyone policing it.
2. Variants โ hover, focus, responsive as prefixes
<button className="bg-blue-600 hover:bg-blue-700 text-white rounded px-4 py-2">
Borrow
</button>
<!-- responsive: stacked on phones, row on wide screens -->
<div className="flex flex-col md:flex-row gap-4">
Read the prefix as a condition
hover:bg-blue-700 = "when hovered, background 700." md:flex-row = "at
medium screens and up, row." These replace :hover selectors and
@media queries โ same CSS features, condition spelled inline. Conditional styling in
JSX uses your oldest tool: className={book.copies <= 2 ? "bg-rose-100" : ""} โ
the low-stock ternary rides again.
3. Zustand โ why state wants one home
The pain it solves: prop drilling
In Phase 6, books lives in App and flows down: App โ BookTable โ BookRow. Fine at 3
levels. But when a deep component needs the data, every layer in between must accept and forward
props it doesn't use โ like passing a message through five people who don't care about it. That's
prop drilling, and it makes refactoring miserable: move a component, rewire every
layer. A store is state that lives outside the tree; any component subscribes
directly. (Zustand is German for "state" โ the library is tiny, ~1KB, and is what modern apps
actually use where Redux once was.)
// store.ts โ the Library's state, one place
import { create } from "zustand";
interface LibraryStore {
books: Book[];
loans: Loan[];
setBooks: (books: Book[]) => void;
borrow: (id: number) => void;
}
export const useLibrary = create<LibraryStore>((set) => ({
books: [],
loans: [],
setBooks: (books) => set({ books }),
borrow: (id) => set((s) => ({
books: s.books.map(b => b.id === id && b.copies > 0
? { ...b, copies: b.copies - 1 } : b)
})),
}));
// any component, any depth โ no props needed
function BookTable() {
const books = useLibrary(s => s.books);
const borrow = useLibrary(s => s.borrow);
// ...same JSX as before
}
Look closely โ the store is your Phase 2 architecture, formalized
One shared books/loans + named functions that change them (borrow, add,
delete, returnLoan) โ that IS your app.ts, reborn: state + actions in one file, UI elsewhere.
The immutability rule still applies inside set (map/spread, no push), and actions
calling the API then updating the store replaces your saveBooks-then-render spine one final time.
What stays in useState
Not everything moves to the store. Rule of thumb: shared app data โ store; local widget
state โ useState. The search box's current text or a form's draft values are one
component's business โ keep them local. books/loans are everyone's business โ store.
4. The Phase 7 arc
| Step | What changes |
| 1. Tailwind into Vite | install + config; delete most of the old CSS file, translating what's kept |
| 2. Restyle screen by screen | tables, forms, buttons, nav โ the app starts looking professional |
| 3. Responsive pass | md:/lg: prefixes; the Library works on a phone |
| 4. Zustand store | books/loans + actions move in; prop wiring comes out |
| 5. Milestone | looks professional, state lives in a store, all features intact |
5. The mental model to keep
| Idea | One line to remember |
| Utility class | one CSS rule with a short name, applied on the element โ no naming, no leaking |
| Spacing scale | numbers = ร4px steps; consistency comes from the shared menu |
| Variant prefix | hover:/md: = your :hover and @media, spelled inline |
| Prop drilling | layers forwarding props they don't use โ the smell that calls for a store |
| Store | state + actions outside the tree; components subscribe to slices |
| Split rule | shared data โ store; widget-local state โ useState |
โ Prev: React ยท
Back to contents ยท
Next: Auth โ