Skip to content

Rayied991/ReactJs-Domination

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

98 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🚀 Complete React + TanStack Query Guide

A comprehensive guide covering React fundamentals, hooks, routing, and advanced data fetching with TanStack Query


📑 Table of Contents

  1. React Fundamentals
  2. React Hooks Deep Dive
  3. Form Handling & Storage
  4. API Integration
  5. React Router
  6. Context API
  7. Performance Optimization
  8. TanStack Query (React Query)
  9. Vitest Testing Guide for React
  10. React Redux Documentation
  11. Redux Toolkit Complete Guide
  12. Firebase with React - Complete Guide

React Fundamentals

What is React?

React is a JavaScript library for building user interfaces, developed by Meta (Facebook) in 2013. It uses a component-based architecture where each UI element is a reusable component.

Key Problem Solved: Before React, updating individual parts of a page (notifications, messages, friend requests) required full page reloads. React introduced selective re-rendering through the Virtual DOM.

Virtual DOM vs Real DOM

Type Description
Real DOM Directly updates HTML, re-renders entire page
Virtual DOM Updates only changed parts for faster performance

Setup with Vite

npm create vite
npm install
npm run dev

Basic Component Structure

// App.jsx
const App = () => {
  const name = "Rayied";
  return <h1>Hello {name}!</h1>;
};

export default App;

JSX Rules

  • Must return a single parent element (use fragments <>...</>)
  • Use className instead of class
  • Close all tags (<img />, <input />)
  • JavaScript expressions go inside {}

Component Props

// Parent Component
const App = () => {
  const jobs = [
    { company: "Google", role: "Frontend Engineer", location: "Dhaka" },
    { company: "Meta", role: "Backend Developer", location: "Bangladesh" },
  ];

  return (
    <div>
      {jobs.map((job, index) => (
        <Card key={index} {...job} />
      ))}
    </div>
  );
};

// Child Component
const Card = ({ company, role, location }) => (
  <div className="card">
    <h2>{company}</h2>
    <p>{role}</p>
    <p>{location}</p>
  </div>
);

Best Practice: Always provide unique key props when rendering lists. Use stable IDs instead of array indices when possible.


React Hooks Deep Dive

1. useState - State Management

Manages local component state with automatic re-rendering.

import { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: "Sarthak", age: 25 });

  // Functional updates for batch operations
  const increment = () => setCount((prev) => prev + 1);

  // Object updates (immutable)
  const updateAge = () => setUser((prev) => ({ ...prev, age: 50 }));

  // Array updates (immutable)
  const [numbers, setNumbers] = useState([1, 2, 3]);
  const addNumber = () => setNumbers((prev) => [...prev, 4]);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

Batch Updates Example:

const [count, setCount] = useState(10);

const batchUpdate = () => {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1); // Increments by 3
};

2. useEffect - Side Effects

Handles side effects like API calls, subscriptions, timers, and DOM updates.

import { useEffect, useState } from "react";

const DataFetcher = () => {
  const [data, setData] = useState(null);

  // Run once on mount (componentDidMount)
  useEffect(() => {
    fetchData();
  }, []);

  // Run when dependency changes
  useEffect(() => {
    console.log("Count changed:", count);
  }, [count]);

  // Cleanup function
  useEffect(() => {
    const timer = setInterval(() => console.log("tick"), 1000);

    return () => clearInterval(timer); // Runs on unmount
  }, []);

  return <div>{data}</div>;
};

Dependency Rules:

  • [] → Runs once on mount
  • [dep1, dep2] → Runs when dependencies change
  • No array → Runs after every render (avoid!)

3. useRef - Mutable Values & DOM Access

Creates mutable values that don't trigger re-renders.

import { useRef } from "react";

const InputFocus = () => {
  const inputRef = useRef(null);
  const countRef = useRef(0);

  const focusInput = () => {
    inputRef.current.focus();
  };

  const incrementSilent = () => {
    countRef.current += 1; // No re-render
    console.log(countRef.current);
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
};

Use Cases:

  • Accessing DOM elements
  • Storing timers/intervals
  • Keeping mutable values without re-rendering

4. useContext - Global State

Shares state across components without prop drilling.

import { createContext, useContext, useState } from "react";

// Create Context
const ThemeContext = createContext();

// Provider Component
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

// Consumer Component
const ThemedButton = () => {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      Current: {theme}
    </button>
  );
};

// App Setup
const App = () => (
  <ThemeProvider>
    <ThemedButton />
  </ThemeProvider>
);

5. useReducer - Complex State Logic

Manages complex state with predictable updates.

import { useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + 1 };
    case "decrement":
      return { ...state, count: state.count - 1 };
    case "reset":
      return { count: 0 };
    default:
      return state;
  }
};

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <h1>Count: {state.count}</h1>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </div>
  );
};

6. useId - Unique Accessibility IDs

Generates unique IDs for form elements.

import { useId } from "react";

const RegistrationForm = () => {
  const id = useId();

  return (
    <form>
      <div>
        <label htmlFor={`${id}-username`}>Username:</label>
        <input type="text" id={`${id}-username`} />
      </div>
      <div>
        <label htmlFor={`${id}-email`}>Email:</label>
        <input type="email" id={`${id}-email`} />
      </div>
      <div>
        <label htmlFor={`${id}-password`}>Password:</label>
        <input type="password" id={`${id}-password`} />
      </div>
    </form>
  );
};

Best Practice: Use one useId() call with suffixes instead of multiple calls.

7. use Hook (React 19+)

Flexible context and promise reading with conditional support.

import { use } from "react";

const Profile = ({ isLoggedIn }) => {
  // ✅ Can be called conditionally
  if (isLoggedIn) {
    const user = use(UserContext);
    return <h1>Welcome, {user.name}!</h1>;
  }

  return <div>Please log in</div>;
};

Key Differences from useContext:

Feature useContext use
Conditional calls ❌ Not allowed ✅ Allowed
Inside loops ❌ Not allowed ✅ Allowed
Promise support ❌ No ✅ Yes
React version All versions React 19+

8. useMemo & useCallback - Performance

Optimizes expensive calculations and function references.

import { useMemo, useCallback, useState } from "react";

const ExpensiveComponent = ({ items }) => {
  // Memoize expensive calculation
  const total = useMemo(() => {
    return items.reduce((sum, item) => sum + item.price, 0);
  }, [items]);

  // Memoize function reference
  const handleClick = useCallback(() => {
    console.log("Clicked!");
  }, []);

  return <div>Total: {total}</div>;
};

Difference:

  • useMemo → Caches computed values
  • useCallback → Caches function references

Form Handling & Storage

Controlled Components

import { useState } from "react";

const LoginForm = () => {
  const [formData, setFormData] = useState({ email: "", password: "" });

  const handleSubmit = (e) => {
    e.preventDefault(); // Prevent page reload
    console.log(formData);
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="Password"
      />
      <button>Submit</button>
    </form>
  );
};

localStorage vs sessionStorage

Feature localStorage sessionStorage
Lifetime Persistent (until cleared) Session only
Scope All tabs (same origin) Single tab
Size ~5-10 MB ~5 MB
Use Case User preferences, tokens Temporary session data
// Store data
localStorage.setItem("user", JSON.stringify({ name: "Rayied", age: 25 }));

// Retrieve data
const user = JSON.parse(localStorage.getItem("user"));

// Remove item
localStorage.removeItem("user");

// Clear all
localStorage.clear();

API Integration

Using fetch()

const fetchData = async () => {
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("Error:", error);
  }
};

Using axios

npm install axios
import axios from "axios";

const fetchData = async () => {
  try {
    const response = await axios.get("https://api.example.com/data");
    console.log(response.data);
  } catch (error) {
    console.error("Error:", error);
  }
};

// POST request
const createUser = async () => {
  try {
    const response = await axios.post("https://api.example.com/users", {
      name: "Rayied",
      email: "rayied@example.com",
    });
    console.log(response.data);
  } catch (error) {
    console.error("Error:", error);
  }
};

Comparison:

