Gatsby: page not rendering when I pass parameters in URL [Help]? - reactjs

Im stuck at this thing. I want to get url parameter (/mypage/?title=my-title) from url and display it in the page.
Currently I tried client-only routes in gatsby
import path from "path"
exports.onCreatePage = ({ page, actions }) => {
const { createPage } = actions
if (page.path.match(/^\/headline\/*/)) {
createPage({
path: "/headline",
matchPath: "/headline/*",
component: path.resolve("src/templates/headline.tsx"),
})
}
}
// gatsby-node.js
Here is my template
import React from "react"
import { Router, RouteComponentProps } from "#reach/router"
type Props = RouteComponentProps<{
title: string
}>
const Test = ({ title = "Test title", location }: Props) => {
console.log(title)
console.log(location)
return <h1>Im test</h1>
}
const Headline = () => {
console.log("handle temp called")
return (
<Router>
<Test path="/compare/headline/:title" />
</Router>
)
}
export default Headline
// src/templates/headline.tsx
Nothing is rendering when I hit the /headline/?title=test-my-app
My component (Test) is not being called from Router tag.
Any help ?

I think the issue is simply that you have an extra "compare" in your URL in the router. If you replace the part inside Router with <Test path="headline/:title" /> it ought to work with the URL you are testing.
The router will return an empty page if the path does not match any of the paths specified. You can add a default route as a catch-all.
Normally you shouldn't need to create a router inside the page by the way, but I guess that depends on your use case.

Related

Get URL Params (Next.js 13)

I am building a Next.js 13 project with the /app directory. I have a problem - in the root layout, I have a permanent navbar component in which the component is imported from /components/Navbar.jsx. Basically inside the Navbar.jsx, I want to be able to access the slug parameter in url, for ex: localhost:3000/:slug in which I want the slug id. I have already defined a Next.js 13 page.jsx for that slug. But how do I get the slug id in the navbar component. I also don't want to use window.location.pathname because it doesn't change when the page routes to a different slug and only does when I refresh.
I have tried the old Next.js 12 method:
//components/navbar.jsx;
import { useRouter } from "next/navigation";
export default function Navbar () {
const router = useRouter();
const { slug } = router.query;
useEffect(() => {
console.log(slug);
}, []);
return <p>Slug: {slug}</p>
}
However it does not work.
To get the URL parameters in a Server Component in Next.js 13, you can use the searchParams argument of the Page function.
URL
localhost:3000/?slug
page.js
export default function Page({searchParams}) {
return <Navbar slug={searchParams}></Navbar>
}
navbar.js
export default function Navbar(props) {
return <p>Slug: {props.slug}</p>
}
More info: https://beta.nextjs.org/docs/api-reference/file-conventions/page
Another way is to use the hook useSearchParams.
From the documentation:
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams();
// E.g. `/dashboard?page=2&order=asc`
const page = searchParams.get('page');
const order = searchParams.get('order');
return (
<div>
<p>Page: {page}</p>
<p>Order: {order}</p>
</div>
);
}
Had to use usePathname
import { usePathname } from 'next/navigation';

how to pass data from one page to another page in Next.js?

I am stuck with a problem with passing data from one page to another page in next.js as I am building a basic news application in which I am fetching get requests from news api and I got results of 10 articles and I mapped them correctly but I want to pass the single article date to a new Page named singleNews. So How can I do it?
here is place where I am fetching all 10 articles:
export default function news({data}) {
// const randomNumber = (rangeLast) => {
// return Math.floor(Math.random()*rangeLast)
// }
// console.log(data)
return (
<>
<div>
<h1 className="heading">Top Techcrunch Headlines!</h1>
</div>
<div className={styles.newsPage}>
{ // here you always have to check if the array exist by optional chaining
data.articles?.map(
(current, index) => {
return(
<Card datas={current} key={index+current.author} imageSrc={current.urlToImage} title={current.title} author={current.author}/>
)
}
)
}
</div>
</>
)
}
export async function getStaticProps() {
const response = await fetch(`https://newsapi.org/v2/top-headlines?sources=techcrunch&apiKey=${process.env.NEWS_API_KEY}&pageSize=12`)
const data = await response.json() // by default Article length is 104
// const articles = data.articles;
return{
props : {
data,
}
}
}
You can pass data to another page via Link component this way:
import Link from 'next/link'
<Link
href={{
pathname: '/to-your-other-page',
query: data // the data
}}
>
<a>Some text</a>
</Link>
and then receive that data in your other page using router:
import { useRouter } from 'next/router'
const router = useRouter();
const data = router.query;
Update - Next.js v13:
Some changes have been made on this version. Next.js added a new app directory feature which uses React Server Components by default. This feature is still experimental and it's not recommended for production just yet. Regular pages folder can still be used.
app directory feature can be enabled under the experimental flag on next.config.js file:
/** #type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
experimental: {
appDir: true
}
}
module.exports = nextConfig
Some changes have been implemented on the Link component and useRouter hook, also some new hooks have been added for client-side use. A brief list of these changes:
It's no longer needed to wrap an a tag with the Link component. The new Link component extends the HTML <a> element. <a> tag attributes can be added to Link as props. For example, className or target="_blank". These will be forwarded to the underlying <a> element on render.
The new useRouter hook should be imported from next/navigation and not next/router.
The query object has been removed and is replaced by useSearchParams().
Passing data from one page to another using app directory feature and Server Components:
import Link from 'next/link'
const SomePage = () => {
return (
<section>
<h1>Some page</h1>
<Link
href={{
pathname: '/anotherpage',
query: {
search: 'search'
}
}}
>
Go to another page
</Link>
</section>
)
}
export default SomePage
Receiving the data through searchParams prop:
const AnotherPage = ({ searchParams }) => {
console.log(searchParams.search) // Logs "search"
...
}
export default AnotherPage
On Client Components:
'use client'
import { useSearchParams } from 'next/navigation'
const SomeClientComponent = () => {
const searchParams = useSearchParams()
console.log(searchParams.get('search')) // Logs "search"
...
}
export default SomeClientComponent
This also works with page components on pages folder. Don't include the 'use client' directive in this case.

unit test not using url path with parameter when using reach router with react-testing-library

I am trying to create a unit test for a react component where the props to the component checks part of the url path to decide a course of action and at the end of the path takes in a parameter or value.
Using the example given in https://testing-library.com/docs/example-reach-router I created my own unit test but when I run the test my component complain that the uri value is not there. In addition, when I add a console.log(props) to my component and run the test again, props is undefined.
I have tried a variation of wrapping my component with LocationProvider adding in history as shown in https://reach.tech/router/api/LocationProvider
The relevant code snippet is -
function renderWithRouter(
ui,
{ route = '/', history = createHistory(createMemorySource(route)) } = {}
) {
return {
...render(
<LocationProvider history={history}>{ui}</LocationProvider>
)
,
history,
}
}
describe('View Order Test', () => {
describe('Order', () => {
it('Renders the Order View for a specific Order', async () => {
const { queryByText } =
renderWithRouter(
<State initialState={initialState} reducer={reducer}>
<ViewOrder />
</State>
, { route: '/order/orderView/1234', }
)
expect(queryByText('OrderID: 1234'))
}
)
})
Nothing seems to work. Is there some way of passing props to a component which are uri (#reach/router) in a unit? As I said my unit is a carbon copy of the those given above
Solved by adding Reach Router and wrapping it around the component.
I had a similar problem when testing a component using url parameters with reach-router.
Route file example:
import { Router } from "#reach/router";
import Item from "pages/Item";
const Routes = () => (
<Router>
<Item path="/item/:id" />
</Router>
);
...
In my testing code, I did this in render function:
import {
Router,
createHistory,
createMemorySource,
LocationProvider,
} from "#reach/router";
import { render } from "#testing-library/react";
export function renderWithRouter(
ui,
{ route = "/", history = createHistory(createMemorySource(route)) } = {}
) {
return {
...render(
<LocationProvider history={history}>
<Router>{ui}</Router>
</LocationProvider>
),
history,
};
}
and the test case:
test("renders the component", async () => {
renderWithRouter(<Item path="/item/:id" />, { route: "/item/1" });
...
});
In this way, I was able to get the url parameters and all tests worked well.

Using 'as' prop for Link element in Next.js leads to 404 and refresh

So I'm following along a tutorial and it's a very short video on the 'as' prop in next js. Seems simple. Add an 'as' prop and your Link element will route to the href in the link but will have the 'as' prop contents in the browser bar.
My code worked just fine before, but when I added 'as' for the link below, I got a 404. Browser console output showed that an attempt at a GET request was made... for the link in the 'as' prop.
Why is that? Isn't that the opposite of what it's meant to do? Why is my code trying to GET the contents of the 'as' prop instead of the 'href' prop?
For what it's worth, the instructor is using getInitialProps and I'm using getServerSide props (for both the index shown below and the post that it leads to). But I don't see why that would cause a GET to the 'as' prop, and also cause a refresh.
In the course it's highlighted that this will cause a 404 for a refresh, but it should work just fine if used without refreshing.
import React, { Component } from 'react';
import axios from 'axios';
import Link from 'next/link';
const Index = ({ posts }) => {
return (
<div>
<h1>Our index page!!!</h1>
{posts.map(post => (
<li key={post.id}>
<Link href={`/post?id=${post.id}`} as={`/p/${post.id}`}>
<a>{post.title}</a>
</Link>
</li>
))}
</div>
);
};
export async function getServerSideProps(context) {
const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
const {data} = res;
return {
props: { posts: data }, // will be passed to the page component as props
}
}
export default Index;
Here is the code for the component it renders:
import { withRouter } from 'next/router';
import axios from 'axios';
const Post = ({ id, comments }) => {
return (
<div>
<h1>Comments for Post #{id}</h1>
{comments.map(comment => (
<Comment key={comment.id} {...comment}/>
))}
</div>
);
};
const Comment = ({ email, body }) => (
<div>
<h5>{email}</h5>
<p>{body}</p>
</div>
);
export async function getServerSideProps(context) {
const { query } = context;
const res = await axios.get(`https://jsonplaceholder.typicode.com/comments?postId=${query.id}`);
return {
props: { ...query, comments: res.data }, // will be passed to the page component as props
}
}
export default withRouter(Post);
Thank you.
Output:
Console output shows a GET request to the link specified in the 'as' prop! Why!?
It's actually the other way around. The href prop should be the system path of the page component you're linking to. It's not a dynamic value that changes at runtime.
And the as prop is the path that the browser requests (as shown in the URL bar).
This is the correct usage:
<Link href={`/post/[pid]`} as={`/post?id=${post.id}`}>
<a>{post.title}</a>
</Link>
This assumes your page component is stored at /pages/post/[pid]. If you've stored the Post component at /pages/Post.js, move it to /pages/post/[pid].
Update based on your new comment:
If you instead want the page URLs to be /p/1, /p/2 etc. you can do the following:
Create a Page component at pages/p/[pid].js. Include getServerSideProps:
export async function getServerSideProps(context) {
const { params } = context;
const res = await axios.get(`https://jsonplaceholder.typicode.com/comments?postId=${params.pid}`);
return {
props: { id: params.pid, comments: res.data }, // Will be passed to the page component as props
}
}
In your Index component, use the Link component like this (assuming you haven't made any other changes to the component):
{posts.map(post => (
<li key={post.id}>
<Link href="/p/[pid]" as={`/p/${post.id}`}>
<a>{post.title}</a>
</Link>
</li>
))}
Next.js uses a file-based router, and the links have to match the component path. You can't put whatever you want in the as prop.

How to intercept back button in a React SPA using function components and React Router v5

I'm working in a SPA in React that doesn't use React Router to create any Routes; I don't need to allow users to navigate to specific pages. (Think multi-page questionnaire, to be filled in sequentially.)
But, when users press the back button on the browser, I don't want them to exit the whole app; I want to be able to fire a function when the user presses the back button that simply renders the page as it was before their last selection. (Each page is a component, and they're assembled in an array, tracked by a currentPage state variable (from a state hook), so I can simply render the pages[currentPage -1].
Using (if necessary) the current version of React Router (v5), function components, and Typescript, how can I access the back-button event to both disable it and replace it with my own function?
(Other answers I've found either use class components, functions specific to old versions of React Router, or specific frameworks, like Next.js.)
Any and all insight is appreciated.
After way too many hours of work, I found a solution. As it ultimately was not that difficult once I found the proper path, I'm posting my solution in the hope it may save others time.
Install React Router for web, and types - npm install --save react-router-dom #types/react-router-dom.
Import { BrowserRouter, Route, RouteComponentProps, withRouter } from react-router-dom.
Identify the component whose state will change when the back button is pressed.
Pass in history from RouteComponentProps via destructuring:
function MyComponent( { history }: ReactComponentProps) {
...
}
On screen state change (what the user would perceive as a new page) add a blank entry to history; for example:
function MyComponent( { history }: ReactComponentProps) {
const handleClick() {
history.push('')
}
}
This creates a blank entry in history, so history has entries that the back button can access; but the url the user sees won't change.
Handle the changes that should happen when the back-button on the browser is pressed. (Note that component lifecycle methods, like componentDidMount, won't work in a function component. Make sure useEffect is imported from react.)
useEffect(() => {
// code here would fire when the page loads, equivalent to `componentDidMount`.
return () => {
// code after the return is equivalent to `componentWillUnmount`
if (history.action === "POP") {
// handle any state changes necessary to set the screen display back one page.
}
}
})
Wrap it in withRouter and create a new component with access to a Route's properties:
const ComponentWithHistory = withRouter(MyComponent);
Now wrap it all in a <BrowserRouter /> and a <Route /> so that React Router recognizes it as a router, and route all paths to path="/", which will catch all routes unless Routes with more specific paths are specified (which will all be the same anyway, with this setup, due to history.push(""); the history will look like ["", "", "", "", ""]).
function App() {
return (
<BrowserRouter>
<Route path="/">
<ThemeProvider theme={theme}>
<ComponentWithHistory />
</ThemeProvider>
</Route>
</BrowserRouter>
);
}
export default App;
A full example now looks something like this:
function MyComponent( { history }: ReactComponentProps) {
// use a state hook to manage a "page" state variable
const [page, setPage] = React.useState(0)
const handleClick() {
setPage(page + 1);
history.push('');
}
useEffect(() => {
return () => {
if (history.action === "POP") {
// set state back one to render previous "page" (state)
setPage(page - 1)
}
}
})
}
const ComponentWithHistory = withRouter(MyComponent);
function App() {
return (
<BrowserRouter>
<Route path="/">
<ThemeProvider theme={theme}>
<ComponentWithHistory />
</ThemeProvider>
</Route>
</BrowserRouter>
);
}
export default App;
If there are better ways, I would love to hear; but this is working very well for me.
The answer Andrew mentioned works, but there's better ways to do the same thing.
Method 1
Instead of wrapping your component with 'withRouter' and getting the history via props, you can simply use the useHistory hook to do the same.
That would be something like this:
import { useHistory } from "react-router-dom";
const MyComponent = () => {
const history = useHistory();
useEffect(() => {
return () => {
if(history.action === "POP") {
//DO SOMETHING
}
}
});
}
Method 2
Simply use the component provided by react-router.
Use it something like this:
import { Prompt } from "react-router-dom";
const MyComponent = () => {
return (
<>
<div className="root">
//YOUR PAGE CONTENT
</div>
<Prompt message="You have unsaved changes. Do you still want to leave?"/>
</>
);
}
If you want to run some specific code:
<Prompt
message={(location, action) => {
if (action === 'POP') {
//RUN YOUR CODE HERE
console.log("Backing up...")
}
return location.pathname.startsWith("/app")
? true
: `Are you sure you want to go to ${location.pathname}?`
}}
/>
Refer to the docs for more info

Resources