Nested navigators inside a custom navigator? Resulting in multiple routers - reactjs

I have a nested PageWithStackNavigator here, inside this code from the React Navigation example for a custom tab view:
const CustomTabView = ({ descriptors, navigation }) => {
const { routes, index } = navigation.state;
const descriptor = descriptors[routes[index].key];
const ActiveScreen = descriptor.getComponent();
return (
<SafeAreaView>
<CustomTabBar navigation={navigation} />
<ActiveScreen navigation={descriptor.navigation} />
</SafeAreaView>
);;
};
const CustomTabRouter = TabRouter(
{
PageWithStackNavigator,
PageTwo,
},
{
initialRouteName: 'PageWithStackNavigator',
}
);
const navigator = createNavigator(CustomTabView, CustomTabRouter, {})
const CustomTabs = createNavigationContainer(navigator);
But, this is resulting in multiple routers. CustomTabRouter seen here, and one router in PageWithStackNavigator. "this.props.navigation.goBack()" is doing different things depending on if I call it inside the tabs, or inside PageWithStackNavigator.
Per the docs, I should be doing something like "static router = AuthenticationNavigator.router;", but I don't see how to do this with a custom TabRouter.

The issue was not that there were multiple routers, but rather in the goBack command.
From a comment on my github issue:
I know this is confusing but you need to use goBack(null). the goBack() helper automatically provides the key that you're going back from and is then limited to the stack that it's inside of. goBack(null) says to go back from anywhere.

Related

Cant figure out how to create a 'basic' HOC for a react / NextJS application

I have built an application that works well, but i now need to add some logic into each page that checks to see if the user has a subscription and if they do then send them to payment page to make a payment.
I have a hook (using SWR) that gets me the user session and within the hook it returns a boolean for isSubscribed. I intend to use this.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
An above example is what i currently do. But this solution requires me to copy pasta everytime to the page which obviously i can do, but it's no way efficient. I know that HOC exists and from what i know i an use a HOC to do this. But i have no idea how to write one that would fit this purpose.
As an added benefit, it would be useful to add the session as a prop to the 'new component' so that i dont have to call the hook twice.
Thanks for all and any help.
p.s. i mention it in the title, but i'm using NextJS. Not sure if this has any baring (i dont think it does, but worth mentioning)
You can create a wrapper HOC such as following;
const withSession = (Component: NextComponentType<NextPageContext, any, {}>) => {
const Session = (props: any) => {
const session = useSession();
if (session.isLoading) {
return <>Loading..</>
}
else {
return <Component {...props} />
}
};
// Copy getInitial props so it will run as well
if (Component.getInitialProps) {
Session.getInitialProps = Component.getInitialProps;
}
return Session;
};
And to use it in your page or component, you can simply do like;
const UserDetailPage: React.FC = (props) => {
// ...
// component's body
return (<> HI </>);
};
export default withSession(UserDetailPage);
I think this problem doesn't necessary require a HOC, but can be solved with a regular component composition. Depending on your actual use case, it may or may not be a simpler solution.
We could implement a Session component that would leverage the useSession hook and conditionally render components passed via the children prop:
const Session = props => {
const { isLoading } = useSession();
if (isLoading) {
return "Loading...";
}
return props.children;
};
Then nest the Page component into the Session:
const GuardedPage: React.FC<PageProps> = props => {
return (
<Session>
<Page {...props} />
</Session>
);
};
I see the question has already been answered, just wanted to suggest an alternative. One of the benefits of this approach is that we can wrap an arbitrary tree into the Session, and not just the Page.
Are you trying to return a page loading screen component and direct the user to the appropriate page based on thier subscription status? or isLoading handles one event and isSubscribed handles another?
Let's define (HOC) higher order component for the sake of your problem. By using HOC, logic can be modularized and redistributed throughout components. This HOC your creating should have the capability to call different methods on a single data source or one method to be applied across multiple components. For instance say you have an API component with 5 end points (login, subscribe, logout, unsubsubscribe) the HOC should have the ability to utilize any of the endpoints from any other component you use it in. HOC is used to create an abstraction that will allow you to define logic in a single place.
Your code calls one singular method to check if the session is in use of display the content of a page based on user subscription and page loading. Without seeing the components you are trying to use I can not determine the state that needs to be passed? but I will give it shot.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
First thing I see wrong in above code as a use case for an HOC component you have no export statement to share with other components. Also, why use 2 return statements for isLoading unless both conditions need to be checked (isLoading & isSubscribed) also, are these conditional statements depended on each other or seprate functions that can be called separately from another source? if you posted more of your code or the components you are pasting this into it would help?
To use this as an HOC in NEXT is essentially the same as react.
Dependant logic
const session = useSession(props);
// ad constructor and state logic
...
if(session.isLoading) {
return this.setState({isLoading: true});
} else {
return this.setState({isSubscribed: false});
}
Separate logic
const session = useSession(props);
// ad constructor and state logic
...
isLoading () => {
return this.setState({isLoading: true});
}
isSubscribed() => {
return this.setState({isSubscribed: true});
}
or something like this that uses routes...
import React, { Component } from 'react';
import { Redirect, Route } from 'react-router-dom';
export const HOC = {
isState: false,
isSubscribed(props) {
this.isState = false;
setTimeout(props, 100);
},
isLoading(props) {
this.isState = true;
setTimeout(props, 100);
}
};
export const AuthRoute = ({ component: Component, ...rest}) => {
return (
<Route {...rest} render={(props) => (
HOC.isAuthenticated === true ? <Component {...props} /> : <Redirect to='/' />
)}/>
)};
}
If you could share more of you code it would be more helpful? or one of the components you are having to copy and paste from your original HOC code. I would be easier than stabbing in the dark to assist in your problem but I hope this helps!
Cheers!