Feature fetch() axios
Built-in ✅ Yes ❌ Requires install
JSON parsing ❌ Manual ✅ Automatic
Interceptors ❌ No ✅ Yes
Error handling Basic Advanced

React Router

Installation

npm install react-router-dom@6

Basic Setup

import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import NotFound from "./pages/NotFound";

const App = () => {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
};

Dynamic Routes

import { useParams, useNavigate } from "react-router-dom";

const UserProfile = () => {
  const { id } = useParams();
  const navigate = useNavigate();

  return (
    <div>
      <h1>User ID: {id}</h1>
      <button onClick={() => navigate("/")}>Go Home</button>
    </div>
  );
};

// Route definition
<Route path="/users/:id" element={<UserProfile />} />;

Router Types

Router Use Case
BrowserRouter Standard web apps with server support
HashRouter Static hosting without server fallback
MemoryRouter Testing or non-DOM environments

Context API

Complete Example

// 1. Create Context
import { createContext, useState } from "react";

export const ThemeContext = createContext();

// 2. Provider Component
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

// 3. Wrap App
import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")).render(
  <ThemeProvider>
    <App />
  </ThemeProvider>
);

// 4. Consume Context
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";

const ThemedComponent = () => {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <div className={theme}>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle Theme
      </button>
    </div>
  );
};

Performance Optimization

Lazy Loading with Suspense

import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));

const App = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
};

Infinite Scrolling

import { useEffect, useRef, useState } from "react";

const InfiniteList = ({ fetchPage }) => {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const loaderRef = useRef(null);

  useEffect(() => {
    fetchPage(page).then(({ data, more }) => {
      setItems((prev) => [...prev, ...data]);
      setHasMore(more);
    });
  }, [page]);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasMore) {
          setPage((prev) => prev + 1);
        }
      },
      { threshold: 1 }
    );

    if (loaderRef.current) observer.observe(loaderRef.current);
    return () => observer.disconnect();
  }, [hasMore]);

  return (
    <div>
      {items.map((item, i) => (
        <div key={i}>{item.title}</div>
      ))}
      <div ref={loaderRef} />
    </div>
  );
};

TanStack Query (React Query)

What is TanStack Query?

A powerful library for managing server-side state in React applications with features like:

  • ✅ Automatic caching
  • ✅ Background refetching
  • ✅ Built-in loading/error states
  • ✅ Pagination & infinite scrolling
  • ✅ Optimistic updates

Installation

npm install @tanstack/react-query
npm install @tanstack/react-query-devtools

Setup

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

const queryClient = new QueryClient();

const App = () => (
  <QueryClientProvider client={queryClient}>
    <YourApp />
    <ReactQueryDevtools initialIsOpen={false} />
  </QueryClientProvider>
);

useQuery - Fetching Data

import { useQuery } from "@tanstack/react-query";

const fetchPosts = async () => {
  const response = await fetch("https://api.example.com/posts");
  return response.json();
};

const PostsList = () => {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
  });

  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
};

Configuration Options

const { data } = useQuery({
  queryKey: ["posts"],
  queryFn: fetchPosts,
  staleTime: 10000, // Data fresh for 10s
  gcTime: 300000, // Cache for 5 min
  refetchInterval: 5000, // Poll every 5s
  refetchIntervalInBackground: true,
});

Dynamic Queries

import { useParams } from "react-router-dom";

const PostDetail = () => {
  const { id } = useParams();

  const { data } = useQuery({
    queryKey: ["post", id],
    queryFn: () => fetchPost(id),
  });

  return <div>{data?.title}</div>;
};

Pagination

import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { useState } from "react";

const PaginatedPosts = () => {
  const [page, setPage] = useState(0);

  const { data } = useQuery({
    queryKey: ["posts", page],
    queryFn: () => fetchPosts(page),
    placeholderData: keepPreviousData,
  });

  return (
    <div>
      <ul>
        {data?.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
      <button onClick={() => setPage((p) => Math.max(0, p - 1))}>
        Previous
      </button>
      <button onClick={() => setPage((p) => p + 1)}>Next</button>
    </div>
  );
};

useMutation - Modifying Data

import { useMutation, useQueryClient } from "@tanstack/react-query";

const PostManager = () => {
  const queryClient = useQueryClient();

  // Delete mutation
  const deleteMutation = useMutation({
    mutationFn: (id) => deletePost(id),
    onSuccess: (data, id) => {
      // Update cache
      queryClient.setQueryData(["posts"], (oldData) =>
        oldData.filter((post) => post.id !== id)
      );
    },
  });

  // Update mutation
  const updateMutation = useMutation({
    mutationFn: ({ id, data }) => updatePost(id, data),
    onSuccess: (apiData, { id }) => {
      queryClient.setQueryData(["posts"], (oldData) =>
        oldData.map((post) => (post.id === id ? { ...post, ...apiData } : post))
      );
    },
  });

  return (
    <div>
      <button onClick={() => deleteMutation.mutate(postId)}>Delete</button>
      <button onClick={() => updateMutation.mutate({ id, data })}>
        Update
      </button>
    </div>
  );
};

Infinite Scrolling with useInfiniteQuery

import { useInfiniteQuery } from "@tanstack/react-query";
import { useInView } from "react-intersection-observer";
import { useEffect } from "react";

const InfiniteUsers = () => {
  const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =
    useInfiniteQuery({
      queryKey: ["users"],
      queryFn: ({ pageParam = 1 }) => fetchUsers(pageParam),
      getNextPageParam: (lastPage, allPages) =>
        lastPage.length === 10 ? allPages.length + 1 : undefined,
    });

  const { ref, inView } = useInView({ threshold: 1 });

  useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage]);

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.map((user) => (
            <div key={user.id}>{user.name}</div>
          ))}
        </div>
      ))}

      <div ref={ref}>
        {isFetchingNextPage
          ? "Loading..."
          : hasNextPage
          ? "Load more"
          : "No more"}
      </div>
    </div>
  );
};

TanStack Query Summary

Feature Hook Purpose
Fetch data useQuery GET requests
Modify data useMutation POST, PUT, DELETE
Infinite scroll useInfiniteQuery Progressive data loading
Invalidate cache invalidateQueries Force refetch
Optimistic updates setQueryData Instant UI updates

Quick Reference Tables

Hook Comparison

Hook Purpose Re-renders?
useState Local state ✅ Yes
useEffect Side effects ❌ No
useRef Mutable values ❌ No
useContext Global state ✅ Yes
useReducer Complex state ✅ Yes
useMemo Memoize values ❌ No
useCallback Memoize functions ❌ No

When to Use What

Scenario Solution
Simple counter useState
API data fetching useQuery (TanStack)
Form inputs useState + controlled
Global theme useContext
Complex form state useReducer
Expensive calculations useMemo
Child component callbacks useCallback
Pagination useQuery + page state
Infinite scroll useInfiniteQuery

Best Practices

General React

  1. ✅ Use functional components with hooks
  2. ✅ Keep components small and focused
  3. ✅ Use meaningful variable names
  4. ✅ Always handle loading and error states
  5. ✅ Memoize expensive operations
  6. ✅ Use proper key props in lists
  7. ✅ Clean up side effects in useEffect
  8. ✅ Avoid inline function definitions in JSX

TanStack Query

  1. ✅ Use unique, descriptive queryKeys
  2. ✅ Configure staleTime and gcTime appropriately
  3. ✅ Implement error boundaries
  4. ✅ Use placeholderData for smooth pagination
  5. ✅ Invalidate queries after mutations
  6. ✅ Keep queryFn functions pure
  7. ✅ Enable React Query DevTools in development
  8. ✅ Handle loading states with Suspense boundaries

Production Build

npm run build

This creates an optimized production build in the dist folder. Deploy this folder to your hosting service.


Additional Resources


© 2025 — Complete React + TanStack Query Guide

Vitest Testing Guide for React

Installation

Install the required dependencies:

npm install --save-dev vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom

Configuration

Create vitest.config.js in your project root:

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
  },
});

Update your package.json to add the test script:

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "lint": "eslint .",
  "preview": "vite preview",
  "test": "vitest"
}

Running Tests

npm test

Testing Patterns

