Related
I'm using React useContext to avoid prop-drilling, and building static pages in NextJS, as described in this Technouz post (NB: this is not about the NextJS getStaticProps context parameter).
The basic functionality is working; however, I can't figure out the right way to update the context from components farther down the chain.
At a high level, I have this:
// pages/_app.js
function MyApp({ Component, pageProps }) {
const [ headerData, setHeaderData ] = useState( {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
} );
return (
<HeaderProvider value={{ headerData, setHeaderData }}>
<Header />
<Component {...pageProps} />
</HeaderProvider>
)
}
// components/Header.js
export default function Header() {
const { headerData } = useHeader();
return (
<header>
{ headerData.urgentBanner && <UrgentBanner {...headerData.urgentBanner}/> }
<Navbar />
</header>
)
}
// lib/context/header.js
const HeaderContext = createContext();
export function HeaderProvider({value, children}) {
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
The Navbar component also uses the context.
That all works. I query the data from a headless CMS using getStaticProps, and everything gets passed through pageProps, and when I run npm run build, I get all of my static pages with the appropriate headers.
But, now I'm extending things, and not all pages are the same. I use different models at the CMS level, and want to display different headers for landing pages.
Inside of [pages].js, I handle that thusly:
const Page = ({ data }) => {
switch (data.pageType) {
case 'landing-page':
return (
<PageLandingPage data={data} />
);
case 'page':
default:
return (
<PageStandard data={data} />
);
}
}
Now, if we're building a static landing page instead of a static standard page, the whole hierarchy would look something like this:
<HeaderProvider value={{ headerData, setHeaderData }}>
<Header>
{ headerData.urgentBanner && <UrgentBanner {...headerData.urgentBanner}/> }
<Navbar>
<ul>
{menu && <MenuList type='primary' menuItems={menu.menuItems} />}
</ul>
</Navbar>
</Header>
<PageLandingPage {...pageProps}> // *** Location 2
<LandingPageSection>
<Atf> // *** Location 1
<section>
{ socialProof && <SocialProof { ...socialProof } />}
<Attention { ...attentionDetails }/>
</section>
</Atf>
</LandingPageSection>
</PageLandingPage>
</HeaderProvider>
Location 1 and Location 2 are where I want to update the context. I thought I had that working, by doing the following at Location 1:
// components/Atf.js
export default function Atf({content}) {
// this appeared to work
const { headerData, setHeaderData } = useHeader();
setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
)
return (
<section>
{ socialProof && <SocialProof { ...socialProof } />}
<Attention { ...attentionDetails }/>
</section>
)
}
I say "thought", because I was, in fact, getting my <UrgentBanner> component properly rendered on the landing pages. However, when digging into the fact that I can't get it to work at Location 2, I discovered that I was actually getting warnings in the console about "cannot update a component while rendering a different component" (I'll come back to this).
Now to Location 2. I tried to do the same thing here:
// components/PageLandingPage.js
const PageLandingPage = ({ data }) => {
const giveawayLandingPage = data.giveawayLandingPage;
// this, to me, seems the same as above, but isn't working at all
if (giveawayLandingPage?.headerMenu) {
const { headerData, setHeaderData } = useHeader();
setHeaderData(
{
...headerData,
menu: { ...giveawayLandingPage.headerMenu }
}
);
}
return (
<div>
{giveawayLandingPage.lpSection.map(section => <LandingPageSection details={section} key={section.id} />)}
</div>
)
}
To me, that appears that I'm doing the same thing that "worked" in the <Atf> component, but ... it's not working.
While trying to figure this out, I came across the aforementioned error in the console. Specifically, "Cannot update a component (MyApp) while rendering a different component (Atf)." And I guess this is getting to the heart of the problem — something about how/when/in which order NextJS does its rendering when it comes to generating its static pages.
Based on this answer, I initially tried wrapping the call in _app.js in a useEffect block:
// pages/_app.js
...
/* const [ headerData, setHeaderData ] = useState( {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
} ); */
const [ headerData, setHeaderData ] = useState({});
useEffect(() => {
setHeaderData({
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
});
}, []);
But that didn't have any impact. So, based on this other answer, which is more about NextJS, though it's specific to SSR, not initial static page creation, I also wrapped the setState call in the <Atf> component at Location 1 in a useEffect:
// components/Atf.js
...
const { headerData, setHeaderData } = useHeader();
/* setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
) */
useEffect(() => {
setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
)
}, [setHeaderData])
That did stop the warning from appearing in the console ... but it also stopped the functionality from working — it no longer renders my <UrgentBanner> component on the landing page pages.
I have a moderately good understanding of component rendering in React, but really don't know what NextJS is doing under the covers when it's creating its initial static pages. Clearly I'm doing something wrong, so, how do I get my context state to update for these different types of static pages?
(I presume that once I know the Right Way to do this, my Location 2 problem will be solved as well).
I ended up fixing this by moving from useState to useReducer, and then setting all of the state, including the initial state, at the page level. Now, _app.js is simplified to
function MyApp({ Component, pageProps }) {
return (
<HeaderProvider>
<Header />
<Component {...pageProps} />
</HeaderProvider>
)
}
export default MyApp
And the context hook setup uses the reducer and provides it back to the provider:
// lib/context/header.js
const initialState = {};
const HeaderContext = createContext(initialState);
function HeaderProvider({ children }) {
const [headerState, dispatchHeader] = useReducer((headerState, action) => {
switch (action.type) {
case 'update':
const newState = { ...headerState, ...action.newState };
return newState;
default:
throw new Error('Problem updating header state');
}
}, initialState);
return (
<HeaderContext.Provider value={{ headerState, dispatchHeader }}>
{children}
</HeaderContext.Provider>
);
}
function useHeader() {
return useContext(HeaderContext);
}
export { HeaderProvider, useHeader }
Then, everywhere you want to either get the state or set the state, as long as you're inside of the <Provider>, you're good to go. This was a little confusing at first, because it's not obvious that when you useContext, what it's doing is returning the current value, and the value is provided both with the state, and with the dispatch function, so when you want to set something, you query the "value", but destructure to get the "setter" (i.e., the dispatch function).
So, for example, in my "location 2" from the initial question, it now looks like
import React, { useEffect } from 'react';
import { useHeader } from '../lib/context/header';
const PageLandingPage = ({ data }) => {
const giveawayLandingPage = data.giveawayLandingPage;
// here's where we get the "setter" through destructuring the `value`
// let foo = useHeader();
// console.log(foo);
// > { headerState, dispatchHeader }
const { dispatchHeader } = useHeader();
useEffect(() => {
dispatchHeader({
newState: {
menu: { ...giveawayLandingPage.headerMenu }
},
type: 'update'
});
}, []);
...
I am getting this warning in react:
index.js:1 Warning: Cannot update a component (`ConnectFunction`)
while rendering a different component (`Register`). To locate the
bad setState() call inside `Register`
I went to the locations indicated in the stack trace and removed all setstates but the warning still persists. Is it possible this could occur from redux dispatch?
my code:
register.js
class Register extends Component {
render() {
if( this.props.registerStatus === SUCCESS) {
// Reset register status to allow return to register page
this.props.dispatch( resetRegisterStatus()) # THIS IS THE LINE THAT CAUSES THE ERROR ACCORDING TO THE STACK TRACE
return <Redirect push to = {HOME}/>
}
return (
<div style = {{paddingTop: "180px", background: 'radial-gradient(circle, rgba(106,103,103,1) 0%, rgba(36,36,36,1) 100%)', height: "100vh"}}>
<RegistrationForm/>
</div>
);
}
}
function mapStateToProps( state ) {
return {
registerStatus: state.userReducer.registerStatus
}
}
export default connect ( mapStateToProps ) ( Register );
function which triggers the warning in my registerForm component called by register.js
handleSubmit = async () => {
if( this.isValidForm() ) {
const details = {
"username": this.state.username,
"password": this.state.password,
"email": this.state.email,
"clearance": this.state.clearance
}
await this.props.dispatch( register(details) )
if( this.props.registerStatus !== SUCCESS && this.mounted ) {
this.setState( {errorMsg: this.props.registerError})
this.handleShowError()
}
}
else {
if( this.mounted ) {
this.setState( {errorMsg: "Error - registration credentials are invalid!"} )
this.handleShowError()
}
}
}
Stacktrace:
This warning was introduced since React V16.3.0.
If you are using functional components you could wrap the setState call into useEffect.
Code that does not work:
const HomePage = (props) => {
props.setAuthenticated(true);
const handleChange = (e) => {
props.setSearchTerm(e.target.value.toLowerCase());
};
return (
<div key={props.restInfo.storeId} className="container-fluid">
<ProductList searchResults={props.searchResults} />
</div>
);
};
Now you can change it to:
const HomePage = (props) => {
// trigger on component mount
useEffect(() => {
props.setAuthenticated(true);
}, []);
const handleChange = (e) => {
props.setSearchTerm(e.target.value.toLowerCase());
};
return (
<div key={props.restInfo.storeId} className="container-fluid">
<ProductList searchResults={props.searchResults} />
</div>
);
};
I just had this issue and it took me a bit of digging around before I realised what I'd done wrong – I just wasn't paying attention to how I was writing my functional component.
I was doing this:
const LiveMatches = (props: LiveMatchesProps) => {
const {
dateMatches,
draftingConfig,
sportId,
getDateMatches,
} = props;
if (!dateMatches) {
const date = new Date();
getDateMatches({ sportId, date });
};
return (<div>{component stuff here..}</div>);
};
I had just forgotten to use useEffect before dispatching my redux call of getDateMatches()
So it should have been:
const LiveMatches = (props: LiveMatchesProps) => {
const {
dateMatches,
draftingConfig,
sportId,
getDateMatches,
} = props;
useEffect(() => {
if (!dateMatches) {
const date = new Date();
getDateMatches({ sportId, date });
}
}, [dateMatches, getDateMatches, sportId]);
return (<div>{component stuff here..}</div>);
};
please read the error message thoroughly, mine was pointing to SignIn Component that had a bad setState. which when i examined, I had an onpress that was not an Arrow function.
it was like this:
onPress={navigation.navigate("Home", { screen: "HomeScreen" })}
I changed it to this:
onPress={() => navigation.navigate("Home", { screen: "HomeScreen" }) }
My error message was:
Warning: Cannot update a component
(ForwardRef(BaseNavigationContainer)) while rendering a different
component (SignIn). To locate the bad setState() call inside
SignIn, follow the stack trace as described in
https://reactjs.org/link/setstate-in-render
in SignIn (at SignInScreen.tsx:20)
I fixed this issue by removing the dispatch from the register components render method to the componentwillunmount method. This is because I wanted this logic to occur right before redirecting to the login page. In general it's best practice to put all your logic outside the render method so my code was just poorly written before. Hope this helps anyone else in future :)
My refactored register component:
class Register extends Component {
componentWillUnmount() {
// Reset register status to allow return to register page
if ( this.props.registerStatus !== "" ) this.props.dispatch( resetRegisterStatus() )
}
render() {
if( this.props.registerStatus === SUCCESS ) {
return <Redirect push to = {LOGIN}/>
}
return (
<div style = {{paddingTop: "180px", background: 'radial-gradient(circle, rgba(106,103,103,1) 0%, rgba(36,36,36,1) 100%)', height: "100vh"}}>
<RegistrationForm/>
</div>
);
}
}
I think that this is important.
It's from this post that #Red-Baron pointed out:
#machineghost : I think you're misunderstanding what the message is warning about.
There's nothing wrong with passing callbacks to children that update state in parents. That's always been fine.
The problem is when one component queues an update in another component, while the first component is rendering.
In other words, don't do this:
function SomeChildComponent(props) {
props.updateSomething();
return <div />
}
But this is fine:
function SomeChildComponent(props) {
// or make a callback click handler and call it in there
return <button onClick={props.updateSomething}>Click Me</button>
}
And, as Dan has pointed out various times, queuing an update in the same component while rendering is fine too:
function SomeChildComponent(props) {
const [number, setNumber] = useState(0);
if(props.someValue > 10 && number < 5) {
// queue an update while rendering, equivalent to getDerivedStateFromProps
setNumber(42);
}
return <div>{number}</div>
}
If useEffect cannot be used in your case or if the error is NOT because of Redux
I used setTimeout to redirect one of the two useState variables to the callback queue.
I have one parent and one child component with useState variable in each of them. The solution is to wrap useState variable using setTimeout:
setTimeout(() => SetFilterData(data), 0);
Example below
Parent Component
import ExpenseFilter from '../ExpensesFilter'
function ExpensesView(props) {
const [filterData, SetFilterData] = useState('')
const GetFilterData = (data) => {
// SetFilterData(data);
//*****WRAP useState VARIABLE INSIDE setTimeout WITH 0 TIME AS BELOW.*****
setTimeout(() => SetFilterData(data), 0);
}
const filteredArray = props.expense.filter(expenseFiltered =>
expenseFiltered.dateSpent.getFullYear().toString() === filterData);
return (
<Window>
<div>
<ExpenseFilter FilterYear = {GetFilterData}></ExpenseFilter>
Child Component
const ExpensesFilter = (props) => {
const [filterYear, SetFilterYear] = useState('2022')
const FilterYearListener = (event) => {
event.preventDefault()
SetFilterYear(event.target.value)
}
props.FilterYear(filterYear)
return (
Using React and Material UI (MUI)
I changed my code from:
<IconButton onClick={setOpenDeleteDialog(false)}>
<Close />
</IconButton>
To:
<IconButton onClick={() => setOpenDeleteDialog(false)}>
<Close />
</IconButton>
Simple fix
If you use React Navigation and you are using the setParams or setOptions you must put these inside method componentDidMount() of class components or in useEffects() hook of functional components.
Minimal reproducing example
I was a bit confused as to what exactly triggers the problem, having a minimal immediately runnable example helped me grasp it a little better:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7.14.7/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function NotMain(props) {
props.setN(1)
return <div>NotMain</div>
}
function Main(props) {
const [n, setN] = React.useState(0)
return <>
<NotMain setN={setN} />
<div>Main {n}</div>
</>
}
ReactDOM.render(
<Main/>,
document.getElementById('root')
);
</script>
</body>
</html>
fails with error:
react-dom.development.js:61 Warning: Cannot update a component (`Main`) while rendering a different component (`NotMain`). To locate the bad setState() call inside `NotMain`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
followed by a stack trace:
at NotMain (<anonymous>:16:9)
at Main (<anonymous>:21:31)
Presumably 16:9 would be the exact line where props.setN(1) is being called from, but the line numbers are a bit messed up because of the Babel JSX translation.
The solution like many other answers said is to do instead:
function NotMain(props) {
React.useEffect(() => { props.setN(1) }, [])
return <div>NotMain</div>
}
Intuitively, I think that the general idea of why this error happens is that:
You are not supposed to updat state from render methods, otherwise it could lead to different results depending on internal the ordering of how React renders things.
and when using functional components, the way to do that is to use hooks. In our case, useEffect will run after rendering is done, so we are fine doing that from there.
When using classes this becomes slightly more clear and had been asked for example at:
Calling setState in render is not avoidable
Calling setState() in React from render method
When using functional components however, things are conceptually a bit more mixed, as the component function is both the render, and the code that sets up the callbacks.
I was facing same issue, The fix worked for me was if u are doing
setParams/setOptions
outside of useEffect then this issue is occurring. So try to do such things inside useEffect. It'll work like charm
TL;DR;
For my case, what I did to fix the warning was to change from useState to useRef
react_devtools_backend.js:2574 Warning: Cannot update a component (`Index`) while rendering a different component (`Router.Consumer`). To locate the bad setState() call inside `Router.Consumer`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
at Route (http://localhost:3000/main.bundle.js:126692:29)
at Index (http://localhost:3000/main.bundle.js:144246:25)
at Switch (http://localhost:3000/main.bundle.js:126894:29)
at Suspense
at App
at AuthProvider (http://localhost:3000/main.bundle.js:144525:23)
at ErrorBoundary (http://localhost:3000/main.bundle.js:21030:87)
at Router (http://localhost:3000/main.bundle.js:126327:30)
at BrowserRouter (http://localhost:3000/main.bundle.js:125948:35)
at QueryClientProvider (http://localhost:3000/main.bundle.js:124450:21)
The full code for the context of what I did (changed from the lines with // OLD: to the line above them). However this doesn't matter, just try changing from useState to useRef!!
import { HOME_PATH, LOGIN_PATH } from '#/constants';
import { NotFoundComponent } from '#/routes';
import React from 'react';
import { Redirect, Route, RouteProps } from 'react-router-dom';
import { useAccess } from '#/access';
import { useAuthContext } from '#/contexts/AuthContext';
import { AccessLevel } from '#/models';
type Props = RouteProps & {
component: Exclude<RouteProps['component'], undefined>;
requireAccess: AccessLevel | undefined;
};
export const Index: React.FC<Props> = (props) => {
const { component: Component, requireAccess, ...rest } = props;
const { isLoading, isAuth } = useAuthContext();
const access = useAccess();
const mounted = React.useRef(false);
// OLD: const [mounted, setMounted] = React.useState(false);
return (
<Route
{...rest}
render={(props) => {
// If in indentifying authentication state as the page initially loads, render a blank page
if (!mounted.current && isLoading) return null;
// OLD: if (!mounted && isLoading) return null;
// 1. Check Authentication is one step
if (!isAuth && window.location.pathname !== LOGIN_PATH)
return <Redirect to={LOGIN_PATH} />;
if (isAuth && window.location.pathname === LOGIN_PATH)
return <Redirect to={HOME_PATH} />;
// 2. Authorization is another
if (requireAccess && !access[requireAccess])
return <NotFoundComponent />;
mounted.current = true;
// OLD: setMounted(true);
return <Component {...props} />;
}}
/>
);
};
export default Index;
My example.
Code with that error:
<Form
initialValues={{ ...kgFormValues, dataflow: dataflows.length > 0 ? dataflows[0].df_tpl_key : "" }}
onSubmit={() => {}}
render={({values, dirtyFields }: any) => {
const kgFormValuesUpdated = {
proj_key: projectKey,
name: values.name,
description: values.description,
public: values.public,
dataflow: values.dataflow,
flavours: flavoursSelected,
skipOCR: values.skipOCR
};
if (!_.isEqual(kgFormValues, kgFormValuesUpdated)) {
setNewKgFormValues(kgFormValuesUpdated);
}
Working Code:
<Form
initialValues={{ ...kgFormValues, dataflow: dataflows.length > 0 ? dataflows[0].df_tpl_key : "" }}
onSubmit={() => {}}
render={({ values, dirtyFields }: any) => {
useEffect(() => {
const kgFormValuesUpdated = {
proj_key: projectKey,
name: values.name,
description: values.description,
public: values.public,
dataflow: values.dataflow,
flavours: flavoursSelected,
skipOCR: values.skipOCR
};
if (!_.isEqual(kgFormValues, kgFormValuesUpdated)) {
setNewKgFormValues(kgFormValuesUpdated);
}
}, [values]);
return (
I had the same problem. I was setting some state that was storing a function like so:
// my state definition
const [onConfirm, setOnConfirm] = useState<() => void>();
// then I used this piece of code to update the state
function show(onConfirm: () => void) {
setOnConfirm(onConfirm);
}
The problem was from setOnConfirm. In React, setState can take the new value OR a function that returns the new value. In this case React wanted to get the new state from calling onConfirm which is not correct.
changing to this resolved my issue:
setOnConfirm(() => onConfirm);
I was able to solve this after coming across a similar question in GitHub which led me to this comment showing how to pinpoint the exact line within your file causing the error. I wasn't aware that the stack trace was there. Hopefully this helps someone!
See below for my fix. I simply converted the function to use callback.
Old code
function TopMenuItems() {
const dispatch = useDispatch();
function mountProjectListToReduxStore(projects) {
const projectDropdown = projects.map((project) => ({
id: project.id,
name: project.name,
organizationId: project.organizationId,
createdOn: project.createdOn,
lastModifiedOn: project.lastModifiedOn,
isComplete: project.isComplete,
}));
projectDropdown.sort((a, b) => a.name.localeCompare(b.name));
dispatch(loadProjectsList(projectDropdown));
dispatch(setCurrentOrganizationId(projectDropdown[0].organizationId));
}
};
New code
function TopMenuItems() {
const dispatch = useDispatch();
const mountProjectListToReduxStore = useCallback((projects) => {
const projectDropdown = projects.map((project) => ({
id: project.id,
name: project.name,
organizationId: project.organizationId,
createdOn: project.createdOn,
lastModifiedOn: project.lastModifiedOn,
isComplete: project.isComplete,
}));
projectDropdown.sort((a, b) => a.name.localeCompare(b.name));
dispatch(loadProjectsList(projectDropdown));
dispatch(setCurrentOrganizationId(projectDropdown[0].organizationId));
}, [dispatch]);
};
My case was using setState callback, instead of setState + useEffect
BAD ❌
const closePopover = useCallback(
() =>
setOpen((prevOpen) => {
prevOpen && onOpenChange(false);
return false;
}),
[onOpenChange]
);
GOOD ✅
const closePopover = useCallback(() => setOpen(false), []);
useEffect(() => onOpenChange(isOpen), [isOpen, onOpenChange]);
I got this when I was foolishly invoking a function that called dispatch instead of passing a reference to it for onClick on a button.
const quantityChangeHandler = (direction) => {
dispatch(cartActions.changeItemQuantity({title, quantityChange: direction}));
}
...
<button onClick={() => quantityChangeHandler(-1)}>-</button>
<button onClick={() => quantityChangeHandler(1)}>+</button>
Initially, I was directly calling without the fat arrow wrapper.
Using some of the answers above, i got rid of the error with the following:
from
if (value === "newest") {
dispatch(sortArticlesNewest());
} else {
dispatch(sortArticlesOldest());
}
this code was on my component top-level
to
const SelectSorting = () => {
const dispatch = useAppDispatch();
const {value, onChange} = useSelect();
useEffect(() => {
if (value === "newest") {
dispatch(sortArticlesNewest());
} else {
dispatch(sortArticlesOldest());
}
}, [dispatch, value]);
all I am trying to do where to put generateQuestion function and if I put it in App.js
how to pass props form App.js to Quiz.js
please if you have a better implementation of routing & generateQuestion function feel free to leave your recommendations
The generateQuestion callback is asynchronous, so you won't be able to send data in route state via the Link, but what you can do is move the onClick handler to the Link and call event.preventDefault on the click event to prevent the navigation action, and issue an imperative navigation after the questions have been fetched.
Example:
import { Link, useNavigate } = 'react-router-dom';
...
const navigate = useNavigate();
...
generateQuestions = async (event) => {
event.preventDefault();
try {
const data = await questionsData();
const arrData = data.map(element => ({
...element,
id: nanoid(),
}));
navigate("/quiz", { state: { questions: arrData } });
} catch(error) {
// handle rejected Promise/error/etc...
}
}
...
<Link to="/quiz" onClick={generateQuestions}>
<button type="button">
Start Quiz
</button>
</Link>
...
In the Quiz component use the useLocation hook to access the passed route state.
import { useLocation } from 'react-router-dom';
...
const { state } = useLocation();
const { questions } = state || {};
For future questions I recommend that you actually write the code instead of pasting a picture so that it's easier to try and answer you.
That aside, I had a similar situation, what I did was the following
(Keep in mind that item.document is the property of the data that you want to pass through)
Where you are getting the data in my case I created a new class:
const GetData: React.FunctionComponent<{}> = () => {
const [information, setInformation] = useState([]);
useEffect(() => {
.
.
//all the request stuff for getting the data
.
.
}
return (
<div>
{information.map((item) => (
<div key={item.id}>
<Link to="/yourUrl" state={{from:'test',
body:`${item.document}`}}>
<div>
<h3 >{item.name}</h3>
<p>{item.description}</p>
</div>
</Link>
</div>
))}
</div>
);
}
Remember that .map is in case you want to dynamically creat components depending on the data that you're getting.
Where you want to see and use the data passed:
const Impementation: React.FunctionComponent = () => {
const location = useLocation()
const [itemData, setItemData] = React.useState({
body: '',
})
.
.
.
useEffect(() => {
.
.
.
if (location.state) {
let _state = location.state as any
setItemData(_state)
}, [itemData]);
.
.
.
};
What you did was set the state with the data that came through and then you can use it as you would use states.
This video helped me a lot
https://www.youtube.com/watch?v=HLwR7fTB_NM&t=689s
React Stripe Elements works fine in development but deploying live via Netlify throws 'Webpack: Window is undefined' in Provider.js react stripe elements node module file.
As per some other suggestions I have tried ComponentDidMount method and also editing the Provider.js with this:
if (typeof window !== 'undefined') {
let iInnerHeight = window.innerHeight;
}
Both still result in failed deploys.
Also, I have tried setting stripe or apiKey in StripeProvider component, setting stripe throws error requiring Stripe object, e.g. Stripe(...) --> when switched with this get Stripe is not defined and apiKey throws window undefined error.
This is my gatsby-ssr.js file:
import React from 'react'
import { ShopkitProvider } from './src/shopkit'
import { StripeProvider, Elements } from 'react-stripe-elements'
import Layout from './src/components/Layout'
export const wrapRootElement = ({ element }) => {
return (
<StripeProvider apiKey={process.env.GATSBY_STRIPE_PUBLISHABLE_KEY}>
<ShopkitProvider clientId{process.env.GATSBY_MOLTIN_CLIENT_ID}>
<Elements>{element}</Elements>
</ShopkitProvider>
</StripeProvider>
)
}
export const wrapPageElement = ({ element, props }) => {
return <Layout {...props}>{element}</Layout>
}
Everything is working as expected on development, but SSR present window undefined issue with Webpack. I have also set env variables in Netlify as well in .env file
The problem is that there's a check for Stripe object in window inside StripeProvider. This means you can't use it raw in wrapRootElement. The simple solution is to not use StripeProvider in gatsby-ssr.js, you only need it in gatsby-browser.js.
However, since you're wrapping the root with multiple service providers, and also if you're loading Stripe asynchronously like this:
// somewhere else vvvvv
<script id="stripe-js" src="https://js.stripe.com/v3/" async />
You might as well make a common wrapper that can be used in both gatsby-ssr & gatsby-browser so it's easier to maintain.
I did this by creating a wrapper for StripeProvider where Stripe is manually initiated depending on the availability of window & window.Stripe. Then the stripe instance is passed as a prop to StripeProvider instead of an api key.
// pseudo
const StripeWrapper = ({ children }) => {
let stripe,
if (no window) stripe = null
if (window.Stripe) stripe = window.Stripe(...)
else {
stripeLoadingScript.onload = () => window.Stripe(...)
}
return (
<StripeProvider stripe={stripe}>
{children}
<StripeProvider>
)
}
This logic should be put in a componentDidMount or a useEffect hook. Here's an example with hook:
import React, { useState, useEffect } from 'react'
import { StripeProvider } from 'react-stripe-elements'
const StripeWrapper = ({ children }) => {
const [ stripe, setStripe ] = useState(null)
useEffect(() => {
// for SSR
if (typeof window == 'undefined') return
// for browser
if (window.Stripe) {
setStripe(window.Stripe(process.env.STRIPE_PUBLIC_KEY))
} else {
const stripeScript = document.querySelector('#stripe-js')
stripeScript.onload = () => {
setStripe(window.Stripe(process.env.STRIPE_PUBLIC_KEY))
}
}
}, []) // <-- passing in an empty array since I only want to run this hook once
return (
<StripeProvider stripe={stripe}>
{children}
</StripeProvider>
)
}
// export a `wrapWithStripe` function that can used
// in both gatsby-ssr.js and gatsby-browser.js
const wrapWithStripe = ({ element }) => (
<StripeWrapper>
<OtherServiceProvider>
{element}
</OtherServiceProvider>
</StripeWrapper>
)
By setting async to true in gatsby-config.js
{
resolve: `gatsby-plugin-stripe`,
options: {
async: true
}
}
It is possible to simplify the code above.
const Stripe = props => {
const [stripe, setStripe] = useState(null);
useEffect(() => {
(async () => {
const obj = await window.Stripe(process.env.STRIPE_PUBLIC_KEY);
setStripe(obj);
})();
}, []);
return (
<>
<StripeProvider stripe={stripe}>
{children}
</StripeProvider>
</>
);
};
I am using the https://www.npmjs.com/package/react-swipeable-routes library to set up some swipeable views in my React app.
I have a custom context that contains a dynamic list of views that need to be rendered as children of the swipeable router, and I have added two buttons for a 'next' and 'previous' view for desktop users.
Now I am stuck on how to get the next and previous item from the array of modules.
I thought to fix it with a custom context and custom hook, but when using that I am getting stuck in an infinite loop.
My custom hook:
import { useContext } from 'react';
import { RootContext } from '../context/root-context';
const useShow = () => {
const [state, setState] = useContext(RootContext);
const setModules = (modules) => {
setState((currentState) => ({
...currentState,
modules,
}));
};
const setActiveModule = (currentModule) => {
// here is the magic. we get the currentModule, so we know which module is visible on the screen
// with this info, we can determine what the previous and next modules are
const index = state.modules.findIndex((module) => module.id === currentModule.id);
// if we are on first item, then there is no previous
let previous = index - 1;
if (previous < 0) {
previous = 0;
}
// if we are on last item, then there is no next
let next = index + 1;
if (next > state.modules.length - 1) {
next = state.modules.length - 1;
}
// update the state. this will trigger every component listening to the previous and next values
setState((currentState) => ({
...currentState,
previous: state.modules[previous].id,
next: state.modules[next].id,
}));
};
return {
modules: state.modules,
setActiveModule,
setModules,
previous: state.previous,
next: state.next,
};
};
export default useShow;
My custom context:
import React, { useState } from 'react';
export const RootContext = React.createContext([{}, () => {}]);
export default (props) => {
const [state, setState] = useState({});
return (
<RootContext.Provider value={[state, setState]}>
{props.children}
</RootContext.Provider>
);
};
and here the part where it goes wrong, in my Content.js
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import SwipeableRoutes from 'react-swipeable-routes';
import useShow from '../../hooks/useShow';
import NavButton from '../NavButton';
// for this demo we just have one single module component
// when we have real data, there will be a VoteModule and CommentModule at least
// there are 2 important object given to the props; module and match
// module comes from us, match comes from swipeable views library
const ModuleComponent = ({ module, match }) => {
// we need this function from the custom hook
const { setActiveModule } = useShow();
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
return (
<div style={{ height: 300, backgroundColor: module.title }}>{module.title}</div>
);
};
const Content = () => {
const { modules, previousModule, nextModule } = useShow();
// this is a safety measure, to make sure we don't start rendering stuff when there are no modules yet
if (!modules) {
return <div>Loading...</div>;
}
// this determines which component needs to be rendered for each module
// when we have real data we will switch on module.type or something similar
const getComponentForModule = (module) => {
// this is needed to get both the module and match objects inside the component
// the module object is provided by us and the match object comes from swipeable routes
const ModuleComponentWithProps = (props) => (
<ModuleComponent module={module} {...props} />
);
return ModuleComponentWithProps;
};
// this renders all the modules
// because we return early if there are no modules, we can be sure that here the modules array is always existing
const renderModules = () => (
modules.map((module) => (
<Route
path={`/${module.id}`}
key={module.id}
component={getComponentForModule(module)}
defaultParams={module}
/>
))
);
return (
<div className="content">
<div>
<SwipeableRoutes>
{renderModules()}
</SwipeableRoutes>
<NavButton type="previous" to={previousModule} />
<NavButton type="next" to={nextModule} />
</div>
</div>
);
};
export default Content;
For sake of completion, also my NavButton.js :
import React from 'react';
import { NavLink } from 'react-router-dom';
const NavButton = ({ type, to }) => {
const iconClassName = ['fa'];
if (type === 'next') {
iconClassName.push('fa-arrow-right');
} else {
iconClassName.push('fa-arrow-left');
}
return (
<div className="">
<NavLink className="nav-link-button" to={`/${to}`}>
<i className={iconClassName.join(' ')} />
</NavLink>
</div>
);
};
export default NavButton;
In Content.js there is this part:
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
which is causing the infinite loop. If I comment out the setActiveModule call, then the infinite loop is gone, but of course then I also won't have the desired outcome.
I am sure I am doing something wrong in either the usage of useEffect and/or the custom hook I have created, but I just can't figure out what it is.
Any help is much appreciated
I think it's the problem with the way you are using the component in the Route.
Try using:
<Route
path={`/${module.id}`}
key={module.id}
component={() => getComponentForModule(module)}
defaultParams={module}
/>
EDIT:
I have a feeling that it's because of your HOC.
Can you try
component={ModuleComponent}
defaultParams={module}
And get the module from the match object.
const ModuleComponent = ({ match }) => {
const {type, module} = match;
const { setActiveModule } = useShow();
useEffect(() => {
if (type === 'full') {
setActiveModule(module);
}
},[module, setActiveModule]);
match is an object and evaluated in the useEffect will always cause the code to be executed. Track match.type instead. Also you need to track the module there. If that's an object, you'll need to wrap it in a deep compare hook: https://github.com/kentcdodds/use-deep-compare-effect
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match.type, module]);