React Server Side Rendering + renderToString - server-side-rendering

I am currently working on a new project, so I decided to implement React but with server side rendering.
I use express as router between pages, so when you access to the home page, the entry point is something like this:
const router = require('express').Router();
const { render, fetchUsers } = require('./controller');
router.use('/', fetchUsers, render);
module.exports = router;
So when you acces the home page, this would get all the users and then it will render the component, in order to render the component I do the following:
const render = (req, res) => {
const extraProps = {
users: res.locals.users.data,
}
return renderView(View, extraProps)(req, res);
}
the fetchUsers method sets res.locals.users with an api response. My renderView do something like this:
const renderView = (Component, props = {}) => (req, res) => {
const content = renderToString(
<LayoutWrapper state={props}>
<Component {...props} />
</LayoutWrapper>
);
res.send(content);
};
My LayoutWrapper is a React Component that replace the html template:
const React = require('React');
const serialize = require('serialize-javascript');
const LayoutWrapper = ({ children, state }) => (
<html>
<head></head>
<body>
<div id={'app-root'}>
{children}
</div>
</body>
<script>
{`window.INITIAL_STATE = ${serialize(state, { isJSON: true })}`}
</script>
<script src={`home.js`} />
</html>
)
module.exports = LayoutWrapper;
The script that sets window.INITAL_STATE = props; is used on the client side to get the props that were fetch. But the problem is the way that the renderToString process the component.
The console.log output is the following:
<html data-reactroot="">
<head></head>
<body>
<div id="app-root">
<div>I'm the Home component</div><button>Press me!</button>
<ul>
<li>Leanne Graham</li>
<li>Ervin Howell</li>
<li>Clementine Bauch</li>
<li>Patricia Lebsack</li>
<li>Chelsey Dietrich</li>
</ul>
</div>
</body>
<script>
window.INITIAL_STATE = { & quot;users & quot;: [{ & quot;id & quot;: 1,
& quot;name & quot;: & quot;Leanne Graham & quot;
}, { & quot;id & quot;: 2,
& quot;name & quot;: & quot;Ervin Howell & quot;
}, { & quot;id & quot;: 3,
& quot;name & quot;: & quot;Clementine Bauch & quot;
}, { & quot;id & quot;: 4,
& quot;name & quot;: & quot;Patricia
Lebsack & quot;
}, { & quot;id & quot;: 5,
& quot;name & quot;: & quot;Chelsey Dietrich & quot;
}]
}
</script>
<script src="home.js"></script>
</html>
Is there any way to do this without having to declare the html template as a simple string, and instead having a Wrapper component that sets the html code structure?

You can replace all escaping symbols, but only after renderToString calling.
renderToString(...).replace('& quot;', '')
Security notes.

