We using Vue2 in a website, not an SPA, so we use single file components punctually to improve the user experience.
Ssr works with this code in entry_server.js, we can call this file from the command line for example node ssr-bundle.js or from an express APP:
// entry_server.js
import Test from 'components/Test.vue';
const app = new Vue({
render: h => (Test, {
props: {}
})
});
export default function(context) {
return new Promise((resolve, reject) => {
resolve(app);
});
};
This is a example component:
// Test.vue
<template>
<div>
{{apidata}}
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
apidata: []
};
},
beforeCreate() {
axios.get('/api/v1/versions.json', {params: {card_id: 274}}).then(response => {
this.apidata = response;
console.log('loadData!!!');
console.log(response);
});
},
};
</script>
When we need fetch data from an API the component render ok but it doesn't show this data, the render doesn't wait for API request.
We found many SSR examples and doc using vuex and router for this, but, how to make SSR with a single file component prefetch data from API and without vuex or router?
Related
Lets say we injected this repository on a plugin/service-container.js
import nodeFetch from 'node-fetch'
import { AbortController as NodeAbortController } from 'node-abort-controller'
import HttpClient from '#/services/httpClient'
import PostRepository from '#/repositories/posts'
export default ({ app }, inject) => {
if (!process.client || app.context.env.NUXTJS_DEPLOY_TARGET === 'server') {
inject('postRepository', postRepository)
}
}
I have always acceded to API repositories from the asyncData method, like so:
export default {
async asyncData ({ $postRepository, }) {
const posts = await $postRepository.getAllPaginated(page, 11)
return {
posts,
}
}
}
But I need to access to it in a method, this is actually working but:
I doesn't look the right way because i'm caching in the component's data()
It fires this lint error:
Async method 'asyncData' has no 'await' expression.eslintrequire-await
What's the right way? I Can't find it online (the only examples I found involved using the Store)
export default {
async asyncData ({ $postRepository }) {
this.$postRepository = $postRepository
},
methods: {
async loadMore () {
if (this.page < this.posts.numPages) {
const posts = await this.$postRepository.getAllPaginated(this.page + 1, 11)
}
}
}
}
The error is coming from here
async asyncData ({ $postRepository }) {
this.$postRepository = [missing await here] $postRepository
},
From the documentation
This hook can only be used for page-level components. Unlike fetch, asyncData cannot access the component instance (this). Instead, it receives the context as its argument. You can use it to fetch some data and Nuxt will automatically shallow merge the returned object with the component data.
Hence, you cannot use any kind of this.loadMore in asyncData because it doesn't have access to the instance yet. So, inject is indeed the proper way of doing things.
With a plugin like that
export default ({ _ }, inject) => {
inject('customTest', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
return await response.json()
})
}
And a page like this
<template>
<div>
<pre>item: {{ item }}</pre>
</div>
</template>
<script>
export default {
async asyncData({ $customTest }) {
const item = await $customTest()
return { item }
},
}
</script>
It is not calling a method but you could totally use this.$nuxt.refresh() to fetch it again and increment the index of the repository call after an update in the store.
Which could be referenced like
await fetch(`https://jsonplaceholder.typicode.com/todos/${indexFromVuex}`)
You could of course keep it local too
<template>
<div>
<pre>item: {{ item }}</pre>
<button #click="fetchNewItem">fetch new item</button>
</div>
</template>
<script>
export default {
async asyncData({ $customTest }) {
const item = await $customTest()
return { item }
},
data() {
return {
index: 1,
}
},
methods: {
async fetchNewItem() {
this.index += 1
this.item = await this.$customTest(this.index)
},
},
}
</script>
So yeah, I don't think that there are other possible approaches with asyncData.
The fetch() hook is a bit more flexible but it's also totally different too regarding how it is working.
Anyway, with those 2 approaches you could totally have enough to solve the issue of your HTTP call.
It seems that an injected dependency can be accessed (in this case) with simply this.$postRepository inside any method so I didn't even need that asyncData
I'm going crazy over this as I can't understand what I might be missing.. so I'm basically using vuex to store the state of the user. I've setup a route and use Axios to make a call to this route to retrieve some info from the user. I can see in vue dev tools that the state and everything are set I'm just having a real hard time accessing it. If I do the following console.log(this.$store.state.baseSettings) in mounted then this shows the following:
Now I would have thought by adding .user onto that console.log I'd get exactly what I need but it shows an empty object.
I have also tried the following:
computed: {
user() {
return this.$store.state.baseSettings.user;
},
},
This works if I do {{ user }} in the template itself but if I try and access this.user in computed, mounted, or any methods I also get an empty object. Is there any reason for this? Am I missing something simple/obvious here?
Any help would be greatly appreciated! Here's my full code:
app.js:
import Vue from 'vue';
import Vuetify from 'vuetify';
import router from '../../router';
import store from './store';
import {userComputed, settingsMethods} from './store/helpers';
import App from '../App.vue';
new Vue({
router,
store,
vuetify: new Vuetify(),
render: h => h(App),
computed: {
...userComputed,
},
methods: {
...settingsMethods,
},
mounted() {
return this.getAllSettings();
},
}).$mount('#app');
App.vue
<template>
<v-app>
{{ user }}
<v-main :class="!user ? 'background-img' : null">
<v-container fluid>
<nav-bar/>
<router-view/>
</v-container>
</v-main>
</v-app>
</template>
<script>
export default {
computed: {
user() {
// Works but only in above <template></template>
return this.$store.state.baseSettings.user;
},
},
mounted() {
// Returns object with user data (see screenshot).
console.log(this.$store.state.baseSettings);
// Empty object!
console.log(this.$store.state.baseSettings.user);
// Empty object
console.log(this.user)
}
}
</script>
baseSettings.js:
const state = {
user: {},
};
const getters = {
getUser: state => _.keys(state.user),
};
const actions = {
getAllSettings({ commit }) {
return axios.get('settings').then(baseSettings => {
commit('setUser', _.get(baseSettings, 'data.user', {}));
});
},
};
const mutations = {
setUser(state, user) {
state.user = user;
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};
helpers.js:
import { mapActions, mapState } from 'vuex';
export const userComputed = {
...mapState('baseSettings', ['user']),
};
export const settingsMethods = {
...mapActions('baseSettings', ['getAllSettings']),
};
index.js
import Vue from 'vue';
import Vuex from 'vuex';
import baseSettings from '../modules/baseSettings';
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';
export default new Vuex.Store({
modules: {
baseSettings,
},
strict: debug,
});
Thanks so much!
This seems to be an unfulfilled promise problem.
You do both, retrieving the getAllSettings() as well as trying to access the $store independently of each other at the same lifecycle step. Therefore it is not guaranteed that the Axios call has already reported data and saved it to the store when you try to access it from there (resulting in the empty object at runtime).
However since the computed property reruns once the dependant variable changes, it will display correct in your component, as this happens after the mounted() lifecycle step, when the Axios call has run through.
I have (my first) app in vue that get data from api and render some select elements.
I want to separe each box to another file (one file for get data from api, second for render select, third for render list).
How can i pass data from
I tried to get data from instance of api:
export default {
props: {
filters: apiIntance.$data.kpis.filters,
For this moment i have something like that:
<!-- src/components/Filters.vue -->
<template>
<div class="filters">
<label v-bind:key="filter.id" v-for="filter in filters">
<select class="js-selectbox">
<option v-bind:value="item.id" v-bind:key="item.id" v-for="item in filter.items">{{item.name}}</option>
</select>
</label>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Api from './ApiTest.vue';
export default {
props: {
filters: //Get Data from ApiTest.vue
},
};
</script>
<!-- src/components/ApiTest.vue -->
export default Vue.extend({
data(): DataObject {
return {
kpis: {
...
},
};
},
Have you some tips how to get data in one file and spread it for other files?
You might be looking for Vuex which is a vue data management plugin that all your components will have access to
import Vue from 'vue'
import Vuex from 'vue'
Vue.use(Vuex)
const store = new Vuex.Store({
// data will be stored here
state: {
apiData: null,
},
mutations: {
// you call this mutation to add data
apiData(state) {
state.apiData = apiData
}
},
getters: {
// a getter for the data
apiData(state) {
retrun state.apiData
}
}
})
// new Vue({...,store})
you can add data to the store immediately after the fetching with :
this.$store.commit('apiData',yourdatahere)
and then access/get it on any component with :
this.$store.getters.apiData
you can read more about Vuex
There are numerous ways, to name a few
one file for get data from api don't need to be a vue instance. You may want to add more hooks.
// ApiService.ts
export const ApiService = { getData: () => {/**/} }
// src/components/Filters.vue
...
<script lang="ts">
import Vue from 'vue';
import {ApiService} from '#/.../ApiService'
export default {
data: () => ({
filters: []
}),
created() {
ApiService.getData().then(data => this.filters = data)
}
};
</script>
You can certainly make a Vue instance to provide data, to make it reactive and component independent from data logic. For example:
// ApiService.ts
const ApiService = new Vue({
data: () => ({
filters: []
}),
created() {
request().then(data => this.filters = data)
}
})
// src/components/Filters.vue
...
<script lang="ts">
import Vue from 'vue';
import {ApiService} from '#/.../ApiService'
export default {
computed: {
filters() {
return ApiService.filters
}
}
};
</script>
I'm trying to build pagination using vuetify pagination component and nuxt.js but it's not work with server side rendering.
I added nuxt.js link using code from https://github.com/vuetifyjs/vuetify/issues/4855. It's work on client side but on server side it return error "render function or template not defined in component: anonymous"
Anyone have idea how to build correct SSR pagination or how to fix my solution?
Don't know if this thread is still valid but this is my solution for vuetify pagination with nuxt.
<script>
import PostList from "#/components/site/PostList";
import axios from "axios";
export default {
components: {
PostList
},
watchQuery: ["page"],
async asyncData({ $axios, query }) {
try {
const page = query.page || 1;
let { data } = await $axios.get("/articles/?page=" + page);
return {
posts: data.data,
page: data.current_page,
totalPages: data.last_page
};
} catch (e) {
// console.log(e); display errors
}
},
methods: {
next() {
this.$router.push({ query: { page: this.page } });
}
}
};
</script>
<template>
<v-pagination v-model="page" :length="totalPages" #input="next"></v-pagination>
</template>
It's making it impossible for me to add Vue as a view system to a framework called Nest with Express.
I didn't think that adapting Vue was so complicated. That's why I'm here so that you can guide me on the right path and I won't use Vue directly.
Fist the error:
[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
(found in <Root>)
app.controller.ts
import { Controller, Get, Render, Response } from '#nestjs/common';
import { createRenderer } from 'vue-server-renderer';
import { createApp } from './app';
import HelloComponent from './components/Hello';
const context = {data: {}, view: '', componets: {} };
#Controller()
export class AppController {
#Get()
getHello(#Response() res): any {
context.data = { message: 'Esto es un nuevo mensaje 2' };
context.componets = { 'hello' : HelloComponent };
const app = createApp(context);
const renderer = createRenderer();
renderer.renderToString(app, (err, html) => {
res.end(html);
});
}
}
import { createApp } from './app';
import Vue from 'vue';
export function createApp(context: any) {
return new Vue({
data: context.data,
template: fs.readFileSync('./index.html', 'utf-8'),
components: context.components,
}).$mount('#app');
}
I try is to have a base template and then add the components for each controller or route with NestJS.
I don't know if this is possible and if I'm forced to use Webpack, since I'm not currently using it.
Thanks!
Vue launched an entire site to walk you through getting your server side rendering up and running. It is NOT the same process that is outlined at https://vuejs.org.
Complete information can be found at: https://ssr.vuejs.org/ and is referenced in the main guide about halfway down the sidebar navigation under the heading serverside rendering https://v2.vuejs.org/v2/guide/ssr.html
Here is the gist of it to get you started:
npm install express --save
npm install vue vue-server-renderer --save
Integrating with your server example
const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `<div>The visited URL is: {{ url }}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
Rendering a Vue Instance
// Step 1: Create a Vue instance
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello World</div>`
})
// Step 2: Create a renderer
const renderer = require('vue-server-renderer').createRenderer()
// Step 3: Render the Vue instance to HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html)
// => <div data-server-rendered="true">Hello World</div>
})
// in 2.5.0+, returns a Promise if no callback is passed:
renderer.renderToString(app).then(html => {
console.log(html)
}).catch(err => {
console.error(err)
})
Thankfully it's not that complicated of an issue.
You are attempting to use the runtime build on .ts files, which you cannot. This is because only *.vue because they are pre-compiled.
To get around this, simply create an alias to vue in webpack:
resolve: {
alias: {
vue: 'vue/dist/vue.js'
}
}
Which will give you access to the template-compiler allowing you to use Vue inside of non pre-compiled templates (read: any file not ending in .vue)