1. Basic Component Testing

Component: Greetings.jsx

function Greeting({ name = "World" }) {
  return <h1>Hello, {name}!</h1>;
}

export default Greeting;

Test: greetings.test.jsx

import "@testing-library/jest-dom/vitest";
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import Greeting from "./Greetings";

describe("Greeting", () => {
  it("renders a default greeting", () => {
    render(<Greeting />);
    expect(screen.getByText("Hello, World!")).toBeInTheDocument();
  });

  it("renders a custom greeting", () => {
    render(<Greeting name="Rayied" />);
    expect(screen.getByText("Hello, Rayied!")).toBeInTheDocument();
  });
});

Key Query Methods

  • getByText: Throws an error if nothing is found. Use when you're certain the element exists.
  • queryByText: Returns null if nothing is found (no error). Use when testing that an element doesn't exist or when unsure if it exists.

2. Testing User Interactions

Component: Counter.jsx

import { useCounter } from "../../hooks/useCounter";

function Counter() {
  const { count, increment } = useCounter();

  return (
    <div>
      <p data-testid="counter-value">{count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default Counter;

Test: counter.test.jsx

import "@testing-library/jest-dom/vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import Counter from "./Counter";

describe("Counter", () => {
  it("increments counter on button click", async () => {
    render(<Counter />);

    const button = screen.getByRole("button", { name: /increment/i });
    const counterValue = screen.getByTestId("counter-value");

    expect(counterValue.textContent).toEqual("0");

    // Test button clicks
    await userEvent.click(button);
    expect(counterValue.textContent).toEqual("1");

    await userEvent.click(button);
    expect(counterValue.textContent).toEqual("2");
  });
});

Important Notes

  • Use userEvent for simulating user interactions (more realistic than fireEvent)
  • Always await user events
  • Use data-testid attributes for elements that are hard to query by role or text

3. Testing Async Operations & API Calls

Component: UserProfile.jsx

import { useEffect, useState } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data));
  }, [userId]);

  if (!user) return <p>Loading...</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

export default UserProfile;

Test: userprofile.test.jsx

import "@testing-library/jest-dom/vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import UserProfile from "./UserProfile";

describe("UserProfile", () => {
  beforeEach(() => {
    global.fetch = vi.fn();
  });

  afterEach(() => {
    vi.resetAllMocks();
  });

  it("fetches and displays the user data", async () => {
    global.fetch.mockResolvedValueOnce({
      json: async () => ({
        id: 4,
        name: "John",
        email: "john@gmail.com",
      }),
    });

    render(<UserProfile userId={4} />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();

    await waitFor(() => {
      expect(
        screen.getByRole("heading", { name: /john/i })
      ).toBeInTheDocument();
      expect(screen.getByText(/john@gmail.com/i)).toBeInTheDocument();
    });
  });
});

Key Concepts for Async Testing

  • vi.fn(): Creates a mock function
  • mockResolvedValueOnce(): Mocks a successful Promise resolution
  • beforeEach: Runs before each test (setup)
  • afterEach: Runs after each test (cleanup)
  • waitFor: Waits for async operations to complete
  • vi.resetAllMocks(): Cleans up all mocks

4. Testing Custom Hooks

Hook: useCounter.jsx

import { useState } from "react";

export const useCounter = (initValue = 0) => {
  const [count, setCount] = useState(initValue);

  function increment() {
    setCount((c) => c + 1);
  }

  function decrement() {
    setCount((c) => c - 1);
  }

  function reset() {
    setCount(0);
  }

  return { count, increment, decrement, reset };
};

Test: usecounter.test.jsx

import "@testing-library/jest-dom/vitest";
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { useCounter } from "./useCounter";

describe("useCounter", () => {
  it("initializes with value 5", () => {
    const { result } = renderHook(() => useCounter(5));
    expect(result.current.count).toBe(5);
  });

  it("increments the count", () => {
    const { result } = renderHook(() => useCounter(0));
    expect(result.current.count).toBe(0);

    act(() => {
      result.current.increment();
    });
    expect(result.current.count).toBe(1);

    act(() => {
      result.current.increment();
    });
    expect(result.current.count).toBe(2);
  });

  it("decrements the count", () => {
    const { result } = renderHook(() => useCounter(5));
    expect(result.current.count).toBe(5);

    act(() => {
      result.current.decrement();
    });
    expect(result.current.count).toBe(4);
  });

  it("resets the count to 0", () => {
    const { result } = renderHook(() => useCounter(4));
    expect(result.current.count).toBe(4);

    act(() => {
      result.current.reset();
    });
    expect(result.current.count).toBe(0);
  });
});

Hook Testing Essentials

  • renderHook: Renders a hook in isolation
  • act: Wraps state updates to ensure they're processed
  • result.current: Access the hook's return value

Best Practices

  1. Test user behavior, not implementation details: Focus on what users see and do
  2. Use semantic queries: Prefer getByRole, getByLabelText over getByTestId
  3. Keep tests isolated: Each test should be independent
  4. Clean up after tests: Use afterEach to reset mocks and state
  5. Test loading states: Verify both loading and success states
  6. Use meaningful test descriptions: Describe what the test does clearly
  7. Await async operations: Always use await with userEvent and waitFor

Common Query Methods

Method Returns Throws Error Use Case
getBy* Element Yes Element should exist
queryBy* Element or null No Element might not exist
findBy* Promise Yes Async elements
getAllBy* Array Yes Multiple elements
queryAllBy* Array No Multiple elements (maybe none)
findAllBy* Promise Yes Async multiple elements

Additional Tips

  • Use /i flag in regex for case-insensitive matching: /loading/i
  • Mock external dependencies to keep tests fast and reliable
  • Use screen.debug() to see the current DOM state during debugging
  • Run tests in watch mode during development: npm test -- --watch

React Redux Documentation

Why Redux?

In small apps you can manage data using React's state. But as the app grows, it becomes tricky to pass data between many components.

Redux solves this problem by creating a centralized store that holds all the data. This store can be accessed and updated by any part of the app.

React Redux = Context API + useReducer

What is Redux?

Redux is a tool that helps manage data (also known as "state") in large React apps. It allows us to keep all our app's data in a single place, known as the Redux Store, making it easy to share and update data across different parts of the app.

How Redux Works

  • Store: This is where Redux keeps all your data.
  • Action: This is an object which tells Redux what to do (like adding a task).
  • Reducers: How to do. It actually changes the data in the store based on actions.

Store

The Redux store is like a big box where all your application's data is kept safe. Everything you do with Redux - whether adding, removing or updating data - goes through this store.

Actions

This is an Object which tells Redux what to do (like adding a task).

{
  type: "counter/add",
  payload: {
    incrementBy: 10
  }
}

payload: extra information

Reducers

How to do. It actually changes the data in the store based on the actions.

export const CounterReducer = (state = initialState, action) => {
  switch (action.type) {
    case "counter/add":
      return { ...state, value: state.value + action.payload.incrementBy };
    default:
      return state;
  }
};

Redux Advantages

  • Centralized state management: Redux stores your app's state in one place, making it easier to manage and access data across components.
  • Global access: Any component can access and update the state without passing props down.
  • Predictable Updates: State changes are controlled and predictable using reducers.
  • DevTools: Powerful tools for debugging, inspecting state and replaying actions.
  • Async Support: Middleware like Thunk or Saga handles async tasks, keeping the code clean.

Redux: Reducer Function

A reducer is a function that decides how the state should change based on the action. The reducer takes the current state and an action and returns a new state.

Key Things to Remember

  1. Reducers must always return a new state
  2. They should never modify the old state directly

Syntax: Reducer

function reducer(state = initialState, action) {
  // reducer logic
}

The reducer takes two arguments:

  • State: This is the current state.
  • Action: This tells the reducer what to do. It has a type and sometimes a payload (which is the data).
function reducer(state = initialState, action) {
  switch (action.type) {
    case "ACTION_TYPE":
      return { ...state, data: action.payload };
    default:
      return state;
  }
}

We use a switch statement to check the action's type. Based on the action type, the reducer updates the state.

Best Practices for Reducers

const ACTION_TYPE = "task/add";

function reducer(state = initialState, action) {
  switch (action.type) {
    case ACTION_TYPE:
      return { ...state, data: action.payload };
  }
}
  • Action Types: Use a combination of the state domain (like task) and the event (like add), separated by a slash. For example, task/add.
  • Immutable state: Never directly change the state. Always return a new state object using ...state to copy the old state.

Example: store.jsx

/* eslint-disable no-case-declarations */
const ADD_TASK = "task/add";
const DELETE_TASK = "task/delete";

const initialState = {
  task: [],
};

const taskReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TASK:
      return { ...state, task: [...state.task, action.payload] };

    case DELETE_TASK:
      const updatedTask = state.task.filter((currTask, idx) => {
        return idx !== action.payload;
      });
      return { ...state, task: updatedTask };

    default:
      return state;
  }
};

