React Apollo SSR getDataFromTree not waiting for query - reactjs

I'm trying to set up SSR for my React app, which uses Apollo Client for graphql requests to an API. I've set up the webpack config, and the Express app, correctly, and I can see the components getting loaded. The issue here is that getDataFromTree is not waiting for my query at all. It simply returns the component with loading: true and then nothing. I set ssrForceFetchDelay as well to make the client refetch the queries, but nothing.
I keep getting window.__APOLLO_STATE__={}. The components are very basic, making simple queries and displaying the result.
This is my express app:
app.use((req, res) => {
const client = new ApolloClient({
ssrMode: true,
link: new HttpLink({
uri: 'http://localhost:8000/graphql',
fetch: fetch
}),
cache: new InMemoryCache(),
});
const app = (
<ApolloProvider client={client}>
<Router location={req.url} context={{}}>
<App client={client}/>
</Router>
</ApolloProvider>
);
const jsx = extractor.collectChunks(app); // Using loadable
renderToStringWithData(jsx).then((dataResp) => {
const content = dataResp;
const state = client.extract(); // Always {}
const helmet = Helmet.renderStatic();
const html = ReactDOMServer.renderToStaticMarkup(
<Html content={content} helmet={helmet} assets={assets} state={state} />,
);
res.status(200); res.send(`<!doctype html>${html}`);
res.end();
}).catch(err => {
console.log(`Error thrown`);
});
});
app.listen(port, () => {
console.log(`Server listening on ${port} port`);
});
This is my client-side Apollo config:
const cache = new InMemoryCache().restore(window.__APOLLO_STATE__);
const client = new ApolloClient({
ssrForceFetchDelay: 300,
link: links,
cache: cache,
connectToDevTools: true,
});
My App :
const Wrapped = () => {
console.log(`Inside Wrapped`);
return (
<ApolloProvider client={apolloClient}> //The client one
<Router>
<App /> /*Only returns 2 basic components */
</Router>
</ApolloProvider>
);
};
hydrate(<Wrapped />, document.getElementById('root'));
I can see the components on the page, but no queries have been loaded and APOLLO_STATE is always empty. I've been following the documentation on react-apollo to set this up, but I can't understand why this basic set up won't work. Any ideas on what I need to change here? Thanks!
Html component:
import React from 'react';
const Html = ({ content, helmet, assets, state }) => {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="/manifest.json" />
<link rel="shortcut icon" href="/favicon.ico" />
{helmet.meta.toComponent()}
{helmet.title.toComponent()}
{assets.css &&
assets.css.map((c, idx) => (
<link key={idx} href={c} rel="stylesheet" />
))}
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" dangerouslySetInnerHTML={{ __html: content }} />
<script
dangerouslySetInnerHTML={{
__html: `window.__APOLLO_STATE__=${JSON.stringify(state).replace(
/</g,
'\\u003c',
)};`,
}}
/>
{assets.js &&
assets.js.map((j, idx) => (
<script key={idx} src={j} />
))}
</body>
</html>
);
};
export default Html;
EDIT: Added my HTML component code
Link to repo with basic code that is working correctly. I intend to add Loadable to this to see if that is what is causing the issue.
https://github.com/kvnam/gql-ssr-test

Related

next-i18next not rendering client side

