Loading content…
Loading content…
Learn to test React components with Jest and RTL, mock custom hooks, and secure your applications against XSS and injection attacks
// ThemeToggle.tsx
import { useState } from "react";
export const ThemeToggle = () => {
const [isDark, setIsDark] = useState(false);
return (
<button onClick={() => setIsDark(!isDark)}>
Current mode: {isDark ? "Dark" : "Light"}
</button>
);
};
// ThemeToggle.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ThemeToggle } from "./ThemeToggle";
describe("ThemeToggle Component", () => {
it("renders with initial Light mode", () => {
render(<ThemeToggle />);
const button = screen.getByRole("button");
expect(button).toHaveTextContent("Current mode: Light");
});
it("changes text to Dark mode on click", async () => {
render(<ThemeToggle />);
const button = screen.getByRole("button");
// Simulate user click using userEvent (preferred over fireEvent)
await userEvent.click(button);
expect(button).toHaveTextContent("Current mode: Dark");
});
});
renderHook.// useCounter.ts
import { useState } from "react";
export const useCounter = (initial = 0) => {
const [count, setCount] = useState(initial);
const increment = () => setCount((c) => c + 1);
return { count, increment };
};
// useCounter.test.ts
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";
test("should increment counter state", () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
// State changes must be wrapped in act()
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(11);
});
fetch object (which is brittle and hard to maintain), the industry standard is to use Mock Service Worker (MSW). MSW intercepts network requests at the browser link level and returns mock responses, keeping your test code realistic.const name = "<script>alert('hack')</script>";
return <div>{name}</div>; // Safely rendered as plain text strings, script won't run.
dangerouslySetInnerHTML to warn you of potential vulnerabilities.// ❌ Dangerous: Vulnerable to XSS if bio contains malicious tags
const UserBio = ({ bio }: { bio: string }) => {
return <div dangerouslySetInnerHTML={{ __html: bio }} />;
};
import DOMPurify from "isomorphic-dompurify"; // Server and Client safe DOMPurify
const UserBioSafe = ({ bio }: { bio: string }) => {
// Purge any <script> tags or onerror attributes
const cleanHtml = DOMPurify.sanitize(bio);
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
};
| Storage Location | Vulnerable to XSS? | Vulnerable to CSRF? | Recommendation |
|---|---|---|---|
| LocalStorage / SessionStorage | ⚠️ Yes (accessible via any running script) | 🚫 No | Avoid for sensitive financial or personal user accounts |
| HttpOnly, Secure Cookies | 🚫 No (inaccessible to JavaScript) | ⚠️ Yes | Recommended (Must use SameSite and CSRF tokens) |
/refresh) to fetch a new access token into memory.Common Pitfall
NEXT_PUBLIC_API_KEY) exposes them to the public bundle. Anyone can read them in the source code files. Keep sensitive keys in private server variables.Pro Tip
screen.getByRole. They enforce web accessibility standards (ARIA). If your test fails to find an element by its role, it's a sign that disabled users using screen readers won't be able to find it either.Testing & Security Checklist
DOMPurify.Marking it complete updates your roadmap progress percentage.