Redux Store

The store is where Redux keeps all your app's data. It's like a database for your app, but it's only for managing data in memory (not saving it permanently).

Redux Store - Syntax

import { createStore } from "redux";
const store = createStore(reducer);

The createStore method creates the Redux store using a reducer function that handles how the state changes in response to actions.

dispatch() an Action

dispatch() is used to send actions to the Redux store. An action describes what change you want to make to the state (such as adding a task).

store.dispatch({ type: "ACTION_TYPE", payload: data });

getState()

getState() retrieves the current state of the Redux store. This is useful for accessing the state after it has been updated or to monitor changes.

Example: check if deleted or not, or added or not

Redux Installation

npm i redux

Note: createStore is now deprecated

Complete Example: store.jsx

import { createStore } from "redux";
/* eslint-disable no-case-declarations */

// define action types
const ADD_TASK = "task/add";
const DELETE_TASK = "task/delete";

const initialState = {
  task: [],
  isLoading: false,
};

// step-1: create a simple reducer function
const taskReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TASK:
      return { ...state, task: [...state.task, action.payload] };

    case DELETE_TASK:
      const updatedTask = state.task.filter((currTask, idx) => {
        return idx !== action.payload;
      });
      return { ...state, task: updatedTask };

    default:
      return state;
  }
};

// step-2: create Redux store using the reducer
const store = createStore(taskReducer);
console.log(store);

// step-3: log the initial state
// The getState method is a synchronous function that returns the current state of Redux application.
// It includes the entire state of the application, including reducers and their respective states.
console.log("initial state:", store.getState());

// step-4: dispatch an action to add a task
store.dispatch({ type: ADD_TASK, payload: "Buy LocalStudio" });
console.log("updated state: ", store.getState());

store.dispatch({ type: ADD_TASK, payload: "Buy pdf" });
console.log("updated state: ", store.getState());

store.dispatch({ type: DELETE_TASK, payload: 1 });
console.log("deleted state: ", store.getState());

To Use Store

Import in main.jsx:

import "./store.jsx";

Redux Action

An action is an object that tells Redux what we want to do. It must have a type property that describes the action.

{ type: "ACTION_TYPE", payload: data }

Action Creator

An action creator is a function that creates an action object. This makes it easier to create actions with different data.

function actionCreator(data) {
  return { type: "ACTION_TYPE", payload: data };
}

Easy to Write Code

import { createStore } from "redux";
/* eslint-disable no-case-declarations */

// define action types
const ADD_TASK = "task/add";
const DELETE_TASK = "task/delete";

const initialState = {
  task: [],
  isLoading: false,
};

// step-1: create a simple reducer function
const taskReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TASK:
      return { ...state, task: [...state.task, action.payload] };

    case DELETE_TASK:
      const updatedTask = state.task.filter((currTask, idx) => {
        return idx !== action.payload;
      });
      return { ...state, task: updatedTask };

    default:
      return state;
  }
};

// step-2: create Redux store using the reducer
const store = createStore(taskReducer);
console.log(store);

// step-3: log the initial state
console.log("initial state:", store.getState());

// step-4(old): dispatch an action to add a task
// store.dispatch({ type: ADD_TASK, payload: "Buy LocalStudio" });
// console.log("updated state: ", store.getState());
// store.dispatch({ type: ADD_TASK, payload: "Buy pdf" });
// console.log("updated state: ", store.getState());
// store.dispatch({ type: DELETE_TASK, payload: 1 });
// console.log("deleted state: ", store.getState());

// step-5: create action creators
const addTask = (data) => {
  return { type: ADD_TASK, payload: data };
};

// step-4(new): dispatch an action to add a task
store.dispatch(addTask("Buy LocalStudio"));
console.log("updated state: ", store.getState());

store.dispatch(addTask("Buy pdf"));
console.log("updated state: ", store.getState());

const deleteTask = (id) => {
  return { type: DELETE_TASK, payload: id };
};

store.dispatch(deleteTask(1));
console.log("deleted state: ", store.getState());

export default store;

Connect React with Redux

To use Redux in a React app, we need to connect Redux's store and actions to React components. This allows components to access the global state and dispatch actions.

Steps to Connect

Step 1: Install react-redux

npm install react-redux

Step 2: Wrap the App with Provider

Use the Provider component to pass the Redux store to the entire app.

main.jsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App.jsx";
import "./index.css";
import "./store.jsx";
import store from "./store.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);

store.jsx:

export const store = createStore(taskReducer);

Access Redux State in React using useSelector

Use the useSelector hook to read data from the Redux store.

const count = useSelector((state) => state.property);

Selector function: We define a selector function that takes the entire Redux store state as an argument and returns the specific piece of data we need.

Example: Todo.jsx

import { MdDeleteForever } from "react-icons/md";
import { useSelector } from "react-redux";

const Todo = () => {
  const tasks = useSelector((state) => state.task);

  const handleTaskDelete = (idx) => {};

  return (
    <>
      <div className="container">
        <div className="todo-app">
          <h1>
            <i className="fa-regular fa-pen-to-square"></i>To-do List:
          </h1>
          <div className="row">
            <form>
              <input type="text" id="input-box" placeholder="Add a new Task" />
              <button>Add Task</button>
            </form>
          </div>
          <ul id="list-container">
            {tasks.map((curTask, idx) => {
              return (
                <li key={idx}>
                  <p>
                    {idx + 1}: {curTask}
                  </p>
                  <div>
                    <MdDeleteForever
                      className="icon-style"
                      onClick={() => handleTaskDelete(idx)}
                    />
                  </div>
                </li>
              );
            })}
          </ul>
        </div>
      </div>
    </>
  );
};

export default Todo;

store.jsx:

store.dispatch(addTask("Buy LocalStudio"));
store.dispatch(addTask("Buy Tesla"));
store.dispatch(addTask("Buy Youtube"));

Dispatch Actions in React using useDispatch

Use the useDispatch hook to dispatch actions from a React component.

Example: Todo.jsx

import { useState } from "react";
import { MdDeleteForever } from "react-icons/md";
import { useDispatch, useSelector } from "react-redux";
import { addTask, deleteTask } from "../store";

const Todo = () => {
  const [task, setTask] = useState("");
  const tasks = useSelector((state) => state.task);

  const dispatch = useDispatch();

  const handleTaskDelete = (id) => {
    return dispatch(deleteTask(id));
  };

  const handleFormSubmit = (e) => {
    e.preventDefault();
    dispatch(addTask(task));
    return setTask("");
  };

  return (
    <>
      <div className="container">
        <div className="todo-app">
          <h1>
            <i className="fa-regular fa-pen-to-square"></i>To-do List:
          </h1>
          <div className="row">
            <form onSubmit={handleFormSubmit}>
              <input
                type="text"
                id="input-box"
                placeholder="Add a new Task"
                value={task}
                onChange={(e) => setTask(e.target.value)}
              />
              <button>Add Task</button>
            </form>
          </div>
          <ul id="list-container">
            {tasks.map((curTask, idx) => {
              return (
                <li key={idx}>
                  <p>
                    {idx + 1}: {curTask}
                  </p>
                  <div>
                    <MdDeleteForever
                      className="icon-style"
                      onClick={() => handleTaskDelete(idx)}
                    />
                  </div>
                </li>
              );
            })}
          </ul>
        </div>
      </div>
    </>
  );
};

