Are GatsbyJS GraphQL queries tied to the component where it's written? - javascript

I have a couple yaml files with meta content that represent pages of a site that get ingested by Gatsby's data source/transform system - one of the files looks something like this:
path: /about/
pageTitle: About Us
metaDescription: Learn more about our team and what we do
...
I also have a base yaml file with meta content that other pages default to if meta content is missing or was not particularly written for that page.
path: Base
pageTitle: Welcome to our website
metaDescription: We are the leading company of industry x in ...
...
My current solution is to pass a graphql query variable $slug that is the page path, i.e., /about/ via the onCreatePage hook in gatsby-node.js,
exports.onCreatePage = ({ page, boundActionCreators }) => {
const { createPage, deletePage } = boundActionCreators;
return new Promise(resolve => {
const oldPage = Object.assign({}, page);
deletePage(oldPage);
createPage({
...oldPage,
context: {
$slug: oldPage.path
}
});
resolve();
});
};
add a graphql query in layout/index.js filtering for Base meta content,
query BasePageQuery {
pagesYaml(path: { eq: "Base" }) {
path
pageTitle
metaDescription
...
}
}
and a more generic graphql query on every single page where field path (from the yaml file) matches the query variable $slug:
query AboutPageQuery($slug: String) {
pagesYaml(path: { eq: $slug }) {
path
pageTitle
metaDescription
...
}
}
Instead of adding a graphql query onto every single page, I'm thinking instead to add both queries into layout/index.js, and avoid having to manually add the above query to all other pages.
query BasePageQuery($slug: String) {
base: pagesYaml(path: { eq: "Base" }) {
path
pageTitle
metaDescription
...
}
page: pagesYaml(path: { eq: $slug }) {
path
pageTitle
metaDescription
...
}
}
However, it isn't working as expected - this.props.data in the layout component only returns the base content and completely ignores my second query. It appears as if the query is tied to my layout/index.js component, and the $slug variable isn't reflecting the page component that's being rendered.
Which brings me to my question(s) -
Are graphql queries tied to the component that it's being written in? In this particular case - is $slug always going to be whatever path layout/index.js is?
Is there a way to print out graphql variables, or somehow verify what the variable is at a given point in time?
Is there a better solution to what I'm trying to achieve?

I don't quite understand the question about where GraphQL queries are tied to but I think that it will be more clear to you when you understand that you passed slug as a variable to the createPage function in your gatsby-node.js file. So for every page that is being created when you run createPage, the slug variable will be whatever you passed to it.
Again, do that in your gatsby-node.js file, in the createPage function.
gatsby-config.js would be a more appropriate place for the "default" or "base" information you want. Check out the example gatsby-starter-blog. There are information in the gatsby-config.js that are then being queried in the blog-post.js template alongside with the post's data.

Related

How to get data from the second query using a variable from the first query in graphql gatsby?

