React+Scss+Redux-tookit 实现优质的全局深色模式功能

165 阅读4分钟

React+Scss+Redux-tookit 实现优质的全局深色模式功能

一:下载相关依赖

下载RTK相关依赖

npm i react-redux @reduxjs/toolkit 

下载SCSS相关依赖

npm i sass -D

二:创建一个 themeSlice 来管理主题

src/store/themeSlice.ts 文件中创建一个 slice,管理深色模式的状态。

// src/store/themeSlice.ts

// src/features/theme/themeSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

// 定义初始状态
interface ThemeState {
  isDarkMode: boolean;
}

const initialState: ThemeState = {
  isDarkMode: false,
};

// 创建切片
const themeSlice = createSlice({
  name: 'theme',
  initialState,
  reducers: {
    toggleTheme(state) {
      state.isDarkMode = !state.isDarkMode;
    },
  }
});

// 导出 reducer 和 actions
export const { toggleTheme } = themeSlice.actions;
export default themeSlice.reducer;


三:配置 Redux Store

然后,我们将 themeSlice 加入到 Redux store 中。在 src/store/index.ts 文件中配置 store。

// src/store/index.ts

import { configureStore } from '@reduxjs/toolkit';
import themeReducer from './themeSlice';

const store = configureStore({
  reducer: {
    theme: themeReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

四:更新 index.tsx 文件以提供 Redux Store

确保在 src/index.tsx 中提供了 Redux store:

typescript
复制代码
// src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from './store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

五:在应用中使用 Redux 管理深色模式

在主应用文件(例如 src/App.tsx)中,使用 useSelectoruseDispatch 来访问和更新主题状态,

// src/App.tsx

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from './store';
import { toggleTheme } from './store/themeSlice';
import './App.scss';

const App: React.FC = () => {
  const dispatch: AppDispatch = useDispatch();
  const isDarkMode = useSelector((state: RootState) => state.theme.isDarkMode);

  // 切换模式的处理函数
  const handleToggleTheme = () => {
    dispatch(toggleTheme());
  };

  return (
    <div className={isDarkMode ? 'dark-mode' : 'light-mode'}>
      <h1>{isDarkMode ? 'Dark Mode' : 'Light Mode'}</h1>
      <button onClick={handleToggleTheme}>
        {isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
      </button>
    </div>
  );
};

export default App;

六:应用深色模式样式

App.scss 文件中,使用 dark-modelight-mode 类来切换样式。

// App.scss
@import './setting/variables.scss';

// 默认样式(浅色模式)
body {
  background-color: $global-background-color;
  color: $global-font-color;
}


// 深色模式样式
.dark-mode {
  background-color: $background-color-dark;
  color: $text-color-dark;
}

// 给button也设置深色模式,如有其他需求还可以自己去弄更多样式
.dark-mode button {
  background-color: $primary-dark-dark;
  color: $white-dark;
}

// 浅色模式样式
.light-mode {
  background-color: $global-background-color;
  color: $global-font-color;
}

.light-mode button {
  background-color: $primary-color;
  color: $white;
}

这时,已经实现基本的深色模式切换了!

七:深色模式持久化和跟随系统功能

如果你希望应用在页面刷新后保留深色模式的设置,可以使用 localStoragesessionStorage 来持久化状态。在 themeSlice.ts 中添加持久化功能:

  • 系统初始设置: 在应用首次加载时监听读取 window.matchMedia 的值,设置初始主题,跟随系统切换。

  • 手动切换: 只有用户手动点击切换按钮时才将状态存入 localStorage,并覆盖系统默认的设置。且跟随系统切换功能不生效

// src/store/themeSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

// 读取localStorage中的主题设置
const savedTheme = localStorage.getItem('isDarkMode');

interface ThemeState {
  isDarkMode: boolean;
  isSystemPreference: boolean;
}

const initialState: ThemeState = {
  isDarkMode:savedTheme === 'true',
  isSystemPreference: !savedTheme, // 没有用户设置时,跟随系统
};

const themeSlice = createSlice({
  name: 'theme',
  initialState,
  reducers: {
    toggleTheme(state) {
      state.isDarkMode = !state.isDarkMode;
      state.isSystemPreference = false;
      localStorage.setItem('isDarkMode', state.isDarkMode.toString());
    },
    setBySystemTheme(state,actions:PayloadAction<boolean>) {
      if (state.isSystemPreference) {
        const systemPrefersDark = actions.payload;
        state.isDarkMode = systemPrefersDark;
      }
    }
  }
});

export const { toggleTheme, setBySystemTheme } = themeSlice.actions;
export default themeSlice.reducer;
// src/App.ts
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from './store';
import { setBySystemTheme, toggleTheme } from './store/themeSlice';
import './App.scss';
import ThemeDiv from './themeComponent/themeDiv';
const App: React.FC = () => {
  const dispatch: AppDispatch = useDispatch();
  const isDarkMode = useSelector((state: RootState) => state.theme.isDarkMode);

  // 监听系统的颜色方案,设置初始模式
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const handleSystemThemeChange = () => {
      dispatch(setBySystemTheme(mediaQuery.matches));
    };

    // 初始检查和监听系统主题变化
    dispatch(setBySystemTheme(mediaQuery.matches));
    mediaQuery.addEventListener('change', handleSystemThemeChange);

    return () => {
      mediaQuery.removeEventListener('change', handleSystemThemeChange);
    };
  }, [dispatch]);


  useEffect(() => {
    if (isDarkMode) {
        document.body.classList.add('dark-mode');
        document.body.classList.remove('light-mode');
      } else {
        document.body.classList.add('light-mode');
        document.body.classList.remove('dark-mode');
      }
  }, [isDarkMode]);
  // 切换模式的处理函数
  const handleToggleTheme = () => {
    dispatch(toggleTheme());
  };

  return (
    <ThemeDiv>
      <h1>{isDarkMode ? 'Dark Mode' : 'Light Mode'}</h1>
      <button onClick={handleToggleTheme}>
        {isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
      </button>
    </ThemeDiv>
  );
};

export default App;

八:添加过渡效果

如果你希望增加一个优雅的过渡效果,那只需要在App.scss中 .dark-mode .light-mode 绑定 transition

.dark-mode , .light-mode{
  transition: background-color 0.3s ease, color 0.3s ease;
}

九:封装成 themeDiv 组件,再次降低代码耦合

1.在src目录下创建themeComponent文件夹,用于放置会被深色模式影响的组件 我们这里示范一个themeView组件,创建themeView.tsx文件,原理就是从redux中取到是否为深色模式,然后children获取父组件传来的ReactDom

// src/themeComponent/themeView.tsx
import React, { CSSProperties, ReactNode } from 'react';
import { useSelector } from 'react-redux';
import { RootState } from '../store';

interface ThemeViewProps {
  children: ReactNode;
  style?:CSSProperties
}

const ThemeView: React.FC<ThemeViewProps> = ({ style,children }) => {
  const isDarkMode = useSelector((state: RootState) => state.theme.isDarkMode);

  return (
    <div className={isDarkMode ? 'dark-mode' : 'light-mode'} style={style}>
      {children}
    </div>
  );
};

export default ThemeView;

十:封装成为hook,降低App.tsx维护成本

// src/hooks/useTheme.ts
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from '../store';
import { setBySystemTheme, toggleTheme } from '../store/themeSlice';

const useTheme = () => {
  const dispatch: AppDispatch = useDispatch();
  const isDarkMode = useSelector((state: RootState) => state.theme.isDarkMode);

  // 监听系统的颜色方案,设置初始模式
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const handleSystemThemeChange = () => {
      dispatch(setBySystemTheme(mediaQuery.matches));
    };
    // 初始检查和监听系统主题变化
    dispatch(setBySystemTheme(mediaQuery.matches));
    mediaQuery.addEventListener('change', handleSystemThemeChange);

    return () => {
      mediaQuery.removeEventListener('change', handleSystemThemeChange);
    };
  }, [dispatch]);

  // 设置页面背景色和字体颜色
  useEffect(() => {
   if (isDarkMode) {
        document.body.classList.add('dark-mode');
        document.body.classList.remove('light-mode');
      } else {
        document.body.classList.add('light-mode');
        document.body.classList.remove('dark-mode');
      }
  }, [isDarkMode]);

  // 切换模式的处理函数
  const handleToggleTheme = () => {
    dispatch(toggleTheme());
  };

  return { isDarkMode, handleToggleTheme };
};

export default useTheme;

// src/App.tsx
import React from 'react';
import ThemeDiv from './themeComponent/themeDiv';
import useTheme from './hook/useTheme';
import './App.scss';

const App: React.FC = () => {
  const { isDarkMode, handleToggleTheme } = useTheme(); // 使用自定义 hook
  return (
    <ThemeDiv>
      <h1>{isDarkMode ? 'Dark Mode' : 'Light Mode'}</h1>
      <button onClick={handleToggleTheme}>
        {isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
      </button>
    </ThemeDiv>
  );
};

export default App;

总结

React 应用结合了 RTK 来管理深色模式的切换。通过 useDispatchuseSelector,你可以在应用中动态地切换并应用深色模式。在variables.scss定义全局变量,在 App.scss 中通过 dark-modelight-mode 类来切换对应的样式。同时,使用 localStorage 保持主题状态跨页面刷新,以及通过监听 window.matchMedia('(prefers-color-scheme: dark)');实现跟随系统设置深色主题,最后封装成为hook降低App.tsx的复杂度