export default Todo;

store.jsx:

export const addTask = (data) => {
  return { type: ADD_TASK, payload: data };
};

export const deleteTask = (id) => {
  return { type: DELETE_TASK, payload: id };
};

Installation Redux DevTools

Link: Redux DevTools Chrome Extension

npm i @redux-devtools/extension

store.jsx:

import { composeWithDevTools } from "@redux-devtools/extension";

export const store = createStore(taskReducer, composeWithDevTools());

Redux Thunk in React

Redux Thunk is middleware that allows you to write action creators that return a function instead of an action (action means pure object). This function can perform asynchronous logic (like API requests) and dispatch actions after the operation is complete (e.g., fetching tasks and then dispatching them to the store).

When you return a function from an action creator, Redux Thunk provides the dispatch function as an argument. This allows you to manually dispatch other actions (e.g., when an API call succeeds or fails).

Install Redux-Thunk

npm i redux-thunk

Example: store.jsx

import { composeWithDevTools } from "@redux-devtools/extension";
import { applyMiddleware, createStore } from "redux";
import { thunk } from "redux-thunk";

const FETCH_TASK = "task/fetch";

const taskReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TASK:
      return { ...state, task: [...state.task, action.payload] };

    case DELETE_TASK:
      const updatedTask = state.task.filter((currTask, idx) => {
        return idx !== action.payload;
      });
      return { ...state, task: updatedTask };

    case FETCH_TASK:
      return { ...state, task: [...state.task, ...action.payload] };

    default:
      return state;
  }
};

export const store = createStore(
  taskReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

// middleware
export const fetchTask = () => {
  return async (dispatch) => {
    try {
      const res = await fetch(
        "https://jsonplaceholder.typicode.com/todos?_limit=3"
      );
      const task = await res.json();
      console.log(task);
      dispatch({
        type: FETCH_TASK,
        payload: task.map((curTask) => curTask.title),
      });
    } catch (error) {
      console.log(error);
    }
  };
};

Redux Toolkit Complete Guide

Which one to select?

  1. React-Redux
  2. Redux Toolkit ✅

What is Redux Toolkit?

Redux Toolkit (RTK) is an official toolset from the Redux Team that makes working with Redux easier and less time-consuming.

Instead of doing everything manually—like creating actions, reducers and managing state immutability—RTK gives you built-in functions that handle most of that work for you.

In simpler terms: It's a shortcut that helps you manage your app's state with less code and fewer mistakes. The goal is to make Redux more beginner-friendly and reduce the amount of code you need to write.


Why Redux Toolkit?

Less Boilerplate

In traditional Redux, you write a lot of repetitive code just to get basic things done. RTK cuts down on all that extra code and gives you a cleaner, simpler way to manage state.

Simpler Setup

It automatically sets up your store, adds middleware for things like async actions, and even connects you to Redux DevTools for debugging without extra configuration.

Built-in Async Handling

If you've ever used Redux Thunk for async tasks like fetching data from an API, RTK has a built-in feature called createAsyncThunk that makes it even easier to handle async actions.


Advantages

Less Boilerplate Code

Normally, with Redux, you need to write action types, action creators, and reducers separately. With RTK's createSlice, you can handle all of this in one place, in fewer lines of code.

Easier to Work with State

RTK uses a tool called Immer (library) under the hood, which allows you to write state changes like you're mutating the state directly, but it still follows Redux's rule of immutability (not changing the original state).

Better Async Logic

Handling async tasks, like fetching data, is much simpler with RTK's createAsyncThunk. It automatically handles loading, success, and error states for you, so you don't have to write all that manually.

Great Defaults

RTK sets up Redux DevTools, middleware, and other configuration for you, so you can focus on building your app instead of setup.

Note: RTK is a helper function of Redux React.


Comparison: Redux vs Redux Toolkit

Traditional Redux

// Action types
const INCREMENT = "INCREMENT";

// Action creators
const increment = () => ({ type: INCREMENT });

// Initial state
const initialState = { value: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return {
        ...state, // copying the previous state
        value: state.value + 1, // updating value
      };
    default:
      return state;
  }
}

Redux Toolkit

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment(state) {
      state.value += 1; // Immer handles immutability behind the scenes
    },
  },
});

export const { increment } = counterSlice.actions;
export default counterSlice.reducer;

Installation

npm i @reduxjs/toolkit

RTK Store: configureStore & useSelector

Old Style (Traditional Redux)

// Step-2: create Redux store using the reducer
export const store = createStore(
  taskReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

New Style (Redux Toolkit)

import { configureStore } from "@reduxjs/toolkit";

// Step-2: Create store with configureStore
export const store = configureStore({
  reducer: {
    taskReducer,
  },
});

RTK createSlice

In Redux Toolkit (RTK), createSlice is a utility function that simplifies the process of creating a Redux slice of state. It combines actions and reducers into a single object, making the setup of Redux state management more streamlined and organized.

A slice is essentially a section of the Redux state, along with the actions and reducers that operate on it.

Using createSlice, you can define:

  • The initial state of the slice
  • Reducers that define how the state changes in response to actions
  • Action creators automatically generated based on reducer names

Basic Example

import { createSlice } from "@reduxjs/toolkit";

// RTK slice
const taskReducer = createSlice({
  name: "task",
  initialState,
  reducers: {
    // Here by default are action creators
    addTask(state, action) {},
    deleteTask(state, action) {},
  },
});

const { addTask, deleteTask } = taskReducer.actions;

Perform Add and Delete Task

store.jsx

import { createSlice, configureStore } from "@reduxjs/toolkit";

const initialState = {
  task: [],
};

// RTK slice
const taskReducer = createSlice({
  name: "task",
  initialState,
  reducers: {
    // Here by default are action creators
    addTask(state, action) {
      // Now we can mutate the data
      state.task.push(action.payload);
      // state.task = [...state.task, action.payload];
    },
    deleteTask(state, action) {
      state.task = state.task.filter((curTask, idx) => {
        return idx !== action.payload;
      });
    },
  },
});

export const { addTask, deleteTask } = taskReducer.actions;

export const store = configureStore({
  reducer: {
    taskReducer: taskReducer.reducer,
  },
});

// (New style) Step-3: Log the initial state
console.log("Initial state:", store.getState());

// (New style) Step-4: Dispatch an action to add a task
console.log(store.dispatch(addTask("Buy LocalStudio")));
console.log(store.dispatch(addTask("Buy PDF")));

Connect React + Redux Toolkit

Step 1: Install react-redux

npm install react-redux

Step 2: Wrap App with Provider

Use the Provider component to pass the Redux store to the entire app.

main.jsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App.jsx";
import "./index.css";
import { store } from "./store.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);

Access Redux State in React using useSelector

Use the useSelector hook to read data from the Redux store.

const count = useSelector((state) => state.property);

Selector function: We define a selector function that takes the entire Redux Toolkit store state as an argument and returns the specific piece of data we need.


Dispatch an Action

Dispatch actions in React using useDispatch. Use the useDispatch hook to dispatch actions from a React component.

todo.jsx

import { useState } from "react";
import { MdDeleteForever } from "react-icons/md";
import { useDispatch, useSelector } from "react-redux";
import { addTask, deleteTask } from "../store";

const Todo = () => {
  const [userTask, setUserTask] = useState("");
  const tasks = useSelector((state) => state.taskReducer.task);
  const dispatch = useDispatch();

  const handleFormSubmit = (e) => {
    e.preventDefault();
    dispatch(addTask(userTask));
    setUserTask("");
  };

  const handleDeleteTask = (idx) => {
    return dispatch(deleteTask(idx));
  };

  return (
    <>
      <div className="container">
        <div className="todo-app">
          <h1>
            <i className="fa-regular fa-pen-to-square"></i>To-do List:
          </h1>
          <div className="row">
            <form onSubmit={handleFormSubmit}>
              <input
                type="text"
                id="input-box"
                placeholder="Add a new Task"
                value={userTask}
                onChange={(e) => setUserTask(e.target.value)}
              />
              <button type="submit">Add Task</button>
            </form>
          </div>
          <ul id="list-container">
            {tasks?.map((curTask, idx) => {
              return (
                <li key={idx}>
                  <p>
                    {idx + 1}: {curTask}
                  </p>
                  <div>
                    <MdDeleteForever
                      className="icon-style"
                      onClick={() => handleDeleteTask(idx)}
                    />
                  </div>
                </li>
              );
            })}
          </ul>
        </div>
      </div>
    </>
  );
};

export default Todo;

RTK Folder Structure

src
├── app
│   └── store.js              # Redux store configuration
├── features
│   └── tasks
│       ├── taskSlice.js      # The tasks slice
│       ├── taskActions.js    # Action creators (optional if needed separately)
│       ├── taskSelectors.js  # Selectors (if you have complex selectors)
│       └── taskAPI.js        # Async API calls (if using RTK Query or other async logic)
└── index.js                  # Root entry file

Summary

Redux Toolkit simplifies Redux development by:

  • Reducing boilerplate code with createSlice
  • Automatically configuring the store with configureStore
  • Handling immutability with Immer
  • Providing built-in async handling with createAsyncThunk
  • Setting up Redux DevTools by default

This makes it the recommended approach for all new Redux projects!

Firebase with React - Complete Guide

A comprehensive guide to integrating Firebase services (Authentication, Realtime Database, and Firestore) with React applications.


Table of Contents

  1. Initial Setup
  2. Firebase Configuration
  3. Authentication
  4. Realtime Database
  5. Cloud Firestore
  6. Context API Pattern
  7. Security Best Practices

Initial Setup

1. Create Firebase Project

  1. Visit firebase.google.com
  2. Navigate to Console
  3. Click "Create Project"
  4. Follow the setup wizard
  5. Register your web app by clicking the Web icon (</>)

2. Install Firebase SDK

npm install firebase

Firebase Configuration

Environment Variables Setup

Create a .env file in your project root:

VITE_apiKey=your-actual-api-key-here
VITE_authDomain=your-project-auth-domain
VITE_projectId=your-project-id
VITE_storageBucket=your-storage-bucket
VITE_messagingSenderId=your-messaging-sender-id
VITE_appId=your-app-id
VITE_databaseURL=your-database-url

Firebase Configuration File

Location: src/firebase/firebase.js

import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_apiKey,
  authDomain: import.meta.env.VITE_authDomain,
  projectId: import.meta.env.VITE_projectId,
  storageBucket: import.meta.env.VITE_storageBucket,
  messagingSenderId: import.meta.env.VITE_messagingSenderId,
  appId: import.meta.env.VITE_appId,
  databaseURL: import.meta.env.VITE_databaseURL
};

