React-router server-side rendering on each page - express

I'm trying to build an isomorphic app using express and react-router with data-fetching and first render server-side and data manipulation client side.
I managed to fetch initial data server side and render the jsx components but it works only if the url is directly hit, not following links. In fact, as in all the examples I've read, the app is server-rendered only once and then everything happen client side.
Plus, if I fetch some data, render a component server-side and then follow a link, the data are not updated for the new page.
I don't know if I'm trying to do something that makes no sense?
What I would like to obtain is:
A pre-render server-side for every page, regardless of whether the user arrives directly or through a link
Fetching just the needed initial data the component corresponding to the route is asking for
[BONUS] apply small changes to the layout regarding the component (title, additional css, etc.)
Here is what I have so far:
Express app:
var express = require('express');
require('node-jsx').install();
var React = require('react');
var Router = require('react-router');
var routes = require('./routes');
var url = require('url');
var resolveHash = require('when/keys').all;
var app = express();
/*
....
*/
app.use(function(req, res, next) {
Router.run(routes, url.parse(req.url).pathname, function(Handler, state){
// create the promises hash
var promises = state.routes.filter(function (route) {
// gather up the handlers that have a static `fetchData` method
return route.handler.fetchData;
}).reduce(function (promises, route) {
// reduce to a hash of `key:promise`
promises = route.handler.fetchData(state.params);
return promises;
}, {});
resolveHash(promises).then(function (data) {
var html = '<!DOCTYPE html>' + React.renderToString(React.createFactory(Handler)({path:url.parse(req.url).pathname, initialData:safeStringify(data)}));
res.send(html);
});
});
// A utility function to safely escape JSON for embedding in a <script> tag
function safeStringify(obj) {
return JSON.stringify(obj).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--')
}
});
routes.js:
var React = require("react");
var Router = require("react-router");
var Route = Router.Route;
var DefaultRoute = Router.DefaultRoute;
var NotFoundRoute = Router.NotFoundRoute;
var Layout = require("./components/layout.jsx");
var Stuff = require("./components/stuff.jsx");
var Home = require("./components/home.jsx");
var routes = (
<Route path="/" handler={Layout}>
<Route path="/stuff" handler={Stuff} />
<DefaultRoute handler={Home}/>
</Route>
);
module.exports = routes;
if (typeof document !== 'undefined') {
var initialData = JSON.parse(document.getElementById("initialData").innerHTML);
Router.run(routes, Router.HistoryLocation, function (Handler) {
React.render(<Handler initialData={initialData}/>, document);
});
}
layout.jsx:
'use strict';
var React = require('react');
var Router = require('react-router');
var RouteHandler = Router.RouteHandler;
var Link = Router.Link;
var Layout = React.createClass({
render: function () {
return (
<html>
<head>
<title>{this.props.initialData.title}</title>
</head>
<body>
<nav>
<Link to={'/'}>Home</Link>
<Link to={'/stuff'}>Stuff</Link>
</nav>
<RouteHandler/>
<script id='initialData' type="application/json" dangerouslySetInnerHTML={{__html:this.props.initialData}}></script>
// actually bundle.js is just made of routes.js as I put the client side render there
<script src="/bundle.js"></script>
</body>
</html>
);
}
});
module.exports = Layout;
home.jsx:
'use strict';
var React = require('react');
var Home = React.createClass({
statics: {
fetchData: function(params){
return {test:[1,2,3], title:'Home'};
}
},
render: function () {
return (
<div className="content">
<section>
<article>
<h2>Title</h2>
<p>Body</p>
</article>
</section>
<aside>
Ads
</aside>
</div>
);
}
})
module.exports = Home;
stuff.jsx:
'use strict';
var React = require('react');
var Stuff = React.createClass({
statics: {
fetchData: function(params){
return {test:[4,5,6], title:'Stuff'};
}
},
render: function(){
return (<h1>Hello world from thingy!</h1>)
}
})
module.exports = Stuff;
What are all the things I'm missing, misunderstanding, doing the wrong way?

This is how it is intended to work, it will give you that first load and build that html and render it, after that it can't change that html that was generated unless you do a refresh of the page by clicking the refresh button or having your code window.location.

Related

Populate input data with result from API nuxt 3

