Using Next router to link to a dynamically created page - reactjs

Fairly new to NextJs - I am trying to link between a category index page and a dynamically created page, which is using getServerSideProps
I have created a product template page with the following path -
/products/[...product]
I am trying to link to it from the index page using this code, but it is not working
import { useRouter } from "next/router";
const router = useRouter()
const handleClick = () => {
const productUrl = `/products/${slug}`;
router.push(productUrl);
};
The following code does work, but is not very performant
const handleClick = () => {
const productUrl = `/products/${slug}`;
window.location.replace(productUrl);
}
Any ideas on how to implement dynamic pages with next/router would be appreciated

Generally, for accessibility reasons (and also to avoid perf/js issues like the one you're seeing), if a button press is meant to result in a page-change, you should use anchor tags.
In Next.Js to get performant routing, you wrap these tags in a Link:
import Link from 'next/link'
...
<Link href={`/products/${slug}`}>
<a> See details </a>
</Link>
This will then work with the dynamic slug from props.

Related

React useEffect strange behaviour with custom layout component

I'm trying to use scroll position for my animations in my web portfolio. Since this portfolio use nextJS I can't rely on the window object, plus I'm using navigation wide slider so I'm not actually scrolling in the window but in a layout component called Page.
import React, { useEffect } from 'react';
import './page.css';
const Page = ({ children }) => {
useEffect(() => {
const scrollX = document.getElementsByClassName('page')
const scrollElement = scrollX[0];
console.log(scrollX.length)
console.log(scrollX)
scrollElement.addEventListener("scroll", function () {
console.log(scrollX[0].scrollTop)
});
return () => {
scrollElement.removeEventListener("scroll", () => { console.log('listener removed') })
}
}, [])
return <div className="page">{children}</div>;
};
export default Page;
Here is a production build : https://next-portfolio-kwn0390ih.vercel.app/
At loading, there is only one Page component in DOM.
The behaviour is as follow :
first listener is added at first Page mount, when navigating, listener is also added along with a new Page component in DOM.
as long as you navigate between the two pages, no new listener/page is added
if navigating to a third page, listener is then removed when the old Page is dismounted and a new listener for the third page is added when third page is mounted (etc...)
Problem is : when you navigate from first to second, everything looks fine, but if you go back to the first page you'll notice the console is logging the scrollX value of the second listener instead of the first. Each time you go on the second page it seems to add another listener to the same scrollElement even though it's not the same Page component.
How can I do this ? I'm guessing the two component are trying to access the same scrollElement somewhat :/
Thanks for your time.
Cool site. We don't have complete info, but I suspect there's an issue with trying to use document.getElementsByClassName('page')[0]. When you go to page 2, the log for scrollX gives an HTMLCollection with 2 elements. So there's an issue with which one is being targeted. I would consider using a refs instead. Like this:
import React, { useEffect, useRef } from 'react';
import './page.css';
const Page = ({ children }) => {
const pageRef = useRef(null)
const scrollListener = () => {
console.log(pageRef.current.scrollTop)
}
useEffect(() => {
pageRef.addEventListener("scroll", scrollListener );
return () => {
pageRef.removeEventListener("scroll", scrollListener )
}
}, [])
return <div ref={pageRef}>{children}</div>;
};
export default Page;
This is a lot cleaner and I think will reduce confusion between components about what dom element is being referenced for each scroll listener. As far as the third page goes, your scrollX is still logging the same HTMLElement collection, with 2 elements. According to your pattern, there should be 3. (Though there should really only be 1!) So something is not rendering properly on page 3.
If we see more code, it might uncover the error as being something else. If refs dont solve it, can you post how Page is implemented in the larger scope of things?
also, remove "junior" from the "junior developer" title - you won't regret it

Using React Router functionality outside the rendering

I noticed that in all the examples provided by the React Router, it uses the Router objects as part of the UI that gets rendered. But I have a situation where I need to use the Redirect object outside of the rendering code. I have a set of tabs and when the user clicks on a tab, I need to redirect to a different url.
I came across one location in the Router documentation that did show how to use the Router object as part of the normal Javascript code that is not part of the rendering but I could not find it again. In essence I want to do something like this:
function doRedirect() {
return (<Redirect to={"/" + user.username + "/projects"} />);
}
But this will fail to compile. How can I use the Redirect functionality using the angled brackets inside of normal Javascript code?
You could use the useHistory hook, then just history.push(url) or history.replace(url) like this:
import { useHistory } from 'react-router-dom'
const MyComponent = ({ user }) => {
const history = useHistory()
function handleClick() {
history.replace(`/${user.username}/projects`)
}
return (
<button onClick={handleClick}>Redirect to Projects</button>
)
}
This is just an example, but obviously you can use this with quite a lot of flexibility.
See this question for push vs replace

React only renders embedded app on direct navigation, not when routed to page

I have a simple component that renders an embedded Calendly app:
class BookAppointment extends Component {
render(){
const data_url = `https://calendly.com/username?name=${this.props.firstName}%20${this.props.lastName}&email=${this.props.email}`
return (
<div class="calendly-inline-widget" data-url={data_url} style={{"min-width":"320px","height":"780px"}} />
);
}
}
However, the Calendly widget only appears if I navigate directly to this route (via typing in the url). If I navigate here via clicking a NavBar link and having my react-router-dom route me, it doesn't load. Why might this be the case?
I previously put the script file in the <head> of my index.html file. Unfortunately, to solve this, I had to modify the DOM directly, appending and removing the widget dynamically when the component mounted and unmounted:
.
.
.
componentDidMount() {
const head = document.querySelector('head');
const script = document.createElement('script');
script.id = "calendly-widget";
script.setAttribute('src', 'https://assets.calendly.com/assets/external/widget.js');
head.appendChild(script);
}
componentWillUnmount() {
const calendlyWidget = document.getElementById("calendly-widget");
const head = document.querySelector("head");
head.removeChild(calendlyWidget);
}
I'm not really sure about why I had to do this. If anyone could comment with the reason, I think that would enhance the quality of this answer for others.

How to make react router replace component only after the matched route asynchronously loaded..?

So here's the situation - When a Link is clicked the nprogress bar will start and I want react-router to only replace the current component with the matched route once that's done loading asynchronously.. just like in instagram..
But I am only getting this -
Here's my HOC to load component asynchronously --
import React, { useEffect, useState } from "react";
import nprogress from "nprogress";
import "nprogress/nprogress.css";
export default importComponent => props => {
const [loadedComponent, setComponent] = useState(null);
// this works like componentwillMount
if (!nprogress.isStarted()) nprogress.start();
if (loadedComponent) nprogress.done();
useEffect(() => {
let mounted = true;
mounted &&
importComponent().then(
({ default: C }) => mounted && setComponent(<C {...props} />)
);
// componentUnMount
return () => (mounted = false);
}, []);
// return the loaded component
const Component = loadedComponent || <div style={{ flexGrow: 1 }}>..</div>;
return Component;
};
I didn't find a solution to this anywhere on the internet.. so I am asking this question here in stackoverflow. I am hoping someone here can solve this.
Edit: 15-Nov-2021
The old approach doesn't work with routes that uses route params or query such as useLocation, useRouteMatch, etc.
The one I am using now is to use React.lazy, React.Suspense and updating the fallback prop whenever a page is rendered. So when the next page is being loaded, the fallback will be shown which is basically the same component instance as current page. Moreover, with this new approach, you can render the next page whenever you like; after fetching data from backend, or after animation, etc.
Here is a demo and source code.

react-router redirect to a different domain url

I am using react-router for client side routing. I have a button and when some one clicks the button, I want to redirect the user to a different url.
For e.g I want to redirect the user to "http://www.google.com". I used navigation mixin and used this.transitionTo("https://www.google.com"). But when I do this I get this error
Invariant Violation: Cannot find a route named "https://www.google.com".
I can use window.location but is that the right way to go?
As pointed out in the comments to this answer, default way of solving this would be to use anchor element (the a tag) with href attribute that points at the destination URL that you'd like to route the user to. A button that has appearance of a button but behavior or an anchor is pretty much a web anti-pattern. See more info in this answer: https://stackoverflow.com/a/1667512/1460905.
That said, there certainly is a potential scenario when a web app needs to perform some action and only then redirect the user. In this case, if primary action the user takes is submitting some data or really performing an action, and redirect is more of a side-effect, then the original question is valid.
In this case, why not use location property of window object? It even provides a nice functional method to go to external location. See the ref.
So, if you have a component, say
class Button extends Component {
render() {
return (
<button onClick={this.handleClick.bind(this)} />
);
}
}
then add handleClick that would make the component look like
class Button extends Component {
handleClick() {
// do something meaningful, Promises, if/else, whatever, and then
window.location.assign('http://github.com');
}
render() {
return (
<button onClick={this.handleClick.bind(this)} />
);
}
}
No need to import window since it's global. Should work perfectly in any modern browser.
Also, if you have a component that is declared as a function, you may possibly use the effect hook to change location when state changes, like
const Button = () => {
const [clicked, setClicked] = useState(false);
useEffect(() => {
if (clicked) {
// do something meaningful, Promises, if/else, whatever, and then
window.location.assign('http://github.com');
}
});
return (
<button onClick={() => setClicked(true)}></button>
);
};
You don't need react-router for external links, you can use regular link elements (i.e. <a href="..."/>) just fine.
You only need react-router when you have internal navigation (i.e. from component to component) for which the browser's URL bar should make it look like your app is actually switching "real" URLs.
Edit because people seem to think you can't use an <a href="..." if you need to "do work first", an example of doing exactly that:
render() {
return <a href={settings.externalLocation} onClick={evt => this.leave(evt)}/>
}
async leave(evt) {
if (this.state.finalized) return;
evt.preventDefault();
// Do whatever you need to do, but do it quickly, meaning that if you need to do
// various things, do them all in parallel instead of running them one by one:
await Promise.all([
utils.doAllTheMetrics(),
user.logOutUser(),
store.cleanUp(),
somelib.whatever(),
]);
// done, let's leave.
this.setState({ finalized: true }), () => evt.target.click());
}
And that's it: when you click the link (that you styled to look like a button because that's what CSS is for) React checks if it can safely navigate away as a state check.
If it can, it lets that happen.
If it can't:
it prevents the navigation of occurring via preventDefault(),
does whatever work it needs to do, and then
marks itself as "it is safe to leave now", then retriggers the link.
You can try and create a link element and click it from code. This work for me
const navigateUrl = (url) => {
let element = document.createElement('a');
if(url.startsWith('http://') || url.startsWith('https://')){
element.href = url;
} else{
element.href = 'http://' + url;
}
element.click();
}
As pointed by #Mike 'Pomax' Kamermans, you can just use to navigate to external link.
I usually do it this way, with is-internal-link
import React from 'react'
import { Link as ReactRouterLink} from 'react-router-dom'
import { isInternalLink } from 'is-internal-link'
const Link = ({ children, to, activeClassName, ...other }) => {
if (isInternalLink(to)) {
return (
<ReactRouterLink to={to} activeClassName={activeClassName} {...other}>
{children}
</ReactRouterLink>
)
}
return (
<a href={to} target="_blank" {...other}>
{children}
</a>
)
}
export default Link
Disclaimer: I am the author of this is-internal-link
I had the same issue and my research into the issue uncovered that I could simply use an "a href" tag. If using target="_blank" you should write your link this...
Your Link
I couldn't find a simple way to do that with React Router. As #Mike wrote you should use anchor (<a> tags) when sending the user to external site.
I created a custom <Link> component to dynamically decide whether to render a React-Router <Link> or regular <a> tag.
import * as React from "react";
import {Link, LinkProps} from "react-router-dom";
const ReloadableLink = (props: LinkProps & { forceReload?: boolean }) => {
const {forceReload, ...linkProps} = props;
if (forceReload)
return <a {...linkProps} href={String(props.to)}/>;
else
return <Link {...linkProps}>
{props.children}
</Link>
};
export default ReloadableLink;

Resources