// Initialize Firebase
export const app = initializeApp(firebaseConfig);

Authentication

Setup in Firebase Console

  1. Go to Authentication tab
  2. Click Get Started
  3. Enable Email/Password and Google sign-in methods

Email/Password Authentication

Sign Up Component

import { createUserWithEmailAndPassword, getAuth } from "firebase/auth";
import { getDatabase, ref, set } from "firebase/database";
import { useState } from "react";
import { app } from "../firebase/firebase";

const auth = getAuth(app);
const db = getDatabase(app);

const SignUp = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const createUser = () => {
    createUserWithEmailAndPassword(auth, email, password)
      .then((value) => {
        // Store user data in Realtime Database
        set(ref(db, "users/" + value.user.uid), {
          email: value.user.email,
          createdAt: new Date().toISOString(),
          provider: "email"
        });
        alert("Success");
      })
      .catch(err => console.log(err));
  };

  return (
    <div className="signup-page">
      <h1>Sign Up Page</h1>
      <label>Email:</label>
      <input 
        type="email" 
        onChange={e => setEmail(e.target.value)} 
        value={email}
        placeholder="Enter your email" 
      />
      
      <label>Password:</label>
      <input 
        type="password"
        onChange={e => setPassword(e.target.value)} 
        value={password}
        placeholder="Enter your password" 
      />
      
      <button onClick={createUser}>Sign Up</button>
    </div>
  );
};

export default SignUp;

Sign In Component

import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import { useState } from "react";
import { app } from "../firebase/firebase";

const auth = getAuth(app);

const Signin = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const signInUser = () => {
    signInWithEmailAndPassword(auth, email, password)
      .then(value => console.log("Sign in success"))
      .catch((err) => console.log(err));
  };

  return (
    <div className="signin-page">
      <h1>Sign In Page</h1>
      <label>Enter your Email</label>
      <input 
        type="email" 
        onChange={e => setEmail(e.target.value)} 
        value={email}
        placeholder="Enter your email" 
      />

      <label>Enter your Password</label>
      <input 
        type="password"
        onChange={e => setPassword(e.target.value)}
        value={password}
        placeholder="Enter your password" 
      />
      
      <button onClick={signInUser}>Sign In</button>
    </div>
  );
};

export default Signin;

Google Authentication

import { 
  GoogleAuthProvider, 
  signInWithPopup, 
  getAuth 
} from "firebase/auth";
import { getDatabase, ref, set } from "firebase/database";
import { app } from "../firebase/firebase";

const auth = getAuth(app);
const db = getDatabase(app);
const googleProvider = new GoogleAuthProvider();

const signupWithGoogle = () => {
  signInWithPopup(auth, googleProvider)
    .then((result) => {
      const user = result.user;
      
      // Store user data in Realtime Database
      set(ref(db, "users/" + user.uid), {
        email: user.email,
        displayName: user.displayName,
        photoURL: user.photoURL,
        provider: 'google',
        createdAt: new Date().toISOString()
      })
      .then(() => console.log("User data saved to database"))
      .catch(err => console.error("Error saving to database:", err));
      
      console.log("Google sign-in successful");
    })
    .catch((error) => {
      console.error("Error:", error.code, error.message);
    });
};

Auth State Management

Monitor authentication state and conditionally render UI:

import { getAuth, onAuthStateChanged, signOut } from 'firebase/auth';
import { useEffect, useState } from 'react';
import { app } from './firebase/firebase';
import SignUp from './pages/SignUp';
import Signin from './pages/Signin';

const auth = getAuth(app);

const App = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    onAuthStateChanged(auth, (user) => {
      if (user) {
        console.log("User logged in:", user);
        setUser(user);
      } else {
        console.log("Logged out");
        setUser(null);
      }
    });
  }, []);

  // Show auth forms when user is NOT logged in
  if (user == null) {
    return (
      <div className="App">
        <SignUp />
        <Signin />
      </div>
    );
  }

  // Show welcome message when user IS logged in
  return (
    <div className="App">
      <h1>Hello {user.email || user.displayName || "User"}</h1>
      <button onClick={() => signOut(auth)}>Log Out</button>
    </div>
  );
};

export default App;

Realtime Database

Setup in Firebase Console

  1. Navigate to Realtime Database in Firebase Console
  2. Click Create Database
  3. Choose Start in test mode for development

Writing Data

import { getDatabase, ref, set } from 'firebase/database';
import { app } from './firebase/firebase';

const db = getDatabase(app);

const putData = () => {
  set(ref(db, 'users/rayied'), {
    id: 1,
    name: "rayied",
    age: 26
  });
};

// Nested data structure
const putNestedData = () => {
  set(ref(db, 'grandfather/father/child'), {
    id: 1,
    name: "Alinur",
    age: 26
  });
};

Reading Data (One-time)

import { getDatabase, ref, child, get } from 'firebase/database';
import { app } from './firebase/firebase';

const db = getDatabase(app);

// Read specific path
get(child(ref(db), 'grandfather/father'))
  .then(snapshot => {
    if (snapshot.exists()) {
      console.log(snapshot.val());
    } else {
      console.log("No data available");
    }
  })
  .catch(error => console.error(error));

Real-time Listeners

Listen to data changes in real-time:

import { getDatabase, ref, onValue } from 'firebase/database';
import { useEffect, useState } from 'react';
import { app } from './firebase/firebase';

const db = getDatabase(app);