i am updating dependencies for a client and i updated nextJS from getInitialProps to getServerSideProps.
Therefore, i had to update next-i18next.
Before, here's what was used in getInitialProps for translations :
class Homepage extends React.Component {
static async getInitialProps ({ store }) {
store.dispatch(PlansCreators.plansGet())
return {
namespacesRequired: ['common', 'containers', 'home', 'form']
}
}
render () {
return (
<Layout>
<Head>
<title>{`${config.DEV ? 'DEV - ' : ''} ${t('SEO.BASE.TITLE')}`}</title>
<meta name='description' content={t('SEO.BASE.DESCRIPTION')} />
<meta property='og:title' content={`${t('SEO.BASE.TITLE')}`} />
<meta property='og:description' content={t('SEO.BASE.DESCRIPTION')} />
<meta property='og:image' content='' />
</Head>
<Home />
</Layout>
)
}
}
After my update :
function Homepage() {
return (
<Layout>
<Head>
<title>{`${config.DEV ? 'DEV - ' : ''} ${t('SEO.BASE.TITLE')}`}</title>
<meta name="description" content={t('SEO.BASE.DESCRIPTION')} />
<meta property="og:title" content={`${t('SEO.BASE.TITLE')}`} />
<meta property="og:description" content={t('SEO.BASE.DESCRIPTION')} />
<meta property="og:image" content="" />
</Head>
<Home />
</Layout>
)
}
export async function getInitialProps({ locale, store }) {
store.dispatch(PlansCreators.plansGet())
return {
props: {
...(await serverSideTranslations(locale, ['common']))
}
}
}
The translations seems to be working during the render, but only for like 0.5 seconds. Then, it come back to SEO.BASE.TITLE... So i thought it was the localePath in the next-i18next.config.js that was involved, but that's not the case.
Here's my next-i18next.config.js :
module.exports = {
i18n: {
locales: ['fr', 'en'],
defaultLocale: 'fr',
reloadOnPrerender: process.env.NODE_ENV === 'development',
debug: process.env.NODE_ENV === 'development',
localePath:
typeof window === 'undefined'
? require('path').resolve('./public/locales')
: '/locales'
}
}
I also have this error in my javascript console (browser side) :
react_devtools_backend.js:4012 i18next: hasLoadedNamespace: i18next was not initialized
and
i18next::translator: key "SEO.ACCOUNT.TITLE" for languages "fr" won't get resolved as namespace "common" was not yet loaded This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!
Do i miss something ? Do i have to build a backend even with next-i18next ? I just follow the documentation 3 times...
Here are my versions :
"i18next": "^22.0.6",
"react-i18next": "^12.0.0",
"next-i18next": "^13.0.3",
Here's how i wrap my App in _app.tsx
export default compose(
withRedux(bootstrapStore),
withReduxSaga,
withRouter
)(appWithTranslation(MyApp, nextI18NextConfig))
Thanks for help.

Next.JS slow initial load due to SSR api request

I'm trying to fetch data from the server side using SSR. unfortunately the initial loading time is awful, around 6s.
i tried to use both getServerSideProps and getStaticProps. however, the results remains the same.
it there a way to bypass that or to call the API from the FE would suit the situation better?
import Head from 'next/head';
import { useQuery } from 'react-query';
import { getCountries } from '../infrastructure/http/client';
import { Navbar } from '#/components/navbar/navbar';
import { FilterSection } from '#/components/filter-section/filter-section';
import { CardsSection } from '#/components/cards-section/cards-section';
import { Store } from '#/state/store';
export async function getServerSideProps(context : any) {
const getCountriesData = await getCountries();
const countriesData = await JSON.parse(getCountriesData);
return {
props: {
countriesData,
},
};
};
// Maybe i dont need an api call for the entire page, will try to move it to the
// store
export default function Home({countriesData} : any) {
// pass the data received from getServerSideProps
const { data } = useQuery('todos', getCountries, { initialData: countriesData });
console.log(data);
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Navbar />
<Store >
{/* TODO ---- */}
{/* Routing */}
<FilterSection />
<CardsSection countriesData={countriesData} />
</Store>
</>
);
};

How to dynamically change meta tag description for blog/:id in react js

