How to prevent the router.asPath useEffect from rendering twice on load while using NextJs and getServerSideProps? [duplicate] - next.js

This question already has answers here:
Why is my React component is rendering twice?
(8 answers)
Closed 8 months ago.
I am building an explore page with url query parameters (NextJs page with getServerSideProps)
If an external user goes onto this url domain.com/explore?type=actions&category=food it will fetch on the DB the data for "actions" and "food"
If an internal user uses on-page filters, it generates a new url domain.com/explore?type=actions&category=food&orderBy=points and I then fetch the data and render.
To do so, I am basically setting up a useEffect with the [router.asPath] dependency. The problem is that it renders twice on load for external users (due to gerServerSideProps ?) and therefore fetching the data twice :(
Any hints ? Thanks !
useEffect(() => {
// parsing the url
const url = location.search
const urlQuery = url.slice(1)
const result = {}
urlQuery.split("&").forEach(part => {
const item = part.split("=");
result[item[0]] = decodeURIComponent(item[1]);
});
console.log(result)
// Updating forms/filters states and setting up query parameters
queryParams = [] // reseting the params
setFiltersData(prevFiltersData => {
return {
...prevFiltersData,
thumbType: result.type,
}
})
if (result.field) {
setFiltersData(prevFiltersData => {
return {
...prevFiltersData,
categoryField: result.field,
categoryOperator: result.fieldOp,
categoryValue: result.fieldVal,
}
})
queryParams.push(where(result.field, result.fieldOp, decodeURIComponent(result.fieldVal)))
}
if (result.orderBy) {
setFiltersData(prevFiltersData => {
return {
...prevFiltersData,
orderFieldActions: result.orderBy,
orderOperatorActions: result.orderType,
}
})
queryParams.push(orderBy(result.orderBy, result.orderType))
}
setSearchParams(queryParams) // saving query params to state for subsequent data fetch
getFilteredData(result.type) // Fetching data from the DB
setInitialLoading(false)
}, [router.asPath])

Finally found a solution with this thread. https://github.com/vercel/next.js/issues/35822
The problem is due to React being used in "Strict mode" in the next.config.js.
https://reactjs.org/docs/strict-mode.html
Solution :
/** #type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
experimental: {
scrollRestoration: true,
},
images: {
domains: ['lh3.googleusercontent.com', 'graph.facebook.com', 'firebasestorage.googleapis.com'],
},
}
module.exports = nextConfig
Switching the strictMode to false

Related

NextJS Actioncable Proxy

So I'm trying to do two things at the same time and it's not going too well.
I have a NextJS app and a Rails API server this app connects to. For authentication I'm using a JWT token stored in an http-only encrypted cookie that the Rails API sets and the front end should not be touching. Naturally that creates a necessity for the frontend to send all the api requests though the NextJs server which proxies them to the real API.
To do that I have set up a next-http-proxy-middleware in my /pages/api/[...path] in the following way:
export const config = { api: { bodyParser: false, externalResolver: true } }
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
httpProxyMiddleware(req, res, {
target: process.env.BACKEND_URL,
pathRewrite: [{ patternStr: "^/?api", replaceStr: "" }],
})
}
Which works great and life would be just great, but turns out I need to do the same thing with ActionCable subscriptions. Not to worry, found some handy tutorials, packed #rails/actioncable into my package list and off we go.
import {useCurrentUser} from "../../../data";
import {useEffect, useState} from "react";
const UserSocket = () => {
const { user } = useCurrentUser()
const [roomSocket, setRoomSocket] = useState<any>(null)
const loadConsumer = async () => {
// #ts-ignore
const { createConsumer } = await import("#rails/actioncable")
const newCable = createConsumer('/api/wsp')
console.log('Cable loaded')
setRoomSocket(newCable.subscriptions.create({
channel: 'RoomsChannel'
},{
connected: () => { console.log('Room Connected') },
received: (data: any) => { console.log(data) },
}))
return newCable
}
useEffect(() => {
if (typeof window !== 'undefined' && user?.id) {
console.log('Cable loading')
loadConsumer().then(() => {
console.log('Cable connected')
})
}
return () => { roomSocket?.disconnect() }
}, [typeof window, user?.id])
return <></>
}
export default UserSocket
Now when I go to load the page with that component, I get the log output all the way to Cable connected however I don't see the Room Connected part.
I tried looking at the requests made and for some reason I see 2 requests made to wsp. First is directed at the Rails backend (which means the proxy worked) but it lacks the Cookie headers and thus gets disconnected like this:
{
"type": "disconnect",
"reason": "unauthorized",
"reconnect": false
}
The second request is just shown as ws://localhost:5000/api/wsp (which is my NextJS dev server) with provisional headers and it just hangs up in pending. So neither actually connect properly to the websocket. But if I just replace the /api/wsp parameter with the actual hardcoded API address (ws://localhost:3000/wsp) it all works at once (that however would not work in production since those will be different domains).
Can anyone help me here? I might be missing something dead obvious but can't figure it out.

useFetch of Nuxt 3 sends same request during refresh instead of updating URL

I try to do a simple thing: At route change, refetch data.
Here is my link:
<li v-for="category in categories" :key="category.id" class="mb-1">
<NuxtLink :to="{ query: { cat: category.slug } }">
{{category.title}}
</NuxtLink>
</li>
And my request:
<script setup>
const route = useRoute()
const { data:categories } = await useFetch('http://127.0.0.1:8000/api/tasks/category/', {
key: route.fullPath,
initialCache: false
})
const { data:tasks, refresh } = await useFetch(`http://127.0.0.1:8000/api/tasks/?cat=${route.query.cat}`, {
key: `tasks:${route.query.cat}`,
initialCache: false
})
watch(() => route.query.cat, () => refresh())
</script>
So, at click, url is well changed:
http://localhost:3000/tasks?cat=category1
http://localhost:3000/tasks?cat=category3
http://localhost:3000/tasks?cat=category2
But requests are the same (from Django DRF backend):
GET http://127.0.0.1:8000/api/tasks/?cat=category1
GET http://127.0.0.1:8000/api/tasks/?cat=category1
GET http://127.0.0.1:8000/api/tasks/?cat=category1
It seems that it keeps the first category, even with initialCache: false
As mentioned in the comments, the URL should not be provided as string
const { data:tasks, refresh } = await useFetch(`http://127.0.0.1:8000/api/tasks/?cat=${route.query.cat}`, {
key: `tasks:${route.query.cat}`,
initialCache: false
})
but as a function returning a string:
const { data:tasks, refresh } = await useFetch(() => `http://127.0.0.1:8000/api/tasks/?cat=${route.query.cat}`, {
key: `tasks:${route.query.cat}`,
initialCache: false
})
useFetch is "freezing" the API URL, the changes you make to the string directly will not be reflected. If you want to add parameters to your API URL, as an alternative to using a function, you can use the query option of useFetch. This option is reactive, that is when the ref with your route is changing, the query will reflect this update.
For your case, this will work:
const cat = ref('your_value')
const { data:tasks, refresh } = await useFetch('http://127.0.0.1:8000/api/tasks', {
query: { cat }
})
This results in http://127.0.0.1:8000/api/tasks?cat=your_value
You don't need to mess with the key option, as the key will be auto generated, and you can also leave the cache alone.
The query option is not well documented yet, as discussed in this nuxt issue. I've created a pull request on nuxt/framework to have it reflected in the documentation. Please see a full explanation below:
Using the query option, you can add search parameters to your query. This option is extended from unjs/ohmyfetch and is using ufo to create the URL. Objects are automatically stringified.
const param1 = ref('value1')
const { data, pending, error, refresh } = await useFetch('https://api.nuxtjs.dev/mountains',{
query: { param1, param2: 'value2' }
})
This results in https://api.nuxtjs.dev/mountains?param1=value1&param2=value2

NextJS dev server response time

My main question is; is there a difference in response time for fetching in localhost vs live/production?
I have a project im building in NextJS, with GraphCMS and I'm using GraphQL/graphql-request to fetch the data. When I first start up localhost and the pages loads, I click a link in the page navigation to go to about page and it literally takes 2 seconds for the data to fetch and the page to change. I'm watching the network tab in Chrome DevTools and the .json file status is (pending) and then switches to 200 once the content is downloaded. Here is a screenshot from DevTools:
When I hover over the waterfall, It says the Waiting for server response is 1.86s and content download is 0.46ms. So is the waiting for server response, something because im on a localhost, or is this something to do with the GraphCMS server were its fetching the data from?
Also you may note that the json file size is only 5.2kB, so its not a large fetch.
To give you a little context on the code, my queries & client are stored in the /lib folder:
// /lib/client.js
import { GraphQLClient } from 'graphql-request'
export const graphcmsClient = () =>
new GraphQLClient(process.env.NEXT_PUBLIC_GRAPHCMS_URL, {
headers: {
authorization: `Bearer ${process.env.GRAPHCMS_TOKEN}`,
},
})
// example of a query in ./lib/queries.js
import { gql } from 'graphql-request'
const blogPageQuery = gql`
fragment BlogPostFields on BlogPost {
id
category
content
coverImage {
id
height
url
width
}
excerpt
published
slug
title
}
`
and here is an example where im using getStaticProps and fetching the query data:
export async function getStaticProps({ params, preview = false }) {
const client = graphcmsClient(preview)
const collectionCards = await getAllCollections()
const { page, navigation } = await client.request(pageQuery, {
slug: params.slug
})
if (!page) {
return {
notFound: true
}
}
const parsedPageData = await parsePageData(page)
return {
props: {
page: parsedPageData,
navigation,
collectionCards,
preview
},
revalidate: 60
}
}

Can anyone help implementing Nuxt.js Google Tag Manager with function based id

I installed and add this code to my nuxt.config.js and it works perfectly fine. (Link to package)
modules: [
['#nuxtjs/google-tag-manager', { id: 'GTM-XXXXXXX' }],
]
Now I am trying to implement instead of a static ID a function which will return an ID.
I tried to add this lines into my nuxt.config. js but it is not working. Obviously I have to put it somewhere else or so...
This is what I tried
nuxt.config.js
const code = '1234567'
id: () => {
return 'GTM-' + code
}
export default {
...
modules: [
['#nuxtjs/google-tag-manager', { id: id }],
]
...
}
What would be the correct way implementing this?
I would like to do something like that at the end.
modules: [
['#nuxtjs/google-tag-manager', {
id: ({ req }) => {
if (req.headers.referer == "exmple.com")
return 'GTM-156'
if (req.headers.referer == "exmple.it")
return 'GTM-24424'
if (req.headers.referer == "exmple.es")
return 'GTM-2424'
}
}]]
EDIT:
I solved my problem by rewriting the whole module. It is not possible to use this Module because it is loaded only on build time. I rewrote the module and moved the code into nuxtServerInit.
nuxtServerInit is called on each request (modules only onetime). In the request I asked from which domain the request is coming. Depending on the domain I add different google-tag-manager id's to the head and the plugin.
From package docs:
modules: [
['#nuxtjs/google-tag-manager', {
id: () => {
return axios.get('http://example.com/')
.then(({ data }) => {
return data.gtm_id
})
}
}]]
You can use process.env.NODE_ENV inside function which will return an ID
Edit 1
To put the gtm id, depending on req.headers.referer you need to provide context to the function returning the id. This can be done in middleware
See how it works here
https://github.com/nuxt-community/modules/blob/master/packages/google-tag-manager/plugin.js
Edit 2
As far as I understand your question, it will not work to have a query context in the config.
Look at i18n middleware: request.locale - > store - > update modules (router, vuetify, moment, etc.)
https://nuxtjs.org/examples/i18n/
~/middleware/gtm.js
export default function ({ app, store }) {
// app.$gtm contains id, you can set another from store
}
don't forget to add middleware to the page
page.vue
export default {
middleware: ['gtm']
}

AngularFire2 db.collection Adds key 2 Times

I followed this guide to create a upload-component for Angular5 with AngularFire2.
At the end of the Video he showed a code snippet that allows adding a url path to any other database url.
this.snapshot = this.task.snapshotChanges().pipe(
tap(snap => {
console.log(snap);
if (snap.bytesTransferred === snap.totalBytes) {
// Update firestore on completion
this.db.collection('photos').add({ path, size: snap.totalBytes }).then();
}
})
);
This creates a url entry to photos, but it does it 2 times. Any idea how this can be? Each upload he creates 2 random keys with exactly same content inside.
Try something like this. Add to collection in downloadURL()
import { AngularFireStorage } from 'angularfire2/storage';
constructor(private storage: AngularFireStorage)
uploadFile(file){
const filePath = 'images/' + this.docRef + '/';
const task = this.storage.upload(filePath, file);
task.percentageChanges().subscribe(per => {
console.log(per);
});
task.downloadURL().subscribe(url => {
console.log(url);
this.db.collection('photos').add('required data')
}
}

Resources