A comprehensive guide covering React fundamentals, hooks, routing, and advanced data fetching with TanStack Query
- React Fundamentals
- React Hooks Deep Dive
- Form Handling & Storage
- API Integration
- React Router
- Context API
- Performance Optimization
- TanStack Query (React Query)
- Vitest Testing Guide for React
- React Redux Documentation
- Redux Toolkit Complete Guide
- Firebase with React - Complete Guide
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.
| Type | Description |
|---|---|
| Real DOM | Directly updates HTML, re-renders entire page |
| Virtual DOM | Updates only changed parts for faster performance |
npm create vite
npm install
npm run dev// App.jsx
const App = () => {
const name = "Rayied";
return <h1>Hello {name}!</h1>;
};
export default App;- Must return a single parent element (use fragments
<>...</>) - Use
classNameinstead ofclass - Close all tags (
<img />,<input />) - JavaScript expressions go inside
{}
// 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.
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
};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!)
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
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>
);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>
);
};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.
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+ |
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 valuesuseCallback→ Caches function references
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>
);
};| 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();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);
}
};npm install axiosimport 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 |
npm install react-router-dom@6import { 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>
);
};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 | Use Case |
|---|---|
| BrowserRouter | Standard web apps with server support |
| HashRouter | Static hosting without server fallback |
| MemoryRouter | Testing or non-DOM environments |
// 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>
);
};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>
);
};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>
);
};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
npm install @tanstack/react-query
npm install @tanstack/react-query-devtoolsimport { 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>
);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>
);
};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,
});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>;
};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>
);
};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>
);
};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>
);
};| 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 |
| 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 |
| 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 |
- ✅ Use functional components with hooks
- ✅ Keep components small and focused
- ✅ Use meaningful variable names
- ✅ Always handle loading and error states
- ✅ Memoize expensive operations
- ✅ Use proper key props in lists
- ✅ Clean up side effects in useEffect
- ✅ Avoid inline function definitions in JSX
- ✅ Use unique, descriptive queryKeys
- ✅ Configure staleTime and gcTime appropriately
- ✅ Implement error boundaries
- ✅ Use placeholderData for smooth pagination
- ✅ Invalidate queries after mutations
- ✅ Keep queryFn functions pure
- ✅ Enable React Query DevTools in development
- ✅ Handle loading states with Suspense boundaries
npm run buildThis creates an optimized production build in the dist folder. Deploy this folder to your hosting service.
© 2025 — Complete React + TanStack Query Guide
Install the required dependencies:
npm install --save-dev vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdomCreate 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"
}npm testComponent: 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();
});
});getByText: Throws an error if nothing is found. Use when you're certain the element exists.queryByText: Returnsnullif nothing is found (no error). Use when testing that an element doesn't exist or when unsure if it exists.
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");
});
});- Use
userEventfor simulating user interactions (more realistic thanfireEvent) - Always
awaituser events - Use
data-testidattributes for elements that are hard to query by role or text
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();
});
});
});vi.fn(): Creates a mock functionmockResolvedValueOnce(): Mocks a successful Promise resolutionbeforeEach: Runs before each test (setup)afterEach: Runs after each test (cleanup)waitFor: Waits for async operations to completevi.resetAllMocks(): Cleans up all mocks
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);
});
});renderHook: Renders a hook in isolationact: Wraps state updates to ensure they're processedresult.current: Access the hook's return value
- Test user behavior, not implementation details: Focus on what users see and do
- Use semantic queries: Prefer
getByRole,getByLabelTextovergetByTestId - Keep tests isolated: Each test should be independent
- Clean up after tests: Use
afterEachto reset mocks and state - Test loading states: Verify both loading and success states
- Use meaningful test descriptions: Describe what the test does clearly
- Await async operations: Always use
awaitwithuserEventandwaitFor
| 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 |
- Use
/iflag 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
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
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.
- 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.
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.
This is an Object which tells Redux what to do (like adding a task).
{
type: "counter/add",
payload: {
incrementBy: 10
}
}payload: extra information
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;
}
};- 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.
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.
- Reducers must always return a new state
- They should never modify the old state directly
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.
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
...stateto copy the old state.
/* 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;
}
};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).
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() 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() 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
npm i reduxNote: createStore is now deprecated
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());Import in main.jsx:
import "./store.jsx";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 }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 };
}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;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.
npm install react-reduxUse 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);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.
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"));Use the useDispatch hook to dispatch actions from a React component.
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 };
};Link: Redux DevTools Chrome Extension
npm i @redux-devtools/extensionstore.jsx:
import { composeWithDevTools } from "@redux-devtools/extension";
export const store = createStore(taskReducer, composeWithDevTools());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).
npm i redux-thunkimport { 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);
}
};
};- React-Redux
- 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.
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.
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.
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.
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.
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).
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.
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.
// 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;
}
}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;npm i @reduxjs/toolkit// Step-2: create Redux store using the reducer
export const store = createStore(
taskReducer,
composeWithDevTools(applyMiddleware(thunk))
);import { configureStore } from "@reduxjs/toolkit";
// Step-2: Create store with configureStore
export const store = configureStore({
reducer: {
taskReducer,
},
});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.
- 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
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;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")));npm install react-reduxUse 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>
);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 actions in React using useDispatch. Use the useDispatch hook to dispatch actions from a React component.
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;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
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!
A comprehensive guide to integrating Firebase services (Authentication, Realtime Database, and Firestore) with React applications.
- Initial Setup
- Firebase Configuration
- Authentication
- Realtime Database
- Cloud Firestore
- Context API Pattern
- Security Best Practices
- Visit firebase.google.com
- Navigate to Console
- Click "Create Project"
- Follow the setup wizard
- Register your web app by clicking the Web icon (
</>)
npm install firebaseCreate 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-urlLocation: 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);- Go to Authentication tab
- Click Get Started
- Enable Email/Password and Google sign-in methods
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;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;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);
});
};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;- Navigate to Realtime Database in Firebase Console
- Click Create Database
- Choose Start in test mode for development
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
});
};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));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>;
};- Go to Build → Firestore Database
- Click Create Database
- Choose Start in test mode for development
- Collections contain Documents
- Documents contain Fields (key-value pairs)
- Documents can have Sub-collections
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()
}
);
};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!");
}
};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());
});
};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");
};- Centralized Firebase logic
- Avoid prop drilling
- Easy access to Firebase methods across components
- Better code organization
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>
);
};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>
);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;- ✅ Never commit Firebase config with real API keys to public repositories
- ✅ Use
.envfiles and add them to.gitignore - ✅ Use environment variables for all sensitive data
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}- Firebase requires passwords to be at least 6 characters long
- Consider implementing additional client-side validation
| 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 |
- Chat applications
- Real-time collaboration
- Live location tracking
- Simple data structures
- Used by Gaming Companies
- E-commerce platforms
- Social media apps
- Complex data relationships
- Apps requiring advanced queries
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.
- Introduction
- TypeScript Basics
- Project Structure
- Type Definitions
- Context API Implementation
- Components Breakdown
- Key Concepts
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 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.
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
export type Todo = {
id: string;
task: string;
completed: boolean;
createdAt: Date;
}Explanation: Defines the structure of a single todo item with four properties.
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.
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 itemshandleAddToDo: Function that accepts a string (task) and returns void (call signature)toggleTodoAsCompleted: Function that accepts an id string and returns void
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
TodosProviderto make the todo context available everywhere StrictModeenables additional development checks- The
!aftergetElementById('root')is a TypeScript non-null assertion operator
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.
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:
-
State Management:
useState<Todo[]>([])- Creates state with explicit typeTodo[](array of todos) -
handleAddToDo Function:
- Accepts a
taskparameter of typestring - Creates a new todo object with a random ID, the task, completed status false, and current date
- Uses spread operator
...prevto prepend the new todo to existing todos
- Accepts a
-
toggleTodoAsCompleted Function:
- Accepts an
idparameter of typestring - Maps through todos to find matching id
- Toggles the
completedproperty using spread operator and negation - Returns unchanged todos for non-matching ids
- Accepts an
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
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:
-
Local State:
useState("")manages the input field value -
Context Consumer:
useTodos()hook provides access tohandleAddToDofunction -
Form Submit Handler:
- Type:
FormEvent<HTMLElement>specifies the event type e.preventDefault()prevents page reload- Calls
handleAddToDowith current todo text - Clears input field by resetting state to empty string
- Type:
-
Controlled Input: Value and onChange create a controlled 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 TodosExplanation:
-
Context Consumer: Destructures
todosandtoggleTodoAsCompletedfrom context -
Mapping Todos:
map((todo: Todo) => ...)explicitly types each todo itemkey={todo.id}provides unique key for React's reconciliation
-
Checkbox Input:
iduses template literal for unique identifierchecked={todo.completed}binds to todo's completed stateonChangecalls toggle function with todo's id
-
Label:
htmlForassociates label with checkbox- Displays the todo task text
-
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)
TypeScript catches errors like:
// ❌ Error: Argument of type 'number' is not assignable to parameter of type 'string'
handleAddToDo(123);
// ✅ Correct
handleAddToDo("Buy groceries");TypeScript can infer types automatically:
const [todo, setTodo] = useState("");
// TypeScript infers todo is type stringhandleAddToDo: (task: string) => voidDefines a function that:
- Accepts one parameter
taskof typestring - Returns
void(nothing)
{...todo, completed: !todo.completed}Creates a new object with all properties of todo, but overrides completed
document.getElementById('root')!Tells TypeScript "I'm certain this won't be null"
import { type FormEvent } from "react";The type keyword explicitly imports only the type (not runtime code)
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>
)- Type Safety: Catches bugs during development
- Centralized State: All todo logic in one place
- Reusable Hook:
useTodos()can be used in any component - Scalable: Easy to add new features
- Maintainable: Clear separation of concerns
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.
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
