I am trying to make my Vue app have server-side rendering. I am using vue-server-renderer (https://www.npmjs.com/package/vue-server-renderer). Client-side rendering is working fine.
My app use vue-router and axios
Here is my server.js:
server.get('*', (request, response) => {
bundleRenderer.renderToString({ url: request.url }, (error, htmlPromise) => {
if (error) {
// Log the error in the console
console.error(error)
// Tell the client something went wrong
return response
.status(500)
.send(error)
}
response.send(layout.replace('<div id=app></div>', htmlPromise))
})
})
getInfo() is the method to fetch server data.
Here is getInfo():
export default {
methods: {
getInfo(api) {
return axios
.get(api || this.$route.params.path)
.then((data) => {
this.data = data
this.$set(this, 'isLoading', false)
})
},
},
}
My server entry is:
import { app, router, store } from './index'
export default context => {
let componentPromises = router.getMatchedComponents().filter((component) => {
return component.methods && component.methods.getInfo
}).map((component) => {
return component.methods.getInfo()
})
return Promise.all(componentPromises).then(() => {
return app
})
}
However, I soon realize that all the components from router.getMatchedComponents() does not have $route or $set. Therefore, the method getInfo() stops working.
The document from https://router.vuejs.org/en/api/router-instance.html is very short and does not provide much information:
router.getMatchedComponents()
Returns an Array of the components (definition/constructor, not
instances) matched by the current route. This is mostly used during
server-side rendering to perform data prefetching.
How can I fix the problem?
I have previously incurred into a similar problem and managed to successfully prefetch data by doing the following:
app.$router.onReady(() => {
const matchedComponents = app.$router.getMatchedComponents()
if (!matchedComponents.length) { /* ... */}
Promise.all(matchedComponents.map((Component: any) => {
if (Component.options.methods.asyncData) {
return Component.options.methods.asyncData({
store: app.$store,
route: app.$router.currentRoute
});
}
})).then(() => { /* your callback here ... */ });
}
According to vue ssr documentation (https://ssr.vuejs.org/en/data.html) the suggested way is to use a custom asyncData method in your component to perform data fetching rather than calling component methods directly:
export default {
asyncData ({ store, route }) {
// return the Promise from the action
return store.dispatch('fetchItem', route.params.id)
}
},
Related
I'm using the async fetch hook to render the component in SSR and making the API call inside the same. But on the live server, it is actually loading one more time on the client-side and making one more call to API and as API isn't exposed during client-side so data just washes away and the array object gets again set to empty.
data() {
return {
errored: false,
randomData: [],
};
},
async fetch() {
await this.$axios
.get(this.routeUrl)
.then((res) => {
if (res.data.length == 0 || res.status != 200) this.errored = true;
this.randomData = res.data;
})
.catch((err) => {
console.log(err);
});
},
fetchOnServer: true,
I want to persist this randomData variable, so there shouldn't be another call on the client-side to populate the same.
If you want to pass some data from the server-side into the client-side, you could use the nuxtServerInit Vuex action
/store/index.js
actions: {
nuxtServerInit ({ commit }, { req }) {
if (req.session.user) {
commit('user', req.session.user)
}
}
}
So, the issue was that, as I was Docker and the API was only exposed to the server-side and not the client-side. For a blink, the content was available and then just all gone. Because the async fetch and asyncData hooks are called on the server and as well as on the client-side. I solved it using the Vuex store. Now I started storing the data fetched on the server in store and using the same on the client-side. Here is how I implemented that:
// store/index.js
export const state = () => ({
randomData: [],
});
export const mutations = {
SET_RANDOMDATA: (state, payload) => {
state.randomData = payload;
},
};
// Inside the component
// Fetch the data from API on server-side
async fetch() {
if (process.server) {
await this.$axios
.get(this.routeUrl)
.then((res) => {
this.$store.commit("SET_RANDOMDATA", res.data);
})
.catch((err) => {
console.log(err);
});
}
},
computed: {
...mapState(["randomData"]),
},
fetchOnServer: true,
I have 2 actions that make GET requests and save the response in the Vuex store. The first action getVersion() gets the most recent version of the game and that version is required in order to make the second GET request. Right now I've hard coded the version in the second action, however, my goal is to concatenate it inside the URL.
Sadly I'm not sure how to access it from inside the function. Console.log(state.version) returns null for some reason even though it shouldn't be. I call these functions from inside App.vue like this:
mounted(){
this.$store.dispatch('getVersion')
this.$store.dispatch('getChampions')
}
Vuex store
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
version: null,
champions: null
},
mutations: {
version(state, data){
state.version = data.version
},
champions(state, data){
state.champions = data.champions
}
},
actions: {
getVersion({commit}){
axios.get("http://ddragon.leagueoflegends.com/api/versions.json")
.then((response) => {
commit('version', {
version: response.data[0]
})
})
.catch(function (error) {
console.log(error);
})
},
getChampions({commit, state}){
axios.get("https://ddragon.leagueoflegends.com/cdn/9.24.1/data/en_US/champion.json")
.then((response) => {
commit('champions', {
champions: response.data.data
})
})
.catch(function (error) {
console.log(error);
})
}
},
getters: {
version: (state) => {
return state.version;
},
findChampion: (state) => (id) => {
let championId = id.toString();
let champion = Object.values(state.champions).find(value => value.key === championId);
return champion
}
}
})
With this part:
this.$store.dispatch('getVersion')
this.$store.dispatch('getChampions')
The second dispatch doesn't wait for the first one to finish. Meaning that it is firing before the first one has had a chance to finish getting the version.
You need to create a promise that should resolve before the second dispatch is called.
You could try doing it this way:
async mounted(){
await this.$store.dispatch('getVersion')
await this.$store.dispatch('getChampions')
}
or if you don't want to use async/await
this.$store.dispatch('getVersion').then(() => {
this.$store.dispatch('getChampions');
});
And in the action you should add return to the request (this is important):
return axios.get(...
dispatcher returns a promise
this.$store.dispatch('getVersion').then(()=>{
this.$store.dispatch('getChampions');
});
I've made an application and want to add more components which will use the same json I fetched in "personlist.js", so I don't want to use fetch() in each one, I want to make a separate component that only does fetch, and call it in the other components followed by the mapping function in each of the components, how can make the fetch only component ?
here is my fetch method:
componentDidMount() {
fetch("data.json")
.then(res => res.json())
.then(
result => {
this.setState({
isLoaded: true,
items: result.results
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
error => {
this.setState({
isLoaded: true,
error
});
}
);
}
and here is a sandbox snippet
https://codesandbox.io/s/1437lxk433?fontsize=14&moduleview=1
I'm not seeing why this would need to be a component, vs. just a function that the other components use.
But if you want it to be a component that other components use, have them pass it the mapping function to use as a prop, and then use that in componentDidMount when you get the items back, and render the mapped items in render.
In a comment you've clarified:
I am trying to fetch the json once, & I'm not sure whats the best way to do it.
In that case, I wouldn't use a component. I'd put the call in a module and have the module expose the promise:
export default const dataPromise = fetch("data.json")
.then(res => {
if (!res.ok) {
throw new Error("HTTP status " + res.status);
}
return res.json();
});
Code using the promise would do so like this:
import dataPromise from "./the-module.js";
// ...
componentDidMount() {
dataPromise.then(
data => {
// ...use the data...
},
error => {
// ...set error state...
}
);
}
The data is fetched once, on module load, and then each component can use it. It's important that the modules treat the data as read-only. (You might want to have the module export a function that makes a defensive copy.)
Not sure if this is the answer you're looking for.
fetchDataFunc.js
export default () => fetch("data.json").then(res => res.json())
Component.js
import fetchDataFunc from './fetchDataFunc.'
class Component {
state = {
// Whatever that state is
}
componentDidMount() {
fetchFunc()
.then(res => setState({
// whatever state you want to set
})
.catch(err => // handle error)
}
}
Component2.js
import fetchDataFunc from './fetchDataFunc.'
class Component2 {
state = {
// Whatever that state is
}
componentDidMount() {
fetchFunc()
.then(res => setState({
// whatever state you want to set
})
.catch(err => // handle error)
}
}
You could also have a HOC that does fetches the data once and share it across different components.
I'm new into the React and Redux worlds and after a lot of research, I haven't found a way to handle the problem I have:
I need to perform an api call on app init, but the endpoint is in a configuration file. This configuration in the server so it has to be downloaded and read. This is because I need to distribute the app into many servers and each server has a different configuration.
Therefore the api call has to wait until the configuration has been loaded, they must be chained.
I'm using Redux to manage the state of the app so I have an action which downloads the configuration and an other action which performs the api call.
// Config action
export function fetchConfigRequest() {
return {
type: types.FETCH_CONFIG_REQUEST
}
}
export function fetchConfigSuccess(config) {
return {
type: types.FETCH_CONFIG_SUCCESS,
config
}
}
export function fetchConfig() {
return dispatch => {
dispatch(fetchConfigRequest());
return axios.get('config.json')
.then(response => {
dispatch(fetchConfigSuccess(response.data));
})
;
};
}
// Api client action
export function fetchDataRequest() {
return {
type: types.FETCH_DATA_REQUEST
}
}
export function fetchDataSuccess(data) {
return {
type: types.FETCH_DATA_SUCCESS,
data
}
}
export function fetchDataError(error) {
return {
type: types.FETCH_DATA_ERROR,
error
}
}
export function fetchData(filters = {}) {
return dispatch => {
dispatch(fetchDataRequest());
const apiClient = new apiClient({
url: state.config.apiEndpoint
});
return apiClient.Request()
.then(response => {
dispatch(fetchDataSuccess(data));
})
;
};
}
The only way that I got it working is by waiting until config action promise resolves in App component like this:
// App.component.js
componentDidMount() {
this.props.fetchConfig().then(() => {
this.props.fetchData();
});
}
But I don't think this is the best and the most "Redux style" way to do it, so how should I do it?
I've some ideas in my mind but I don't know what would be the best one:
Keep it as it is now
Create an 'app' action which dispatches the fetch config action, waits until config is loaded, and then dispatches the fetch data action
Do it into a custom middleware
Thanks!
React (from create-react-app) with MobX. Using axios for async backend API calls.
This code works. The initial state (array of issues) is populated, and the webpage presenting this component renders with initial content from state.
import { observable, computed, autorun, reaction } from 'mobx'
import axios from 'axios'
class IssuesStore {
#observable issues = []
constructor() {
autorun(() => console.log("Autorun:" + this.buildIssues))
reaction(
() => this.issues,
issues => console.log("Reaction: " + issues.join(", "))
)
}
getIssues(data) {
return data.map((issue) => ({title: issue.name, url: issue.url, labels: issue.labels}))
}
#computed get buildIssues() {
const authToken = 'token ' + process.env.REACT_APP_GH_OAUTH_TOKEN
axios.get(`https://api.github.com/repos/${process.env.REACT_APP_GH_USER}/gh-issues-app/issues`,
{ 'headers': {'Authorization': authToken} })
.then(response => {
console.log(response)
this.issues = this.getIssues(response.data)
return this.issues
})
.catch(function(response) {
console.log(response)
})
}
}
export default IssuesStore
In an attempt to separate API invocation promises from individual components and stores, I pulled out the axios call into a separate js file, as a collection of functions:
import axios from 'axios'
const authToken = 'token ' + process.env.REACT_APP_GH_OAUTH_TOKEN
export function loadIssues() {
return this.apiPromise(
`https://api.github.com/repos/${process.env.REACT_APP_GH_USER}/gh-issues-app/issues`,
{ 'headers': {'Authorization': authToken} }
)
}
export function apiPromise(endpoint, options) {
return axios.get(endpoint, options)
.then((response) => {
// console.log("response: " + JSON.stringify(response, null, 2))
return response.data.map((issue) => ({title: issue.name, url: issue.url, labels: issue.labels}))
})
.catch(function(response) {
console.log(response)
})
}
Now, my store looks like this:
import { observable, computed, autorun, reaction } from 'mobx'
import * as github from '../api/github'
class IssuesStore {
#observable issues = []
constructor() {
autorun(() => console.log("Autorun:" + this.buildIssues))
reaction(
() => this.issues,
issues => console.log("Reaction: " + issues.join(", "))
)
}
#computed get buildIssues() {
this.issues = github.loadIssues().data
return this.issues
}
}
export default IssuesStore
Much smaller... but the webpage now throws an error because it now sees the initial state of issues as undefined on first render.
Uncaught TypeError: Cannot read property 'map' of undefined
The promise completes successfully later on (as it should), but by then it's too late. Sure, I can set up a few null checks in my rendering components to not run .map or other such functions on empty or as-yet-undefined variables.
But why does the code work with no initial rendering errors before the refactoring, and not after? I thought the refactoring was effectively maintaining the same logic flow, but I must be missing something?
In your refactored version
github.loadIssues().data
Is always going to be undefined because the data property on that Promise will always be undefined.
In the original version, this.issues was only ever set once data returned from the api, so the only values that it was ever set to were the initial value [] and the filled array from the api response.
In yours, the three states are [] -> undefined -> and the filled array.
buildIssues should look something like this:
#computed get buildIssues() {
github.loadIssues().then((data) => {
this.issues = data
}).catch((err) => {
// handle err.
})
}