// 1 install
npm install class-variance-authority clsx tailwind-merge
// 2 /lib/utils.ts
/*
clsx handles conditional logic and class composition
tailwind-merge removes conflicting Tailwind classes
*/
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
cn("px-2 px-4") // => "px-4"
// 3 variant with cva
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"rounded px-4 py-2 font-medium transition",
{
variants: {
variant: {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-black",
},
size: {
sm: "text-sm",
lg: "text-lg",
},
},
defaultVariants: {
variant: "primary",
size: "sm",
},
}
)
export function Button({ variant, size, className }) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
/>
)
}
export function cn(...inputs: any[]) {
return twMerge(clsx(inputs))
}
// 1 install
npm install zustand
// 2 create
// store.js
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0, // Initial state
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
}));
export default useStore;
// 3 use
// App.js
import React from 'react';
import useStore from './store';
const Counter = () => {
const { count, increase, decrease } = useStore((state) => ({
count: state.count,
increase: state.increase,
decrease: state.decrease,
}));
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>Decrease</button>
</div>
);
};
export default Counter;
//4 optional - middleware
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'counter-storage' } // Key for localStorage
)
);
export default useStore;
import { createContext, useEffect, useReducer, useState } from "react"
import { NewTodoForm } from "./NewTodoForm"
import "./styles.css"
import { TodoFilterForm } from "./TodoFilterForm"
import { TodoList } from "./TodoList"
const LOCAL_STORAGE_KEY = "TODOS"
const ACTIONS = {
ADD: "ADD",
UPDATE: "UPDATE",
TOGGLE: "TOGGLE",
DELETE: "DELETE",
}
function reducer(todos, { type, payload }) {
switch (type) {
case ACTIONS.ADD:
return [
...todos,
{ name: payload.name, completed: false, id: crypto.randomUUID() },
]
case ACTIONS.TOGGLE:
return todos.map(todo => {
if (todo.id === payload.id) {
return { ...todo, completed: payload.completed }
}
return todo
})
case ACTIONS.DELETE:
return todos.filter(todo => todo.id !== payload.id)
case ACTIONS.UPDATE:
return todos.map(todo => {
if (todo.id === payload.id) {
return { ...todo, name: payload.name }
}
return todo
})
default:
throw new Error(`No action found for ${type}.`)
}
}
export const TodoContext = createContext()
function App() {
const [filterName, setFilterName] = useState("")
const [hideCompletedFilter, setHideCompletedFilter] = useState(false)
const [todos, dispatch] = useReducer(reducer, [], initialValue => {
const value = localStorage.getItem(LOCAL_STORAGE_KEY)
if (value == null) return initialValue
return JSON.parse(value)
})
const filteredTodos = todos.filter(todo => {
if (hideCompletedFilter && todo.completed) return false
return todo.name.includes(filterName)
})
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
function addNewTodo(name) {
dispatch({ type: ACTIONS.ADD, payload: { name } })
}
function toggleTodo(todoId, completed) {
dispatch({ type: ACTIONS.TOGGLE, payload: { id: todoId, completed } })
}
function updateTodoName(id, name) {
dispatch({ type: ACTIONS.UPDATE, payload: { id, name } })
}
function deleteTodo(todoId) {
dispatch({ type: ACTIONS.DELETE, payload: { id: todoId } })
}
return (
<TodoContext.Provider
value={{
todos: filteredTodos,
addNewTodo,
toggleTodo,
updateTodoName,
deleteTodo,
}}
>
<TodoFilterForm
name={filterName}
setName={setFilterName}
hideCompleted={hideCompletedFilter}
setHideCompleted={setHideCompletedFilter}
/>
<TodoList />
<NewTodoForm />
</TodoContext.Provider>
)
}
export default App
import { useState, useMemo } from "react"
import { checkEmail, checkPassword } from "./validators"
export function StateForm() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isAfterFirstSubmit, setIsAfterFirstSubmit] = useState(false)
const emailErrors = useMemo(() => {
return isAfterFirstSubmit ? checkEmail(email) : []
}, [isAfterFirstSubmit, email])
const passwordErrors = useMemo(() => {
return isAfterFirstSubmit ? checkPassword(password) : []
}, [isAfterFirstSubmit, password])
function onSubmit(e) {
e.preventDefault()
setIsAfterFirstSubmit(true)
const emailResults = checkEmail(email)
const passwordResults = checkPassword(password)
if (emailResults.length === 0 && passwordResults.length === 0) {
alert("Success")
}
}
return (
<form onSubmit={onSubmit} className="form">
<div className={`form-group ${emailErrors.length > 0 ? "error" : ""}`}>
<label className="label" htmlFor="email">
Email
</label>
<input
className="input"
type="email"
id="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
{emailErrors.length > 0 && (
<div className="msg">{emailErrors.join(", ")}</div>
)}
</div>
<div className={`form-group ${passwordErrors.length > 0 ? "error" : ""}`}>
<label className="label" htmlFor="password">
Password
</label>
<input
className="input"
type="password"
id="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
{passwordErrors.length > 0 && (
<div className="msg">{passwordErrors.join(", ")}</div>
)}
</div>
<button className="btn" type="submit">
Submit
</button>
</form>
)
}
import { useEffect, useState } from "react"
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const localValue = localStorage.getItem(key)
if (localValue == null) {
if (typeof initialValue === "function") {
return initialValue()
} else {
return initialValue
}
} else {
return JSON.parse(localValue)
}
})
useEffect(() => {
if (value === undefined) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(value))
}
}, [value, key])
return [value, setValue]
}
import { useState, useCallback } from "react"
export function useArray(initialValue) {
const [array, setArray] = useState(initialValue)
const push = useCallback(element => {
setArray(a => [...a, element])
}, [])
const replace = useCallback((index, newElement) => {
setArray(a => {
return [...a.slice(0, index), newElement, ...a.slice(index + 1)]
})
}, [])
const filter = useCallback(callback => {
setArray(a => {
return a.filter(callback)
})
}, [])
const remove = useCallback(index => {
setArray(a => {
return [...a.slice(0, index), ...a.slice(index + 1)]
})
}, [])
const clear = useCallback(() => {
setArray([])
}, [])
const reset = useCallback(() => {
setArray(initialValue)
}, [initialValue])
return { array, set: setArray, push, replace, filter, remove, clear, reset }
}
import { useEffect, useState } from "react"
import { User } from "./User"
export default function App() {
const [users, setUsers] = useState([])
const [error, setError] = useState(null)
const [status, setStatus] = useState("idle")
useEffect(() => {
setStatus("loading")
setUsers([])
setError(null)
const controller = new AbortController()
fetch("https://jsonplaceholder.typicode.com/users", {
signal: controller.signal,
})
.then(res => {
if (res.ok) return res.json()
throw new Error(`Status code: ${res.status}`)
})
.then(data => {
setUsers(data)
setStatus("fetched")
setError(null)
})
.catch(err => {
if (err.name === "AbortError") return
setError(err)
setUsers([])
setStatus("error")
})
return () => {
controller.abort()
}
}, [])
return (
<>
<h1>User List</h1>
{status === "loading" && <h2>Loading...</h2>}
{status === "error" && (
<>
<h2>Error fetching users</h2>
<p>{error.message}</p>
</>
)}
{status === "fetched" && (
<ul>
{users.map(user => (
<User key={user.id} name={user.name} />
))}
</ul>
)}
</>
)
}
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [data, setData] = useState(null);
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setTimeout(() => {
fetch(url)
.then(res => {
if (!res.ok) { // error coming back from server
throw Error('could not fetch the data for that resource');
}
return res.json();
})
.then(data => {
setIsPending(false);
setData(data);
setError(null);
})
.catch(err => {
// auto catches network / connection error
setIsPending(false);
setError(err.message);
})
}, 1000);
}, [url])
return { data, isPending, error };
}
export default useFetch;
import { useState } from "react";
function Cart() {
const [cart, setCart] = useState({
items: [],
total: 0,
discount: null,
isLoading: false,
});
const addItem = (item) =>
setCart(prev => ({
...prev,
items: [...prev.items, item],
total: prev.total + item.price,
}));
const removeItem = (id) =>
setCart(prev => {
const item = prev.items.find(i => i.id === id);
return {
...prev,
items: prev.items.filter(i => i.id !== id),
total: prev.total - (item?.price || 0),
};
});
const applyDiscount = (code) =>
setCart(prev => ({ ...prev, discount: code }));
const resetCart = () =>
setCart({ items: [], total: 0, discount: null, isLoading: false });
return <div>{JSON.stringify(cart)}</div>;
}