Эта статья является продолжением статьи Тёмная тема в 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 добавление тёмной темы выглядит еще проще. К тому же, если вы всё равно собираетесь его использовать на своем проекте, то этот подход будет предпочтительней контекста. Делитесь в комментариях мыслями о том, как можно улучшить этот код или задавайте вопросы, если что-то осталось не ясно — с удовольствием всем отвечу!
ссылка на оригинал статьи https://habr.com/ru/post/659491/
Добавить комментарий