To escape html characters in renderToString method (like "), use dangerouslySetInnerHTML:
Correct
const scriptStr = window.INITIAL_STATE = {a: 'a'};
return renderToString(
<script dangerouslySetInnerHTML={scriptStr} />
);
It makes:
"<script>window.INITIAL_STATE = {a: \"a\"}</script>"
BAD:
const scriptStr = window.INITIAL_STATE = {a: 'a'};
return renderToString(
<script>
{scriptStr}
</script>
);
It makes:
"<script>window.INITIAL_STATE = {a: "a"}</script>"

Related

Why the context.slots in the Vue setup() function has an empty object?

Accordingly to this Issue it should work with the current version v3.2.x.
But it doesn't.
Here is the playground:
const { createApp } = Vue;
const myComponent = {
template: '#my-component',
setup(props, { slots }) {
console.log(slots)
}
}
const App = {
components: {
myComponent
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<my-component>Default
<template #footer>Footer</template>
</my-component>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script type="text/x-template" id="my-component">
<div>
<slot></slot>
<hr/>
<slot name="footer"></slot>
</div>
</script>
The solution was provided by Duannx.
With console.log(slots) they are listed correctly.
{
"footer": (...n)=>{o._d&&Tr(-1);const r=hn(t);let s;try{s=e(...n)}finally{hn(r),o._d&&Tr(1)}return s},
"default": (...n)=>{o._d&&Tr(-1);const r=hn(t);let s;try{s=e(...n)}finally{hn(r),o._d&&Tr(1)}return s}
}
Explanation
JSON.stringify doesn't show the slots since they are functions.
Here is the explanation from the MDN Docs JSON.stringify():
undefined, Function, and Symbol values are not valid JSON values. If any such values are encountered during conversion, they are either omitted (when found in an object) or changed to null (when found in an array). JSON.stringify() can return undefined when passing in "pure" values like JSON.stringify(() => {}) or JSON.stringify(undefined).
Example
console.log("JSON.stringify(() => {}): " + JSON.stringify(() => {}));
console.log(JSON.stringify({ "func": function () {}, "lmbd": () => {} }))

Vue 3 component data binding doesn't work

In the following example, {{test}} doesn't get updated according to the input of the component. What am I doing wrong?
<html>
<body>
<Component v-model="test"></Component>
{{test}}
<script type="module">
import {createApp} from './node_modules/vue/dist/vue.esm-browser.prod.js';
const Component = {
props: {
modelValue: String,
},
emits: [
'update:modelValue',
],
template: `<input #keyup="updateValue">`,
methods: {
updateValue(event) {
this.$emit('update:modelValue', event.target.value);
},
},
};
const app = createApp({});
app.component('Component', Component);
app.mount('body');
</script>
</body>
</html>
You forget to declare test variable in data options.
const app = createApp({
data: () => {
return {
test: ''
}
}
});

next-i18next with next-rewrite does not work with root page rewrite path

We have an existing app, where the root "/" gets redirected to "/search" by default. It's been working fine via our next-redirects.js file:
async function redirects() {
return [
{
source: '/',
destination: '/search',
permanent: true,
},
];
}
I have to implement translation to the app, using next-i18next, so that we can have translated text + routing out of the box with NextJS. I have followed the steps in the next-i8next docs. I added the next-i18next.config.js file as:
const path = require('path');
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'es'],
},
localePath: path.resolve('./public/static/locales'), // custom path file route
};
And the next.config looks like:
const { i18n } = require('./next-i18next.config');
const defaultConfig = {
env: {
SOME_ENV,
},
images: {
deviceSizes: [980],
domains: [
'd1',
'd2',
],
},
i18n,
redirects: require('./next-redirects'),
webpack: (config, options) => {
if (!options.isServer) {
config.resolve.alias['#sentry/node'] = '#sentry/browser';
}
if (
NODE_ENV === 'production'
) {
config.plugins.push(
new SentryWebpackPlugin({
include: '.next',
ignore: ['node_modules'],
urlPrefix: '~/_next',
release: VERCEL_GITHUB_COMMIT_SHA,
})
);
}
return config;
},
};
module.exports = withPlugins([withSourceMaps], defaultConfig);
We have a custom _app file getting wrapped with the appWithTranslation HOC, and it's setup with the getInitialProps, per nextJS docs:
function MyApp({ Component, pageProps }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentNode.removeChild(jssStyles);
}
TagManager.initialize(tagManagerArgs);
setMounted(true);
}, []);
const Layout = Component.Layout || Page;
return (
<>
<Head>
<link rel="icon" href="/favicon.png" type="image/ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<AppProviders>
<Context {...pageProps}>
<Layout {...pageProps}>
<>
<Component {...pageProps} />
<Feedback />
<PageLoader />
</>
</Layout>
</Context>
</AppProviders>
</>
);
}
MyApp.getInitialProps = async ({ Component, ctx }) => {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps({ ctx });
}
const cookies = Cookie.parse(ctx?.req?.headers?.cookie || '');
if (Object.keys(cookies).length) {
const { token } = JSON.parse(cookies?.user || '{}');
let user = null;
if (token) {
const { data } = await get('api/users', { token });
if (data) {
user = data;
user.token = token;
}
}
pageProps.user = user;
pageProps.cart = cookies?.cart;
pageProps.defaultBilling = cookies?.defaultBilling;
pageProps.reservationEstimateItem = cookies?.reservationEstimateItem;
pageProps.reservationEstimate = cookies?.reservationEstimate;
}
return { pageProps };
};
export default appWithTranslation(MyApp);
And we have our _document file to handle some Emotion theming:
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
const styles = extractCritical(initialProps.html);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style
data-emotion-css={styles.ids.join(' ')}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: styles.css }}
/>
</>
),
};
}
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
<script
type="text/javascript"
src="https://js.stripe.com/v2/"
async
/>
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async ctx => {
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
};
};
At this point the redirect logic should continue to navigate to the search page which is setup like so:
export const SearchPage = () => {
const router = useRouter();
const { t } = useTranslation('search');
return (
<>
<Head>
<title>{`${t('searchTitle')}`}</title>
<meta
property="og:title"
content={`${t('searchTitle')}`}
key="title"
/>
<meta
name="description"
content={t('metaDescription')}
/>
</Head>
<Search />
</>
);
};
SearchPage.namespace = 'SearchPage';
export const getStaticPaths = () => ({
paths: [], // indicates that no page needs be created at build time
fallback: 'blocking' // indicates the type of fallback
});
export const getStaticProps = async ({ locale }) => ({
// exposes `_nextI18Next` as props which includes our namespaced files
props: {
...await serverSideTranslations(locale, ['common', 'search']),
}
});
export default SearchPage;
The search page has the getStaticPaths & getStaticProps functions, as needed on ALL page level files, per next-i18next.
Why does this setup no longer work with the redirect?
There are no errors in the terminal.
The network tab shows a 404 error on the root route of "/"
which implies the re-writing is not working. But what about the i18n makes this not behave?
Is it something in the _app or _document files?
If I navigate to /search directly, it loads fine, so the page routes are OK it seems.
Other notes:
NextJS "next": "^10.0.2",
next-i18next "next-i18next": "^7.0.1",
There seems to be some possible issues with Next & Locales...
https://github.com/vercel/next.js/issues/20488
https://github.com/vercel/next.js/issues/18349
My workaround is not pretty, but it works:
Delete the original next-rewrite
Add a new index.js page file that handles the redirect in the getServerSideProps:
const Index = () => null;
export async function getServerSideProps({ locale }) {
return {
redirect: {
destination: `${locale !== 'en' ? `/${locale}` : ''}/search`,
permanent: true,
},
};
}
export default Index;

