Building a Theme Switcher with Context (Part 2): Adding More Features #101
📖 Introduction
Following our creation of a basic theme switcher, this article explores how to make it more robust and user-friendly. We will add a key feature: persisting the theme to local storage. This will ensure that the user's theme preference is remembered across browser sessions.
📚 Prerequisites
Before we begin, please ensure you have a solid grasp of the following concepts:
- All concepts from Part 1 of this series.
- The
localStorage
API.
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ The Problem: Understanding why a non-persistent theme can be a poor user experience.
- ✅ Core Implementation: How to use the
localStorage
API to save and retrieve the user's theme preference. - ✅ Practical Application: Enhancing our theme switcher to persist the theme across browser sessions.
🧠 Section 1: The Core Concepts of a Persistent Theme Switcher
Our current theme switcher works well, but it has one major flaw: if the user refreshes the page or closes the browser, their theme preference is lost. This is because the theme is only stored in the component's state, which is reset on every page load.
To fix this, we can use the localStorage
API. localStorage
allows us to store key-value pairs in the browser that persist even after the browser is closed.
We will modify our ThemeProvider
to:
- Initialize the theme state from
localStorage
when the component mounts. - Save the theme to
localStorage
whenever it changes.
💻 Section 2: Deep Dive - Implementation and Walkthrough
Let's enhance our ThemeProvider
.
2.1 - The Enhanced ThemeProvider
// ThemeProvider.js
import React, { useState, useEffect, createContext } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
const savedTheme = localStorage.getItem('theme');
return savedTheme || 'light';
});
useEffect(() => {
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
Step-by-Step Code Breakdown:
useState(() => { ... })
: We've updated ouruseState
call to use a function as the initial value. This function is only executed on the initial render.localStorage.getItem('theme')
: We try to get the theme fromlocalStorage
.return savedTheme || 'light';
: If a theme is found inlocalStorage
, we use it. Otherwise, we default to'light'
.
useEffect(() => { ... }, [theme])
: We use auseEffect
hook to save the theme tolocalStorage
whenever thetheme
state changes.
The rest of our application (App.js
, ThemeSwitcher.js
, and App.css
) can remain the same.
💡 Conclusion & Key Takeaways
In this article, we've made our theme switcher much more user-friendly by persisting the theme to local storage. This is a common pattern that you will use in many of your React applications.
Let's summarize the key takeaways:
- The
localStorage
API is a great way to persist data across browser sessions. - You can use a function as the initial value of
useState
to lazily initialize the state. - The
useEffect
hook is perfect for synchronizing state with an external system likelocalStorage
.
Challenge Yourself: To solidify your understanding, try to add a feature that allows the user to choose from a list of themes (e.g., "light", "dark", "sepia") and persist their choice to local storage.
➡️ Next Steps
You now have a solid understanding of how to build a robust and practical feature with the Context API. In the next article, "Combining Context with useReducer
for Complex State (Part 1)", we will explore how to use the useReducer
hook with the Context API to manage more complex state.
Thank you for your dedication. Stay curious, and happy coding!
glossary
localStorage
: A web storage API that allows you to store key-value pairs in a web browser with no expiration date.- Lazy Initialization: A performance optimization where you delay the initialization of an object until the first time it is needed.