Using react Modal that can be triggered from multiple pages react/next.js

I have a Modal that is triggered but many different buttons across different components. I Have been able to get it working on the pages by passing the variables in the Layout, for example
const index = () => {
const [show, setShow] = useState(false)
const handleShow = () => { setShow(true) }
const handleClose = () => { setShow(false) }
<Layout pageTitle={pageTitle} metaUrl={metaUrl} show={show} onHide={handleHide}>
...
</Layout>
}
And using these variables to pass to the Modal component from the Layout which triggers the modal just fine. But it only works with pages because I can pass them in the layout, however I have buttons in the Navbar and Footer that are being imported into the Layout and not using the Layout so im not sure how to actually pass the variables to the modal from those.
Any Help would be amazing!
for this situation, I think the perfect solution would be to use React Context you could separate it in its own hook and then use this hook when needed across the app.
first, you will need to create the context
const ModalContext = React.createContext()
// give it a display name so it would be easier to debug
ModalContext.dispalyName = 'ModalContext'
then you need need to create the Provider for this context and put it higher in your app tree because you can only use this context under its Provider since you are using Next.js I would suggest doing it in the _app.js or just around your main app component.
const ModalContextProvider = ({children}) => {
const [isOpend, setIsOpend] = React.useState(false);
// handle any additional data needed with useState/useReducer
const [title, setTitle] = React.useState('default title');
const value = {setIsOpened, setTitle};
return <ModalContext.Provider value={value}>
<ModalComponent isOpend={isOpend} title={title}/>
{children}
</ModalContext.Provider>
}
so after creating the ModalContext component and putting it above your main app component you can extract this context functionality in its own hook like this
function useModalContext() {
const context = React.useContext(ModalContext);
// if context is undefined this means it was used outside of its provider
// you can throw an error telling that to your fellow developers
if(!context) {
throw new Error('useModalContext must be used under <ModalContextProvider/>');
}
return context;
}
then in any component, you can use this hook like this
const {setIsOpened, setTitle} = useModalContext();
const handleOpenModal() {
setIsOpened(true);
setTitle('Modal Title');
}
return <button onClick={handleOpenModal}>Show Modal</button>
and you can use this any place in the app if it was under the context provider component.

Pagetransition before next page in Nextjs

