Project_ThemeApp.jsx
React.js/lessons/12_useContext/projects/Project_ThemeApp.jsx
/**
* Project: 多主題應用 — useContext 練習
*
* 學習重點:
* - createContext + Provider + useContext
* - 建立自訂 Hook 封裝 Context
* - 多個 Context 並存(ThemeContext + LanguageContext)
* - 跨元件共享狀態而無需 Props Drilling
*
* 使用方式:將此檔案內容複製到你的 App.jsx 中
*/
import { createContext, useContext, useState } from 'react';
// ============================================================
// Theme Context
// ============================================================
const ThemeContext = createContext();
const themes = {
light: { bg: '#ffffff', surface: '#f8fafc', text: '#0f172a', textSecondary: '#64748b', border: '#e2e8f0', primary: '#4f46e5', primaryBg: '#eef2ff' },
dark: { bg: '#0f172a', surface: '#1e293b', text: '#f1f5f9', textSecondary: '#94a3b8', border: '#334155', primary: '#818cf8', primaryBg: '#1e1b4b' },
ocean: { bg: '#042f2e', surface: '#0f3d3c', text: '#ccfbf1', textSecondary: '#5eead4', border: '#115e59', primary: '#2dd4bf', primaryBg: '#134e4a' },
sunset: { bg: '#1c1917', surface: '#292524', text: '#fef3c7', textSecondary: '#fbbf24', border: '#44403c', primary: '#f97316', primaryBg: '#431407' },
};
function ThemeProvider({ children }) {
const [themeName, setThemeName] = useState('light');
const colors = themes[themeName];
return (
<ThemeContext.Provider value={{ themeName, setThemeName, colors, availableThemes: Object.keys(themes) }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}
// ============================================================
// Language Context
// ============================================================
const LanguageContext = createContext();
const i18n = {
'zh-TW': { title: '主題切換應用', greeting: '歡迎', nav: { home: '首頁', about: '關於', settings: '設定' }, card: { title: '功能卡片', desc: '此應用展示了 useContext 的用法' }, theme: '主題', lang: '語言', dark: '深色', light: '淺色', ocean: '海洋', sunset: '夕陽' },
en: { title: 'Theme Switcher App', greeting: 'Welcome', nav: { home: 'Home', about: 'About', settings: 'Settings' }, card: { title: 'Feature Card', desc: 'This app demonstrates useContext usage' }, theme: 'Theme', lang: 'Language', dark: 'Dark', light: 'Light', ocean: 'Ocean', sunset: 'Sunset' },
ja: { title: 'テーマ切替アプリ', greeting: 'ようこそ', nav: { home: 'ホーム', about: '概要', settings: '設定' }, card: { title: '機能カード', desc: 'useContextの使い方を示すアプリ' }, theme: 'テーマ', lang: '言語', dark: 'ダーク', light: 'ライト', ocean: 'オーシャン', sunset: 'サンセット' },
};
function LanguageProvider({ children }) {
const [language, setLanguage] = useState('zh-TW');
const t = (path) => {
const keys = path.split('.');
let result = i18n[language];
for (const key of keys) {
result = result?.[key];
}
return result || path;
};
return (
<LanguageContext.Provider value={{ language, setLanguage, t, availableLanguages: Object.keys(i18n) }}>
{children}
</LanguageContext.Provider>
);
}
function useLanguage() {
const ctx = useContext(LanguageContext);
if (!ctx) throw new Error('useLanguage must be used within LanguageProvider');
return ctx;
}
// ============================================================
// UI Components
// ============================================================
function Navbar() {
const { colors, themeName, setThemeName, availableThemes } = useTheme();
const { language, setLanguage, t, availableLanguages } = useLanguage();
return (
<nav style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '16px 32px', backgroundColor: colors.surface,
borderBottom: `1px solid ${colors.border}`, transition: 'all 0.3s',
}}>
<h2 style={{ margin: 0, color: colors.primary, fontSize: '1.2rem' }}>{t('title')}</h2>
<div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
{Object.values(t('nav')).map((item, i) => (
<a key={i} href="#" style={{ color: colors.textSecondary, textDecoration: 'none', fontSize: '0.9rem' }}>{item}</a>
))}
<div style={{ width: '1px', height: '20px', backgroundColor: colors.border }} />
<select value={language} onChange={(e) => setLanguage(e.target.value)} style={{
padding: '4px 8px', borderRadius: '6px', border: `1px solid ${colors.border}`,
backgroundColor: colors.surface, color: colors.text, cursor: 'pointer', fontSize: '0.85rem',
}}>
{availableLanguages.map((lang) => (
<option key={lang} value={lang}>{lang === 'zh-TW' ? '中文' : lang === 'en' ? 'English' : '日本語'}</option>
))}
</select>
</div>
</nav>
);
}
function ThemePicker() {
const { colors, themeName, setThemeName, availableThemes } = useTheme();
const { t } = useLanguage();
const themeIcons = { light: '☀️', dark: '🌙', ocean: '🌊', sunset: '🌅' };
return (
<div style={{
padding: '24px', backgroundColor: colors.surface,
borderRadius: '16px', border: `1px solid ${colors.border}`, transition: 'all 0.3s',
}}>
<h3 style={{ margin: '0 0 16px', color: colors.text }}>{t('theme')}</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px' }}>
{availableThemes.map((name) => (
<button
key={name}
onClick={() => setThemeName(name)}
style={{
padding: '12px 8px', borderRadius: '10px', cursor: 'pointer',
border: `2px solid ${themeName === name ? colors.primary : colors.border}`,
backgroundColor: themeName === name ? colors.primaryBg : 'transparent',
color: colors.text, fontSize: '0.85rem', transition: 'all 0.2s',
}}
>
<div style={{ fontSize: '1.5rem', marginBottom: '4px' }}>{themeIcons[name]}</div>
{t(name)}
</button>
))}
</div>
</div>
);
}
function FeatureCards() {
const { colors } = useTheme();
const { t } = useLanguage();
const features = [
{ icon: '🎨', title: 'Context API', desc: 'createContext + Provider' },
{ icon: '🪝', title: 'Custom Hook', desc: 'useTheme() / useLanguage()' },
{ icon: '🌍', title: 'i18n', desc: t('card.desc') },
];
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
{features.map((f, i) => (
<div key={i} style={{
padding: '24px', borderRadius: '16px', backgroundColor: colors.surface,
border: `1px solid ${colors.border}`, textAlign: 'center', transition: 'all 0.3s',
}}>
<div style={{ fontSize: '2.5rem', marginBottom: '12px' }}>{f.icon}</div>
<h4 style={{ margin: '0 0 8px', color: colors.text }}>{f.title}</h4>
<p style={{ margin: 0, color: colors.textSecondary, fontSize: '0.85rem', lineHeight: 1.5 }}>{f.desc}</p>
</div>
))}
</div>
);
}
function ContextInfo() {
const { themeName, colors } = useTheme();
const { language } = useLanguage();
return (
<div style={{
padding: '20px', backgroundColor: colors.primaryBg,
borderRadius: '12px', border: `1px solid ${colors.border}`,
fontFamily: "'SF Mono', monospace", fontSize: '0.8rem',
color: colors.primary, transition: 'all 0.3s',
}}>
<p style={{ margin: '0 0 4px' }}>{'// 目前 Context 值'}</p>
<p style={{ margin: '0 0 2px' }}>{'{'}</p>
<p style={{ margin: '0 0 2px', paddingLeft: '16px' }}>theme: "{themeName}",</p>
<p style={{ margin: '0 0 2px', paddingLeft: '16px' }}>language: "{language}",</p>
<p style={{ margin: 0 }}>{'}'}</p>
</div>
);
}
// ============================================================
// Main App
// ============================================================
function AppContent() {
const { colors } = useTheme();
const { t } = useLanguage();
return (
<div style={{
minHeight: '100vh', backgroundColor: colors.bg,
transition: 'background-color 0.3s', fontFamily: "'Segoe UI', sans-serif",
}}>
<Navbar />
<main style={{ maxWidth: '800px', margin: '0 auto', padding: '32px 16px', display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ textAlign: 'center' }}>
<h1 style={{ color: colors.text, marginBottom: '4px', transition: 'color 0.3s' }}>
{t('greeting')}! 👋
</h1>
<p style={{ color: colors.textSecondary, transition: 'color 0.3s' }}>{t('card.desc')}</p>
</div>
<ThemePicker />
<FeatureCards />
<ContextInfo />
</main>
</div>
);
}
function App() {
return (
<ThemeProvider>
<LanguageProvider>
<AppContent />
</LanguageProvider>
</ThemeProvider>
);
}
export default App;
関連記事
App.js
App.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.js).
記事を読む →App.test.js
App.test.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.test.js).
記事を読む →index.js
index.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/index.js).
記事を読む →reportWebVitals.js
reportWebVitals.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/reportWebVitals.js).
記事を読む →setupTests.js
setupTests.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/setupTests.js).
記事を読む →Project_HelloReact.jsx
Project_HelloReact.jsx — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/projects/Project_HelloReact.jsx).
記事を読む →