Dark theme in React with Redux-toolkit
This article is a continuation of the article Dark theme in React using css variables in scss. If last time we added a dark theme through the native React context, now we will try to do the same thing, but with the help of Redux
more precisely redux-toolkit
road map
We will follow almost the same steps as last time:
Run-up
Let’s createcreate-react-app
project and fix the structure a littleRedux
Let’s add a theme switcher component withredux-состоянием
CSS Variables
Let’s declare variables for each theme that will affect the styles of the componentsBonus
Let’s add routing
1. Preparation
Via
create-react-app
create a project and immediately addsass
Andclassnames
for convenience of work with styles
> npx create-react-app with-redux-theme --template redux
> cd with-redux-theme
> npm i sass classnames -S
Since we will perform all further actions while in the folder
/src
then for convenience we pass to it
> cd src
Delete unnecessary files
# находимся внутри папки /src
> rm -rf app features App.css App.js App.test.js index.css logo.svg
4. Let’s create a convenient application structure
# находимся внутри папки /src
> mkdir -p components/Theme
> touch index.scss root.js store.js
> touch components/Theme/{index.js,index.module.scss,slice.js}
Project subtree inside a folder /src
it should turn out like this
# перейдем в корень и проверим структуру
> tree src
src
├── components
│ └── Theme
│ ├── index.js
│ ├── index.module.scss
│ └── slice.js
├── index.js
├── index.scss
├── root.js
├── store.js
└── ...
Now we will write the code.
Since we have made changes to the structure, we will rewrite our 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>
)
Instead of App.js
i am using file root.js
with component Root
which in the end we will store routes to pages, but for now …
// src/root.js
const Root = () => (
<div>There are will be routes</div>
)
export default Root
Now you can proceed to the second part – writing the actual theme change logic
2. Add logic for the theme
Let’s configure our store. In it we will have only one theme reducer. We do it by analogy with counter
which came out of the box, only ours will be simpler.
// src/store.js
import { configureStore } from '@reduxjs/toolkit'
import themeReducer from './components/theme/slice'
export const store = configureStore({
reducer: {
theme: themeReducer,
},
})
Now we will implement the reducer itself with the rest of the logic necessary for the theme to work.
// 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
At this stage, everything works for us, but there is no component that would change the theme.
Let’s implement it:
// 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
And add styles to the src/components/theme/index.module.scss file
// 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;
}
}
Let’s add our component Theme
to Home Page.
// src/root.js
import Theme from './components/Theme'
const Root = () => (
<>
<h1>Тёмная тема в React с помощью Redux-toolkit</h1>
<Theme />
</>
)
export default Root
And in order for everything to work as it should, you need to set variables for each topic. We will ask them through css
variables, since those variables that are used in scss
will not suit us. scss
compiles to css
rather stupid, it just substitutes the values of the variables in all the places where they appear.
// 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);
}
Hooray! Everything works! And now the promised bonus – adding routes
Adding Routing
Let’s install the library first.
> npm i react-router-dom -S
Let’s wrap everything in a provider BrowserRouter
from 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()
Now you can in the file src/root.js
add code like this
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
Create the missing components
> 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
Application pages I placed in a folder /pages
. Done in a similar way NextJS
and I think it’s good practice.
// 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
And now we are by default on the page Home
and if we pass to any anotherthen we will open the page NoMatch
Conclusion
Via redux-toolkit
adding a dark theme looks even easier. In addition, if you are going to use it on your project anyway, then this approach will be preferable to the context. Share your thoughts in the comments on how you can improve this code or ask questions if something is not clear – I will be happy to answer everyone!