getInitialProps is never executed - reactjs

I'm trying to make a Twitter component that loads the tweet from the twitter API and displays its HTML, which will be a <blockquote> and a <script> tag.
The advantage of this, if server side rendered, is that it would work even if the user has privacy settings that block the call to the twitter script: the user will still get the blockquote, which is better than the current behavior (shows nothing).
So, my idea was to do something like this:
import fetch from 'node-fetch'
function Tweet(props) {
return <>
<div dangerouslySetInnerHTML={{
__html: props.__html
}} />
<p>here I am</p>
</>
}
Tweet.getInitialProps = async ctx => {
console.log("here I am on the road again")
const res = await fetch(`https://api.twitter.com/1/statuses/oembed.json?id=${ctx.tweetId}`)
const json = await res.json()
return { __html: json.html }
}
export default Tweet
And then on the pages use it like <Tweet tweetId="blah" />.
I found two problems with my idea though:
As per docs I cant access the tweetId property on getInitialProps
getInitialProps is never called. The <p>here I am</p> appears on the HTML and the log is never printed anywhere.
So, my question is: what I am doing wrong? Is this even possible to do?
Thanks!

As per Next.js documentation, getInitialProps can only be used to a page, and not in a component:
getInitialProps can only be added to the default component exported by a page, adding it to any other component won't work.

Related

Next js how to fetch localStorage data before client side rendering

