I've currently set up the a React context to share the state and the current version looks like the following:
QueryContext.js
import { createContext } from "react";
const queryContext = createContext({
query: "",
setQuery: (value) => {},
});
export default queryContext;
App.js
import React, { useState } from "react";
import QueryContext from "./contexts/QueryContext";
import Header from "./Header";
import Nav from "./Nav";
import Main from "./Main";
import Footer from "./Footer";
const App = () => {
const [query, setQuery] = useState("");
return (
<>
<QueryContext.Provider value={{ query, setQuery }}>
<Header />
</QueryContext.Provider>
<Nav />
<QueryContext.Provider value={{ query, setQuery }}>
<Main />
</QueryContext.Provider>
<Footer />
</>
);
};
export default App;
Since Nav is placed between Header and Main, there was no choice but wrapping both with separate providers.
Should I just use props instead of context like the following?
App.js
import React, { useState } from "react";
import Header from "./Header";
import Nav from "./Nav";
import Main from "./Main";
import Footer from "./Footer";
const App = () => {
const [query, setQuery] = useState("");
return (
<>
<Header value={{ query, setQuery }} />
<Nav />
<Main value={{ query, setQuery }} />
<Footer />
</>
);
};
export default App;
Context is technology which allows developers to pass data through a lot of levels of application tree. The first thing to remember here - if something is global, it should probably be placed in context and accessed from anywhere with level lower. Passing data through several levels using properties will probably be bad practice - code quality will decrease along with performance, probably.
I assume, that in your case it is better to make context (you already did - QueryContext) and place it as higher as allowed:
import React, { useState } from "react";
import QueryContext from "./contexts/QueryContext";
import Header from "./Header";
import Nav from "./Nav";
import Main from "./Main";
import Footer from "./Footer";
const App = () => {
const [query, setQuery] = useState("");
return (
<QueryContext.Provider value={{ query, setQuery }}>
<Header />
<Nav />
<Main />
<Footer />
</QueryContext.Provider>
);
};
export default App;
All components which require data from QueryContext could extract it via useContext(QueryContext). As bonus against props way here - you are allowed to place Header and Main components as deep as you want, they will still have access to context.
In my opinion, the best solution here is to implement 2 components for each working with query: 1-st one will work with props, the 2-nd one with context, but you probably dont need it as long as it could be overengineering. Here is the example:
interface Props {
query: string;
setQuery: (value: string) => void;
}
function Header(props: Props) {
return (...);
}
function HeaderConnected() {
const {query, setQuery} = useContext(QueryContext);
return <Header query={query} setQuery={setQuery}/>
}
This component separation method provides developer 2 flexible ways of using component. You could do the same with components, attached to Redux storage - writing their pure variant along with "connected" to storage.
My remarks for your code here:
In your case, there is no need to create several context providers placed on the same level with same value. Make it single and move to upper level.
In my opinion, there is no need to make such prop as value containing query and its setter. This is rather unclear, what value is and seems that these values should be extracted from some global scope (context), but it depends on task context.
Related
I bought this template and am trying to understand which page getLayout in this code"{getLayout(<Component {...pageProps} />)}" goes to on initial load. I'm guessing it's a global variable somewhere, but I can't find it using the definitions. I'm trying to under the next.js documentation, but I'm having issues. If anyone has a good tutorial for this I'll happily take it.
import type { AppProps } from 'next/app';
import { appWithTranslation } from 'next-i18next';
import { SessionProvider } from 'next-auth/react';
import '#/assets/css/main.css';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify';
import { ModalProvider } from '#/components/ui/modal/modal.context';
import ManagedModal from '#/components/ui/modal/managed-modal';
import ManagedDrawer from '#/components/ui/drawer/managed-drawer';
import DefaultSeo from '#/components/seo/default-seo';
import { SearchProvider } from '#/components/ui/search/search.context';
import PrivateRoute from '#/lib/private-route';
import { CartProvider } from '#/store/quick-cart/cart.context';
import SocialLogin from '#/components/auth/social-login';
import { NextPageWithLayout } from '#/types';
import QueryProvider from '#/framework/client/query-provider';
import { getDirection } from '#/lib/constants';
import { useRouter } from 'next/router';
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function CustomApp({
Component,
pageProps: { session, ...pageProps },
}: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
const authenticationRequired = Component.authenticationRequired ?? false;
const { locale } = useRouter();
const dir = getDirection(locale);
return (
<div dir={dir}>
<SessionProvider session={session}>
<QueryProvider pageProps={pageProps}>
<SearchProvider>
<ModalProvider>
<CartProvider>
<>
<DefaultSeo />
{authenticationRequired ? (
<PrivateRoute>
{getLayout(<Component {...pageProps} />)}
</PrivateRoute>
) : (
getLayout(<Component {...pageProps} />)
)}
<ManagedModal />
<ManagedDrawer />
<ToastContainer autoClose={2000} theme="colored" />
<SocialLogin />
</>
</CartProvider>
</ModalProvider>
</SearchProvider>
</QueryProvider>
</SessionProvider>
</div>
);
}
export default appWithTranslation(CustomApp);
Basically, you can define a per component layout. In order to do so, when you are defining a component, you add a property named getLayout. Here's an example, for a better understanding.
// ...
const Component: React.FC = () => <div>
I have a custom layout
</div>
// Here we define a layout, let's imagine it's a component
// we have inside /layouts and we have previously imported it
Component.getLayout = (page: React.ReactElement) =>
<LayoutComponent>{page}</LayoutComponent>
Now, when a page is rendered (note that in a Next JS app, all pages are rendered as a children of what is inside _app.{js,jsx,ts,tsx}) your code checks if the prop getLayout has been defined or not. Then, it is basically calling such function, if exists, otherwise it renders the base component.
I'm using Next.js with context API and styled components and I can't seem to get getStaticProps working.
I have read other posts and often they talk about the custom _app which I do have but I never ran into the issue before using context API. I have also tried the getInitialProps function and to no avail.
I should also note that even after not including the context wrapper I don't get a response from a function so I'm not at all sure of where to look.
Here is my code. Can you see what's going on?
import React from 'react';
import fetch from 'node-fetch';
export default function Header(props) {
console.log(props.hi);
return <div>Hey dis header</div>;
}
export async function getStaticProps(context) {
return {
props: {
hi: 'hello',
},
};
}
I have tried logging from the function but nothing is logging so I would imagine the problem is that the function isn't running for whatever reason.
Heres my custom _app file
import { GlobalContextWrapper } from 'context';
import Header from 'components/header/Header';
import App from 'next/app';
function MyApp({ Component, pageProps }) {
return (
<GlobalContextWrapper>
<Header />
<Component {...pageProps} />
<p>footer</p>
</GlobalContextWrapper>
);
}
MyApp.getInitialProps = async (appContext) => {
// calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext);
return { ...appProps };
};
export default MyApp;
Here is my context file
import { useReducer } from 'react';
import initialState from './intialState';
import reducer from './reducer';
import GlobalStyle from '../GlobalStyle';
import theme from '../theme';
import { ThemeProvider } from 'styled-components';
export const GlobalContext = React.createContext();
export function GlobalContextWrapper({ children }) {
const [globalState, dispatch] = useReducer(reducer, initialState);
return (
<GlobalContext.Provider value={{ globalState, dispatch }}>
<GlobalStyle />
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</GlobalContext.Provider>
);
}
The issue was that i was not exporting this function from a page but instead a component and a custom app file.
Does anyone know a way i can get around this? The problem is that i have a header that gets data from a response and i want this header to be shown on every page without having to manually add it to each page along with repeating the getStaticProps function
A solution based on your code is just getting data in your _app.js - getInitialProps and pass to the Header
function MyApp({ Component, pageProps }) {
return (
<GlobalContextWrapper>
<Header data={pageProps.header}/>
<Component {...pageProps} />
<p>footer</p>
</GlobalContextWrapper>
);
}
MyApp.getInitialProps = async (appContext) => {
// calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext);
const headerData = ....
return { ...appProps, header: headerData };
};
I'm sharing code here but I'm getting an error
TypeError: Cannot destructure property 'userAge' of 'Object(...)(...)' as it is undefined.
Child component:
import React, { useContext, createContext, useState } from "react";
export const AgeIs = createContext();
export default function Childc() {
const [age, setAge] = useState("24");
return(
<AgeIs.Provider value={{ userAge: [age, setAge] }}>
<ParentComp />
</AgeIs.Provider>
)}
Sending age to parent component:
import { AgeIs } from "./Childc";
const { userAge } = useContext(AgeIs);
export default function ParentComp() {
const [age, setAge] = userAge;
return (
<div>
<h1>{age}</h1>
</div>
)}
If useContext doesnt cater this problem please suggest a solution for this kind of problem. Thanks
useContext should be used inside the component.
Something like this should work:
function ParentComp() {
const { userAge } = useContext(AgeIs);
const [age, setAge] = userAge;
return (
<div>
<h1>{age}</h1>
<button onClick={() => setAge((age) => age + 1)}>Increase Age</button>
</div>
);
}
More in depth explanation
useContext, useState and others are all hooks. This define a local state for a component. If you use it outside of the component then it will be called only when the file is imported, meaning that
the Context was not defined when you requested it for the first time
the Context will not update whenever the component is updated or
created for the first time
You can read more about how hooks work in the React docs
I have a stack navigator which opens a screen and the screen uses Context and providers but useContext seems to return undefined.
We have this:
<Stack.Navigator>
<Stack.Screen name="preLogin" component={SomeScreen} />
<Stack.Screen name="postLogin" component={SomeScreen2} />
</Stack.Navigator>
In SomeScreen2 we have this:
import React from 'react';
import {SampleContextProvider} from './provider/SampleContext';
import WelcomeScreen from './welcome';
const SomeScreen2Container = ({navigation}) => {
return (
<>
<SampleContextProvider>
<WelcomeScreen navigation={navigation}/>
</SampleContextProvider>
</>
);
};
export default SomeScreen2Container;
SampleContext:
import React from 'react';
const SampleContext = React.createContext({a:"a",b:"b"});
const SampleContextProvider = ({children}) => {
return (
<SampleContext.Provider>
{children}
</SampleContext.Provider>
);
};
export { SampleContext, SampleContextProvider };
And in the WelcomeScreen I am using useContext:
import {SampleContext} from '../provider/SampleContext';
const ctx = useContext(SampleContext);
console.log("contextx=>", ctx);
It's undefined.
Feel free to ask for more information
Change your SampleContext file to this:
import React from 'react';
const SampleContext = React.createContext();
const SampleContextProvider = ({children}) => {
return (
<SampleContext.Provider value={{a: 'a', b: 'b'}}>
{children}
</SampleContext.Provider>
);
};
export {SampleContext, SampleContextProvider};
I defined your default context value in the provider value prop instead of as a parameter of createContext.
The value prop of your provider overrides the default value set in createContext. Meaning if you don't set value if you use a provider, the default value will override to undefined.
For more info look at this: React.createContext point of defaultValue?.
In your welcome screen try doing this
import {SampleContext} from '../provider/SampleContext';
const {a,b}= useContext(SampleContext);
console.log("contextx=> ", a+' '+b);
i think destructuring the value might help you confirm if the sampleContext has the context values or not !
I'm trying to switch from using redux in my new projects to using hooks. My understanding is that I can use a custom hook such as this one to an load it from various components to access the same state similar to how redux would let me access state.
import { useState } from 'react';
export const useSelectedStore = () => {
const [selectedStore, setSelectedStore] = useState();
return [
selectedStore,
setSelectedStore,
];
};
The issue that I'm having is that I have a page with an item where when the user clicks an item I need to set the selected store and then redirect them to the page. Here is the click action within the component:
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Home from './pages/Home/Home';
import Stores from './pages/Stores/Stores';
import StoreDetails from './pages/StoreDetails/StoreDetails';
function App() {
return (
<Router>
<div>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/stores">
<Stores />
</Route>
<Route path="/store-details">
<StoreDetails />
</Route>
</Route>
</Switch>
</div>
</Router>
);
}
export default App;
import React, { useState } from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useSelectedStore } from '../../hooks/useSelectedStore';
function Stores(props) {
const { history } = props;
const [selectedStore, setSelectedStore] = useSelectedStore();
const goToStoreDetails = (index) => {
// Now add our store data
setSelectedStore(storeRequestDetails.stores[index]);
console.log(selectedStore);
history.push('/store-details');
};
The console log outputs undefined because the store hasn't been set yet so the history push happens before the value truly gets set.
I tried to use useEffect like this in the component to pick up on the change and handle the change before redirect but the state is getting reset on the component for the store-details page.
import React, { useEffect, useState } from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useSelectedStore } from '../../hooks/useSelectedStore';
function Stores(props) {
const { history } = props;
const [selectedStore, setSelectedStore] = useSelectedStore();
useEffect(() => {
if (selectedStore) {
console.log(selectedStore);
history.push('/store-details');
}
}, [selectedStore]);
const goToStoreDetails = (index) => {
// Now add our store data
setSelectedStore(storeRequestDetails.stores[index]);
};
The console log here will show the actual selected store details but again once the next page loads like so the state is now undefined again.
import React from 'react';
import { useSelectedStore } from '../../hooks/useSelectedStore';
function StoreDetails() {
const [selectedStore, setSelectedStore] = useSelectedStore();
console.log(selectedStore); // Returns undefined
How do I share the state correctly between these two components?
My understanding is that I can use a custom hook such as this one to an load it from various components to access the same state similar to how redux would let me access state.
The custom hook you showed will cause multiple components to each create their own independent local states. These sorts of custom hooks can be useful for code reuse, but it doesn't let components share data.
If you want to share data between components, the general react approach is to move the state up the tree until it's at the common ancestor of every component that needs it. Then the data can be passed down either through props or through context. If you want the data to be globally available, then you'll move it to the top of the component tree and almost certainly make use of context rather than props.
This is what redux does: You set up the store near the top of the tree, and then it uses context to make it available to descendants. You can do similar things yourself, by using a similar pattern. Somewhere near the top of the tree (perhaps in App, but you could also move it elsewhere), create state, and share it via context:
export const SelectedStoreContext = React.createContext();
function App() {
const value = useState();
return (
<SelectedStoreContext.Provider value={value}>
<Router>
<div>
//etc
</div>
</Router>
</StoreContext.Provider>
);
}
And to use it:
function StoreDetails() {
const [selectedStore, setSelectedStore] = useContext(SelectedStoreContext);
If you want to call this useSelectedStore and slightly hide the fact that context is involved, you can create a custom hook like this:
const useSelectedStore = () => useContext(SelectedStoreContext);
// used like
const [selectedStore, setSelectedStore] = useSelectedStore();
Customs hook do not share any data. They are just compositions of the built-in hooks. But you can still share the store state by using react context:
/* StoreContext.js */
const StoreContext = createContext(null);
export const StoreProvider = ({children}) => {
const storeState = useState();
return (
<StoreContext.Provider value={storeState}>
{children}
</StoreContext.Provider>
);
};
export const useStore = () => useContext(StoreContext);
Usage:
/* App.js */
/* wrap your components using the store in the StoreProvider */
import {StoreProvider} from './StoreContext';
const App = () => (
<StoreProvider>
{/* some children */}
</StoreProvider>
);
and
/* StoreDetails.js */
/* use the useStore hook in your components */
import {useStore} from './StoreContext';
function StoreDetails() {
const [selectedStore, setSelectedStore] = useStore();
// ...
}