Эта статья является продолжением статьи Тёмная тема в React с использованием css переменных в scss. Если в прошлый раз мы добавляли тёмную тему через родной реактовский контекст, то сейчас мы попробуем сделать всё то же самое, но с помощью Redux
, точнее redux-toolkit
Roadmap
Мы проделаем почти те же шаги, что и в прошлый раз:
Run-up
Создадимcreate-react-app
проект и немного поправим структуруRedux
Добавим компонент переключателя темы сredux-состоянием
CSS Variables
Объявим переменные для каждой темы, которые будут влиять на стили компонентовBonus
Добавим роутинг
1. Подготовка
С помощью
create-react-app
создаём проект и сразу добавляемsass
иclassnames
для удобства работы со стилями
> npx create-react-app with-redux-theme --template redux
> cd with-redux-theme
> npm i sass classnames -S
Поскольку все дальнейшие действия мы будем производить, находясь в папке
/src
, то для удобства перейдем в неё
> cd src
Удалим ненужные файлы
# находимся внутри папки /src
> rm -rf app features App.css App.js App.test.js index.css logo.svg
4. Создадим удобную структуру приложения
# находимся внутри папки /src
> mkdir -p components/Theme
> touch index.scss root.js store.js
> touch components/Theme/{index.js,index.module.scss,slice.js}
Поддерево проекта внутри папки /src
должно получиться таким
# перейдем в корень и проверим структуру
> tree src
src
├── components
│ └── Theme
│ ├── index.js
│ ├── index.module.scss
│ └── slice.js
├── index.js
├── index.scss
├── root.js
├── store.js
└── ...
Теперь будем писать код.
Поскольку мы внесли изменения в структуру, то перепишем наш src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import Root from './root'
import store from './store'
import './index.scss'
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')
const root = ReactDOM.createRoot(rootElement)
root.render(
<React.StrictMode>
<Provider store={store}>
<Root />
</Provider>
</React.StrictMode>
)
Вместо App.js
я использую файл root.js
с компонентом Root
, который в конце концов у нас будет хранить роуты на страницы, но а пока...
// src/root.js
const Root = () => (
<div>There are will be routes</div>
)
export default Root
Теперь можно приступить ко второй части - написание самой логики изменения темы
2. Добавляем логику для темы
Сконфигурируем наш стор. В нем у нас будет один лишь редьюсер темы. Делаем его по аналогии с counter
, который шел из коробки, только наш будет попроще.
// src/store.js
import { configureStore } from '@reduxjs/toolkit'
import themeReducer from './components/theme/slice'
export const store = configureStore({
reducer: {
theme: themeReducer,
},
})
Теперь реализуем сам редьюсер с остальной логикой, необходимой для работы темы.
// src/components/theme/slice.js
import { createSlice } from '@reduxjs/toolkit'
// пытаемся получить тему из локального хранилища браузера
// если там ничего нет, то пробуем получить тему из настроек системы
// если и настроек нет, то используем темную тему
const getTheme = () => {
const theme = `${window?.localStorage?.getItem('theme')}`
if ([ 'light', 'dark' ].includes(theme)) return theme
const userMedia = window.matchMedia('(prefers-color-scheme: light)')
if (userMedia.matches) return 'light'
return 'dark'
}
const initialState = getTheme()
export const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
set: (state, action) => action.payload,
},
})
export const { set } = themeSlice.actions
export default themeSlice.reducer
На этом этапе у нас всё работает, но нет компонента, который бы изменял тему.
Реализуем его:
// src/components/theme/index.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import cn from 'classnames'
import { set } from './slice'
import styles from './index.module.scss'
const Theme = ({ className }) => {
const theme = useSelector((state) => state.theme)
const dispatch = useDispatch()
React.useEffect(() => {
document.documentElement.dataset.theme = theme
localStorage.setItem('theme', theme)
}, [ theme ])
const handleChange = () => {
const next = theme === 'dak' ? 'light' : 'dark'
dispatch(set(next))
}
return (
<div
className={cn(
className,
styles.root,
theme === 'dark' ? styles.dark : styles.light)}
onClick={handleChange}
/>
)
}
export default Theme
И добавим стили в файл src/components/theme/index.module.scss
// src/components/theme/index.module.scss
.root {
position: relative;
border-radius: 50%;
display: block;
height: 24px;
overflow: hidden;
width: 24px;
transition: 0.5s all ease;
input {
display: none;
}
&:hover {
cursor: pointer;
}
&:before {
content: "";
display: block;
position: absolute;
}
&.light:before {
animation-duration: 0.5s;
animation-name: sun;
background-color: var(--text-color);
border-radius: 50%;
box-shadow: 10px 0 0 -3.5px var(--text-color),
-10px 0 0 -3.5px var(--text-color),
0 -10px 0 -3.5px var(--text-color),
0 10px 0 -3.5px var(--text-color),
7px -7px 0 -3.5px var(--text-color),
7px 7px 0 -3.5px var(--text-color),
-7px 7px 0 -3.5px var(--text-color),
-7px -7px 0 -3.5px var(--text-color);
height: 10px;
left: 7px;
top: 7px;
width: 10px;
&:hover {
background-color: var(--background-color);
box-shadow: 10px 0 0 -3.5px var(--background-color),
-10px 0 0 -3.5px var(--background-color),
0 -10px 0 -3.5px var(--background-color),
0 10px 0 -3.5px var(--background-color),
7px -7px 0 -3.5px var(--background-color),
7px 7px 0 -3.5px var(--background-color),
-7px 7px 0 -3.5px var(--background-color),
-7px -7px 0 -3.5px var(--background-color);
}
}
&.dark {
&:before {
animation-duration: .5s;
animation-name: moon;
background-color: var(--text-color);
border-radius: 50%;
height: 20px;
left: 2px;
top: 2px;
width: 20px;
z-index: 1;
&:hover {
background-color: var(--background-color);
}
}
&:after {
animation-duration: .5s;
animation-name: moon-shadow;
background: var(--background-color);
border-radius: 50%;
content: "";
display: block;
height: 18px;
position: absolute;
right: -2px;
top: -2px;
width: 18px;
z-index: 2;
}
}
}
@keyframes sun {
from {
background-color: var(--background-color);
box-shadow: 0 0 0 -5px var(--background-color),
0 0 0 -5px var(--background-color),
0 0 0 -5px var(--background-color),
0 0 0 -5px var(--background-color),
0 0 0 -5px var(--background-color),
0 0 0 -5px var(--background-color),
0 0 0 -5px var(--background-color),
0 0 0 -5px var(--background-color);
}
to {
background-color: var(--text-color);
box-shadow: 10px 0 0 -3.5px var(--text-color),
-10px 0 0 -3.5px var(--text-color),
0 -10px 0 -3.5px var(--text-color),
0 10px 0 -3.5px var(--text-color),
7px -7px 0 -3.5px var(--text-color),
7px 7px 0 -3.5px var(--text-color),
-7px 7px 0 -3.5px var(--text-color),
-7px -7px 0 -3.5px var(--text-color);
}
}
@keyframes moon {
from {
height: 0;
left: 12px;
top: 12px;
width: 0;
}
to {
height: 20px;
left: 2px;
top: 2px;
width: 20px;
}
}
@keyframes moon-shadow {
from {
background-color: var(--background-color);
height: 0;
right: 7px;
top: 7px;
width: 0;
}
to {
background-color: var(--background-color);
height: 18px;
right: -2px;
top: -2px;
width: 18px;
}
}
Добавим наш компонент Theme
на главную страницу.
// src/root.js
import Theme from './components/Theme'
const Root = () => (
<>
<h1>Тёмная тема в React с помощью Redux-toolkit</h1>
<Theme />
</>
)
export default Root
И чтобы все заработало как надо, нужно задать переменные для каждой темы. Задавать мы их будем через css
переменные, поскольку те переменные, которые используются в scss
нам не подойдут. scss
компилится в css
довольно глупо, он просто подставляет значения переменных во всех местах, где они фигурируют.
// src/index.scss
:root[data-theme="light"] {
--background-color: #ffffff;
--text-color: #1C1E21;
}
:root[data-theme="dark"] {
--background-color: #18191a;
--text-color: #f5f6f7;
}
body {
background: var(--background-color);
color: var(--text-color);
}
Ура! Все работает! И теперь обещанный бонус - добавление роутов
Добавляем роутинг
Для начала установим библиотеку
> npm i react-router-dom -S
Обернем все в провайдер BrowserRouter
от react-router
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import * as serviceWorker from './serviceWorker'
import Root from './root'
import store from './store'
import './index.scss'
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')
const root = ReactDOM.createRoot(rootElement)
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<Root />
</Provider>
</BrowserRouter>
</React.StrictMode>
)
serviceWorker.unregister()
Теперь можно в файле src/root.js
добавить такой код
import { Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Home from './pages/Home'
import NoMatch from './pages/NoMatch'
const Root () => (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
)
export default Root
Создадим недостающие компоненты
> mkdir -p src/pages/{Home,NoMatch} src/components/Layout
> touch src/pages/Home/index.js src/pages/NoMatch/index.js
> touch src/components/Layout/index.js
Страницы приложения я поместил в папку /pages
. Подобным образом сделано в NextJS
и мне кажется это хорошей практикой.
// src/components/Layout/index.js
import { Outlet } from 'react-router-dom'
import Theme from '../Theme'
const Layout = () => (
<>
<Theme />
<main>
<Outlet />
</main>
</>
)
export default Layout
// src/pages/Home/index.js
const Home = () => <h1>Home</h1>
export default Home
// src/pages/NoMatch/index.js
import { Link } from 'react-router-dom'
const NoMatch = () => (
<>
<h1>Page Not Found</h1>
<h2>We could not find what you were looking for.</h2>
<p>
<Link to="/">Go to the home page</Link>
</p>
</>
)
export default NoMatch
И теперь мы по умолчанию находимся на странице Home
, а если перейдем на любую другую, то нам откроется страница NoMatch
Заключение
С помощью redux-toolkit
добавление тёмной темы выглядит еще проще. К тому же, если вы всё равно собираетесь его использовать на своем проекте, то этот подход будет предпочтительней контекста. Делитесь в комментариях мыслями о том, как можно улучшить этот код или задавайте вопросы, если что-то осталось не ясно - с удовольствием всем отвечу!
Комментарии (3)
yavoloh
07.04.2022 22:18Спасибо за статью. Смутил факт, что для использования нужно обязательно монтировать компонент. Интересно увидеть вариант сайд эффектов в subscribe, middleware redux'a или подобном, а не в эффекте переключателя.
yanranevskiy
Подскажите, пожалуйста, есть ли с таким же оформлением тема для vs code?
borisyuzhakov Автор
Не совсем понял, в чем вопрос. Если вопрос не относится непосредственно к теме статьи, то пишите лучше в личку.
В
vs code
я, лично, пользуюсьDark+
темой