I created a blog website with reactjs, I want to implement a dynamic meta tag so that when user share each link on any social media platform, the meta tag description for each blog will show up.
I saw this tutorial and I implement this in my project, but The meta tags are not showing
https://blog.logrocket.com/adding-dynamic-meta-tags-react-app-without-ssr/
here is my route
import React from "react"
import './App.css';
import Home from "./pages/home/home";
// import { useDispatch, useSelector } from "react-redux";
import {Routes, Route} from "react-router-dom"
import Auth from "./pages/Auth/Auth";
import Singlepost from "./pages/home/singlepost";
function App() {
return (
<div>
{/* <Navbar /> */}
<Routes>
<Route exact path ='/' caseSensitive={false} element={<Home/>} />
<Route path='/auth' caseSensitive={false} element={<Auth />}/>
<Route path='/:topic' caseSensitive={false} element={<Singlepost />} />
</Routes>
</div>
);
}
export default App;
Here is my server below
const express = require('express');
const axios = require('axios')
const path = require('path');
const fs = require("fs");
const app = express();
const PORT = process.env.PORT || 8000;
const indexPath = path.resolve(__dirname, '..', 'build', 'index.html');
// static resources should just be served as they are
app.use(express.static(
path.resolve(__dirname, '..', 'build'),
{ maxAge: '30d' },
));
// here we serve the index.html page
app.get('/*', async(req, res, next) => {
fs.readFile(indexPath, 'utf8', (err, htmlData) => {
if (err) {
console.error('Error during file reading', err);
return res.status(404).end()
}
// get post info
const postId = req.query.topic;
const post = axios.get('https://inspiredformen.herokuapp.com/posts', postId);
console.log("post", post)
// getPostById(postId);
if(!post) return res.status(404).send("Post not found");
// inject meta tags
htmlData = htmlData.replace(
"<title>React App</title>",
`<title>${post.topic}</title>`
)
.replace('__META_OG_TITLE__', post.topic)
.replace('__META_OG_DESCRIPTION__', post.topic)
.replace('__META_DESCRIPTION__', post.topic)
// .replace('__META_OG_IMAGE__', post.thumbnail)
return res.send(htmlData);
});
});
// listening...
app.listen(PORT, (error) => {
if (error) {
return console.log('Error during app startup', error);
}
console.log("listening on " + PORT + "...");
});
here is index.html file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>__META_OG_TITLE__</title>
<meta name="description" content="__META_DESCRIPTION__"/>
<meta name="og:title" content="__META_OG_TITLE__"/>
<meta name="og:description" content="__META_OG_DESCRIPTION__"/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
...
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
...
</body>
</html>
Please, I will appreciate if anyone can help me with to solve this problem.
Thanks all

React-Helmet not updating meta and title in SSR application

