How to toggle a theme in React? - reactjs

I am trying to implement a button to toggle between light and dark. I have a file AppRouter which looks something like this..
const theme = getTheme(true);
const AppRouter = () => {
return (
<MuiThemeProvider theme={theme}>
<ThemeProvider theme={theme}>
...
<Switch> *all my routes are here including AppRoot, which routes to components* </Switch>
</MuiThemeProvider></ThemeProvider>
);
}
So when i set getTheme(true) it triggers the dark theme, and getTheme(false) triggers the light theme. Here comes the problem, I would like to insert a toggle button into one of the components which is inside Switch that can allow user to switch between these two themes. Essentially, the component will be controlling the state of AppRouter. Is it possible?
Any idea where I could start from? I am using react hooks and type script. I have tried various methods to no avail. Would reducer work?
Update:
AppRouter.tsx
const [lightTheme, setLightTheme] = React.useState(true);
const theme = getTheme(lightTheme);
const AppRouter = () => {
return (
<MuiThemeProvider theme={theme}>
<ThemeProvider theme={theme}>
...
<Switch>
<PrivateRoute path="..." component={AppRoot} render{()=> <PageTitle toggleTheme={() => toggleTheme}/>}
</Switch>
</MuiThemeProvider></ThemeProvider>
);
}
PageTitle.tsx
interface PageTitleProps {
...
toggleTheme?: () => void;
}
const PageTitle: React.FC<PageTitleProps> = ({ ... toggleTheme}) => {
...
return (
...
{toggleTheme && <Button OnClick={() => toggleTheme()}>Change Theme</Button>
)}
I have tried running the code above, but toggleTheme is undefined - so the button does not show up at all.
toggleTheme has to be optional.

Create a local state inside your AppRouter and pass down the toggler to the component(s) that will toggle the theme.
AppRouter.tsx:
import React, { useState } from 'react';
const AppRouter = () => {
const [lightTheme, setLightTheme] = useState(true);
const theme = getTheme(lightTheme);
const toggleTheme = () => setLightTheme(!lightTheme);
return (
<MuiThemeProvider theme={theme}>
<ThemeProvider theme={theme}>
...
<Switch>
<Route path='your-path' render={(props) =>
<PageTitle toggleTheme={ toggleTheme } />} />
</Switch>
</MuiThemeProvider></ThemeProvider>
);
}
PageTitle.tsx:
interface PageTitleProps {
...
toggleTheme?: () => void;
}
const PageTitle: React.FC<PageTitleProps> = ({ ... toggleTheme}) => {
...
return (
...
<button onClick={() => toggleTheme()}>Change Theme</button>
)}

Related

UseContext with React Router

Shortly, when I try to use useState with useContext in one of my components, all pages just disappear. UseState in some reason block my Routers and I have no idea why... Can you tell me where is my mistake?
Some code below:
Index.js
export default function App() {
const [value, setValue] = useState(false) -----> here I set the state
return (
<BrowserRouter>
<UserContext.Provider value={{ value, setValue }}>
<Routes>
<Route path='/' element={<Layout />}>
<Route index element={<Home />} />
<Route path='Home' element={<Home />} />
<Route path='Menu' element={<Menu />} />
<Route path='Story' element={<Story />} />
<Route path='Coffee' element={<Coffee />} />
<Route path='Cart' element={<Cart />} />
</Route>
</Routes>
</UserContext.Provider>
</BrowserRouter>
)
}
// ReactDOM.render(<App />, document.getElementById("root"))
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)
Buy.js component
import { useState } from "react"
import { useContext } from "react"
import { UserContext } from "../../UserContext"
const Buy = () => {
const [buttonText, setButtonText] = useState("Add to cart")
const [isActive, setIsActive] = useState(false)
// const [value, setValue] = useContext(UserContext) --> after I declare state everything disappears
const addToCart = () => {
setIsActive((current) => !current)
// setValue(true)
if (isActive) {
setButtonText("Add to cart")
}
}
return (
<div>
<button
class='buy'
style={{
fontSize: isActive ? "0.8rem" : "1rem",
color: isActive ? "lightgray" : "white",
}}
onClick={() => {
addToCart()
}}
>
{buttonText}
</button>
</div>
)
}
export default Buy
UserContext.js
import { createContext } from "react"
export const UserContext = createContext(null)
Actually, I need this Context only for routes "Coffee" and "Cart", but if I wrap only this 2 routes will be the same problem and all is disappear. Should I maybe use Context in my Layout.jsx instead of Index.js? Thank you.
const Layout = () => {
return (
<>
<Navbar />
<Outlet />
<Footer/>
</>
);
};
export default Layout;
The errors in console:
Your context provides an object, not an array, so you should destructure using curly braces when you use it:
const { value, setValue } = useContext(UserContext);
Or if you want to keep this way of destructuring, you can provide an array instead:
<UserContext.Provider value={[value, setValue]}>
There are a few issues I see with the React code that I myself struggled with while learning React.
Issue #1
Your button in Buy.js has a class='buy' property. Rename that to className='buy' because that's just React's syntax.
Issue #2
Your button's onClick={} property should reference only the function's name, and should not call the function itself. Change the onClick to onClick={addToCart}. Do not add the anonymous arrow function, simply input the name of the function.
Possible Issue #3
Most of the conditional functionality you are looking for can be implemented with React's useEffect() hook. Change addToCart() in the following way:
const addToCart = () => {
setIsActive();
}
useEffect(() => {
if(isActive) {
setButtonText("Add to cart");
}
}, [isActive]);
Make sure to import useEffect() before using this.
First i cannot see your Buy.js component between Context.Provider tree.
Then please try to use as object to destruct state values, not array.