Dynamic component in Vue3 Composition API

A simple working example of a Vue2 dynamic component
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Everything works, everything is great. I started trying how it would work with the Composition API.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
We launch, the component does not appear on the screen, although no errors are displayed.
You can learn more about 'defineAsyncComponent' here
https://labs.thisdot.co/blog/async-components-in-vue-3
or on the official website
https://v3.vuejs.org/api/global-api.html#defineasynccomponent
import { defineAsyncComponent, defineComponent, ref, computed } from "vue"
export default defineComponent({
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")): '')
const onClick = () => {
isShow.value = true;
}
}
})
Here is how you can load dynamic components in Vue 3. Example of dynamic imports from the icons collection inside /icons folder prefixed with "icon-".
BaseIcon.vue
<script>
import { defineComponent, shallowRef } from 'vue'
export default defineComponent({
props: {
name: {
type: String,
required: true
}
},
setup(props) {
// use shallowRef to remove unnecessary optimizations
const currentIcon = shallowRef('')
import(`../icons/icon-${props.name}.vue`).then(val => {
// val is a Module has default
currentIcon.value = val.default
})
return {
currentIcon
}
}
})
</script>
<template>
<svg v-if="currentIcon" width="100%" viewBox="0 0 24 24" :aria-labelledby="name">
<component :is="currentIcon" />
</svg>
</template>
You don't need to use computed or watch. But before it loads and resolved there is nothing to render, this is why v-if used.
UPD
So if you need to change components (icons in my case) by changing props use watchEffect as a wrapper around the import function.
watchEffect(() => {
import(`../icons/icon-${props.name}.vue`).then(val => {
currentIcon.value = val.default
})
})
Don't forget to import it from vue =)
The component should be added to components option then just return it name using the computed property based on the ref property isShow :
components:{
MyComponent:defineAsyncComponent(() => import("./DynamicComponent.vue"))
},
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? 'MyComponent': '')
const onClick = () => {
isShow.value = true;
}
}
Instead of string you should provide Component
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>

How to access canonical URL in Next.js with Automatic Static Optimization turned on?

I'm working on SEO component which needs a canonical URL.
How can I get URL of static page in Next.js with Automatic Static Optimization turned on?
Using useRouter from next/router you can get the pathname for the current page and use it in a <Head/> tag as following:
import { useRouter } from "next/router";
const site = "https://gourav.io";
const canonicalURL = site + useRouter().pathname;
<Head>
<link rel="canonical" href={canonicalURL} />
</Head>
Building on fzembow's comment about useRouter().asPath and GorvGoyl's answer, here's an implementation which manages to handle both dynamic routes and excludes anchor and query param URL extensions:
import { useRouter } from "next/router";
const CANONICAL_DOMAIN = 'https://yoursite.com';
const router = useRouter();
const _pathSliceLength = Math.min.apply(Math, [
router.asPath.indexOf('?') > 0 ? router.asPath.indexOf('?') : router.asPath.length,
router.asPath.indexOf('#') > 0 ? router.asPath.indexOf('#') : router.asPath.length
]);
const canonicalURL= CANONICAL_DOMAIN + router.asPath.substring(0, _pathSliceLength);
<Head>
<link rel="canonical" href={ canonicalURL } />
</Head>
use package called next-absolute-url . It works in getServerSideProps. Since getStaticProps run on build time so dont have data available.
can be used as
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const { req, query } = ctx;
const { origin } = absoluteUrl(req);
return {
props: {
canonicalUrl: `${origin}/user-listings`,
},
};
};
export const getStaticProps:GetStaticProps = async (ctx) => {
return {
props: {
canonicalUrl: 'https://www.test.com',
},
};
};
A super ugly, yet, the most adequate solution I found:
const canonicalUrl = typeof window === 'undefined' ?
'' :
`${window.location.origin}/${window.location.pathname}`;
My solution was to use new URL with router.asPath.
const CURRENT_URL = process.env.NEXT_PUBLIC_CURRENT_SITE_URL
const getCanonical = (path: string) => {
const fullURL = new URL(path, CURRENT_URL)
return`${fullURL.origin}${fullURL.pathname}
}
export const Page = () => {
const router = useRouter()
const canonical = getCanonical(router.asPath)
<Head>
<link rel="canonical" href={canonical} />
</Head>
<div>
...
<div>
}
I added a canonical link tag to _app.js so that it appears on every page:
<link
rel="canonical"
href={typeof window !== 'undefined' && `${window.location.origin}${useRouter().asPath}`}
key="canonical"
/>

Resources