/ Gists / React

Gists - React

On gists

cn, cva, clx

React

how.js #

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


On gists

Zustand

React

zustand.js #

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

On gists

React TodoList with Redurec

React

App.jsx #

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

On gists

Forms in React

React

StateForm.jsx #

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>
  )
}

On gists

useLocalStorage

React

useLocalStorage.js #

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]
}

On gists

useArray custom hook

React

useArray.js #

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

On gists

Fetch userlist with handling via AbortController

React

App.jsx #

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

On gists

useFetch (custom hook)

React

useFetch.js #

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;

On gists

useState vs useReducer vs mySolution

React

useState.jsx #

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>;
}