I made a simple site with Gatsby.js and can't configure dynamic routes.
I have index.js page (was automatically created by react), that looks like:
import * as React from 'react'
const IndexPage = () => {
return (
<Layout
pageTitle="Home Page"
>
Some text for my main page
</Layout>
)
}
export const Head = () => <title>Home Page</title>
export default IndexPage
Layout components includes Header that looks like:
import React, { useState, useEffect } from 'react';
import { Link } from 'gatsby';
const Header = () => {
return (
<Wrapper style={{ *some styles* }}>
<Link to="/">Index</Link>
<Link to="/projects">Projects</Link >
</Wrapper>
)
};
export default Header;
I have my Projects page that looks like this:
import * as React from 'react'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Layout from '../layout'
const Projects = () => {
return (
<BrowserRouter>
<Layout>
<Wrapper>
<Routes>
<Route path="projects/:projectID/" component={ProjectDetails} />
</Routes>
<MyProjectLink to="/projects/1">
Project 1
</MyProjectLink>
<MyProjectLink to="/projects/2">
Project 2
</MyProjectLink>
</Wrapper>
</Layout>
</BrowserRouter>
)
}
export const Head = () => <title>Our Projects</title>
export default Projects
And I have my ProjectDetails component:
import React from 'react';
import { useParams } from 'react-router';
import Layout from '../../pages/layout';
const ProjectDetails = () => {
const { projectID } = useParams();
return (
<Layout>
<Wrapper>
<h2>Project {projectID}</h2>
</Wrapper>
</Layout>
);
}
export default ProjectDetails;
The problem is that when I navigate to localhost:8000/projects/1 (or "2", or "3", or "100500") I see a 404 page instead of my ProjectDetails component.
I've tried wrapping the index page to BrowserRouter and move the routes with my route there, but that's a dumb idea in my opinion (and it doesnt work).
Did I miss something? Some features of Gatsby (v5) that I don't know about? I'm new to Getsby and to be honest I thought that dynamic routes here work the same way as in React Router.
Gatsby extends its routing from React, however, the way to create routes is slightly different.
As far as I understand your code, you are trying to create a template page for projects: this can be simply done by creating a file inside /templates folder. A simple component like this should work:
const Projects = ({ data }) => {
return (
<Layout>
<Wrapper></Wrapper>
</Layout>
);
};
export const Head = () => <title>Our Projects</title>
export default Projects
This template, as long as you use it when creating pages (using either gatsby-node.js or File System Route API) will be used to hold each specific project data.
Each project data will be queried using GraphQL and held inside props.data but without knowing your source (can be markdown, JSON, CMS, etc) I can't provide a sample query.
Once Gatsby infers its GraphQL nodes from your data source, you can use it to get all projects, a specific project, or any other GraphQL data you need on any page/template (page query) or even using static queries.
The idea should be similar to:
// gatsby-node.js
projects.forEach(({ node }, index) => {
createPage({
path: node.fields.slug,
component: path.resolve(`./src/templates/project.js`),
context: {
title: node.title,
},
})
})
In your gatsby-node.js (or File System Route API) you get all projects, loop through them and createPage for each project. The path (URL) for each project will be the slug field (node.fields.slug) but you can use whatever you want. Gatsby will create dynamic pages based on this field.
Then you decide which component will be used as a template: path.resolve(`./src/templates/project.js`) in this case and finally, you populate the context to add a unique value (title in this case: again, this can be an id, the slug, etc). This value will be used to filter the node in the template.
In your Project template:
export const query = graphql`
query ($title: String) {
mdx(title: {eq: $title}) {
id
title
}
}
`
In this case, I'm using markdown-based sources (that's why the mdx node) and this node is filtered by the title ($title) using the context value. The data will be inside props.data of the template. Again, if you want to fetch all projects you will have available an allMarkdown or allMdx (or allJSON...) depending on the source node)
Related
I have a Nextjs application. The app will grow to a nice size (200 pages)
What is the best way to manage links. Some pages will have navigation buttons and and hrefs, hyperlinks.
I am looking for solution to manage
Example: pages -> about, contact, support, home, blog. But when you have 200 pages and you are hardcoding if I delete or change the about page name I have to go in all 200 pages and update this
Navigate with click :
import { useRouter } from "next/router";
//...
const router = useRouter()
<button
onClick={() => {
router.push('/url')
}}
>Navigate</button>
Navigate with Link :
import Link from 'next/link';
//...
<Link href="/url">Navigate</Link>
And if you have 200 url you should make 200 links you cannot optimize that, but you can create a file for all your link then import it everywhere and when you have to update one path you do it inside this file :
import Link from "next/link";
export const HomeLink = () => {
return <Link href="/">Navigate To Home</Link>;
};
export const SettingsLink= () => {
return <Link href="/settings">Navigate To Settings</Link>;
};
//...
and from the file in which you want to navigate :
import { HomeLink, SettingsLink } from "../your_path";
//...
<HomeLink />
<SettingsLink/>
I create a routes file with all my hard-coded routes and use the const throughout my app. I use trailing slash, so they need to be appended to all routes.
This setup works well for me, and I've used it on numerous projects with hundreds of routes. It saved a lot of time over the years when inevitably, I needed to change a route's href or name.
My routes file is similar to this:
export const ROUTE_HOME = {
href: "/",
name: "Home",
};
export const ROUTE_ABOUT = {
href: "/about/",
name: "About",
};
I use them like
import Link from 'next/link';
import {ROUTE_HOME, ROUTE_ABOUT} from 'your/path/to/routes';
<Link href={ROUTE_HOME}>{ROUTE_HOME.name}</Link>
<Link href={ROUTE_ABOUT}>{ROUTE_ABOUT.name}</Link>
On larger projects, I break the routes out into separate files.
src > const > routes.ts // top-level routes
src > modules > auth > const > routes.ts // only auth routes
The question is clear but I would like to give my use case for better understanding.
I'm creating a personal blog app and my blog posts will in .mdx format. These blog post .mdx files reside in a directory in the code base called posts. I can render any .mdx file in the following BlogPost component for each url like /posts/post1.
import React, { Suspense } from 'react'
import { useParams } from 'react-router-dom'
export const BlogPost = () => {
let params = useParams()
const Post = React.lazy(() => import(`../../posts/${params.postId}.mdx`))
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Post />
</Suspense>
</div>
)
}
And my file structure is like this:
/posts
--post1.mdx
/src
--/views
-----BlogPost.tsx
-----BlogList.tsx
So, whenever I want to add a new blog post I basically add a new .mdx file to the repository. However I don't want to edit any component. I mean, I don't want to have a variable that keeps list of the list of blog posts like following.
export const listOfBlogPosts = ["post1", "post2", ...]
Because it will need to be updated every time I add a new post. Instead I would like to get list of files dynamically in BlogList component.
import { Link } from 'react-router-dom'
import style from './BlogList.module.css'
export const BlogList = () => {
const listOfBlogPosts = ['post1', 'post2'] // get somehow dynamically
return (
<div className={style}>
{listOfBlogPosts.map((post) => {
return <Link to={post}> {post}</Link>
})}
</div>
)
}
How can achieve this? Is there a logical problem with my approach(i.e keeping dynamic files in the repo)? Any suggestion or comment would be appreciated
I have a VPS with Apache + Cpanel.
I can't configure Nginx over it, so the only way, as far as I know, is to 'static export' first then deploy it.
Turns out I can't access the product page by link pasted on url bar directly (not by click a link text).
The link is look like this : www.example.com/products/4 or www.example.com/products/213
My first suggestion is because I 'static export' the project.
I use next-routes with <Link />
My code
import React, { Component } from 'react';
import { withRouter } from 'next/router';
import { connect } from 'react-redux';
import fetch from 'isomorphic-fetch';
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';
import CheckoutBody from '../components/CheckoutBody';
class Product extends Component {
static async getInitialProps({ query }) {
let { id } = { ...query };
if (id === undefined) id = 14;
const res = await fetch(`http://www.example.com/api/product?id=${id}`);
const data = await res.json();
return { campaignDetail: data };
}
render() {
let { lang } = this.props;
return (
<React.Fragment>
<Navbar />
<CheckoutBody
key={this.props.productDetail.id}
productDetail={this.props.productDetail}
lang={lang}
/>
<Footer />
</React.Fragment>
);
}
}
export default Product ;
Same question but different problem: https://github.com/zeit/next.js/issues/9893
I have tried this to .htaccess. It is not working. I am very newbie to regex and htaccess.
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule !.*\.html$ %{REQUEST_FILENAME}.html [L]
What should I do?
Is it what it's called dynamic routes?
The issue might be related to using next export rather than rewrite rule configuration. What we found is nextjs router pathname is not populated with the expected route on first hit if you use next export. Until this issue is fixed within nextjs, you can use a provider in _app.js that wraps your components and adjusts the route or put this before the return statement inside you _app.js default function:
import { useRouter } from 'next/router'
const { asPath, push, pathname } = useRouter()
if (asPath.split('?')[0] != pathname.split('?')[0] && !pathname.includes('[')) {
// Work around for next export breaking SPA routing on first hit
console.log('Browser route ' + asPath + ' did not match nextjs router route ' + pathname)
push(asPath)
return <div>Loading...</div>
}
Trying next with layout pattern:
https://github.com/zeit/next.js/tree/canary/examples/layout-component
And the problem is that Layout component get remounted on every page change. I need to use layout component as a Container so it'll fetch data from server on every mount. How can I prevent layout to get re-mounted? Or am I missing something there?
This helped me for persistent layouts. The author puts together a function that wraps your page components in your Layout component and then passes that fetch function to your _app.js. This way the _app.js is actually the components that renders the Layout but you get to specify which pages use which layout (in case you have multiple layouts).
So you have the flexibility of having multiple layouts throughout your site but those pages that share the same layout will actually share the same layout component and it will not have to be remounted on navigation.
Here is the link to the full article
Persistent Layout Patterns in Next.js
Here are the important code snippets. A page and then _app.js
// /pages/account-settings/basic-information.js
import SiteLayout from '../../components/SiteLayout'
import AccountSettingsLayout from '../../components/AccountSettingsLayout'
const AccountSettingsBasicInformation = () => (
<div>{/* ... */}</div>
)
AccountSettingsBasicInformation.getLayout = page => (
<SiteLayout>
<AccountSettingsLayout>{page}</AccountSettingsLayout>
</SiteLayout>
)
export default AccountSettingsBasicInformation
// /pages/_app.js
import React from 'react'
import App from 'next/app'
class MyApp extends App {
render() {
const { Component, pageProps, router } = this.props
const getLayout = Component.getLayout || (page => page)
return getLayout(<Component {...pageProps}></Component>)
}
}
export default MyApp
If you put your Layout component inside page component it will be re-remounted on page navigation (page switch).
You can wrap your page component with your Layout component inside _app.js, it should prevent it from re-mounting.
Something like this:
// _app.js
import Layout from '../components/Layout';
class MyApp extends App {
static async getInitialProps(appContext) {
const appProps = await App.getInitialProps(appContext);
return {
...appProps,
};
}
render() {
const { Component, pageProps } = this.props;
return (
<Layout>
<Component {...pageProps} />
<Layout />
);
}
}
export default MyApp;
Also, make sure you replace all the to <Link href=""></Link>, notice that only have change the Html tag to link.
I struggled because with this for many days, although I was doing everything else correctly, these <a> tags were the culprit that was causing the _app.js remount on page change
Even though this is the topic Layout being mounted again and again, the root cause of this problem is that you have some data loaded in some child component which is getting fetched again and again.
After some fooling around, I found none of these problem is actually what Next.Js or SWR solves. The question, back to square one, is how to streamline a single copy of data to some child component.
Context
Use context as a example.
Config.js
import { createContext } from 'react'
export default createContext({})
_App.js
import Config from '../Config'
export default function App({ Component, pageProps }) {
return (
<Config.Provider value={{ user: { name: 'John' }}}>
<Component {...pageProps} />
</Config.Provider>
)
}
Avatar.js
import { useContext } from 'react'
import Config from '../Config'
function Avatar() {
const { user } = useContext(Config)
return (
<span>
{user.name}
</span>
)
}
export default Avatar
No matter how you mount and dismount, you won't end up with re-render, as long as the _app doesn't.
Writable
The above example is only dealing with readable. If it's writable, you can try to pass a state into context. setUser will take care the set in consumer.
<Provider value={useState({})} />
const [user, setUser] = useContext(Config)
setUser is "cached" and won't be updated. So we can use this function to reset the user anytime in child consumer.
There're other ways, ex. React Recoil. But more or less you are dealing with a state management system to send a copy (either value or function) to somewhere else without touching other nodes. I'll leave this as an answer, since even we solved Layout issue, this problem won't disappear. And if we solve this problem, we don't need to deal with Layout at all.
I have multiple apps as part of one React-redux-typescript project. All of these individual apps are part of a core-application. I want to dynamically set up routing.
My current routes look like this:
Routes.tsx
import HomePageApp from "../Components/Home/HomeApp";
import TestApp from "../Components/Test/TestApp";
export default function Routes() {
return (
<Switch>
<RedirectIfAuthenticated
exact={true}
isAuthenticated={true}
path={Path.homePath} --> "/"
component={HomePage} ---> AppName coming from import statement on top
redirectPath={Path.homePath} --> "/"
/>
<RedirectIfAuthenticated
isAuthenticated={true}
path={Path.apps.test} --> "/test"
component={TestApp} --> AppName from import on top
redirectPath={Path.homePath} --> "/"
/>
</Switch>
);
}
And RedirectIfAuthenticated simply redirects to correct applications' landing pages.
RedirectIfAuthenticated.tsx
export default function RedirectIfAuthenticated({
component,
redirectPath,
isAuthenticated,
...rest
}: IRedirectIfAuthenticatedProps) {
const Component = component;
const render = (renderProps: RouteComponentProps<any>) => {
let element = <Component {...renderProps} />;
return element;
};
return <Route {...rest} render={render}/>;
}
I've a config file like this:
Manifest.ts
export let manifest = {
apps: [
{
componentPath: "/Test/App",
path: "/test",
name: "Test"
},
...more objects for other apps
]
};
In my Routes.tsx, I want to make use of my manifest to render the RedirectIfAuthenticated component.
so I can figure out this change:
for brevity showing the dirty approach but the actual code iterates over the manifest using .map and renders RedirectIfAutenticated.
const app = manifest.apps.find(app => app.name === "Test");
<Switch>
<RedirectIfAuthenticated
isAuthenticated={true}
path={app.path} --> "/test"
component={What should I do here? How to pass component reference by path??}
redirectPath={"/"} ==> I typically get this from my manifest..
/>
</Switch>
One option is to do this:
component={require("path from manifest").default}
but our tslint throws a bunch of errors at this. Other than this I can't figure out how to pass the component reference here dynamically. Looking for a better approach.
The Routes.tsx needs to be dynamic so that adding new apps is a matter of configuration so I can't do imports on top because I dont know what's gonna be added in config. Thanks.
I was able to use dynamic imports to achieve this. I used this article to understand a few concepts.
private loadComponentFromPath(path: string) {
import(`../../ScriptsApp/${path}`).then(component =>
this.setState({
component: component.default
})
);
}
One important distinction here is if I don't give the path from the root of the app, I get an error saying "Unable to find the module". This is why I've given the full path from the root.