Hook: useTheme.js
Relevant source files
The following files were used as context for generating this wiki page:The useTheme hook is a custom React hook responsible for managing the application's visual theme (light/dark). It handles theme state, persists the user's preference to localStorage, and applies the corresponding CSS class to the DOM to enable dynamic styling via Tailwind CSS.
A key architectural feature is the prevention of a "Flash of Unstyled Content" (FOUC) by using an inline script in index.html to set the theme before the React application mounts. This ensures a seamless user experience when loading the page. The hook is self-contained and does not depend on the application's "private mode" for its persistence logic.
Sources: src/hooks/useTheme.js, index.html:8-15, README.md:27
Core Functionality & API
The useTheme hook provides the necessary state and functions to read and update the application's theme.
Sources: src/hooks/useTheme.js:21-35
Return Value
When called, the hook returns an array containing the current theme, a function to update it, and a boolean indicating if the dark theme is active.
const [theme, setTheme, isDark] = useTheme();
Sources: src/App.jsx:266, src/hooks/useTheme.js:35
| Index | Name | Type | Description |
|---|---|---|---|
| 0 | theme | string | The current theme state, either 'light' or 'dark'. |
| 1 | setTheme | Function | A state setter function to update the theme. Accepts a new theme string. |
| 2 | isDark | boolean | A derived boolean value, true if theme is 'dark', otherwise false. |
Sources: src/hooks/useTheme.js:22, 33, 35
Architecture and Data Flow
The theme system is designed to be robust, providing an immediate visual state on page load and persisting user choice across sessions.
Theme Initialization
The initial theme is determined by the getInitialTheme function, which follows a specific order of precedence. This logic is executed both in the pre-render inline script and within the React hook itself.
The following diagram illustrates the theme initialization logic.
This flowchart shows the decision process for selecting the initial theme.
localStorage: It first attempts to read thethemekey fromlocalStorage. If a valid value ('light'or'dark') is found, it is used.- System Preference: If no valid theme is in
localStorage, it checks the user's operating system preference viawindow.matchMedia('(prefers-color-scheme: dark)'). - Default: If neither of the above provides a theme, it defaults to
'light'.
This ensures that a user's explicit choice is always respected, falling back to their system settings for a better initial experience.
Sources: src/hooks/useTheme.js:5-12, index.html:9-11
DOM Integration and FOUC Prevention
To prevent a "flash" of the default light theme before React loads, an inline script is placed in the <head> of index.html. This script executes the core initialization logic and applies the dark class to the <html> element immediately.
<!-- index.html -->
<script>
// Khởi tạo theme TRƯỚC KHI React load
const saved = localStorage.getItem('theme');
const initial = saved || 'light';
const isDark = initial === 'dark';
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.dataset.theme = initial;
if (!saved) localStorage.setItem('theme', initial);
</script>
Sources: index.html:8-15
The following sequence diagram shows the entire page load and theme application process.
This diagram shows how the inline script provides the initial theme, and the React hook synchronizes it thereafter.
Tailwind CSS Configuration
The theme switching is powered by Tailwind CSS's class strategy for dark mode. The tailwind.config.js file enables this, allowing dark: utility classes to be applied whenever the dark class is present on a parent element (in this case, <html>).
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class', // Bật dark mode với class strategy
content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'],
theme: { extend: {} },
plugins: [],
};
Sources: tailwind.config.js:1-7
Persistence and State Management
The useEffect hook within useTheme is responsible for two side effects whenever the theme state changes:
- DOM Manipulation: It adds or removes the
darkclass fromdocument.documentElement(<html>tag). - Persistence: It saves the new theme value to
localStorageunder the keytheme. This action is independent of any privacy settings and is considered safe as it only stores a UI preference.
// src/hooks/useTheme.js
useEffect(() => {
// Apply theme to document
document.documentElement.classList.toggle('dark', theme === 'dark');
// Always save theme (privacy-independent)
try {
localStorage.setItem('theme', theme);
} catch (error) {
console.warn('Failed to save theme:', error);
}
}, [theme]);
Sources: src/hooks/useTheme.js:24-32, SECURITY_AUDIT_REPORT.md:100-105
Usage Example
The App component is the primary consumer of the useTheme hook. It uses the returned values to create a theme toggle button.
The following diagram shows the relationship between the files involved in the theme system.
This diagram illustrates how different files collaborate to implement the theme feature.
The setTheme function is called in the onClick handler of the button, and the isDark boolean is used to determine the button's state and ARIA attributes.
// src/App.jsx
export default function App() {
// ...
const [, setTheme, isDark] = useTheme();
// ...
return (
// ...
<button
onClick={() => setTheme((t) => (t === 'dark' ? 'light' : 'dark'))}
aria-pressed={isDark}
aria-label={isDark ? 'Chuyển sang chế độ sáng' : 'Chuyển sang chế độ tối'}
// ...
>
<span
className={`... ${
isDark ? 'translate-x-8 sm:translate-x-9' : 'translate-x-1'
}`}
>
{isDark ? '🌙' : '☀️'}
</span>
</button>
// ...
);
}
Sources: src/App.jsx:266, 731-747
Conclusion
The useTheme.js hook provides a complete and efficient solution for theme management in the QR-Tool application. By combining an initial inline script with a React hook, it delivers a seamless user experience without visual flashes, while correctly persisting user preferences. Its integration with Tailwind CSS's class-based dark mode makes it a clean and maintainable implementation.