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>
);
};
Related
I have two components App and MyComponent, where MyComponent is used in App.
import { useState } from "react";
import { MyComponent } from "./myComponent";
export const App = () => {
const [state, setState] = useState(0);
return (
<>
<MyComponent
render={() => (
<button onClick={() => setState((prev) => prev + 50)}>{state}</button>
)}
/>
</>
);
}
export const MyComponent = (props) => {
const Content = props.render;
return (
<div>
<Content/>
</div>
);
};
Is it ok to use state in the return value of the render prop? Is it considered anti-pattern?
Is it ok to use react state in render prop?
Yes, but... why? children prop was created to achieve exactly what you want here.
<MyComponent>
<button onClick={() => setState((prev) => prev + 50)}>{state}.</button>
</MyComponent>
export const MyComponent = ({ children }) => (
<div>
{children}
</div>
);
How should I test if className was added to child component in Jest with react-testing-library when props comes from context? I'm using CSS modules
Here's an example i wrote for this issue:
const Context = createContext()
const ContextProvider = ({ children }) => {
const [state, setState] = useState(false);
return (
<ContextProvider value={state}>
{children}
</ContextProvider>
);
};
const Display = () => {
const state = useContext(Context)
return <div className={`${state && styles[my-class]}`}></div>
}
const App = () => {
return (
<ContextProvider>
<Display />
</ContextProvider>
)
}
I tried something like this
it('should add classname', () => {
const { baseElement } = render(
<Context.Provider value={value}>
<Display />
</Context.Provider>
);
expect(baseElement).toHaveAttribute(
'class',
'my-class'
);
});
I want to keep the same state of the sidebar when i navigate between pages , how do i achive that ?
code in _app.js
function MyApp({ Component, pageProps }) {
const open = useState(false);
const [jsonData, setJsonData] = useState({});
const headerVisibility = useState("");
return (
<>
<HeaderContext.Provider value={headerVisibility}>
<NavigationBar open={open} />
<Content open={open}>
<Component {...pageProps} />
</Content>
</HeaderContext.Provider>
</>
);
}
// console.clear();
export default MyApp;
this is how i am fetching data in navigation/sidebar
useEffect(async () => {
const rawData = await fetch("http://localhost:8000/data");
const jsonData = await abc.json();
setData((prevState) => jsonData);
setnavData((prevState) => jsonData);
}, []);
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.
Please consider the following code:
import React from "react";
import "./styles.css";
const Component = ({ title }) => {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("Mounted");
}, []);
return (
<div>
<h2>{title}</h2>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>count up</button>
</div>
);
};
export default function App() {
const [index, setIndex] = React.useState(0);
const changeComponent = () => {
setIndex(c => (c === 1 ? 0 : 1));
};
const components = [
{
render: () => <Component title="one" />
},
{
render: () => <Component title="two" />
}
];
return (
<>
<button onClick={changeComponent}>toggle component</button>
{components[index].render()}
</>
);
}
https://codesandbox.io/s/mystifying-hermann-si7cn
When you click toggle component, title changes, but component is not unmounted, you can see it because count is not reset.
How to make it so that new component is mounted on toggle component click?
React needs a way to differentiate one component instance from the other. This will fix it
const components = [
{
render: () => <Component key={1} title="one" />
},
{
render: () => <Component key={2} title="two" />
}
];
Its the same reason react requires dynamically rendered lists to have a key prop. It informs react of which component to update.