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

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.

Related

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 set state in parent from one child component and access the state in another child component using react and typescript?

i want to set the state in Parent component on clicking a button in child component. Also i want to access this state in other child component.
what i am trying to do?
On clicking upload button (UploadButton component) i want the state isDialogOpen to be set to true. and i want to access isDialogOpen state in UserButton component.
below is the snippet,
function Main() {
return (
<Wrapper>
<React.Suspense fallback={null}>
<Switch>
<Route
exact
path="/page1"
render={routeProps => (
<Layout>
<React.Suspense fallback={<PlaceHolder></>}>
<child1 {...routeProps} />
</React.Suspense>
</Layout>
)}
/>
<Route
exact
path="/page2"
render={routeProps => (
<Layout>
<Child2 {...routeProps} />
</Layout>
)}
/>
</Switch>
</React>
</Wrapper>
)
}
function Child1() {
return (
<UploadButton/>
);
}
type Props = RouteComponentProps<{ itemId: string; productId: string }>;
function UploadButton({ match }: Props) { //here i set the state isDialogOpen
const [isDialogOpen, setDialogOpen] = React.useState(false);
const handle_click = () => {
setDialogOpen(!isDialogOpen);
};
return (
<>
<Button onClick={handle_click}/>
{isDialogOpen && (
<UploadForm/>
)}
</>
);
}
function Child2() {
return (
<UserButton/>
);
}
function UserButton() {
return (
<Icon/>
);
}
In the above snippet, isDialogOpen state is set in UploadButton component.
Now i want to modify above snippet such that the Icon component in UserButton component is hidden if isDialogOpen is true.
i want to access this isDialogOpen state in UserButton component.
what i have tried?
I can define a function in main component that sets isDialogOpen to true when Upload button is clicked in UploadButton component. but this needs passing the function as prop from main component to Upload Button and similarly passing the state to UserButton from main component.
Is there some neat way to do this? i am new to typescript and react. could someone help me solve this. thanks.
You should define state value and function which update state as props respectively to child components as props. You can take example of the code which I provide bellow
const Child1 = (props) => {
return <div>This is the counter value {props.counter}</div>
}
const Child2 = (props) => {
return <div>
<h2>Here the button to update the counter</h2>
<button onClick={props.update}>
Update counter state in the parent
</button>
</div>
}
class MainComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
updateCounter = () => {
this.setState({counter: this.state.counter + 1});
}
render() {
return <div>
<Child1 counter={this.state.counter} />
<Child2 update={this.updateCounter} />
</div>
}
}
ReactDOM.render(<MainComponent />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can use the same component with context API and React HOOKS like this
import React, { useContext, useState} from 'react';
const CounterContext = React.createContext({
counter: 0
});
const MainComponent = (props) => {
const [counter, setCounter] = useState(0);
const updateCounter = () => {
setCounter(counter + 1);
}
return <CounterContext.Provider value={
counter,
update: updateCounter
}>
<div>
<Child1 />
<Child2 />
</div>
</CounterContext.Provider>;
}
const Child1 = (props) => {
const counter = useContext(CounterContext);
return <div>This is the counter value {counter.counter}</div>
}
const Child2 = (props) => {
const counter = useContext(CounterContext);
return <div>
<h2>Here the button to update the counter</h2>
<button onClick={counter.update}>
Update counter state in the parent
</button>
</div>
}

How to toggle a theme in React?

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>
)}

Passing location and pageContext from page to child components in Gatsby

My page layout for a Gatsby site looks like this.
const Container = ({location, children, pageContext}) => {
return (
<>
<Header location={location} />
<Breadcrumbs pageContext={pageContext} />
{children}
<Footer />
</>
)
}
I need to pass location and pageContext from the page to the child components. I have tried to add location and pageContext to the DataProvider like this:
export const DataContext = React.createContext();
const DataProvider = ({ children, location, pageContext }) => {
return (
<DataContext.Provider value={{
location,
pageContext
}}>
{children}
</DataContext.Provider>
)
};
export default DataContext
export { DataProvider }
Then I use DataProvider in gatsby-ssr.js and gatsby-browser.js like this:
export const wrapRootElement = ({ element }) => (
<ThemeProvider theme={theme}>
<DataProvider>
{element}
</DataProvider>
</ThemeProvider>
);
In the child component:
const HeaderLinks = () => {
return (
<DataContext.Consumer>
{
context => (
<Menu
theme="light"
mode="horizontal"
selectedKeys={[context.location.pathname]}
>
<Menu.Item key={key}>
<Link to={url}>{name}</Link>
</Menu.Item>
</Menu>
)
}
</DataContext.Consumer>
)
}
But it doesn't seem to work, as it is not getting updated when I move to another page. (I also have wrapPageElement with Container, may be that's reasons.)
How can I pass location and pageContext to the child components? Is it better to use React Context or simply pass them as props? If I should use React Context, how can I correct my code to make it work?
Instead of using wrapRootElement to use ContexProvider you can make use of wrapPageElement where you can get the page props and pass them on to the DataProvider. This will make sure that pageContext and location change on each page
export const wrapRootElement = ({ element }) => (
<ThemeProvider theme={theme}>
{element}
</ThemeProvider>
);
export const wrapPageElement = ({ element, props }) => (
<DataProvider value={props}>
{element}
</DataProvider>
);
export const DataContext = React.createContext();
const DataProvider = ({ children, value }) => {
const {location, pageContext} = value;
return (
<DataContext.Provider value={{
location,
pageContext
}}>
{children}
</DataContext.Provider>
)
};
export default DataContext
export { DataProvider }
I ended up using useLocation from #reach/router to return location in child components. And I simply pass pageContext as a prop to <Breadcrumbs />, as it is used only once and is not passed down to any child components.

Storing component into a variable and reuse it

Ok i got components imported as
import Payment from './pages/payment';
import Chat from './pages/chat';
Now I am using Drawer component and using it together with Navigator my renderScene become something like this
if( route.id == 'payment'){
return <Drawer xx={} yy={} and a lot more >
<Payment navigator={navigator} />
</Drawer>
}
if(route.id == 'chat'){
return <Drawer xx={} yy={} and a lot more >
<Chat navigator={navigator} />
</Drawer>
}
Those lengthy Drawer code are being used again and again. I want to store that <Payment navigator={navigator} > or the other into a variable and then return that with Drawer only once.
How can i store it and return it with Drawer?
Thanks
Not sure if you are asking this but what about something like:
const routes = {
payment: Payment,
chat: Chat
...
}
And then, just:
const Scene = routes[route.id];
return (
<Drawer>
<Scene navigator={navigator}/>
</Drawer>
)
Here you have 3 options:
// 1. Group the drawer props in an object
const drawerProps = {
xx: ...,
yy: ...
};
<Drawer {...drawerProps}>
<Chat navigator={navigator} />
</Drawer>
// 2. Define a wrapper object that populates the common Drawer props
const CustomDrawer = ({ children }) => (
<Drawer xx={} yy={} and a lot more>
{children}
</Drawer>
);
// 3. Define a wrapper object that populates the common Drawer props with default props. (Can be
// overriden.)
const CustomDrawer = ({
xx='XX',
yy='YY',
children
}) => (
<Drawer xx={xx} yy={yy} and a lot more>
{children}
</Drawer>
);
EDIT: I missunderstood your question, for storing the inner part you just have to assign it to a varible and use it.
const routes = {
chat: <Chat navigator={navigator} />,
payment: <Payment navigator={navigator} />,
}
<Drawer {...drawerProps}>
{ routes[route.id] }
</Drawer>
I propose this solution with a React hook (React v16.8+).
The useMemo returns a component according to the route agument passed to the switch. The useMemo is updated each time one of the internal variables (passed as a second argument as route) is updated.
import React, { useState, useMemo } from 'react';
export default function App ({
route,
navigator
}) {
const [route, setRoute] = useState('payment');
const mainContent = useMemo(() => {
return () => {
switch (route) {
case 'payment':
return (
<Payment navigator={navigator} />
);
case 'chat':
return (
<Chat navigator={navigator} />
);
}
}
}, [route, navigator])
return (
<Drawer xx={} yy={} and a lot more >
{ mainContent() }
</Drawer>
);
}

Resources