I am learning Nextjs framework and I am having difficulty with dynamic route and how to fetch the data from an array. I followed the basics of Nextjs but this got me stuck.
What I want?
Get the information of this array which is stored as a items.tsx file:
export const projects = [
{
id: '2',
title: 'Italian recipes book',
description: `This a web design and soon-to-be a fully functional and responsive React application`
},
{
id: '1',
title: 'Chat App website',
description: `This is a homepage design and build for a concept project – a chat application. I designed the page
first then built a responsive web page using Webflow.`
},
]
As per nextjs website I was able to fetch the id and created a function to get the project data like so
import { projects } from "../models/projects";
export function getAllItemsIds() {
return projects.map(project => {
return {
params: {
id: project.id,
}
}
})
}
export function getProjectData(id) {
return {
id,
}
}
I believe the function getProjectData(id) is missing the rest of the data and I'm having problems on how to fetch it. I want to show the info of the array in my [id].tsx file according to the parameter (in this case, to the ID 1 or 2).
This is my [id].tsx file:
export async function getStaticPaths() {
const paths = getAllItemsIds()
return {
paths,
fallback: false
}
}
export async function getStaticProps({ params }) {
const projectData = getProjectData(params.id)
return {
props: {
projectData
}
}
}
const Project = ({ projectData }) => {
return (
<>
<div>
{projectData.id}
{projectData.title}
{projectData.description}
</div>
</>
)
}
export default Project;
As of now I am only able to display my projectData.id and I got a bit confused with all the getStaticPaths() and getStaticProps() thing. I'd really appreciate the help.
I don't know if that's the right way to do it, but I managed to do what I wanted like so:
added the other parameters and mapped the array inside the getProjectData
export function getProjectData(id, title, description ) {
projects.map(data => {
if (data.id === id) {
title = data.title;
description = data.description;
}
});
return {
id,
title,
description
}
}
and in the [id].tsx I updates the getStaticProps like so
export async function getStaticProps({ params, title, description }) {
const projectData = getProjectData(params.id, title, description)
return {
props: {
projectData
}
}
}
It did fetch the data according to the id parameter!
Related
I'm trying to leverage nuxtjs SSG capabilities by creating a static web site where the pages content and navigation are fetched from an API.
I already found my way around on how to dynamically generate the routes by defining a module where I use the generate:before hook to fetch the pages content and routes. When creating the routes I store the page content as the route payload. The following code does just that and works as intended.
modules/dynamicRoutesGenerator.js
const generator = function () {
//Before hook to generate our custom routes
this.nuxt.hook('generate:before', async (generator, generatorOptions) => {
generator.generateRoutes(await generateDynamicRoutes())
})
}
let generateDynamicRoutes = async function() {
//...
return routes
}
export default generator
Now the problem I'm facing is that I have some navigation components that need the generated routes and I was thinking to store them into the vuex store.
I tried the generate:done hook but I don't know how to get the vuex store context from there. What I ended up using was the nuxtServerInit() action because as stated in the docs:
If nuxt generate is ran, nuxtServerInit will be executed for every dynamic route generated.
This is exactly what I need so I'm trying to use it with the following code:
store/index.js
export const actions = {
nuxtServerInit (context, nuxtContext) {
context.commit("dynamicRoutes/addRoute", nuxtContext)
}
}
store/dynamicRoutes.js
export const state = () => ({
navMenuNivel0: {}
})
export const mutations = {
addRoute (state, { ssrContext }) {
//Ignore static generated routes
if (!ssrContext.payload || !ssrContext.payload.entrada) return
//If we match this condition then it's a nivel0 route
if (!ssrContext.payload.navMenuNivel0) {
console.log(JSON.stringify(state.navMenuNivel0, null, 2));
//Store nivel0 route, we could use url only but only _id is guaranteed to be unique
state.navMenuNivel0[ssrContext.payload._id] = {
url: ssrContext.url,
entrada: ssrContext.payload.entrada,
navMenuNivel1: []
}
console.log(JSON.stringify(state.navMenuNivel0, null, 2));
//Nivel1 route
} else {
//...
}
}
}
export const getters = {
navMenuNivel0: state => state.navMenuNivel0
}
The action is indeed called and I get all the expected values, however it seems like that with each call of nuxtServerInit() the store state gets reset. I printed the values in the console (because I'm not sure even if it's possible to debug this) and this is what they look like:
{}
{
"5fc2f4f15a691a0fe8d6d7e5": {
"url": "/A",
"entrada": "A",
"navMenuNivel1": []
}
}
{}
{
"5fc2f5115a691a0fe8d6d7e6": {
"url": "/B",
"entrada": "B",
"navMenuNivel1": []
}
}
I have searched all that I could on this subject and altough I didn't find an example similar to mine, I put all the pieces I could together and this was what I came up with.
My idea was to make only one request to the API (during build time), store everything in vuex then use that data in the components and pages.
Either there is a way of doing it better or I don't fully grasp the nuxtServerInit() action. I'm stuck and don't know how to solve this problem and can't see another solution.
If you made it this far thanks for your time!
I came up a with solution but I don't find it very elegant.
The idea is to store the the API requests data in a static file. Then create a plugin to have a $staticAPI object that expose the API data and some functions.
I used the build:before hook because it runs before generate:before and builder:extendPlugins which means that by the time the route generation or plugin creation happen, we already have the API data stored.
dynamicRoutesGenerator.js
const generator = function () {
//Add hook before build to create our static API files
this.nuxt.hook('build:before', async (plugins) => {
//Fetch the routes and pages from API
let navMenuRoutes = await APIService.fetchQuery(QueryService.navMenuRoutesQuery())
let pages = await APIService.fetchQuery(QueryService.paginasQuery())
//Cache the queries results into staticAPI file
APIService.saveStaticAPIData("navMenuRoutes", navMenuRoutes)
APIService.saveStaticAPIData("pages", pages)
})
//Before hook to generate our custom routes
this.nuxt.hook('generate:before', async (generator, generatorOptions) => {
console.log('generate:before')
generator.generateRoutes(await generateDynamicRoutes())
})
}
//Here I can't find a way to access via $staticAPI
let generateDynamicRoutes = async function() {
let navMenuRoutes = APIService.getStaticAPIData("navMenuRoutes")
//...
}
The plugin staticAPI.js:
import APIService from '../services/APIService'
let fetchPage = function(fetchUrl) {
return this.pages.find(p => { return p.url === fetchUrl})
}
export default async (context, inject) => {
//Get routes and files from the files
let navMenuRoutes = APIService.getStaticAPIData("navMenuRoutes")
let pages = APIService.getStaticAPIData("pages")
//Put the objects and functions in the $staticAPI property
inject ('staticAPI', { navMenuRoutes, pages, fetchPage })
}
The APIService helper to save/load data to the file:
//...
let fs = require('fs');
let saveStaticAPIData = function (fileName = 'test', fileContent = '{}') {
fs.writeFileSync("./static-api-data/" + fileName + ".json", JSON.stringify(fileContent, null, 2));
}
let getStaticAPIData = function (fileName = '{}') {
let staticData = {};
try {
staticData = require("../static-api-data/" + fileName + ".json");
} catch (ex) {}
return staticData;
}
module.exports = { fetchQuery, apiUrl, saveStaticAPIData, getStaticAPIData }
nuxt.config.js
build: {
//Enable 'fs' module
extend (config, { isDev, isClient }) {
config.node = { fs: 'empty' }
}
},
plugins: [
{ src: '~/plugins/staticAPI.js', mode: 'server' }
],
buildModules: [
'#nuxtjs/style-resources',
'#/modules/staticAPIGenerator',
'#/modules/dynamicRoutesGenerator'
]
I would like to put my calls to my API in a separate page and not in the template page of my app. So, I create a file "customersAPI.js" and I put this code :
export function findAllCustomers () {
axios.get('http://127.0.0.1:8000/api/customers')
.then((reponse)=>{
console.log(reponse.data['hydra:member'])
return reponse.data['hydra:member']
}).catch(err=>console.log(err))
}
So I try to retrieve my data in my template page and put these data in data but It does not work because of the asynchronous thing of api call and because I don't know how to pass the data...
I do this in my template page :
data() {
return {
customer: [],
}
},
mounted() {
this.getAllCustomers();
},
getAllCustomers() {
this.customer = findAllCustomers();
}
I know it is not the good way to do this but I don't know how to do... So I need clarification about that. And, every time I go into the documentation, there are no examples with an API call outside of the part where there is the page template. Is it a good practice to want to put the api call apart? And in general calls to functions so that the code is not too long?
Thanks for help
In your case I advise you to try add async in mounted or in func.
async mounted() {
this.customers = await this.findAllCustomers();
},
------
methods: {
async getAllCustomers(){
this.customer = await findAllCustomers();
}
}
But better practice to fetch information from store:
COMPONENT
<script>
import {mapActions} from 'vuex'
export default {
data() {
return {
customer: [],
}
},
mounted() {
this.customer = this.fetchAll();//better to get via getters
},
methods() {
...mapActions('customers', ['fetchAll']),
//OR
// fetchAllCustomers(){
// this.$store.dispath('customers/fetchAll')
// }
}
}
</script>
STORE
// async action that put all customers in store
const fetchAll = async ({ commit }) => {
commit(types.SET_ERROR, '')
commit(types.TOGGLE_LOADING, true)
try {
const { data} = await customerAPI.findAll(namespace)
commit(types.SET_ALLIDS, data['hydra:member'])
commit(types.TOGGLE_LOADING, false)
return data['hydra:member']
} catch (error) {
commit(types.TOGGLE_LOADING, false)
commit(types.SET_ERROR, error)
}
},
API
// func that receive promise
export function findAll () {
return axios.get('http://127.0.0.1:8000/api/customers')
}
Please read about vuex
https://vuex.vuejs.org/guide/actions.html
I made a fake api call to my json file with axios. I get a promise back from the function where i can get my data from. but i don't want that. I want to receive the data from the function.
my code now: products.js
export default {
getAllProducts(axios) {
return axios.get('fakedb.json').then(response => {
return response.data;
});
}
}
the view file: a product.vue file
import productController from '~/data/controllers/product';
export default {
data() {
return {
products: productController.getAllProducts(this.$axios).then(res => this.products = res)
}
},
}
but this is not what i want to achieve.
what i want to achieve is this code in my product.vue file:
import productController from '~/data/controllers/product';
export default {
data() {
return {
products: productController.getAllProducts(this.$axios)
}
},
}
i want to receive the data without having to handle the promise in the view file. any solution how to return my data in the products.js file?
if i do a normal return from the products.js file like this it works fine:
export default {
getAllProducts(axios) {
return [
{
"name": "Product1",
"price": 9.75
},
{
"name": "Product2",
"price": 10.75
}
]
}
}
But i want it to go with axios
Since you have a .vue-file, I assume that this is a single-page vue component, right? And therefore you use vue-cli or webpack. I therefore assume that you can use async/await syntax.
Retrieving data from axios is asynchronous, because you basically cannot know how long it takes for it to retrieve the data over the network. And this kind of situation is what async/await is for.
So make the functions async:
products.js
export default {
async getAllProducts(axios) {
const response = await axios.get('fakedb.json');
return response.data;
}
}
product.vue:
import productController from '~/data/controllers/product';
export default {
data() {
return {
products: [],
};
},
async mounted: {
this.products = await productController.getAllProducts(this.$axios);
}
}
I do not think you can make the data function asynchronous, so return an empty data object (I have assumed that it is an array), and then use the mounted hook to retrieve data.
You can't. A function returns immediately and there is nothing meaningful for it to return if you don't want a promise, since loading won't have begun yet. This is the problem that promises solve to begin with. It's a bad pattern to make an async call into your data to begin with, though. Use this pattern instead:
data() {
return {
products: null
}
},
methods: {
getAllProducts() {
return axios.get('fakedb.json').then(response => {
this.products = response.data;
});
}
},
created() {
this.getAllProducts();
}
The async call is abstracted out into the created hook, and when it's resolved it will set the data accordingly.
Here's a little demo
I have the following GraphQL query:
{
events {
name
images {
uri
}
}
}
Currently, I am not able (don't know how) to handle pagination in that images field, because it is fetched using DataLoader.
The eventImagesLoader (DataLoader) implementation is the following:
import DataLoader from 'dataloader';
import { getRepository } from 'typeorm';
import { EventImage } from '../entities/event-image';
export const eventImagesLoader = () =>
new DataLoader<string, EventImage[]>(async (eventIds) => {
const images = await getRepository(EventImage)
.createQueryBuilder('image')
.where('image.eventId IN (:...ids)', { ids: eventIds })
.getMany();
const imagesMap = new Map<string, EventImage[]>();
images.forEach((image) =>
imagesMap.has(image.eventId)
? imagesMap.get(image.eventId)!.push(image)
: imagesMap.set(image.eventId, [image])
);
return eventIds.map((eventId) => imagesMap.get(eventId) || []);
});
The problem that I am facing is to handle the pagination in this scenario, because if you see in the above excerpt of code, I am querying to the database in this way:
const images = await getRepository(EventImage)
.createQueryBuilder('image')
.where('image.eventId IN (:...ids)', { ids: eventIds })
.getMany();
Which translates in a SQL like that:
SELECT * FROM `event_image` AS image WHERE image.id IN ("id:1", "id:2", "id:n");
And I use DataLoader in this case to batch all database queries into one (the above one).
So with that, how am I able to handle pagination?
I would like to do something like that:
{
events {
name
images(page: Int) {
uri
}
}
}
I am using nuxt and apollo together with: https://github.com/nuxt-community/apollo-module
I have a working GraphQL query (tested in GraphiQL):
(Because I want to fetch the info about my page and also some general SEO information)
{
entries(section: [pages], slug: "my-page-slug") {
slug
title
}
seomatic(uri: "/") {
metaTitleContainer
metaTagContainer
metaLinkContainer
metaScriptContainer
metaJsonLdContainer
}
}
I want to fetch this data as well with apollo in nuxt:
So I tried:
<script>
import page from '~/apollo/queries/page'
import seomatic from '~/apollo/queries/seomatic'
export default {
apollo: {
entries: {
query: page,
prefetch: ({ route }) => ({ slug: route.params.slug }),
variables() {
return { slug: this.$route.params.slug }
}
},
seomatic: {
query: seomatic,
prefetch: true
}
},
…
If I do that I will get an error message:
GraphQL error: Cannot query field "seomatic" on type "Query".
I then found this issue
https://github.com/apollographql/apollo-tooling/issues/648
and I would like to know if ths could be a problem of the apollo nuxt module.
Because following that fix indicated in the issue does not resolve anything.
I further tried to combine the two calls into one:
fragment SeoMaticFragment on Root {
seomatic(uri: "/") {
metaTitleContainer
metaTagContainer
metaLinkContainer
metaScriptContainer
metaJsonLdContainer
}
}
query myQuery($slug: String!) {
entries(section: [pages], slug: $slug) {
slug
title
}
SeoMaticFragment
}
~/apollo/queries/page.gql
But this would first throw an error
fragment Unknown type "Root"
So what is the best way to combine?
Why are the requests failing
is there an option to activate batching like described here: https://blog.apollographql.com/query-batching-in-apollo-63acfd859862
-
const client = new ApolloClient({
// ... other options ...
shouldBatch: true,
});
thank you so much in advance.
Cheers
There is actually a solution to this problem.
I found out that the result hook in vue-apollo solves this problem:
Example code that works:
<script>
import gql from 'graphql-tag'
const query = gql`
{
entries(section: [pages], slug: "my-example-page-slug") {
slug
title
}
seomatic(uri: "/") {
metaTitleContainer
metaTagContainer
metaLinkContainer
metaJsonLdContainer
}
}
`
export default {
data: () => {
return {
page: false,
seomatic: {}
}
},
apollo: {
entries: {
query,
prefetch: ({ route }) => ({ slug: route.params.slug }),
variables() {
return { slug: this.$route.params.slug }
}
},
result(result) {
this.entries = result.data.entries
this.seomatic = result.data.seomatic
}
}
}
</script>