I have a nuxt 3 app that fetches data from the API. I would like to use that data to populate the input fields in the template but I keep getting an error.
Here is a snippet of my code
<script setup>
const config = useRuntimeConfig();
const route = useRoute();
const router = useRouter();
const { data: pkg } = useFetch(
() => '/api/id/1/'
);
const request = ref({
field: pkg.value.field_value,
});
When I console.log(pkg.value.field_value) I get the value printed on the browser developer tools console tab but on hard refresh, I get the error Cannot read properties of null (reading 'field_value')
The reason why I need to dynamically set the value of field is so that I am able to update it.
Anyone encountered that problem before and how did you address it
Add await to the useFetch function because at the first rendering the pkg is not available :
<script setup>
const config = useRuntimeConfig();
const route = useRoute();
const router = useRouter();
const { data: pkg } = await useFetch(
() => '/api/id/1/'
);
const request = ref({
field: pkg.value.field_value,
});

Nested useFetch in Nuxt 3

How do you accomplish nested fetching in Nuxt 3?
I have two API's. The second API has to be triggered based on a value returned in the first API.
I tried the code snippet below, but it does not work, since page.Id is null at the time it is called. And I know that the first API return valid data. So I guess the second API is triggered before the result is back from the first API.
<script setup>
const route = useRoute()
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
const { data: paragraphs } = await useFetch(`/api/page/${page.Id}/paragraphs`)
</script>
Obviously this is a simple attempt, since there is no check if the first API actually return any data. And it is not even waiting for a response.
In Nuxt2 I would have placed the second API call inside .then() but with this new Composition API setup i'm a bit clueless.
You could watch the page then run the API call when the page is available, you should paragraphs as a ref then assign the destructed data to it :
<script setup>
const paragraphs = ref()
const route = useRoute()
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
watch(page, (newPage)=>{
if (newPage.Id) {
useFetch(`/api/page/${newPage.Id}/paragraphs`).then((response)=>{
paragraphs.value = response.data
})
}
}, {
deep: true,
immediate:true
})
</script>
One solution is to avoid using await. Also, use references to hold the values. This will allow your UI and other logic to be reactive.
<script setup>
const route = useRoute()
const page = ref()
const paragraphs = ref()
useFetch(`/api/page/${route.params.slug}`).then(it=> {
page.value = it
useFetch(`/api/page/${page.value.Id}/paragraphs`).then(it2=> {
paragraphs.value = it2
}
}
</script>
You can set your 2nd useFetch to not immediately execute until the first one has value:
<script setup>
const route = useRoute()
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
const { data: paragraphs } = await useFetch(`/api/page/${page.value?.Id}/paragraphs`, {
// prevent the request from firing immediately
immediate: false,
// watch reactive sources to auto-refresh
watch: [page]
})
</script>
You can also omit the watch option there and manually execute the 2nd useFetch.
But for it to get the updates, pass a function that returns a url instead:
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
const { data: paragraphs, execute } = await useFetch(() => `/api/page/${page.value?.Id}/paragraphs`, {
immediate: false,
})
watch(page, (val) => {
if (val.Id === 69) {
execute()
}
})
You should never call composables inside hooks.
More useFetch options can be seen here.

ExpressJS with NuxtJS middleware passing post data to page

Can someone help me understand how to pass data from post request to the nuxt page that is loaded. I dont know how to send the data to the page that will be loaded.
I want to be able to process the POST request, then send that data for usage on the following page. I am open to suggestions but I can't find proper documentation, tutorials or examples to accomplish this task.
I don't want to use axios here (with JSON type response), because I would prefer to send POST data and load new page. Therefor if page is reloaded, POST data must be submitted again.
const express = require('express')
const bodyParser = require('body-parser')
const { Nuxt, Builder } = require('nuxt')
const app = express()
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 3000
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))
// parse application/json
app.use(bodyParser.json())
app.set('port', port)
// Import and Set Nuxt.js options
let config = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')
async function start() {
// Init Nuxt.js
const nuxt = new Nuxt(config)
// Build only in dev mode
if (config.dev) {
const builder = new Builder(nuxt)
await builder.build()
}
// Routes added
app.post('/events/booking', function (req, res, next) {
console.log('REQUEST:', req.body)
res.set('eventId', req.body.eventId)
res.set('moreData', ['some', 'more', 'data'])
next()
})
// Give nuxt middleware to express
app.use(nuxt.render)
// Listen the server
app.listen(port, host)
console.log('Server listening on http://' + host + ':' + port) // eslint-disable-line no-console
}
start()
I believe the source of your issue is the disconnect between Nuxt's implementation of Express, the deprecation/version-conflicts of bodyParser middleware and/or the Node event system.
I would personally take a step back by removing the custom express routing, handle the body parsing yourself in the middleware and take advantage of the Vuex store.
store/index.js
export const state = () => ({
postBody: null,
postError: null
})
export const mutations = {
postBody: (state, postBody) => {
state.postBody = postBody;
},
postError: (state, postError) => {
state.postError = postError;
},
}
export const getters = {
postBody: state => state.postBody,
postError: state => state.postError,
}
middleware/index.js
export default ({req, store}) => {
if (process.server && req && req.method === 'POST') {
return new Promise((resolve, reject) => {
req.on('data', data => resolve(store.commit('postBody', JSON.parse(data))));
req.on('error', data => reject(store.commit('postError', JSON.parse(data))));
})
}
}
pages/index.vue
<template>
<div>
<h1>Test page</h1>
<div v-if="postBody">
<h2>post body</h2>
<p>{{postBody}}</p>
</div>
<div v-if="postError">
<h2>post error</h2>
<p>{{postError}}</p>
</div>
<div v-if="!postError && !postBody">
Please post JSON data to this URL to see a response
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
middleware: 'post-data',
computed: mapGetters({
postBody: 'postBody',
postError: 'postError'
})
}
</script>
Below is a live and working example project of the above. POST JSON data using a client app (Postman, web form, etc) to see the posted data rendered on the page.
Live Code: https://glitch.com/edit/#!/terrific-velociraptor
Live Example: https://terrific-velociraptor.glitch.me/