Is there any way to archive a page transition that fits the following requirements:
I want to show a page transition when a page changes
The page transition should finish before the next page loads/shows
The page transition should run X seconds
The page transition is a component placed in _app.js
Currently, I do this in a gruesome way.
In NuxtJS, it is possible to archive it via the Javascript Hooks. https://nuxtjs.org/docs/2.x/components-glossary/pages-transition
Thank you for the help :)
You can hook into Next.js Router events, but you can not set how much time the transition should take.
E.g. if you want the transition to be 3 seconds:
if the Next.js transition takes 1 second, you can wait 2 seconds, but
if the Next.js transition takes 4 seconds, you can't do anything about it.
And you can not know for sure how long the Next.js transition will take.
custom "routing"
Anyway, if you rely on the Next.js transition never taking more time than your animation, you would have to "store" the old page view somehow, and show it instead of the new page as long as the animation runs.
From Next.js perspective the new page should be shown. I think this would not work with Next.js routing, you would have to do the "routing" (display of content depending on the url) yourself, I guess. E.g. design your App to always have something like two pages at once, one is shown, one is hidden and might be loading. Then you can switch between these two pages whenever you want.
You might want to have a look at the experimental feature React "Suspense" (I don't know, I haven't used it).
wait before routing
If you want to wait a while before the transition starts, you might do something like this:
const startAnimation = () => {
return new Promise( ( resolve, reject ) => {
console.log('...animate...');
setTimeout( resolve, 3000 ); // <-- replace this line with your animation, and call resolve() when finished
} );
};
const routerWrapper = {
push: ( url ) => {
startAnimation().then( () => {
console.log('Next.js routing starts...');
Router.push( url );
})
}
};
// ...
routerWrapper.push( newUrl ); // <-- this instead of Router.push( newUrl )
stop animation when routing completed
If you want to start an animation when Route.push() is called, and stop it when the Next.js transition is completed, you can use Next.js routerevents, e.g.:
export default function App( props: AppProps ) {
const { Component, pageProps } = props;
const router = useRouter()
useEffect(() => {
const routeChangeStart = (url, { shallow }) => {
console.log('start animation');
};
const routeChangeComplete = (url, { shallow }) => {
console.log('stop animation');
};
router.events.on('routeChangeStart', routeChangeStart);
router.events.on('routeChangeComplete', routeChangeComplete);
return () => {
router.events.off('routeChangeStart', routeChangeStart);
router.events.off('routeChangeComplete', routeChangeComplete);
}
}, [])
// --
return (
<Component { ...pageProps } />
);
}
I have not personally done this yet, but it looks like you can do something like this with Framer Motion.
Destructuring props from the router in your main App component
function App({ Component, pageProps, router }) {
Then you can use the AnimatePresence component from Framer Motion to animate the route load and exit. You said you were familiar with the library in the comments so I won't talk about that component but here is what you add to the motion.div within the AnimatePresence component
<motion.div key={router.route}>
Below I will link a good article on how to do it and a good repo to use as an example. I did not write either of these but they are quite useful and descriptive.
https://www.freecodecamp.org/news/how-to-add-interactive-animations-and-page-transitions-to-a-next-js-web-app-with-framer-motion/#step-3-adding-page-transitions-with-framer-motion-to-a-next-js-app
https://github.com/colbyfayock/my-rick-and-morty-wiki/commit/3c1455370f750ff86b75a3b3edc446ebe553bf5b

Reuse Child Component 'Instance' Across Multiple Parents

I am currently building a Dashboard using React and D3. I'd like to compose each Dashboard as their own component (rather than have a single mega component manage everything) but re-use the child components within each Dashboard which will allow smooth animations and transitioning.
The following is a very rough example of what I'm trying to achieve.
const Dashboard1 = () => {
const data = useData('/main');
return <ChartComponent data={data}/>;
};
const SubDashboard = () => {
const data = useData('/sub');
return <ChartComponent data={data}/>;
};
const App = (props) => {
return props.topLevel ? <TopDashboard/> : <SubDashboard/>;
};
The issue I am finding with this approach is that <ChartComponent> will un-mount and re-mount when navigating between each Dashboard (due to the parents being different) which causing the page to flicker and all visuals to be redrawn which is expensive for some of the more complex visuals. What I would like to happen is for the same instance of <ChartComponent> to be used on both dashboards between each render so that it doesn't unmount, gets the new data passed into it and can animate from the previous values to the new ones.
Are there are ways or patterns to achieve this?
You need a parent item that stores all the data and passes it down to child components but I'd suggest to do this with redux.
const Dashboard1 = (props) => {
return <ChartComponent data={props.data.main}/>;
};
const SubDashboard = (props) => {
return <ChartComponent data={props.data.sub}/>;
};
const App = (props) => (
// this should be load elsewhere, store in redux and use it here once loaded
const data = {
main: useData('/main')
sub: useData('/sub'),
};
return props.topLevel ? <TopDashboard data={data} /> : <SubDashboard data={data} />;
};
you need to use this react reparating npm module to do that. React doesn't provide support for it in-built.
https://paol-imi.github.io/react-reparenting/

Context API consume from anywhere

I am trying to implement a shared state into my application using the React context api.
I am creating an errorContext state at the root of my tree. The error context looks like so:
// ErrorContext.js
import React from 'react';
const ErrorContext = React.createContext({
isError: false,
setError: (error) => {}
});
export default ErrorContext;
Desired Result
I would like to update (consume) this context from anywhere in the app (specifically from within a promise)
Ideally the consume step should be extracted into a exported helper function
Example Usage of helper function
http.get('/blah')
.catch((error) => {
HelperLibrary.setError(true);
})
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
render() {
return (
<ErrorContext.Provider value={this.state}>
{this.props.children}
</ErrorContext.Provider>
)
}
}
Then I can consume this provider by using the Consumer wrapper from inside a render call:
<ErrorContext.Consumer>
{(context) => {
context.setError(true);
}}
</ErrorContext.Consumer>
The Problem with this approach
This approach would require every developer on my team to write lots of boilerplate code every-time they wish to handle a web service error.
e.g. They would have to place ErrorContext.Consumer inside the components render() method and render it conditionally depending on the web service response.
What I have tried
Using ReactDOM.render from within a helper function.
const setError = (error) =>{
ReactDOM.render(
<ErrorContext.Consumer>
// boilerplate that i mentioned above
</ErrorContext.Consumer>,
document.getElementById('contextNodeInDOM')
) }
export default setError;
Why doesn't this work?
For some reason ReactDOM.render() always places this code outside the React component tree.
<App>
...
<ProviderClass>
...
<div id="contextNodeInDOM'></div> <-- even though my node is here
...
</ProviderClass>
</App>
<ErrorContext.Consumer></ErrorContext.Consumer> <-- ReactDOM.render puts the content here
Therefore there is no context parent found for the consumer, so it defaults to the default context (which has no state)
From the docs
If there is no Provider for this context above, the value argument
will be equal to the defaultValue that was passed to createContext().
If anyone can assist me on my next step, I am coming from Angular so apologies if my terminology is incorrect or if I am doing something extremely stupid.
You can export a HOC to wrap the error component before export, eliminating the boilerplate and ensuring that the context is provided only where needed, and without messing with the DOM:
// error_context.js(x)
export const withErrorContext = (Component) => {
return (props) => (
<ErrorContext.Consumer>
{context => <Component {...props} errorContext={context} />}
</ErrorContext.Consumer>
)
};
// some_component.js(x)
const SomeComponent = ({ errorContext, ...props }) => {
http.get('/blah')
.catch((error) => {
errorContext.setError(true);
})
return(
<div></div>
)
};
export default withErrorContext(SomeComponent);
Now that React 16.8 has landed you can also do this more cleanly with hooks:
const SomeComponent = props => {
const { setError } = useContext(ErrorContext)
http.get("/blah").catch(() => setError(true))
return <div />
}
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
I don't think so - there should be setState used. There is a general rule in react "don't mutate state - use setState()" - abusing causes large part of react issues.
I have a feeling you don't understand context role/usage. This is more like a shortcut to global store eliminating the need of explicitly passing down props to childs through deep components structure (sometimes more than 10 levels).
App > CtxProvider > Router > Other > .. > CtxConsumer > ComponentConsumingCtxStorePropsNMethods
Accessing rendered DOM nodes with id is used in some special cases, generally should be avoided because following renders will destroy any changes made externally.
Use portals if you need to render sth somewhere outside of main react app html node.

Resources