Having issues with dynamic routes on Next 9 - reactjs

GraphQL queries in my components are not running on dynamic routes when I try to access the query string with router.query.xxx.
I have the following file
// ./pages/section/[slug].js
import { useRouter } from 'next/router';
import AppLayout from '../../components/styles/_AppLayout';
const Section = () => {
const router = useRouter();
return <AppLayout>Hi</AppLayout>;
};
export default Section;
The page displays fine, but as soon as I add {router.query.slug} and refresh the page, it gives me a TypeError because the GraphQL queries do not run. As you can see in the image below, me.firstName is undefined because the GraphQL query did not run
This is the code in _AppLayout.js
import styled from 'styled-components';
import Navigation from '../Navigation';
const Wrapper = styled.div`...`;
const AppLayout = props => {
return (
<Wrapper>
<Navigation />
<main>{props.children}</main>
</Wrapper>
);
};
export default AppLayout;
Any ideas why this might be happening and how to fix it?
Thanks

I was able to solve my issue two ways
Using withRouter
import { withRouter } from 'next/router';
import TestComponent from '../../components/TestComponent';
import AppLayout from '../../components/styles/_AppLayout';
const Section = props => {
return <AppLayout>Hi {props.query.slug}</AppLayout>;
};
export default withRouter(Section);
and passing the query parameter as props via getInitialProps
const Section = ({slug}) => {
return <AppLayout>Hi {slug}</AppLayout>;
};
Section.getInitialProps = async ({ query }) => {
const { slug } = query;
return { slug };
};
export default Section;