Vue 2.0 SSR error page

I'm trying to implement ErrorPage vue component for my SSR application. I'm using boilerplate with
// server.js
...
const context = { url: req.url };
const stream = renderer.renderToStream(context);
// If an error occurs while rendering
stream.on('error', (error) => {
var app = new Vue({
template: '<div>Error page</div>'
});
errRenderer.renderToString(app, function (error, html) {
return res
.status(500)
.send(html);
});
});
I'm aware that this is not super pretty, but is there another way of handling that? Ideally I would love to just load an external Vue component and send it to the browser.

Use vue-resource within vueify component

I'd like to use the vue-resource $http methods within the script tag of my vueify component but I always get this error:
Uncaught TypeError: Cannot read property 'get' of undefined
My guess would be that the "this" keyword doesn't work (wouldn't know why) or the module isn't installed correctly (although it should be, checked that). My vue-component looks like this:
<template>
<!-- just displaying the data -->
</template>
<script>
module.exports = {
data: function () {
return {
foo: "bar"
}
},
ready: function() {
// the error occurs on the next line
this.$http.get('/api/users', function(data){
this.$set('users', data);
});
}
}
</script>
The answer was quite simple, I had to require('vue') in the component as well. I really did not think about this because I'm quite new to browser-/vueify.
The working code looks like this for anyone wondering:
<script>
var Vue = require('vue');
module.exports = {
data: function () {
return {
message: "Hello World!"
}
},
ready: function() {
var _self = this;
Vue.http.get('/api/users', function(data){
_self.$set('users', data);
});
}
}
</script>
EDIT: here is how I setup the whole dependencies and modules in my main.js file
// require dependencies
var Vue = require('vue');
var VueRouter = require('vue-router');
var VueResource = require('vue-resource');
// use vue-router and vue-resource
Vue.use(VueRouter);
Vue.use(VueResource);
// Laravel CSRF protection
Vue.http.headers.common['X-CSRF-TOKEN'] = document.getElementById('token').getAttribute('value');
// Routing
var router = new VueRouter();
var App = Vue.extend();
// start router in element with id of app
router.start(App, '#app');
FYI, for your .vue file, you don't have to require Vue. Instead you can reference the instance like this:
this.$http.get(...