I have updated this to use react-helmet-async in place of react-helmet.
I have a React application that has implemented server side rendering. I am following the steps on this site: Documentation for react-helmet-async, to implement the react-helmet-async functionality to dynamically assign meta and title. I am using a provider and have added the Helmet component to the App.js and the learnerSetupContainer.js scripts. When I look at the source code for both these pages I can that the title and meta for the home page is properly updated but the learnerSetupContainer is not being updated and is showing the same title and meta as the home page,
<title data-rh="true" itemprop="name" lang="en">Welcome to our Homepage</title>
<meta data-rh="true" name="description" content="Teach Learn Game"/><meta data-rh="true" name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"/>
I see other stackoverflow questions that say, "It's not documented in the readme, but you have to add data-react-helmet="true" to your meta tags that you want to replace with react-helmet." I tried adding this string but there was no change. Maybe I am not adding correctly. Can anyone see what I am doing wrong?
Below is the code I am using:
template.js
export default ({ markup, helmet , css }) => {
return `<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-1234567-9">
</script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-123456789-9');
</script>
<script data-ad-client="ca-pub-123456789012345" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<meta charset="utf-8">
<meta name="msvalidate.01" content="1234567890ASDFGHJJ" />
<meta name="google-site-verification" content="1234567890ASDFGHJKL" />
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<style>
a{
text-decoration: none;
color: #bb4d00
}
</style>
</head>
<body ${helmet.bodyAttributes.toString()}>
<div id="root">${markup}</div>
<style id="jss-server-side">${css}</style>
<script type="text/javascript" src="/dist/bundle.js"></script>
</body>
</html>`;
};
express.js (only code relevant to react-helmet)
import {Helmet, HelmetProvider} from "react-helmet-async";
app.get("*", (req, res) => {
const sheets = new ServerStyleSheets();
const context = {};
const helmetContext = {};
const markup = ReactDOMServer.renderToString(
//const markup = ReactDOMServer.renderToStaticMarkup(
sheets.collect(
<StaticRouter location={req.url} context={context}>
<ThemeProvider theme={theme}>
<HelmetProvider context = {helmetContext}>
<MainRouter />
</HelmetProvider>
</ThemeProvider>
</StaticRouter>
)
);
const {helmet} = helmetContext;
console.log ("Express - markup = ", markup);
console.log ("Express - helmet = ", helmet);
console.log ("Express - helmetContext = ", helmetContext);
console.log ("Express - helmet.title.toString() = ", helmet.title.toString());
console.log ("Express - helmet.meta.toString() = ", helmet.meta.toString());
if (context.url) {
return res.redirect(303, context.url);
}
const css = sheets.toString();
res.status(200).send(
Template({
markup: markup,
helmet: helmet,
css: css,
})
);
});
App.js (only code relevant to react-helmet)
import {Helmet, HelmetProvider} from "react-helmet-async";
return (
<>
<React.StrictMode>
<BrowserRouter>
<ThemeProvider theme={responsivetheme}>
<SpeechDataProvider>
<CriteriaProvider>
<RandomOptionsProvider>
<HelmetProvider>
<MainRouter />
</HelmetProvider>
</RandomOptionsProvider>
</CriteriaProvider>
</SpeechDataProvider>
</ThemeProvider>
</BrowserRouter>
</React.StrictMode>
</>
);
MenuRouter.js (only code relevant to react-helmet)
import {Helmet, HelmetProvider} from "react-helmet-async";
return (
<div>
<Helmet
data-rh="true"
htmlAttributes={{lang: "en", amp: undefined}} // amp takes no value
//titleTemplate="%s | React App"
titleAttributes={{itemprop: "name", lang: "en"}}
title="Welcome to our Homepage"
meta={[
{name: "description", content: "Teach Learn Game"},
{name: "viewport", content: "minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"},
]}
/>
</div>
learnerSetupContainer.js (only code relevant to react-helmet)
import {Helmet, HelmetProvider} from "react-helmet-async";
return (
<>
<div>
<Helmet>
<title data-rh="true">Welcome to our Learner Setup Page </title>
<meta data-rh="true" name="description" content="Game Options: Vocabulary, Math, History and Geography" />
<meta data-rh="true" name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
</Helmet>
</div>
</>

Load Google Place API in Gatsbyjs (Reactjs) project

I am trying to use the AutoComplete address service from Google Place API.
Found this library:
https://github.com/kenny-hibino/react-places-autocomplete#load-google-library
It asks for loading the library in my project:
https://github.com/kenny-hibino/react-places-autocomplete#getting-started
I would do it in the public/index.html if it's pure Reactjs project. However, the public/index.html in Gatsbyjs project will be deleted and re-generated every time when running:
Gatsby develop
command line.
How can I use the Google Place API in my Gatsbyjs project?
Update
I have tried 2 ways to achieve this.
Use React-Helmet in /layouts/index.js , here is how it looks like:
<Helmet>
<script src="https://maps.googleapis.com/maps/api/js?key={API}&libraries=places&callback=initAutocomplete" async defer></script>
</Helmet>
Put the script reference in the /public/index.html, which looks like this:
<!DOCTYPE html>
<html>
<head>
<meta charSet="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title data-react-helmet="true"></title>
<script src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key={API_KEY}&libraries=places" async defer ></script>
</head>
<body>
<div id="___gatsby"></div>
<script src="/commons.js"></script>
</body>
</html>
For the 1st solution, every time after I refresh my page, the project throws an error asking for loading the Google JavaScript Map API.
For the 2nd solution, every time after I re-start the Gatsby by the command line: gatsby develop
it re-generates the index.html which flushes away my JavaScript reference in it.
You shouldn't modify any files in the public forlder with GatsbyJS.
Instead, I recommend you to customize your html.js file.
To do so, first run:
cp .cache/default-html.js src/html.js
You should have the html.js file in /src/html.js.
Now you can put your <script> tag within the <head>.
Update Feb 24, 2020
Here's a more modern implementation using React hooks with some performance optimizations based on React.memo and a custom shouldUpdate function. See this blog post for details.
import { functions, isEqual, omit } from 'lodash'
import React, { useState, useEffect, useRef } from 'react'
function Map({ options, onMount, className, onMountProps }) {
const ref = useRef()
const [map, setMap] = useState()
useEffect(() => {
// The Map constructor modifies its options object in place by adding
// a mapTypeId with default value 'roadmap'. This confuses shouldNotUpdate.
// { ...options } prevents this by passing in a copy.
const onLoad = () =>
setMap(new window.google.maps.Map(ref.current, { ...options }))
if (!window.google) {
const script = document.createElement(`script`)
script.src = `https://maps.googleapis.com/maps/api/js?key=` + YOUR_API_KEY
document.head.append(script)
script.addEventListener(`load`, onLoad)
return () => script.removeEventListener(`load`, onLoad)
} else onLoad()
}, [options])
if (map && typeof onMount === `function`) onMount(map, onMountProps)
return (
<div
style={{ height: `60vh`, margin: ` 1em 0`, borderRadius: ` 0.5em` }}
{...{ ref, className }}
/>
)
}
function shouldNotUpdate(props, nextProps) {
const [funcs, nextFuncs] = [functions(props), functions(nextProps)]
const noPropChange = isEqual(omit(props, funcs), omit(nextProps, nextFuncs))
const noFuncChange =
funcs.length === nextFuncs.length &&
funcs.every(fn => props[fn].toString() === nextProps[fn].toString())
return noPropChange && noFuncChange
}
export default React.memo(Map, shouldNotUpdate)
Map.defaultProps = {
options: {
center: { lat: 48, lng: 8 },
zoom: 5,
},
}
Old Answer
Using html.js
Modifying src/html.js like so (as Nenu suggests) is one option.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class HTML extends Component {
render() {
return (
<html {...this.props.htmlAttributes}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{this.props.headComponents}
</head>
<body {...this.props.bodyAttributes}>
{this.props.preBodyComponents}
<div
key={`body`}
id="___gatsby"
dangerouslySetInnerHTML={{ __html: this.props.body }}
/>
{this.props.postBodyComponents}
// MODIFICATION // ===================
<script
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"
async
defer
/>
// ===================
</body>
</html>
)
}
}
HTML.propTypes = {
htmlAttributes: PropTypes.object,
headComponents: PropTypes.array,
bodyAttributes: PropTypes.object,
preBodyComponents: PropTypes.array,
body: PropTypes.string,
postBodyComponents: PropTypes.array,
}
Then you can access the Google Maps API anywhere in your project from window.google.maps.(Map|Marker|etc.).
The React way
To me that felt a little anachronistic, though. If you want a reusable React component that you can import into any page or template as import Map from './Map', I suggest this instead. (Hint: See update below for equivalent function component.)
// src/components/Map.js
import React, { Component } from 'react'
export default class Map extends Component {
onLoad = () => {
const map = new window.google.maps.Map(
document.getElementById(this.props.id),
this.props.options
)
this.props.onMount(map)
}
componentDidMount() {
if (!window.google) {
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = `https://maps.google.com/maps/api/js?key=YOUR_API_KEY`
const headScript = document.getElementsByTagName('script')[0]
headScript.parentNode.insertBefore(script, headScript)
script.addEventListener('load', () => {
this.onLoad()
})
} else {
this.onLoad()
}
}
render() {
return <div style={{ height: `50vh` }} id={this.props.id} />
}
}
Use it like so:
// src/pages/contact.js
import React from 'react'
import Map from '../components/Map'
const center = { lat: 50, lng: 10 }
const mapProps = {
options: {
center,
zoom: 8,
},
onMount: map => {
new window.google.maps.Marker({
position: center,
map,
title: 'Europe',
})
},
}
export default function Contact() {
return (
<>
<h1>Contact</h1>
<Map id="contactMap" {...mapProps} />
</>
)
}
What woked for me was to create a gatsby-ssr.js file in the root of my project, and then include the script there, like this:
import React from "react"
export function onRenderBody({ setHeadComponents }) {
setHeadComponents([
<script
key="abc"
type="text/javascript"
src={`https://maps.googleapis.com/maps/api/js?key=${process.env.GATSBY_API_KEY}&libraries=places`}
/>,
])
}
Don't forget to include GATSBY_API_KEY or whatever you want to call it in your .env.development and .env.production files:
GATSBY_API_KEY=...

Resources