const MyComponent = () => {
  const [name, setName] = useState("");

  useEffect(() => {
    const dataRef = ref(db, 'grandfather/father/child');
    
    // Set up real-time listener
    const unsubscribe = onValue(dataRef, (snapshot) => {
      const data = snapshot.val();
      if (data) {
        setName(data.name);
      }
    });

    // Cleanup listener on unmount
    return () => unsubscribe();
  }, []);

  return <h3>Name is: {name}</h3>;
};

Cloud Firestore

Setup in Firebase Console

  1. Go to BuildFirestore Database
  2. Click Create Database
  3. Choose Start in test mode for development

Firestore Structure

  • Collections contain Documents
  • Documents contain Fields (key-value pairs)
  • Documents can have Sub-collections

Writing Data

import { addDoc, collection, getFirestore } from "firebase/firestore";
import { app } from "./firebase/firebase";

const firestoredb = getFirestore(app);

// Add document to collection
const writeData = async () => {
  const result = await addDoc(collection(firestoredb, 'cities'), {
    name: 'Dhaka',
    pinCode: 1234,
    lat: 123,
    long: 456
  });
  console.log("Document created with ID:", result.id);
};

// Create sub-collection
const makeSubCollection = async () => {
  await addDoc(
    collection(firestoredb, "cities/urDuHSu1d8kxKwGj8OH8/places"),
    {
      name: "Old Dhaka",
      desc: "dhaka city",
      date: Date.now()
    }
  );
};

Reading Data (Known Document ID)

import { doc, getDoc, getFirestore } from "firebase/firestore";
import { app } from "./firebase/firebase";

const firestoredb = getFirestore(app);

const getDocument = async () => {
  const docRef = doc(firestoredb, 'cities', 'urDuHSu1d8kxKwGj8OH8');
  const snapshot = await getDoc(docRef);
  
  if (snapshot.exists()) {
    console.log("Document data:", snapshot.data());
  } else {
    console.log("No such document!");
  }
};

Querying Data (Unknown Document ID)

import { 
  collection, 
  getDocs, 
  getFirestore, 
  query, 
  where 
} from "firebase/firestore";
import { app } from "./firebase/firebase";

const firestoredb = getFirestore(app);

const getDocuments = async () => {
  const collectionRef = collection(firestoredb, 'users');
  const q = query(collectionRef, where('isMale', '==', true));
  const snapshot = await getDocs(q);

  snapshot.forEach(doc => {
    console.log(doc.id, " => ", doc.data());
  });
};

Updating Data

import { doc, updateDoc, getFirestore } from "firebase/firestore";
import { app } from "./firebase/firebase";

const firestoredb = getFirestore(app);

const updateDocument = async () => {
  const docRef = doc(firestoredb, "cities", "urDuHSu1d8kxKwGj8OH8");
  await updateDoc(docRef, {
    name: "Sylhet"
  });
  console.log("Document updated successfully");
};

Context API Pattern

Why Use Context?

  • Centralized Firebase logic
  • Avoid prop drilling
  • Easy access to Firebase methods across components
  • Better code organization

Firebase Context Setup

Location: src/context/Firebase.jsx

import { initializeApp } from "firebase/app";
import { createUserWithEmailAndPassword, getAuth } from "firebase/auth";
import { getDatabase, ref, set } from "firebase/database";
import { createContext, useContext } from "react";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_apiKey,
  authDomain: import.meta.env.VITE_authDomain,
  projectId: import.meta.env.VITE_projectId,
  storageBucket: import.meta.env.VITE_storageBucket,
  messagingSenderId: import.meta.env.VITE_messagingSenderId,
  appId: import.meta.env.VITE_appId,
  databaseURL: import.meta.env.VITE_databaseURL
};

const FirebaseApp = initializeApp(firebaseConfig);
const database = getDatabase(FirebaseApp);
const FirebaseAuth = getAuth(FirebaseApp);
const FirebaseContext = createContext(null);

// Custom Hook
export const useFirebase = () => {
  return useContext(FirebaseContext);
};

export const FirebaseProvider = (props) => {
  const signupUserWithEmailAndPassword = (email, password) => {
    return createUserWithEmailAndPassword(FirebaseAuth, email, password);
  };

  const putData = (key, data) => {
    set(ref(database, key), data);
  };

  return (
    <FirebaseContext.Provider 
      value={{ signupUserWithEmailAndPassword, putData }}
    >
      {props.children}
    </FirebaseContext.Provider>
  );
};

Wrapping App with Provider

Location: main.jsx

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import { FirebaseProvider } from './context/Firebase.jsx';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <FirebaseProvider>
      <App />
    </FirebaseProvider>
  </StrictMode>
);

Using Firebase Context

Location: App.jsx

import { useState } from 'react';
import './App.css';
import { useFirebase } from './context/Firebase';

function App() {
  const firebase = useFirebase();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSignUp = () => {
    firebase.signupUserWithEmailAndPassword(email, password);
    firebase.putData("users/" + "rayd", { email, password });
  };

  return (
    <div className="App">
      <h1>Firebase + React</h1>
      <input 
        type="email"
        onChange={e => setEmail(e.target.value)}
        value={email}
        placeholder='Enter email' 
      />
      <input 
        type="password"
        onChange={e => setPassword(e.target.value)}
        value={password}
        placeholder='Enter password' 
      />
      <button onClick={handleSignUp}>Sign Up</button>
    </div>
  );
}

export default App;

Security Best Practices

1. Environment Variables

  • Never commit Firebase config with real API keys to public repositories
  • ✅ Use .env files and add them to .gitignore
  • ✅ Use environment variables for all sensitive data

2. Firebase Security Rules

Realtime Database Rules

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "$uid === auth.uid",
        ".write": "$uid === auth.uid"
      }
    }
  }
}

Firestore Rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

3. Password Requirements

  • Firebase requires passwords to be at least 6 characters long
  • Consider implementing additional client-side validation

4. Test Mode Warning

⚠️ Important: Test mode allows unrestricted read/write access. Always configure proper security rules before deploying to production!


Key Differences: Realtime Database vs Firestore

Feature Realtime Database Firestore
Data Structure JSON tree Collections & Documents
Querying Limited Advanced with compound queries
Scaling Regional Automatic multi-region
Offline Support Basic Advanced
Pricing Per GB stored Per operation
Best For Simple data sync Complex queries & structure

Common Use Cases

Realtime Database

  • Chat applications
  • Real-time collaboration
  • Live location tracking
  • Simple data structures
  • Used by Gaming Companies

Firestore

  • E-commerce platforms
  • Social media apps
  • Complex data relationships
  • Apps requiring advanced queries

Additional Resources


Summary

This guide covered:

  • ✅ Firebase project setup and configuration
  • ✅ Email/Password and Google authentication
  • ✅ Realtime Database operations (read/write/listen)
  • ✅ Cloud Firestore CRUD operations
  • ✅ Context API pattern for Firebase
  • ✅ Security best practices

Firebase provides a powerful backend solution for React applications, enabling rapid development with authentication, databases, and real-time capabilities out of the box.

React with TypeScript - Todo Application Guide

Table of Contents


Introduction

This guide explains a Todo application built with React and TypeScript. TypeScript is a superset of JavaScript that adds static typing, helping catch errors during development and improving code quality.


TypeScript Basics

TypeScript allows you to define types for:

  • Variables: const name: string = "John"
  • Function Parameters: function greet(name: string) {}
  • Return Values: function add(a: number, b: number): number {}

This enhances code quality and catches errors before runtime.


Project Structure

src/
├── main.tsx              # Application entry point
├── App.tsx               # Main app component
├── index.css             # Global styles
├── store/
│   └── Todos.tsx         # Context provider and types
└── components/
    ├── AddToDo.tsx       # Add todo form component
    └── Todos.tsx         # Todo list component

Type Definitions

Todo Type

export type Todo = {
    id: string;
    task: string;
    completed: boolean;
    createdAt: Date;
}

Explanation: Defines the structure of a single todo item with four properties.

TodosProviderProps Type

export type TodosProviderProps = {
    children: ReactNode;
}