Changing props state using api causes infinite loop

I don't know why, changing the props state inside useEffect causes infinite loop of errors. I used them first locally declaring within the function without using props which was running ok.
EDIT:
Home.js
import Axios from "axios";
import React, { useEffect, useState } from "react";
function Home(props) {
// const [details, setDetails] = useState({});
// const [login, setLogin] = useState(false);
useEffect(() => {
try {
const data = localStorage.getItem("expensesAccDetails");
if (data) {
Axios.post("http://localhost:3001/eachCollectionData", {
collection: data,
}).then((res) => {
if (res.data.err) {
console.log("Error");
} else {
console.log(res.data[0]);
props.setLogin(true);
props.setUserdetails(res.data[0]);
}
});
}
} catch (err) {
console.log(err);
}
}, []);
return props.login ? (
<div>
<div>Welcome {props.setUserdetails.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
}
export default Home;
App.js
function App() {
const [login, setLogin] = useState(false);
const [userdetails, setUserdetails] = useState({});
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
setLogin={setLogin}
login={login}
setUserdetails={setUserdetails}
userdetails={userdetails}
/>
<Bars login={login} />
</>
}
/>
<Routes>
<Router>
);
Here I initialized the states directly in App.js so I don't have to declare it on every page for the route renders. I just passed them as props to every component.
I suggest to create a componente Home with the post and two sub-component inside:
const Home = () => {
const [userDetails, setUserDetails] = useState({});
const [login, setLogin] = useState(false);
useEffect(() => {
// api call
}, []);
return (
<>
<Welcome login={login} details={userDetails} />
<Bars login={login} details={userDetails} />
</>
);
};
where Welcome is the following:
const Welcome = ({ userdetails, login }) => (
<>
login ? (
<div>
<div>Welcome {userdetails.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
</>
);
A better solution is to use only one state variable:
const [userDetails, setUserDetails] = useState(null);
and test if userDetails is null as you test login is true.
An alternative if you have to maintain the call as you write before, you can use two state as the follow:
function App() {
const [userdetails, setUserdetails] = useState(null);
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
setUserdetails={setUserdetails}
/>
<Bars login={!!userdetails} />
</>
}
/>
<Routes>
<Router>
);
and on Home component use a local state:
const Home = ({setUserdetails}) => {
const [userDetailsLocal, setUserDetailsLocal] = useState(null);
useEffect(() => {
// api call
// ... on response received:
setUserdetails(res.data[0]);
setUserDetailsLocal(res.data[0]);
// ...
}, []);
userDetailsLocal ? (
<div>
<div>Welcome {userDetailsLocal.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
};
I advise to follow Max arquitecture for your solution. the problem lies in the Router behavior. React Router is not part of React core, so you must use it outside your react logic.
from documentation of React Router:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render.
https://v5.reactrouter.com/web/api/Route/component
Edit:
ok, you make me write it. A solution could be like:
function App() {
const [login, setLogin] = useState(false);
const [userdetails, setUserdetails] = useState({});
useEffect(() => {
try {
const data = localStorage.getItem("expensesAccDetails");
if (data) {
Axios.post("http://localhost:3001/eachCollectionData", {
collection: data,
}).then((res) => {
if (res.data.err) {
console.log("Error");
} else {
console.log(res.data[0]);
setLogin(true);
setUserdetails(res.data[0]);
}
});
}
} catch (err) {
console.log(err);
}
}, []);
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
login={login}
userdetails={userdetails}
/>
<Bars login={login} />
</>
}
/>
<Routes>
<Router>
);

App.tsx didn't receive the context updated with a new state

I want to change with a toggle in navbar which theme the app will apply, I can update normally the context with the consumer and provider of defaultTheme, but my app didn't update this information.
I've console logged some components to see if they're receiving my context updates, and all is normal, but in my App.tsx, the context only send the first state, and all updates isn't received by it
context.js
const Context = createContext({
defaultTheme: dark,
toggleTheme: () => {},
});
export function ThemeContextProvider({ children }) {
const [theme, setTheme] = useState(dark);
function toggleTheme() {
setTheme(theme === dark ? light : dark);
}
return (
<Context.Provider value={{ defaultTheme: theme, toggleTheme }}>
{children}
</Context.Provider>
)
}
export function useTheme() {
const theme = useContext(Context)
return theme;
}
App.tsx
function App() {
const { defaultTheme } = useTheme();
return (
<ThemeContextProvider>
{defaultTheme.title === 'dark' ? (
<ThemeProvider theme={dark}>
<GlobalStyle />
<Routes />
</ThemeProvider>
) : (
<ThemeProvider theme={light}>
<GlobalStyle />
<Routes />
</ThemeProvider>
) }
</ThemeContextProvider>
);
}
Navbar.tsx
const { colors } = useContext(ThemeContext);
const { defaultTheme, toggleTheme } = useTheme();
return (
<div id='navbar'>
<div className='navbar-container'>
<div className='theme-switcher'>
{ defaultTheme.title === 'dark' ? <RiMoonClearFill /> : <RiMoonClearLine />}
<Switch
onChange={toggleTheme}
checked={defaultTheme.title === 'light'}
checkedIcon={true}
uncheckedIcon={false}
height={10}
width={40}
handleDiameter={20}
offHandleColor={colors.main}
onHandleColor={colors.text}
offColor={colors.background}
onColor={colors.main}
/>
{ defaultTheme.title === 'light' ? <FaSun /> : <FaRegSun />}
</div>
...
App.tsx is not wrapped within ThemeContextProvider so you cant access that context value inside App.tsx.
Its context value is only accessible to children components where ThemeContextProvider is wrapped around.
So i suggest you to move this whole chunk to a new component and call useTheme() inside that child component.
<ThemeProvider theme={defaultTheme.title === 'dark' ? dark : light}>
<GlobalStyle />
<Routes />
</ThemeProvider>
And i have made changes to your conditional rendering to make to more compact and readable.

custom hook not triggering in component

I followed this tutorial for creating themes for night/day modes with styled-components.
I created a hook useDarkMode and for some reason, while it's detecting changes locally to the theme state within the hook, it's not sending these updates to my component (_app.tsx) where it needs to be read.
Am I missing something obvious here, why isn't theme changing on _app.tsx?
useDarkMode hook
import { useEffect, useState } from 'react';
export const useDarkMode = () => {
const [theme, setTheme] = useState('light');
const setMode = (mode) => {
window.localStorage.setItem('theme', mode);
setTheme(mode);
};
const themeToggler = () => {
theme === 'light' ? setMode('dark') : setMode('light');
};
useEffect(() => {
console.log('theme:', theme); <=== triggers and shows theme has been updated
}, [theme]);
useEffect(() => {
const localTheme = window.localStorage.getItem('theme');
console.log('localTheme', window.localStorage.getItem('theme'));
localTheme && setTheme(localTheme);
}, []);
return [theme, themeToggler];
};
_app.tsx
function App({ Component, pageProps }: AppProps) {
const store = useStore(pageProps.initialReduxState);
const [theme] = useDarkMode();
useEffect(() => {
console.log('t', theme); <=== only triggers on component mount, theme is not updating
}, [theme]);
const themeMode = theme === 'light' ? LIGHT_THEME : DARK_THEME;
return (
<Provider store={store}>
<ThemeProvider theme={themeMode}>
<RootPage Component={Component} pageProps={pageProps} />
</ThemeProvider>
</Provider>
);
}
where it's being invoked
const TopNav = () => {
const [theme, themeToggler] = useDarkMode();
return (
<Content className="full-bleed">
<NavInner>
<AuthLinks>
<>
<button onClick={themeToggler}>Switch Theme</button>
<Link href="/user/login" passHref>
<div>
<Typography format="body1">Login</Typography>
</div>
</Link>
<Link href="/user/register" passHref>
<div>
<Typography format="body1">Register</Typography>
</div>
</Link>
</>
...
</AuthLinks>
</NavInner>
</Content>
);
};
Issue
Each react hook is its own instance, they don't share state.
Suggested Solution
Use a single dark mode theme state in the provider and expose the themeToggler in a context so all components can update the same context value.
Theme toggle context
const ThemeToggleContext = React.createContext({
themeToggler: () => {},
});
App
import { ThemeToggleContext } from 'themeToggleContext';
function App({ Component, pageProps }: AppProps) {
const store = useStore(pageProps.initialReduxState);
const [theme, themeToggler] = useDarkMode();
useEffect(() => {
console.log('t', theme); <=== only triggers on component mount, theme is not updating
}, [theme]);
const themeMode = theme === 'light' ? LIGHT_THEME : DARK_THEME;
return (
<Provider store={store}>
<ThemeProvider theme={themeMode}>
<ThemeToggleContext.Provider value={themeToggler} > // <-- pass themeToggler to context provider
<RootPage Component={Component} pageProps={pageProps} />
</ThemeToggleContext>
</ThemeProvider>
</Provider>
);
}
Component
import { ThemeToggleContext } from 'themeToggleContext';
const TopNav = () => {
const themeToggler = useContext(ThemeToggleContext); // <-- get the context value
return (
<Content className="full-bleed">
<NavInner>
<AuthLinks>
<>
<button onClick={themeToggler}>Switch Theme</button>
<Link href="/user/login" passHref>
<div>
<Typography format="body1">Login</Typography>
</div>
</Link>
<Link href="/user/register" passHref>
<div>
<Typography format="body1">Register</Typography>
</div>
</Link>
</>
...
</AuthLinks>
</NavInner>
</Content>
);
};

How to use the useHook in the component rather than passing the value returned from it as a prop to the components using react and typescript?

i want to use useHook within the component itself rather than passing it as a prop to each component using react and typescript.
what i am trying to do?
I have a useHook named useRefresh which returns isLoading state. This isLoading state is used to display a loading indicator in each of the pages.
so i have three pages and whenever this isLoading is true should display a loading indicator in these pages.
below is my code,
function App(){
const user = useGetUser();
return (
<Router>
<Switch>
<Route
path="/"
render={props: any => (
user ? (<Main {...props} />) : (
<LoginPage/>
);
)}
</Route>
</Switch>
</Router>
);
}
export function useLoad() {
const { refetch: refetchItems } = useGetItems();
const { refetch: refetchOwnedItems } = useListOwnedItems();
return async function() {
await refreshCompany();
refetchItems();
refetchOwnedItems();
};
}
function useAnother(Id: string) {
const [compId, setCompId] = React.useState(undefined);
const [isLoading, setIsLoading] = React.useState(false);
const comp = useCurrentComp(Id);
const load = useLoad();
if (comp && comp.id !== compId) {
setCompId(comp.id);
const prevCompId = compId !== undefined;
if (prevCompId) {
setIsLoading(true);
load().then(() => {
setIsLoading(false);
});
}
}
}
function Main ({user}: Props) {
useAnother(user.id);
return (
<Router>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</Router>
);
}
function FirstComp () {
return(
<Wrapper>
//some jsx
</Wrapper>
);
}
function SecondComp () {
return(
<Wrapper>
//some jsx
</Wrapper>
);
}
Now i want to pass isLoading state to each of the components in Main component....so i have passed it like below,
function Main ({user}: Props) {
const isLoading = useAnother(user.id); //fetching isLoading here from useHook
return (
<Router>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp isLoading={isLoading} {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp isLoading={isLoading} {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</Router>
);
}
function FirstComp ({isLoading}: Props) {
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
function SecondComp ({isLoading}: Props) {
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
This works. but doesnt seem like a right approach to me.. i dont want to pass this isLoading state as a prop to each of these components. there are more than 10 of them.
is there someway that i can do it other way than this. could someone help me with this. thanks.
The most common solution is to create a context that wraps the entire tree of components. This context holds the state that your hook pulls in
////LoadingContext.tsx
const LoadingContext = createContext();
const LoadingContextProvider = () => {
const [isLoading, setIsLoading] = useState(false);
return (
<LoadingContextProvider.Provider
value={{
isLoading,
setIsLoading
}}
/>
)
}
export const useLoading = () => useContext(LoadingContext);
You need to wrap the context around anything that will be calling useLoading:
import { LoadingContextProvider } from './LoadingContext' //or wherever this is relative to Main.tsx
<LoadingContextProvider>
<Router>
...(router stuff)
</Router>
</LoadingContextProvider>
Now you can call useLoading in your lower-level components.
//in another file defining a lower-level component:
import { useLoading } from '../../LoadingContext' //or wherever the context stuff is relative to this component definition
const FirstComp = () =>
const [isLoading, setIsLoading] = useLoading();
const handleClick = () => {
setIsLoading(true);
callMyApi().then(() => setIsLoading(false));
}
if(isLoading){
return <LoadingGif />
}
else{
return <div onClick={handleClick}>Click me!</div>
}
)}
What you would like to accomplish here is called global state. There are many ways to do it, but I think the simplest is the native React Context API.
All you have to do is create a ContextProvider and then use the useContext hook inside your components to access the values it provides.
Here is an example that should work for your case:
Main.js
export const LoadingContext = React.createContext(true); //creating and exporting the context
function Main ({user}: Props) {
const isLoading = useAnother(user.id); //fetching isLoading here from useHook
return (
<LoadingContext.Provider value={isLoading}> {/* providing the value to the children */}
<Router>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</Router>
</LoadingContext.Provider>
);
}
export default Main;
Other components
import {LoadingContext} from './Main.js'
function FirstComp ({}: Props) {
const isLoading = useContext(LoadingContext); //accessing the value
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}
function SecondComp ({}: Props) {
const isLoading = useContext(LoadingContext); //accessing the value
return(
<Wrapper>
displayIndicatorWhen(isLoading);
//some jsx
</Wrapper>
);
}

Resources