The following method worked for me, I am using React Hooks with Context and I need to also use the nextJS route with it, so following configuration can be followed.
Note: If you are using GraphQL then that can be also wrapped around the final JSX in _app.js
_app.js:
import { withRouter } from "next/router";
BuilderProvider is here Context Provider
const InjectRouterContext = withRouter(({ router, children }) => {
return <BuilderProvider value={router}>{children}</BuilderProvider>;
});
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<InjectRouterContext>
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
</InjectRouterContext>
);
}
}
Now in the Page, here it is somepage.js:
import { useRouter } from "next/router";
const somepage = () => {
const router = useRouter();
const { id } = router.query;
return (//JSX Here);
}
somepage.getInitialProps = async ({ query }) => {
const { slug } = query;
return { slug };
};
export default somepage;

Related

Next.js: How to get the URL paramters [duplicate]

When I click on a link in my /index.js, it brings me to /about.js page.
However, when I'm passing parameter name through URL (like /about?name=leangchhean) from /index.js to /about.js, I don't know how to get it in the /about.js page.
index.js
import Link from 'next/link';
export default () => (
<div>
Click{' '}
<Link href={{ pathname: 'about', query: { name: 'leangchhean' } }}>
<a>here</a>
</Link>{' '}
to read more
</div>
);
Use router-hook.
You can use the useRouter hook in any component in your application.
https://nextjs.org/docs/api-reference/next/router#userouter
pass Param
import Link from "next/link";
<Link href={{ pathname: '/search', query: { keyword: 'this way' } }}><a>path</a></Link>
Or
import Router from 'next/router'
Router.push({
pathname: '/search',
query: { keyword: 'this way' },
})
In Component
import { useRouter } from 'next/router'
export default () => {
const router = useRouter()
console.log(router.query);
...
}
Using Next.js 9 or above you can get query parameters:
With router:
import { useRouter } from 'next/router'
const Index = () => {
const router = useRouter()
const {id} = router.query
return(<div>{id}</div>)
}
With getInitialProps:
const Index = ({id}) => {
return(<div>{id}</div>)
}
Index.getInitialProps = async ({ query }) => {
const {id} = query
return {id}
}
url prop is deprecated as of Next.js version 6:
https://github.com/zeit/next.js/blob/master/errors/url-deprecated.md
To get the query parameters, use getInitialProps:
For stateless components
import Link from 'next/link'
const About = ({query}) => (
<div>Click <Link href={{ pathname: 'about', query: { name: 'leangchhean' }}}><a>here</a></Link> to read more</div>
)
About.getInitialProps = ({query}) => {
return {query}
}
export default About;
For regular components
class About extends React.Component {
static getInitialProps({query}) {
return {query}
}
render() {
console.log(this.props.query) // The query is available in the props object
return <div>Click <Link href={{ pathname: 'about', query: { name: 'leangchhean' }}}><a>here</a></Link> to read more</div>
}
}
The query object will be like: url.com?a=1&b=2&c=3 becomes: {a:1, b:2, c:3}
For those looking for a solution that works with static exports, try the solution listed here: https://github.com/zeit/next.js/issues/4804#issuecomment-460754433
In a nutshell, router.query works only with SSR applications, but router.asPath still works.
So can either configure the query pre-export in next.config.js with exportPathMap (not dynamic):
return {
'/': { page: '/' },
'/about': { page: '/about', query: { title: 'about-us' } }
}
}
Or use router.asPath and parse the query yourself with a library like query-string:
import { withRouter } from "next/router";
import queryString from "query-string";
export const withPageRouter = Component => {
return withRouter(({ router, ...props }) => {
router.query = queryString.parse(router.asPath.split(/\?/)[1]);
return <Component {...props} router={router} />;
});
};
Get it by using the below code in the about.js page:
// pages/about.js
import Link from 'next/link'
export default ({ url: { query: { name } } }) => (
<p>Welcome to About! { name }</p>
)
I know 2 ways to do this:
A Server-Side way, and a Client-Side way.
Method #1: SSR (Server-Side Rendering):
You should use Query Context for that page.
So use getServerSideProps instead of getStaticProps
import React from "react";
export async function getServerSideProps(context) {
const page = (parseInt(context.query.page) || 1).toString();
// Here we got the "page" query parameter from Context
// Default value is "1"
const res = await fetch(`https://....com/api/products/?page=${page}`);
const products = await res.json();
return {props: {products: products.results}}
// will be passed to the page component as props
}
const Page = (props) =>{
const products = props.products;
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>);
}
export default Page
The reason is that: this data cannot be pre-rendered ahead of user's request, so it must be Server-Side Rendered (SSR) on every request.
Static Pages: Use getStaticProps
Changing Content: use getServerSideProps
And here the content is changing based on query Parameters
Reference: https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props
Method #2: Next Router (Client Side):
import {useState, useEffect} from "react";
import { useRouter } from 'next/router'
const Page = () =>{
const [products, setProducts] = useState([]);
const [page, setPage] =useState((useRouter().query.page || 1).toString());
// getting the page query parameter
// Default value is equal to "1"
useEffect(()=>{
(async()=>{
const res = await fetch(`https://....com/api/products/?page=${page}`);
const products = await res.json();
setProducts(products.results);
// This code will be executed only once at begining of the loading of the page
// It will not be executed again unless you cahnge the page
})()
},[page]);
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
export default Page
Reference: https://nextjs.org/docs/api-reference/next/router
If you need to retrieve a URL query from outside a component:
import router from 'next/router'
console.log(router.query)
import { useRouter } from 'next/router';
function componentName() {
const router = useRouter();
console.log('router obj', router);
}
We can find the query object inside a router using which we can get all query string parameters.
Using {useRouter} from "next/router"; helps but sometimes you won't get the values instead u get the param name itself as value.
This issue happens when u are trying to access query params via de-structuring like:
let { categoryId = "", sellerId = "" } = router.query;
and the solution that worked for me is try to access the value directly from query object:
let categoryId = router.query['categoryId'] || '';
let sellerId = router.query['sellerId'] || '';
Post.getInitialProps = async function(context) {
const data = {}
try{
data.queryParam = queryString.parse(context.req.url.split('?')[1]);
}catch(err){
data.queryParam = queryString.parse(window.location.search);
}
return { data };
};
import { useRouter } from 'next/router'
const Home = () => {
const router = useRouter();
const {param} = router.query
return(<div>{param}</div>)
}
Also you can use getInitialProps, more details refer the below tutorial.
get params from url in nextjs
What worked for me in Nextjs 13 pages in the app directory (SSR)
Pass params and searchParams to the page:
export default function SomePage(params, searchParams) {
console.log(params);
console.log(searchParams);
return <div>Hello, Next.js!</div>;
With some builds there may be a bug that can be solved by adding:
export const dynamic='force-dynamic';
especially when deploying on Vercel.
ref: https://beta.nextjs.org/docs/api-reference/file-conventions/page#searchparams-optional
https://github.com/vercel/next.js/issues/43077

export 'withRouter' (imported as 'withRouter') was not found in 'react-router-dom'

I am totally a beginner in React and while practising I ran into this issue. Through searching, I found out that 'withRouter' is not supported anymore by 'react-router-dom v6'. But I can't figure out how to change my code compatibly to v6. Does anyone know how to change this code instead of using 'withRouter'? Thanks in advance!
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { readPost, unloadPost } from '../../modules/post';
import PostViewer from '../../components/post/PostViewer';
const PostViewerContainer = ({ match }) => {
// 처음 마운트될 때 포스트 읽기 API요청
const { postId } = match.params;
const dispatch = useDispatch();
const { post, error, loading } = useSelector(({ post, loading }) => ({
post: post.post,
error: post.error,
loading: loading['post/READ_POST']
}));
useEffect(() => {
dispatch(readPost(postId));
// 언마운트될 때 리덕스에서 포스트 데이터 없애기
return () => {
dispatch(unloadPost());
};
}, [dispatch, postId]);
return <PostViewer post={post} loading={loading} error={error} />;
};
export default withRouter(PostViewerContainer);
enter image description here
That is correct, the withRouter Higher Order Component (HOC) was removed in react-router-dom#6.
Since PostViewerContainer is a function component, just use the React hooks directly. There's no need really for the withRouter HOC. In this case it's the useParams hook you need to import and use.
Example:
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom'; // <-- import useParams hook
import { readPost, unloadPost } from '../../modules/post';
import PostViewer from '../../components/post/PostViewer';
const PostViewerContainer = () => { // <-- remove match prop
// 처음 마운트될 때 포스트 읽기 API요청
const { postId } = useParams(); // <-- call hook and destructure param
const dispatch = useDispatch();
const { post, error, loading } = useSelector(({ post, loading }) => ({
post: post.post,
error: post.error,
loading: loading['post/READ_POST']
}));
useEffect(() => {
dispatch(readPost(postId));
// 언마운트될 때 리덕스에서 포스트 데이터 없애기
return () => {
dispatch(unloadPost());
};
}, [dispatch, postId]);
return <PostViewer post={post} loading={loading} error={error} />;
};
For reference, if you needed to still use an HOC for class based components you'd need to either convert them to function components or create a custom withRouter HOC.
Example:
import { useLocation, useNavigate, useParams } from 'react-router-dom';
const withRouter = Component => props => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
return (
<Component
{...props}
location={location}
navigate={navigate}
params={params}
/>
);
};
export default withRouter;

Is there <Navigate> as react-router-dom?

In parent
import LoginPage from "pages/admin";
export function Home() {
return <LoginPage />;
}
In child
import { useRouter } from "next/router";
export default function LoginPage() {
const router = useRouter();
return (router.replace("/users");)
I got JSX error.
Because child is not used JSX.
But in react-router-dom, There is <Navigate>.
So in that case, it can fix.
Is there <Navigate> as react-router-dom in Next.js?
You can use router.push for redirections:
import { useEffect } from 'react'
import { useRouter } from 'next/router'
// Here you would fetch and return the user
const useUser = () => ({ user: null, loading: false })
export default function Page() {
const { user, loading } = useUser()
const router = useRouter()
useEffect(() => {
if (!(user || loading)) {
router.push('/login')
}
}, [user, loading])
return <p>Redirecting...</p>
}
Next documentation: https://nextjs.org/docs/api-reference/next/router
Custom JSX component
If you need a component to do this:
import { useEffect } from 'react'
import { useRouter } from 'next/router'
function Redirect({ to }) {
const router = useRouter()
useEffect(() => {
router.push(to)
}, [])
return null
}
Then just use it as a react-router <Redirect to="" />

Does history.push supposed to render the component associated with the route?

So I'm creating a loader component that requests data to a server, then based on the response - then the loader page redirects/reroutes them to other pages.
But for some reason history.push changes the route but does not render the component and remains with the <LoaderPage> component. Not sure what am I missing. So kindly help.
I have wrapped all pages with the <LoaderPage> component as my goal is when every time a user visits any route the <LoaderPage> component renders first then it does its job then redirects/reroutes users to other pages.
loader.tsx
import React from 'react';
import { useHistory } from 'react-router-dom'
import { AnimatedLodingScreen } from './loaders/AnimatedLodingScreen'
type loaderProps = {
children: React.ReactNode;
};
export const LoaderPage = ({ children }:loaderProps) => {
const history = useHistory();
React.useEffect(() => {
const session = http.get('/some-route');
session.then((result) => {
if(result.authenticated) {
history.push('home'); // when user is logged
}
history.push('/login'); // not authenticated
}
}, [])
return (
<AnimatedLodingScreen/>
);
}
app.tsx
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { observer } from 'mobx-react';
import { LoaderPage } from 'pages/loaders/LoaderPage';
import { SomeComponent1, SomeComponent2 } from 'pages/index'
export const App: React.FC = observer(() => {
return (
<BrowserRouter>
<LoaderPage>
<Route path='/home' exact component={SomeComponent1}/>
<Route path='/login' exact component={SomeComponent2}/>
// and so on... I have alot of routes in fact these routes are looped via .map and
// type-checked i just put it like this for simplicity
</LoaderPage>
</BrowserRouter>
);
});
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from 'app/app';
ReactDOM.render(<App/>, document.getElementById('root'));
The children props taken by your LoaderPage component isn't used for render anywhere within it, and thus nothing is rendered.
export const LoaderPage = ({ children }:loaderProps) => {
const history = useHistory();
React.useEffect(() => {
const session = http.get('/some-route');
session.then((result) => {
if(result.authenticated) {
history.push('home'); // when user is logged
}
history.push('/login'); // not authenticated
}
}, [])
return (
{children || <AnimatedLodingScreen/>}
);
}
You can use a state to save whether the data loading is completed or not and render children based on that
export const LoaderPage = ({ children }:loaderProps) => {
const history = useHistory();
const [isLoaded, setLoaded] = React.useState(false)
const [redirectPath, setRedirectPath] = React.useState('')
React.useEffect(() => {
const session = http.get('/some-route');
session.then((result) => {
if(result.authenticated) {
return setRedirectPath('/home') // when user is logged
}
setRedirectPath('/login') // not authenticated
}
}, [])
function redirectToPath() {
setLoaded(true);
history.push(redirectPath)
}
if(isLoaded) { return <>{children}</> }
return <AnimatedLodingScreen onAnimationEnd={redirectToPath} /> // onAnimationEnd is the function passed as prop to the component that should be invoked on animation ends
}

NextJS: How to add screen loading for production build?

I want add screen loading in next js project. And I tried to do that with the Router component in next/router.
This is my _app.js in next.js project:
import {CookiesProvider} from 'react-cookie';
import App from 'next/app'
import React from 'react'
import {Provider} from 'react-redux'
import withRedux from 'next-redux-wrapper'
import withReduxSaga from 'next-redux-saga'
import createStore from '../src/redux/store'
import Router from "next/router";
import {Loaded, Loading} from "../src/util/Utils";
class MyApp extends App {
static async getInitialProps({Component, ctx}) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps({ctx})
}
return {pageProps}
}
render() {
Router.onRouteChangeStart = () => {
Loading()
};
Router.onRouteChangeComplete = () => {
Loaded()
};
Router.onRouteChangeError = () => {
Loaded()
};
const {Component, pageProps, store} = this.props;
return (
<CookiesProvider>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</CookiesProvider>
)
}
}
export default withRedux(createStore)(withReduxSaga(MyApp))
This is Loaded() and Loading() functions:
export const Loaded = () => {
setTimeout(() => {
let loading = 'has-loading';
document.body.classList.remove(loading);
}, 100);
};
export const Loading = () => {
let loading = 'has-loading';
document.body.classList.add(loading);
};
The code works well when the project is under development mode. But when the project is built, the loading won't disappear.
Do you know solution of this issue or are you suggesting another solution?
Using apollo client and react hooks you could do as follow.
Example:
import { useQuery } from '#apollo/react-hooks';
import gql from 'graphql-tag';
import { withApollo } from '../lib/apollo';
import UserCard from '../components/UserCard';
export const USER_INFO_QUERY = gql`
query getUser ($login: String!) {
user(login: $login) {
name
bio
avatarUrl
url
}
}
`;
const Index = () => {
const { query } = useRouter();
const { login = 'default' } = query;
const { loading, error, data } = useQuery(USER_INFO_QUERY, {
variables: { login },
});
if (loading) return 'Loading...'; // Loading component
if (error) return `Error! ${error.message}`; // Error component
const { user } = data;
return (
<UserCard
float
href={user.url}
headerImg="example.jpg"
avatarImg={user.avatarUrl}
name={user.name}
bio={user.bio}
/>
);
};
export default withApollo({ ssr: true })(Index);
More info here: https://github.com/zeit/next.js/tree/canary/examples/with-apollo
I added the following codes to a wrapper component and the problem was resolved.
componentDidMount() {
Loaded();
}
componentWillUnmount() {
Loading();
}

Resources