I am using react + next.js in my client side and I am getting this weird warning, which I've failed to fix over the previous 3 days.
I assume the Warning arise in because the isUserAuthenticated value is saved on the localStorage.
and localStorage is only available on the client side, and therefore the value is diffrent from the initial data rendered on the server.
after searching on google I read a few post which suggested using componentDidMount() to in order to load the localStorage values before the the client side in rendred.
unfortunelty I've failed to implement this idea, threfore I am asking for help, can someone help me to to solve this problem? thanks in advance
The warning:
react-dom.development.js:67 Warning: Text content did not match.
Server: "false" Client: "true"
at div
at ul
at div
at nav
at Navbar
Navbar.js
const Navbar = () => {
const { isUserAuthenticated, } = useSelector((state) => state.authReducer);
return (
<nav data-testid='navbar'>
<div>
<>{isUserAuthenticated ? <div>true</div> : <div>false</div>}</>
</div>
</nav>
);
};
export default Navbar;
the solution
// TODO recipes crud to navbar
// test author and guest links
import { useSelector } from 'react-redux';
import React from 'react';
const Navbar = () => {
const [isUserAuthenticated, setIsUserAuthenticated] = useState();
useEffect(() => {
setIsUserAuthenticated(store.getState().authReducer.isUserAuthenticated);
}, []);
return (
<nav data-testid='navbar'>
<div>
<>{isUserAuthenticated ? <div>true</div> : <div>false</div>}</>
</div>
</nav>
);
};
export default Navbar;
This error happens when your server returns a different html structure than your client side, this will force React to re-render the entire App instead just attach event listeners (which is much faster).
From the error, looks like your isUserAuthenticated & loggedUserData have different values at client side & server side, Print them, if this is the situation, check the reason for this.
Edit
As you've mentioned, localStorage available only at client side, this means that your code most support it.
At the serverSide (there is no localStorage) your isUserAuthenticated should return false.
At the client side, you should "continue" the server state (which means that you don't load the value from localStorage firstly), then when the app is mounted, load the value.
if you are using classes, use the componentDidMount (which runs only at client side) in order to update the value of isUserAuthenticated.
if you are using hooks, use the useEffect hook (which runs only at client side) in order to update the value of isUserAuthenticated.

How to fetch details of item, when user opens details page directly?

I was going through official Redux tutorial (Redux Essential) and I believe there is one thing missing. There is a simple "List/Details" example and it's all clear, but the Details page (SinglePostPage) will not work, if user types the exact address in the address bar manually. In other words, if you open the website directly on the SinglePostPage - the list of posts will not be fetched, thus Page will display: Post not found!. The only way to view the post, is to open the list first, then click "View Post".
Here is a SinglePostPage
import React from 'react'
import { useSelector } from 'react-redux'
export const SinglePostPage = ({ match }) => {
const { postId } = match.params
const post = useSelector(state =>
state.posts.find(post => post.id === postId)
)
if (!post) {
return (
<section>
<h2>Post not found!</h2>
</section>
)
}
return (
<section>
<article className="post">
<h2>{post.title}</h2>
<p className="post-content">{post.content}</p>
</article>
</section>
)
}
I could simply fetch this post, like it's done for posts, but my biggest concern is, that in the first render useSelector will return undefined post, before it will dispatch fetch action. How will I know if "undefined" means that post was not found or if it wasn't even fetched?
So my question is, what's the best way to handle it? Should I create a separate slice for single post with loading status and error? Similar to postsSlice? I'm not sure If I should have two slices per one feature. Should I?
EDIT: Just to clarify, I'm not saying that tutorial has a bug. It's a very simple, not-real-life-app sample. The fact that you can't open post's details by typing direct URL in the browser was not covered by the tutorial. So my question is about possible solution to this "issue".

How can I create a separate dynamic window in my React app?

My React app incorporates a video chat function (via Twilio). The user goes to a dashboard and then clicks a button to start the call. This prompts the VideoCall component to be instantiated and shown. On instantiation, it connects to a backend Twilio service to get an access token, and then connects to Twilio to create the call, set up events handlers etc.
Currently I'm showing the video windows in a div within the dashboard, but I would like them to appear in a pop-out window instead. I've tried using react-new-window and I've tried React Portals, but I didn't know enough about what I was doing to make it work.
Currently I have the following Dashboard component:
function Dashboard(props) {
const { displayInfo} = props
const [ callInitiated, setCallInitiated ] = useState(false)
const initCall = () => {
setCallInitiated(true)
}
return (
<div id="dashboard">
{callInitiated ? <VideoCall displayInfo={displayInfo} /> : null }
<div> ...rest of dashboard, including button which calls 'initCall' on being clicked... </div>
</div>
)
}
export default Dashboard
My VideoCall component is:
const Video = require('twilio-video');
// Get access token from backend service
async function getAccessToken(identity) {
const url = `${process.env.REACT_APP_TWILIO_TOKEN_SERVICE}?identity=${identity}`;
try {
const response = await axios.get(`${url}`, AXIOS_HEADERS);
return response.data.accessToken;
} catch {
return null;
}
}
// VideoCall component
function VideoCall(props) {
const { displayInfo} = props
// Connect to Twilio server function to get access token
getAccessToken('Tester')
.then((token) => {
Video.connect(token, {
name: 'Test room'
})
.then(room => {
participantConnected(room.localParticipant);
room.participants.forEach(participantConnected);
room.on('participantConnected', participantConnected);
room.on('participantDisconnected', participantDisconnected);
room.once('disconnected', error => room.participants.forEach(participantDisconnected))
})
});
function participantConnected(participant) {
const div = document.createElement('div');
div.id = participant.sid;
participant.on('trackSubscribed', track => trackSubscribed(div, track));
participant.on('trackUnsubscribed', trackUnsubscribed);
participant.tracks.forEach(publication => {
trackSubscribed(div, publication.track);
});
if(participant.identity === 'Tester') {
document.getElementById('myVideoWindow').appendChild(div)
}
}
function participantDisconnected(participant) {
document.getElementById(participant.sid).remove();
}
function trackSubscribed(div, track) {
div.appendChild(track.attach());
}
function trackUnsubscribed(track) {
track.detach().forEach(element => element.remove());
}
return (
<div id="callWrapper" className="callOuterWrapper">
<div className="titleBar">
<h1 className="pageHeader">Calling {displayInfo.receiverName}</h1>
</div>
<div className="videoRoom">
<div id="myVideoWindow" className="callWindow"></div>
<div className="callInfo"> ...this will contain info on call status... </div>
<div id="receiverWindow" className="callWindow"></div>
</div>
</div>
)
}
export default VideoCall
So far, this works. The user clicks the button and the video windows appear at the top of the dashbaord, as expected.
Now I want to pull the VideoCall component out into a separate window (so that the user can still see the dashboard while on the call.
I tried the package react-new-window, which just involved wrapping the VideoCall in a NewWindow. I tried wrapping it within the Dashboard component:
<div id="dashboard">
{callInitiated ? <NewWindow><VideoCall displayInfo={displayInfo} /></NewWindow> : null }
<div> ...rest of dashboard, including button which calls 'initCall' on being clicked... </div>
</div>
and when that didn't work I tried wrapping within the VideoCall component:
<NewWindow>
<div id="callWrapper" className="callOuterWrapper">...</div>
</NewWindow>
In both cases this displayed the new window with the empty callWrapper div; however, once it reached document.getElementById('myVideoWindow').appendChild(div) it was unable to find the div. The DOM being referenced appears to be the one from the Dashboard window rather than the new windows (also, any console.log commands get logged to the console of the original window, not the new one).
I then tried taking apart the NewWindow code and creating my own bespoke version, but I don't know enough about how it works to make it do what I needed.
So, is there a way to access the DOM of the new window from the component within it? Or is there a different approach I should be taking?
Twilio developer evangelist here.
Directly accessing the DOM with native DOM methods, like document.getElementById, is a little frowned upon within React. React itself should be in charge of adding and removing things from the DOM.
I wrote a post on how to build a video chat with React that covers how to add your participants to the page without accessing the DOM directly.
I'd recommend a look through that and perhaps updating your app so that you don't have to use document.getElementById and then hopefully the <NewWindow> component should work as advertised.

How to fetch data for a reusable layout component in Next.js

I need to put slugs in my Header component which is reusable in all the site inside the layout:
const Layout = ({ children }) => {
return (
<>
<Header />//////////this one.
{children}
<Footer />
</>
);
};
My slugs goings from the server side and I need to dynamically fetch them only one time in the header.
Inside the header something like:
categoriesLinks.map(category=> {
return <div>{category.SLUG}</div>
})
But because it's a reusable component I don't know how and where to invoke the getServerSideProps or getStaticProps.
if I use the index component for example I need to fetch data in every component
if I tried to fetch it inside getInitialProps in the _app, it doesn't get it (get undefined)
What is the right way to handle this? It seems like a really big issue and I couldn't find the right solution for this.
You can't call getServerSideProps or getStaticProps in a component. You have to write the getServersideprops or getStaticProps inside the page and then pass the fetched data to the layout. You can also use a state management system like react context or recoil (recommended) to save the data so you won't have to re-fetch every time and then in the layout do the following
useEffect(() => ChangeData(data), [])
This code will render your data once. ChangeData is the stateChanger for recoil or react context.
This is what I found on Vercel https://nextjs.org/blog/layouts-rfc#data-fetching-in-layouts via an ongoing discussion on https://github.com/vercel/next.js/discussions/10949
You can fetch data in a layout.js file by using the Next.js data fetching methods getStaticProps or getServerSideProps.
For example, a blog layout could use getStaticProps to fetch categories from a CMS, which can be used to populate a sidebar component:
// app/blog/layout.js
export async function getStaticProps() {
const categories = await getCategoriesFromCMS();
return {
props: { categories },
};
}
export default function BlogLayout({ categories, children }) {
return (
<>
<BlogSidebar categories={categories} />
{children}
</>
);
}
To see

nextjs Dynamic route rendering content not working

I am stuck on this problem for many days. I am using Next.js and have 3 pages.
pages/index.js
pages/categories.js
pages/categories/[slug].js
The categories/[slug].js is using Next.js fetching method name getServerSideProps that runs on each request and used for build dynamic pages on runtime. The categories/[slug].js is rendering a dynamic content on the page that dynamic content comes from the CMS as a response from the API Endpoint. Dynamic content is nothing but a string that contains HTML with <script /> elements.
Note: To fetch the content from the CMS we have to send a POST request with the CMS credentials like username, password, and the page slug for the content. I am using axios library to send a post request and the method is inside post.js file.
post.js:
import axios from 'axios';
const postCMS = async (slug) => {
const url = `${process.env.CMS_API_URL}/render-page/`;
let pageSlug = slug;
// If the pageSlug is not start with `/`, then create the slug with `/`
if (!pageSlug.startsWith('/')) {
pageSlug = `/${pageSlug}`;
}
const head = {
Accept: 'application/json',
'Content-Type': 'application/json'
};
const data = JSON.stringify({
username: process.env.CMS_API_USERNAME,
password: process.env.CMS_API_PASSWORD,
slug: pageSlug
});
try {
const response = await axios.post(url, data, {
headers: head
});
return response.data;
} catch (e) {
return e;
}
};
export default postCMS;
But for the rendering content on the categories/[slug].js page, I am using the Reactjs prop name dangerouslySetInnerHTML to render all the HTML which also contains <script /> elements in the JSON string.
pages/categories/[slug].js:
<div dangerouslySetInnerHTML={{ __html: result.html }} />
The content is loading fine based on each slug. But when I navigate to that category page i.e.pages/categories/index.js.
<Link href="/categories/[slug]" as="/categories/online-cloud-storage">
<a>Online Cloud Storage</a>
</Link>
It has a <Link /> element and when I click it.
The dynamic content is loading fine but that dynamic content contains accordion and slider elements they are not working. I think <script /> of these elements is not working. But when I refresh the page they work fine. See this.
They also work fine when I set the Link something like this.
<Link href="/categories/online-cloud-storage" as="/categories/online-cloud-storage">
<a>Online Cloud Storage</a>
</Link>
But after setting the link like the above method, the click is caused to hard reload the page. But I don't want this. Everything should work. When the user clicks on the category link.
Is there a way to fix this?
Why the content elements are not working when you click from the categories/index.js page?
Github repo
Code:
pages/index.js:
import React from 'react';
import Link from 'next/link';
const IndexPage = () => {
return (
<div>
<Link href="/categories">
<a>Categories</a>
</Link>
</div>
);
};
export default IndexPage;
pages/categories/index.js:
import React from 'react';
import Link from 'next/link';
const Categories = () => {
return (
<div>
<Link href="/categories/[slug]" as="/categories/online-cloud-storage">
<a>Online Cloud Storage</a>
</Link>
</div>
);
};
export default Categories;
pages/categories/[slug].js:
import React from 'react';
import Head from 'next/head';
import postCMS from '../../post';
const CategoryPage = ({ result }) => {
return (
<>
<Head>
{result && <link href={result.assets.stylesheets} rel="stylesheet" />}
</Head>
<div>
<h1>Category page CMS Content</h1>
<div dangerouslySetInnerHTML={{ __html: result.html }} />
</div>
</>
);
};
export const getServerSideProps = async (context) => {
const categorySlug = context.query.slug;
const result = await postCMS(categorySlug);
return {
props: {
result
}
};
};
export default CategoryPage;
The problem here is that <script> tags which are dynamically inserted with dangerouslySetInnerHTML or innerHTML, are not executed as HTML 5 specification states:
script elements inserted using innerHTML do not execute when they are inserted.
If you want to insert new <script> tag after the page has initially rendered, you need to do it through JavaScript's document.createElement('script') interface and appended to the DOM with element.appendChild() to make sure they're executed.
The reason why the scripts don't work after changing routes, but they do work after you refresh the page is tied to Next.js application lifecycle process.
If you refresh the page, Next.js pre-renders the entire website on the server and sends it back to the client as a whole. Therefore, the website is parsed as a regular static page and the <script> tags are executed as they normally would.
If you change routes, Next.js does not refresh the entire website/application, but only the portion of it which has changed. In other words, only the page component is fetched and it is dynamically inserted into existing layout replacing previous page. Therefore, the <script> tags are not executed.
Easy solution
Let some existing library handle the hard work for you by parsing the HTML string and recreating the DOM tree structure. Here's how it could look in jQuery:
import { useEffect, useRef } from 'react';
import $ from 'jquery';
const CategoryPage = ({ result }) => {
const element = useRef();
useEffect(() => {
$(element.current).html($(result.html));
}, []);
return (
<>
<Head>
{result && <link href={result.assets.stylesheets} rel="stylesheet" />}
</Head>
<div>
<h1>Category page CMS Content</h1>
<div ref={element}></div>
</div>
</>
);
};
export const getServerSideProps = async (context) => {
/* ... */
return { props: { result } };
}
Harder solution
You would have to find a way to extract all <script> tags from your HTML string and add them separately to your page. The cleanest way would be to modify the API response to deliver static HTML and dynamic script in two separate strings. Then, you could insert the HTML with dangerouslySetInnerHTML and add script in JS:
const scripts = extractScriptTags(result.html); // The hard part
scripts.forEach((script) => {
const scriptTag = document.createElement('script');
scriptTag.innerText = script;
element.current.appendChild(scriptTag);
});
IMHO, I believe that the script non-proper loading is due to erroneous import of the scripts on Client-Side Rendering (CSR) and Server-Side Rendering (SSR). Read more here, but also take a look on this interesting article. This would also explain the problematic behavior of your link component.
In your case, I understand that this behavior is due to false handling of the lifecycle of the page and its components during CSR, as many scripts might need to properly shared across the navigation of pages, possibly in SSR. I do not have the full picture of what the problem is or extended expertise on NextJS, but I believe that those scripts should be imported in one place and possibly rendered on the server, instead of false importing on each page, falsely letting CSR do the work in a non-NextJS optimized manner.
The suggested way is to use a custom Document implementation (SSR-only) for your application, where you can define the scripts. See here for more details on this. Also I suppose you already have setup a custom App file for your App, where you will use it in your document, for both CSR and SSR rendering common to all pages (See this SO question for more on that).
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
// ...
}
render() {
return (
<Html>
<Head>
{/*Your head scripts here*/}
</Head>
<body>
<Main />
{/*Your body scripts here*/}
<NextScript />
</body>
</Html>
)
}
}
The Head native component does a lot of work on the background in order to setup things for scripts, markup, etc. I suggest you go that way, instead of just adding the scripts into each page directly.

Resources