I'm trying to render all photos from a directory that will be defined in the frontmatter of an mdx file called carouselPhotosDir. I thought that to do this, I would need to query the specific mdx's carouselPhotosDir frontmatter field. Then, store that into a variable and then query allFile where the relative path is equal to the carouselPhotosDir frontmatter field that I got from the first query. I'm not so sure how to do this. I tried doing the query that you see below but it just queried everything. I tested the query with a hardcoded path that is defined in the frontmatter field and it worked so the variable from the first query isnt working. Is the variable not defined? I thought it would work similarily to the id variable.
export const pageQuery = graphql`
query ProjectQuery($id: String, $carouselPhotosDir: String) {
mdx(id: { eq: $id }) {
id
body
slug
frontmatter {
title
repo
carouselPhotosDir
}
}
allFile(filter: { relativeDirectory: { eq: $carouselPhotosDir } }) {
edges {
node {
id
name
base
childImageSharp {
gatsbyImageData(
placeholder: BLURRED
layout: CONSTRAINED
transformOptions: { cropFocus: CENTER, fit: COVER }
)
}
}
}
}
}
`;
Example mdx frontmatter:
---
title: blob
author: JuanCarlos
thumbnail: ./blob-thumbnail.png
repo: https://github.com/blob/repo
carouselPhotosDir: blob/carousel-photos
order: 3
---
Short answer: you can't.
Is the variable not defined?
Indeed.
In GraphQL, fields at each "level" of the request are executed and resolved in parallel. In your example, mdx and allFile are both fields of the same type (the root query type) so they will be resolved at the same time. That means each query essentially is not aware of the other or what the other resolved to.
If you were using some GraphQL client as Apollo you can use some kind of composing to mix the queries on the client-side but not in this scenario where the queries are run in the build-time.
That said, you have two options:
When you createPage in for each template page in the gatsby-node you know which mdx is being created at that time hence you can pass the carouselPhotosDir variable as a context (see https://www.gatsbyjs.com/docs/how-to/querying-data/page-query/). In that way, carouselPhotosDir will be queried in the gatsby-node but used in the page/template query.
Use JavaScript filtering in the template once the data fetch is done. You just need to filter among allFile data the specific carouselPhotosDir for each template. You can use the same pageContext variable as before but not in a GraphQL query but in a JavaScript filtering loop to get the specific node of files.

Is it possible to inject a variable value into a javascript comment?

TL;DR: is it possible to inject constant variables (that won't change during runtime) in comments
I've come across a very unique situation where I need my comments to have a specific value in them.
I'm code splitting in React and the way to name chunks in react is to add comment next to the import like this:
const MyComponent = lazy(() =>
import('./MyComponent' /* webpackChunkName: "MyComponent" */)
)
This gives my lazy loaded chunks readable names over generated id names.
There's a section in my codebase where I generate lazy loaded routes for my components based on my folder structure, but I've hit a wall where I can't set my chunk name to a variable set by a function.
Here's my function:
function generateLink(label: string) {
const path = label.replaceAll(' ', '');
return {
Element: lazy(
() => import(`./pages/${path}`, /* webpackChunkName: "{{this should be path}}" */)
),
label,
path
};
}
Is there anyway for me to inject the path variable into that comment?
An extra side note, this generateLink function is not run during runtime, the links are static.
Unfortunately there's no simple method for injecting variables into your comments. However, there is a webpack solution to your specific problem, just use Magic Comments
Updating your generateLink function to the following should resolve your problem.
function generateLink(label: string) {
const path = label.replaceAll(' ', '');
return {
Element: lazy(
() => import(`./pages/${path}`, /* webpackChunkName: "[request]" */)
),
label,
path
};
}
Now if your path is HomePage, your chunk name will resolve to /HomePage.[chunkId].js
Just make sure that all your file names are unique, otherwise webpack will suffix a number to your chunks for non-unique file names.
E.g. if you have 2 file names of HomePage.jsx, then you will end up with /HomePage0.[chunkId].js and /HomePage2.[chunkId].js
This question has been answered before in https://stackoverflow.com/a/52345836/2188587
You can use a placeholder [request] and webpack will automatically use the resolved file name.
import(/* webpackChunkName: "[request]" */ `./pages/${path}`)
I checked the docs, it still exists.
Exception: It will use the file name, so if you have nested files and path variable can also use directories, you're probably out of luck.

Nextjs - first level dynamic route

I want to have user profiles in my app at domain.com/:username .How do I do it? creating a new folder in /pages will create a new URL section like /user/:username which I don't want.
Just name your file inside pages as [username].js
Creating a dynamic route
You can create a dynamic route by putting the filename in brackets. For instance:
pages/[id].js
pages/[slug].js
pages/posts/[slug].js
pages/[author]/[slug].js
pages/[author]/bio.js
Notice how the dynamic route can be at any level. It doesn't simply need to be at the root of the pages folder, and it doesn't need to be the last level of the url either. Also notice that you can name the dynamic route anything you want. You're not restricted to just using [id] or [slug].
In your case, use pages/[username].js
Accessing the route name
If you create a dynamic route at pages/posts/[slug].js, any page at https://example.com/posts/slug-of-the-post will lead to that page. However, you likely want different content on each page (i.e., that the user is on https://example.com/posts/slug-of-the-post, not https://example.com/posts/slug-of-a-different-post. For this, you need to access the contents of the route.
Inside the page
You can use the router to get the name of the route.
// pages/posts/[slug].js
import { router } from 'next/router'
export default function PostPage() {
const router = useRouter()
const slug = router.query.slug // "slug" because that was the name of the file
return <>{/* page contents */} </>
}
So on page https://example.com/posts/slug-of-the-post, the variable slug will be slug-of-the-post.
If you used [id].js, use router.query.id instead.
GetStaticProps/GetServerSideProps
Using the server-side functions, the process is very similar.
// pages/posts/[slug].js
// bottom of the file
export async function getServerSideProps(context) {
const slug = context.params.slug
return {
props: {},
}
}
Docs
By the way, I'd also recommend checking out the docs if you haven't already:
https://nextjs.org/docs/routing/dynamic-routes

Next.js: getStaticProps not updating fetch values in production

I'm basically developing a blog on Next.js. Because it is another team which is in charge of the back-end, I'm currently making fetch API calls from getStaticProps to get my articles even though it's better practice to make database queries directly:
export async function getStaticProps({ params, res }) {
try {
const result = await fetch(`${API.baseURL}/get_article/${params.id}`);
const article = await result.json();
return {
props: { article },
};
} catch (error) {
res.statusCode = 404;
return { props: {} };
}
}
While this works perfectly in development mode, getting the article, editing it, and then accessing it again does not work in production (even locally, with the built version).
I guess it has something to do with Next.js handling cache somehow... What am I doing wrong? Thank you!
First of all the argument of function getStaticProps i.e the context object doesn't have any property named res. So res.statusCode = 404; doesn't do anything here.
And getStaticProps is meant be used for static site generation, additionally for dynamic routes, you can export another function getStaticPaths which should generate and return an array of paths with the dynamic route params for which getStaticProps will be called at build time for pre-rendering the pages.
In development mode, data-fetching methods will be called per-request basis so your code works. But in production mode, it will show the pre-rendered static pages which means the page will show the content as it was rendered and if you edit and update the content it will not reflect on the pages.
If you decide to go with static-site-generation either you have to rebuild the entire site after an update to a blog or you have to have some kind of client-side data-fetching logic that will update the blog when you update its content.
For client-side data-fetching you can use something like swr or react-query
Here is some psuedo-code which might help with pre-rendering the pages,
for route /article/[articleId]
export async function getStaticPaths() {
const articles = await /* db query to get the list of articles or fetch from remote API*/
// generate a list of paths with route params
const paths = articles.map(article => ({ params: { articleId: article.id }}))
return {
paths,
fallback: false
// fallback can be true if you want to show a fallback version of page
// and serve JSON for unknown articles
}
}
export async function getStaticProps(ctx) {
try {
const result = await fetch(`${API.baseURL}/get_article/${params.id}`);
const article = await result.json();
return {
props: { article },
};
} catch (error) {
return {
props: null
}
}
}
Learn more about how fallback works in returned value of function getStaticPaths docs.
Another alternative is to use getServerSideProps as the data-fetching method which will be called on each request for the page, but TTFB(time to first byte) will be higher. So for a blog-post site, I will not suggest using getServerSideProps.
You have to add the revalidate parameter. Find out more here.
In your case
export async function getStaticProps({ params, res }) {
try {
const result = await fetch(`${API.baseURL}/get_article/${params.id}`);
const article = await result.json();
return {
props: { article },
revalidate: 10 // 10 seconds
};
} catch (error) {
res.statusCode = 404;
return { props: {} };
}
}
Please note that revalidate parameter.
There is a way to update the HTML that is generated using getStaticProps, this process is called incremental server regeneration. This will ensure that your page is updated whenever you push an update to your blog post.
NextJS has documented this
https://nextjs.org/docs/basic-features/data-fetching
You search for ISR on the above page to understand how it's done
Basically you'll have to specify a time after which NextJS will try to update the page and in case there is a new post altogether, it will be server rendered on first request and then cached, once cached it'll work almost like a static page, refer fallback: 'blocking'
Quoting from website :
Next.js allows you to create or update static pages after you’ve built your site. Incremental Static Regeneration (ISR) enables you to use static-generation on a per-page basis, without needing to rebuild the entire site. With ISR, you can retain the benefits of static while scaling to millions of pages.
Consider our previous getStaticProps example, but now with Incremental Static Regeneration enabled through the revalidate property

Is react-router-relay inconsistent with the Relay pattern?

I'm using react-router-relay in a project. The design seems off to me given that every component basically ends up with a fragment having the same name as the root query. Shouldn't each component be able to have uniquely named fragments of any arbitrary type under the root query? Is this possible using this package or is my thinking flawed here?
Edit: Perhaps my question was a bit vague. My problem is that there are essentially two rules for the queries attribute defined by react-router-relay that enforce what seems to me to be a weird design pattern. Those two rules are:
Each query can only go "one level" deep.
Each query must map to a fragment with an identical name on the component that uses it.
This leaves you with a scenario whereby you either:
Use the same "viewer" query for every component and define a complimentary "viewer" fragment on each component. These fragments would all define different data requirements, despite having the same name, which seems very confusing.
You create unique fragment names for different components and then repeat the same exact root query with different names depending on the type of data you want to fetch, which seems downright silly.
Good question. When you're dealing with Relay, you're thinking is correct in that every component should have its own fragment so that the query itself maps exactly to the data needed for that particular component. The naming of the fragments can be however you like them named, but the type cannot be arbitrary. It must be a declared type underneath the Root Query object (or whatever field you are appending the fragment to). Otherwise the fragment will throw an error saying that you cannot query that type on Query or field.
For example:
var componentOneFragment = Relay.QL`
fragment on User {
name
}
`;
One thing to note here is that you don't need to have a name for fragments like fragment userFragment on User { ... }. This will give you more flexibility when referencing component fragments dynamically from Relay queries in your router by declaring ${Component.getFragment(componentOneFragment)}. Hope this helps!
EDIT:
Use the same "viewer" query for every component and define a
complimentary "viewer" fragment on each component. These fragments
would all define different data requirements, despite having the same
name, which seems very confusing.
Although the fact that the identical names of the fragments may seem confusing, this is the best way to think about things. Each component does indeed have different data requirements, so naturally their Relay containers will have different fragments, but still under the same fragment name.
This fragment may be included in one of your Relay containers that need User data:
const WidgetList = Relay.createContainer(/* ... */, {
initialVariables: {
color: null,
size: null,
limit: null
},
fragments: {
viewer: () => Relay.QL`
fragment on User {
widgets(color: $color, size: $size, first: $limit) {
edges {
node {
name,
},
},
},
}
`
}
});
While this fragment (with still the same name) may be included in another Relay container that needs Widget data:
const ActionsList = Relay.createContainer(/* ... */, {
initialVariables: {
input: null
},
fragments: {
viewer: () => Relay.QL`
fragment on Widget {
actions(input: $input) {
edges {
node {
name,
},
},
},
}
`
}
});
These can both be used dynamically (i.e. $Component.getFragment('viewer')) in the same GraphQL query as long as User and Widget are both types under the Root Query object.

Categories

Resources