Explanation: Defines props for the TodosProvider component. ReactNode is a generic type that covers JSX elements, strings, numbers, and other React components.

TodosContext Type

export type TodosContext = {
    todos: Todo[];
    handleAddToDo: (task: string) => void;
    toggleTodoAsCompleted: (id: string) => void;
}

Explanation: Defines the shape of the context value with:

  • todos: Array of Todo items
  • handleAddToDo: Function that accepts a string (task) and returns void (call signature)
  • toggleTodoAsCompleted: Function that accepts an id string and returns void

Context API Implementation

main.tsx - Application Entry Point

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { TodosProvider } from './store/Todos.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <TodosProvider>
      <App />
    </TodosProvider>
  </StrictMode>,
)

Explanation:

  • Wraps the entire app with TodosProvider to make the todo context available everywhere
  • StrictMode enables additional development checks
  • The ! after getElementById('root') is a TypeScript non-null assertion operator

store/Todos.tsx - Context Provider

Creating the Context

export const TodosContext = createContext<TodosContext | null>(null);

Explanation: Creates a context with type TodosContext | null. Initially set to null because there's no default value.

Provider Component

export const TodosProvider = ({children}: TodosProviderProps) => {
    const [todos, setTodos] = useState<Todo[]>([]);
    
    const handleAddToDo = (task: string) => {
        setTodos((prev) => {
            const newTodos: Todo[] = [
                {
                    id: Math.random().toString(),
                    task: task,
                    completed: false,
                    createdAt: new Date()
                },
                ...prev
            ]
            return newTodos;
        })
    }
    
    const toggleTodoAsCompleted = (id: string) => {
        setTodos((prev) => {
            const newTodos = prev.map((todo) => {
                if(todo.id === id) {
                    return {...todo, completed: !todo.completed}
                }
                return todo;
            })
            return newTodos;
        })
    }
    
    return (
        <TodosContext.Provider value={{todos, handleAddToDo, toggleTodoAsCompleted}}>
            {children}
        </TodosContext.Provider>
    )
}

Explanation:

  1. State Management: useState<Todo[]>([]) - Creates state with explicit type Todo[] (array of todos)

  2. handleAddToDo Function:

    • Accepts a task parameter of type string
    • Creates a new todo object with a random ID, the task, completed status false, and current date
    • Uses spread operator ...prev to prepend the new todo to existing todos
  3. toggleTodoAsCompleted Function:

    • Accepts an id parameter of type string
    • Maps through todos to find matching id
    • Toggles the completed property using spread operator and negation
    • Returns unchanged todos for non-matching ids

Custom Hook - useTodos

export const useTodos = () => {
    const todosConsumer = useContext(TodosContext);
    if(!todosConsumer) {
        throw new Error("useTodos used outside of Provider");
    }
    return todosConsumer;
}

Explanation:

  • Custom hook to consume the TodosContext
  • Performs null check to ensure it's used within the provider
  • Throws error if used outside provider (development safety)
  • Returns the context value with proper typing

Components Breakdown

AddToDo.tsx - Add Todo Form

import { useState, type FormEvent } from "react";
import { useTodos } from "../store/Todos";

const AddToDo = () => {
    const [todo, setTodo] = useState("");
    const {handleAddToDo} = useTodos();

    const handleFormSubmit = (e: FormEvent<HTMLElement>) => {
        e.preventDefault();
        handleAddToDo(todo);
        setTodo("");
    }
    
    return (
        <form onSubmit={handleFormSubmit}>
            <input 
                type="text" 
                value={todo} 
                onChange={(e) => setTodo(e.target.value)} 
            />
            <button type="submit">Add</button>
        </form>
    )
}

export default AddToDo;

Explanation:

  1. Local State: useState("") manages the input field value

  2. Context Consumer: useTodos() hook provides access to handleAddToDo function

  3. Form Submit Handler:

    • Type: FormEvent<HTMLElement> specifies the event type
    • e.preventDefault() prevents page reload
    • Calls handleAddToDo with current todo text
    • Clears input field by resetting state to empty string
  4. Controlled Input: Value and onChange create a controlled component


Todos.tsx - Todo List Component

import { useTodos, type Todo } from "../store/Todos";

const Todos = () => {
    const {todos, toggleTodoAsCompleted, handleDeleteTodo} = useTodos();

    const filterData = todos;
    
    return (
        <ul>
            {filterData.map((todo: Todo) => {
                return (
                    <li key={todo.id}>
                        <input 
                            type="checkbox"  
                            id={`todo-${todo.id}`}
                            checked={todo.completed}
                            onChange={() => toggleTodoAsCompleted(todo.id)}
                        />
                        <label htmlFor={`todo-${todo.id}`}>
                            {todo.task}
                        </label>

                        {todo.completed && (
                            <button 
                                type="button" 
                                onClick={() => handleDeleteTodo(todo.id)}
                            >
                                Delete
                            </button>
                        )}
                    </li>
                )
            })}
        </ul>
    )
}

export default Todos

Explanation:

  1. Context Consumer: Destructures todos and toggleTodoAsCompleted from context

  2. Mapping Todos:

    • map((todo: Todo) => ...) explicitly types each todo item
    • key={todo.id} provides unique key for React's reconciliation
  3. Checkbox Input:

    • id uses template literal for unique identifier
    • checked={todo.completed} binds to todo's completed state
    • onChange calls toggle function with todo's id
  4. Label:

    • htmlFor associates label with checkbox
    • Displays the todo task text
  5. Conditional Delete Button:

    • {todo.completed && (...)} only renders when todo is completed
    • Uses logical AND operator for conditional rendering
    • Calls handleDeleteTodo (note: this function needs to be added to context)

Key Concepts

1. Type Safety

TypeScript catches errors like:

// ❌ Error: Argument of type 'number' is not assignable to parameter of type 'string'
handleAddToDo(123);

// ✅ Correct
handleAddToDo("Buy groceries");

2. Type Inference

TypeScript can infer types automatically:

const [todo, setTodo] = useState("");
// TypeScript infers todo is type string

3. Call Signatures

handleAddToDo: (task: string) => void

Defines a function that:

  • Accepts one parameter task of type string
  • Returns void (nothing)

4. Spread Operator

{...todo, completed: !todo.completed}

Creates a new object with all properties of todo, but overrides completed

5. Non-null Assertion Operator (!)

document.getElementById('root')!

Tells TypeScript "I'm certain this won't be null"

6. Type Imports

import { type FormEvent } from "react";

The type keyword explicitly imports only the type (not runtime code)


Missing Implementation Note

The Todos.tsx component references handleDeleteTodo which isn't implemented in the provided code. To complete the application, add this to the context:

// In TodosContext type
export type TodosContext = {
    todos: Todo[];
    handleAddToDo: (task: string) => void;
    toggleTodoAsCompleted: (id: string) => void;
    handleDeleteTodo: (id: string) => void; // Add this
}

// In TodosProvider component
const handleDeleteTodo = (id: string) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
}

// In provider value
return (
    <TodosContext.Provider 
        value={{todos, handleAddToDo, toggleTodoAsCompleted, handleDeleteTodo}}
    >
        {children}
    </TodosContext.Provider>
)

Benefits of This Architecture

  1. Type Safety: Catches bugs during development
  2. Centralized State: All todo logic in one place
  3. Reusable Hook: useTodos() can be used in any component
  4. Scalable: Easy to add new features
  5. Maintainable: Clear separation of concerns

Summary

This todo application demonstrates:

  • TypeScript's type system for safer React development
  • Context API for global state management
  • Custom hooks for cleaner code
  • Controlled components for forms
  • Functional updates for state management
  • Conditional rendering patterns

The combination of React and TypeScript creates a robust, maintainable application with excellent developer experience and fewer runtime errors.

ReactJS Optimization

alt text

Re-Rendering

Code Splitting

Split bundle into different chunks. Bundler->webpack,browserify,rollup

Bundle-c1-homepage Bundle-c2-contact page Bundle-c3-about page Bundle-c4-features page Bundle-c5-info page Bundle-c6-login page

About

No description or website provided.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors