I'm doing some theming for a gatsby project I'm working on. I have the ThemeContext.Provider and ThemeContext.Consumer. The layouts differ depending on what page you're on. I was wondering if it's possible to store location.pathname in the ThemeProvider and have the path returned in my theme object as the page route changes. I want to pass the path to specific components to adjust the layout depending on the route. Thank you.
ThemeProvider:
import React, { useState, createContext } from 'react'
const defaultState = {
dark: false,
setDark: () => {},
}
export const ThemeContext = createContext(defaultState)
interface ThemeProviderProps {
children: any
}
export const ThemeProvider = (props: ThemeProviderProps) => {
const { children } = props.children
const [dark, setDarkTheme] = useState<boolean>(false)
const setDark = () => {
setDarkTheme(true)
}
return (
<ThemeContext.Provider
value={{
dark,
setDark,
}}
>
{children}
</ThemeContext.Provider>
)
}
In that case, you're storing unnecessary data in a context which make the possibility of bad rendering performance higher.
All you need is to use useLocation hook if you use Function Component or withRouter HOC if you Class Component to access the location object in your Component. just remember to use Router provider and you good to go.
Related
BlogDetailsPage.js
import { connect } from "react-redux";
import { useParams } from "react-router-dom";
const BlogDetailsPage = (props) => {
const { id } = useParams();
return <div>Blog Details: {}</div>;
};
const mapStateToProps = (state, props) => {
const { id } = useParams();
return {
blog: state.blogs.find((blog) => {
return blog.id === id;
}),
};
};
export default connect(mapStateToProps)(BlogDetailsPage);
How to use mapStateToProps in "useParams()" react-router-dom ?
and whatever links that navigate to /slug path are ended up in BlogDetailsPage.js, Since BlogDetailsPage.js is being nested nowhere else so i couldn't get specific props pass down but route params. From my perspective this is completely wrong but i couldn't figure out a better way to do it.
Compiled with problems:X
ERROR
src\components\BlogDetailsPage.js
Line 11:18: React Hook "useParams" is called in function "mapStateToProps" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks
Search for the keywords to learn more about each error.```
Issue
React hooks can only be called from React function components or custom React hooks. Here it is being called in a regular Javascript function that is neither a React component or custom hook.
Solutions
Preferred
The preferred method would be to use the React hooks directly in the component. Instead of using the connect Higher Order Component use the useSelector hook to select/access the state.blogs array.
Example:
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
const BlogDetailsPage = () => {
const { id } = useParams();
const blog = useSelector(state => state.blogs.find(
blog => String(blog.id) === id
));
return <div>Blog Details: {}</div>;
};
export default BlogDetailsPage;
Alternative/Legacy
If you have the need to access path params in any mapStateToProps function, if you are using a lot of oder code for example, then you'll need to create another HOC to access the path params and have them injected as props so they are available in the mapStateToProps function.
Example:
import { useParams, /* other hooks */ } from "react-router-dom";
const withRouter = Component => props => {
const params = useParams();
// other hooks, useLocation, useNavigate, etc..
return <Component {...props} {...{ params, /* other injected props */ }} />;
};
export default withRouter;
...
import { compose } from 'redux';
import { connect } from 'react-redux';
import withRouter from '../path/to/withRouter';
const BlogDetailsPage = ({ blog }) => {
return <div>Blog Details: {}</div>;
};
const mapStateToProps = (state, { params }) => {
const { id } = params || {};
return {
blog: state.blogs.find((blog) => {
return String(blog.id) === id;
}),
};
};
export default compose(
withRouter, // <-- injects a params prop
connect(mapStateToProps) // <-- props.params accessible
)(BlogDetailsPage);
I think, react hook functions are allowed to use inside of react component.
Outside of react components, it's not allowed to use react api hook functions.
Thanks, I'd liked to help you my answer.
A "dark mode" feature has been implemented on my Next.js application using React's Context api.
Everything works fine during development, however, Context provider-related problems have arisen on the built version — global states show as undefined and cannot be handled.
_app.tsx is wrapped with the ThemeProvider as such:
// React & Next hooks
import React, { useEffect } from "react";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
// Irrelevant imports
// Global state management
import { Provider } from "react-redux";
import store from "../redux/store";
import { AuthProvider } from "../context/UserContext";
import { ThemeProvider } from "../context/ThemeContext";
// Components
import Layout from "../components/Layout/Layout";
import Footer from "../components/Footer/Footer";
// Irrelevant code
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
// Applying different layouts depending on page
switch (Component.name) {
case "HomePage":
return (
<Provider store={store}>
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
<Footer color="fff" />
</AuthProvider>
</ThemeProvider>
</Provider>
);
case "PageNotFound":
return (
<>
<Component {...pageProps} />
<Footer color="#f2f2f5" />
</>
);
default:
// Irrelevant code
}
}
export default MyApp;
The ThemeContext correctly exports both its Provider and Context:
import { createContext, ReactNode, useState, useEffect } from "react";
type themeContextType = {
darkMode: boolean | null;
toggleDarkMode: () => void;
};
type Props = {
children: ReactNode;
};
// Checks for user's preference.
const getPrefColorScheme = () => {
return !window.matchMedia
? null
: window.matchMedia("(prefers-color-scheme: dark)").matches;
};
// Gets previously stored theme if it exists.
const getInitialMode = () => {
const isReturningUser = "dark-mode" in localStorage; // Returns true if user already used the website.
const savedMode = localStorage.getItem("dark-mode") === "true" ? true : false;
const userPrefersDark = getPrefColorScheme(); // Gets user's colour preference.
// If mode was saved ► return saved mode else get users general preference.
return isReturningUser ? savedMode : userPrefersDark ? true : false;
};
export const ThemeContext = createContext<themeContextType>(
{} as themeContextType
);
export const ThemeProvider = ({ children }: Props) => {
// localStorage only exists on the browser (window), not on the server
const [darkMode, setDarkMode] = useState<boolean | null>(null);
// Getting theme from local storage upon first render
useEffect(() => {
setDarkMode(getInitialMode);
}, []);
// Prefered theme stored in local storage
useEffect(() => {
localStorage.setItem("dark-mode", JSON.stringify(darkMode));
}, [darkMode]);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
return (
<ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
{children}
</ThemeContext.Provider>
);
};
The ThemeToggler responsible for updating the darkMode state operates properly during development (theme toggled and correct value console.loged upon clicking), however it doesn't do anything during production (console.logs an undefined state):
import React, { FC, useContext } from "react";
import { ThemeContext } from "../../context/ThemeContext";
const ThemeToggler: FC = () => {
const { darkMode, toggleDarkMode } = useContext(ThemeContext);
const toggleTheme = () => {
console.log(darkMode) // <--- darkMode is undefined during production
toggleDarkMode();
};
return (
<div className="theme-toggler">
<i
className={`fas ${darkMode ? "fa-sun" : "fa-moon"}`}
data-testid="dark-mode"
onClick={toggleTheme}
></i>
</div>
);
};
export default ThemeToggler;
The solutions/suggestions I've looked up before posting this were to no avail.
React Context API undefined in production — react and react-dom are on the same version.
Thanks in advance.
P.S. For those wondering why I am using both Redux and Context for global state management:
Context is best suited for low-frequency and simple state updates such as themes and authentication.
Redux is better for high-frequency and complex state updates in addition to providing a better debugging tool — Redux DevTools.
P.S.2 Yes, it is better – performance-wise – to install FontAwesome's dependencies rather than use a CDN.
Thanks for sharing the code. It's well written. By reading it i don't see any problem. Based on your component topology, as long as your ThemeToggler is defined under any page component, your darkMode can't be undefined.
Here's your topology of the site
<MyApp>
<Provider>
// A. will not work
<ThemeProvider>
<HomePage>
// B. should work
</HomePage>
</ThemeProvider>
// C. will not work
</Provider>
</MyApp>
Although your ThemeProvider is a custom provider, inside ThemeContext.Provider is defined with value {{ darkMode, toggleDarkMode }}. So in theory you can't get undefined unless your component ThemeToggler is not under a HomePage component. I marked two non working locations, any component put under location A or C will give you undefined.
Since you have a condition for HomePage, you can run into this problem if you are on other pages. So in general you should wrap the ThemeProvider on top of your router.
<ThemeProvider>
<AuthProvider>
{Component.name != "PageNotFound" && (
<Component {...pageProps} />
)}
</AuthProvider>
</ThemeProvider>
You get the point, you want to first go through a layer that theme always exist before you fire up a router.
You can confirm if this is the case by doing the following test.
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</ThemeProvider>
)
}
If this works in production, then it confirms it. To be honest, this problem also exists in the dev, however maybe due to your routes change too quickly, it normally hides these issues.
I have three pages that I want to share data between (these are the core of the web app) but I also have a bunch of blog pages that don't care about that data. Everywhere I've looked suggests putting the Provider in the _app.tsx file. If I understand that correctly if I wrapp MyApp with the provider, if someone goes straight to www.myurl.com/blog/my-blog-1 (from google), that will cause the provider to run its functions; even if that page won't call useContext
How do I only wrap three pages in the Provider and leave out the blog pages?
For example:
www.myurl.com -> I want this to use the shared data from the provider
www.myurl.com/my-functionality -> I want this to use the shared data from the provider
www.myurl.com/profile-> I want this to use the shared data from the provider
www.myurl.com/blog/* -> I don't want this to call the functions in the auth provider
Here's what my _app.tsx looks like:
import { AppProps } from 'next/app'
import '../styles/index.css'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
A bit late to the party but there is actually an official way to do this:
Use a per page layout.
First slightly tweak your _app.tsx file:
import type { ReactElement, ReactNode } from 'react';
import { AppProps } from 'next/app';
import type { NextPage } from 'next';
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
export const App = ({ Component, pageProps }: AppPropsWithLayout): unknown => {
// Use the custom layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(<Component {...pageProps} />);
};
Then in a Page component write the following:
// DashboardPage.ts that need to have a UserProvider and CompanyProvider
import Layout from '#components/Layout'; // Default Layout that is imported for all the page
import { ReactElement } from 'react';
import { UserProvider } from '#contexts/user';
import { CompanyProvider } from '#contexts/company';
const DashboardPage = (): JSX.Element => {
return <DashboardContainer />;
};
// Custom Layout to wrap the page within the User and company providers
DashboardPage.getLayout = function getLayout(page: ReactElement) {
return (
<Layout title="Dashboard page">
<UserProvider>
<CompanyProvider>{page}</CompanyProvider>
</UserProvider>
</Layout>
);
};
export default DashboardPage;
Well, this is an interesting issue.
Challenge is in the way Next implements file-based routing. Since there is no way to create a wrapper for the group of pages only out-of-the-box thing you can do is to wrap the App in the context providers. But that doesn't really resolve your issue.
SOOO... I think there is a workaround. If you want to be able to wrap a certain group of pages in the context provider, first, you need to replace the file-based router with the react-router.
There is a very interesting article on this topic by Andrea Carraro. I think you should try this out:
https://dev.to/toomuchdesign/next-js-react-router-2kl8
I will try to find another solution as well, but let me know did this worked for you.
There is workaround to wrap components of a specific path with a wrapper without react-router.
Check out this video:
https://www.youtube.com/watch?v=69-mnojSa0M
The video shows you how to wrap components of a sub-path with a nested layout. Create a nested layout and wrap that that layout with your Context Provider.
I'm kind new to Next.js and looking for the same question.
Maybe we can just use pathname and query from the useRouter hook and try some conditionals in our Contexts or in the custom _app
next/router
Very late to answer my question, but my approach might help someone in the future.
I used a page as a wrapper, and to navigate around I used query params.
For example
www.something.com/blog
wwww.somethiing.com/blog?id=1
www.something.come/blog?profileid=2
I use contextProvider and check the path pages in _app.js
myContextProvider.js
import React, { useState } from 'react';
const Context = React.createContext({});
export function myContextProvider({ children }) {
const [ state1, setState1 ] = useState(true);
return <Context.Provider value={{ state1, setState1 }}>
{children}
</Context.Provider>;
}
export default Context;
_app.js
import { useRouter } from 'youreFavoRouter';
import { myContextProvider } from './myContextProvider.js';
export default function MyApp({ Component, pageProps }) {
const router = useRouter();
const isBlogPage = router.pathname.includes('blog');
return (
<div>
{isBlogPage ? (
<Component {...pageProps} />
) : (
<myContextProvider>
<Component {...pageProps} />
</myContextProvider>
)}
</div>
);
}
Now all states and function on myContextProvider can see and use on pages than you choose.
I hope help anyone. :)
I'm using a provider for holding global location using gatsby's browser API for accessing location by consumer in other components. The problem is i can't change the global location from onRouteUpdate, Here is my code:
gatsby-browser.js:
import React, {useContext} from 'react';
import Provider,{ appContext } from './provider';
export const onRouteUpdate = ({ location, prevLocation }) => {
console.log('new pathname', location.pathname)
console.log('old pathname', prevLocation ? prevLocation.pathname : null)
// wanna set the new location for provider to use in other pages
// this code does not work
return(
<appContext.Consumer>
{context => {
context.changeLocation(location.pathname)
}})
</appContext.Consumer>
)
}
Provider.js:
import React, { useState } from 'react';
import { globalHistory as history } from '#reach/router'
export const appContext = React.createContext();
const Provider = props => {
const [location, setLocation] = useState(history.location);
return (
<appContext.Provider value={{
location,
changeLocation: (newLocation)=> {setLocation({location:newLocation}); console.log('changing')}
}}>
{props.children}
</appContext.Provider>
)
};
export default Provider;
Thanks.
onRouteUpdate isn’t expected to return React nodes, and so the React element you’re returning isn't going to be evaluated like you’d expect.
Since you’re only looking to store the current page, you don’t actually need to do anything manually onRouteUpdate because this functionality is available out-of-the-box with Gatsby.
// gatsby-browser.js
import React from "react"
import { appContext } from "src/provider"
export const wrapPageElement = ({ element, props }) => (
<AppContext.Provider value={{ location: props.location }}>
{React.createElement(element, props)}
</AppContext.Provider>
)
Is there a way with new react hooks API to replace a context data fetch?
If you need to load user profile and use it almost everywhere, first you create context and export it:
export const ProfileContext = React.createContext()
Then you import in top component, load data and use provider, like this:
import { ProfileContext } from 'src/shared/ProfileContext'
<ProfileContext.Provider
value={{ profile: profile, reloadProfile: reloadProfile }}
>
<Site />
</ProfileContext.Provider>
Then in some other components you import profile data like this:
import { ProfileContext } from 'src/shared/ProfileContext'
const context = useContext(profile);
But there is a way to export some function with hooks that will have state and share profile with any component that want to get data?
React provides a useContext hook to make use of Context, which has a signature like
const context = useContext(Context);
useContext accepts a context object (the value returned from
React.createContext) and returns the current context value, as given
by the nearest context provider for the given context.
When the provider updates, this Hook will trigger a rerender with the
latest context value.
You can make use of it in your component like
import { ProfileContext } from 'src/shared/ProfileContext'
const Site = () => {
const context = useContext(ProfileContext);
// make use of context values here
}
However if you want to make use of the same context in every component and don't want to import the ProfileContext everywhere you could simply write a custom hook like
import { ProfileContext } from 'src/shared/ProfileContext'
const useProfileContext = () => {
const context = useContext(ProfileContext);
return context;
}
and use it in the components like
const Site = () => {
const context = useProfileContext();
}
However as far a creating a hook which shares data among different component is concerned, Hooks have an instance of the data for them self and don'tshare it unless you make use of Context;
updated:
My previous answer was - You can use custom-hooks with useState for that purpose, but it was wrong because of this fact:
Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
The right answer how to do it with useContext() provided #ShubhamKhatri
Now i use it like this.
Contexts.js - all context export from one place
export { ClickEventContextProvider,ClickEventContext} from '../contexts/ClickEventContext'
export { PopupContextProvider, PopupContext } from '../contexts/PopupContext'
export { ThemeContextProvider, ThemeContext } from '../contexts/ThemeContext'
export { ProfileContextProvider, ProfileContext } from '../contexts/ProfileContext'
export { WindowSizeContextProvider, WindowSizeContext } from '../contexts/WindowSizeContext'
ClickEventContext.js - one of context examples:
import React, { useState, useEffect } from 'react'
export const ClickEventContext = React.createContext(null)
export const ClickEventContextProvider = props => {
const [clickEvent, clickEventSet] = useState(false)
const handleClick = e => clickEventSet(e)
useEffect(() => {
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [])
return (
<ClickEventContext.Provider value={{ clickEvent }}>
{props.children}
</ClickEventContext.Provider>
)
}
import and use:
import React, { useContext, useEffect } from 'react'
import { ClickEventContext } from 'shared/Contexts'
export function Modal({ show, children }) {
const { clickEvent } = useContext(ClickEventContext)
useEffect(() => {
console.log(clickEvent.target)
}, [clickEvent])
return <DivModal show={show}>{children}</DivModal>
}