Mình có tìm hiểu thêm về tạo Dark Mode trong ReactJS kết hợp cùng Redux và TailWindCSS nên hôm nay cũng muốn được chia sẻ với mọi người một chút.
Let's go!
1. Triển khai ứng dụng và cài đặt một số thư viện hỗ trợ:
npx create-react-app dark-mode-app --template redux-typescript // Mình dùng yarn thay cho npm nhé.
npm install --global yarn yarn add -D @babel/plugin-proposal-private-property-in-object autoprefixer eslint eslint-config-prettier postcss styled-components tailwindcss
2. Khởi tạo cấu hình cho eslint, prettier:
// Khởi tạo file config eslintrc.js
yarn run eslint --init
Chọn "To check syntax and find problems" -> "JavaScript modules (import/export)" -> "React" -> "Yes" -> "Browser" -> "JSON" -> "npm". Tổ hợp các lựa chọn trên sẽ tự động cài đặt cho các bạn một số thư viện đi kèm của eslint:
eslint-plugin-react@latest, @typescript-eslint/eslint-plugin@latest, @typescript-eslint/parser@latest // Cài đặt thêm thư viện của prettier và eslint
yarn add -D prettier eslint-plugin-react-hooks // Tạo file .prettierrc.json ngang hàng với file package.json và thêm đoạn mã ví dụ dưới đây để tinh chỉnh format code theo ý muốn
{ "trailingComma": "es5", "tabWidth": 4, "semi": false, "singleQuote": true
} // Tạo file .prettierignore và thêm đoạn mã dưới đây để loại trừ một số thư mục cho quá trình artifacts (nếu có)
node_modules
# Ignore artifacts:
build
Coverage // Cài đặt thêm 2 thư việc sau để cấu hình cho eslint và prettier làm việc với nhau
yarn add -D eslint-config-prettier eslint-plugin-prettier // Quay lại file eslintrc.js, do mình làm đơn giản nên thêm 2 rules này vào eslintrc.js để ignore một số lỗi:
"react/react-in-jsx-scope": "off",
"react/jsx-uses-react": "off", // Đi đến file package.json, chúng ta thêm vào phần scripts các cấu hình lệnh để lint và format codes như sau:
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
"lint:fix": "eslint --fix \"src/**/*.{js,jsx,ts,tsx}\"",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,md}\" --config ./.prettierrc.json" // Cùng file package.json các bạn có thể thêm cấu hình sau để đặt phạm vi sử dụng phiên bản của node và npm, đây là cấu hình ví dụ, bạn có thể tìm hiểu thêm [tại đây](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) "engines": { "node": ">=0.10.3 <15" "npm": "~1.0.20" }
3. Khởi tạo cấu hình cho tailwindcss:
// Trước đó chúng ta đã cài đặt các thư viện cần thiết của tailwindcss, bây giờ chúng ta sẽ triển khai cấu hình theo lệnh sau:
npx tailwindcss init // Sau khi chạy lệnh trên, hệ thống sẽ tạo cho chúng ta một file cấu hình tailwind.config.js của tailwindcss, chúng ta mở file lên và cấu hình cơ bản như sau:
/** @type {import('tailwindcss').Config} */
module.exports = { content: ['./src/**/*.{js,jsx,ts,tsx}'], darkMode: 'class', theme: { extend: {}, }, variants: {}, plugins: [],
} // Bây giờ chúng ta mở file index.css và thay thế toàn bộ nội dung hiện tại bằng các layers và import mặc định của tailwindcss.
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities"; // Tiếp theo ta mở file App.tsx lên và import file index.css vào, như vậy chúng ta có thể sử dụng tailwindcss được rồi.
import "./index.css"
4. Triển khai cơ bản cấu trúc thư mục dự án như sau:
5. Triển khai chi tiết:
// Trước tiên chúng ta truy cập vào thư mục src/ tạo một thư mục utils/ và file theme.js có nội dung như bên dưới, nhìn code này chắc các bạn đã hiểu để làm gì nên mình sẽ không giải thích về nó:
export const lightTheme = { body: 'white',
}; export const darkTheme = { body: 'black',
}; // Trong thư mục src/ chúng ta tạo một thư mục styles/ và file global.ts để làm style chung có tính chuyển đổi khi chúng ta chuyển sang dark mode với nội dung sau:
import { createGlobalStyle } from 'styled-components'; export const GlobalStyleProvider = createGlobalStyle` *, *::after, *::before { box-sizing: border-box; } body { align-items: center; background: ${({ theme }) => theme.body}; color: ${({ theme }) => theme.text}; display: flex; flex-direction: column; justify-content: center; margin: 0; padding: 0; font-family: BlinkMacSystemFont, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; transition: all 0.25s linear; }
`; // Chúng ta vào thư mục redux/ hoặc store/ tạo một thư mục reducers và file themeSlice.ts để xử lý redux state logic khi thay đổi mode của theme với nội dung sau:
import { PayloadAction, createSlice } from '@reduxjs/toolkit'; export interface ThemeState { mode: boolean;
} const initialState: ThemeState = { mode: false,
}; export const themeSlice = createSlice({ name: 'theme', initialState: initialState, reducers: { toggleTheme: ( state, { payload: { mode } }: PayloadAction<ThemeState> ) => { switch (mode) { case false: state.mode = true; localStorage.setItem('theme', 'dark'); document.documentElement.classList.add('dark'); break; case true: state.mode = false; localStorage.setItem('theme', 'light'); document.documentElement.classList.remove('dark'); break; default: state.mode = false; localStorage.setItem('theme', 'light'); document.documentElement.classList.remove('dark'); break; } }, },
}); export const { toggleTheme } = themeSlice.actions;
export default themeSlice.reducer;
*Chú thích: Giá trị khởi tại ban đầu sẽ là Light Mode, khi chúng ta bật Dark Mode thì state sẽ được cập nhật lại giá trị cũng như ở phía localstatorage và đồng thời sẽ tạo một css custom class "dark" để chúng ta thay đổi giá trị style trong css global và các giá trị khác bên ngoài khi thay đổi mode. // Tiếp đến chúng tạo một file có tên reducer.ts, file này sẽ là file chính chứa tập hợp các reducers/slices con khác và gửi chúng đến store:
import { combineReducers } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import themeReducer from './reducers/themeSlice'; const rootReducer = combineReducers({ counter: counterReducer, theme: themeReducer,
}); export default rootReducer; // Sau khi đã có rootReducer vừa được xuất ra, chúng ta sẽ mở file store.ts và thêm rootReducer và như sau:
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import rootReducer from './reducer'; export const store = configureStore({ reducer: rootReducer,
}); export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction< ReturnType, RootState, unknown, Action<string>
>; // Như vậy về phần xử lý state login chúng ta tạm xong, tiếp theo cũng tại src/ chúng ta sẽ tạo thư mục components/theme và file themeToggle.component.tsx trong theme/ với nội dung như sau:
import { useDispatch, useSelector } from 'react-redux';
import { MdOutlineDarkMode } from "react-icons/md"; import { MdOutlineLightMode } from "react-icons/md"; import { toggleTheme } from '../../redux/reducers/themeSlice';
import { RootState } from '../../redux/store';
import { useThemeHook } from '../../hooks/theme/theme'; export default function ToggleTheme() { const theme: boolean = useSelector((state: RootState) => state.theme.mode); const dispatch = useDispatch(); return ( theme ? <MdOutlineLightMode className=" dark:cursor-pointer dark:text-sky-500 dark:text-4xl dark:bg-transparent dark:border dark:rounded-lg dark:border-gray-800 dark:hover:bg-stone-900 dark:hover:animate-bounce w-10 h-10 " onClick={() => dispatch(toggleTheme({mode: theme}))} /> : <MdOutlineDarkMode className=" cursor-pointer text-sky-500 text-4xl bg-transparent border rounded-lg border-gray hover:bg-gray-100 hover:animate-bounce w-10 h-10 " onClick={() => dispatch(toggleTheme({mode: theme}))}/> ); }
*Chú thích: 2 nút trên sẽ được thay đổi theo trạng thái của state khi chúng ta thay đổi. // Bây giờ chúng ta sẽ mở file App.tsx và triển khai component chúng ta vừa tạo cũng như các component nhỏ cơ bản khác với nội dung sau:
import "./index.css"
import { useSelector } from 'react-redux';
import { RootState } from './redux/store'; import ToggleTheme from './components/theme/themeToggle.component';
import { darkTheme, lightTheme } from "./utils/theme";
import { GlobalStyleProvider } from "./styles/global";
import { ThemeProvider } from "styled-components"; export default function App() { const theme: boolean | null | undefined = useSelector((state: RootState) => state.theme.mode) return ( <ThemeProvider theme={theme ? darkTheme : lightTheme}> <GlobalStyleProvider /> <h1 className="flex justify-center mt-80 mb-5 text-6xl text-orange-500 dark:text-green-500 dark:text-6x"> {theme ? "DARK" : "BRIGHT"} </h1> <span className="flex justify-center"> <ToggleTheme /> </span> </ThemeProvider> );
}
*Chú thích: Tại đây ThemeProvider sẽ tiếp nhận style của darkTheme hoặc lightTheme mà chúng ta đã tạo ban đầu theo trạng thái của state và truyền đến component con GlobalStyleProvider để nhận các giá trị style và cập nhật lại theme cho chúng ta.
6. Thêm Hook ghi nhận và bảo toàn trạng thái phía client khi đóng/mở lại trình duyệt:
// Bây giờ tại thư mục src/ chúng ta tạo 2 thư mục có tên hooks/theme và theme.js bên trong theme/ có nội dung như sau:
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { toggleTheme } from '../../redux/reducers/themeSlice'; export const useThemeHook = () => { const dispatch = useDispatch(); useEffect(() => { if (localStorage.getItem('theme') === 'dark') { localStorage.setItem('theme', 'dark'); document.documentElement.classList.add('dark'); dispatch(toggleTheme({ mode: true })); } else { localStorage.setItem('theme', 'light'); document.documentElement.classList.remove('dark'); dispatch(toggleTheme({ mode: false })); } }, []);
}; // Cuối cùng chúng ta vào lại file themeToggle.component.tsx trong thư mục components/theme và thêm hook về như sau:
export default function ToggleTheme() { const theme: boolean = useSelector((state: RootState) => state.theme.mode); const dispatch = useDispatch(); useThemeHook(); ...
Vậy là chúng ta đã hoàn thành chức năng Dark Mode nổi tiếng theo cách đơn sơ dễ hiểu rồi, mình không pro nên có chỗ nào thiếu sót mong các bạn bỏ qua và hướng dẫn mình thêm nhé, xin đa tạ các bạn rất nhiều vì đã theo dỗi bài viết của mình !!!