Async Computed in Components - VueJS? - javascript

I'm finding a solution to async computed method in Components:
Currently, my component is:
<div class="msg_content">
{{messages}}
</div>
<script>
export default {
computed: {
messages: {
get () {
return api.get(`/users/${this.value.username}/message/`, {'headers': { 'Authorization': 'JWT ...' }})
.then(response => response.data)
}
}
},
}
</script>
Result:
{}
How to rewrite it in Promise mode? Because I think we can async computed by writing into Promise mode.

Computed properties are basically functions that cache their results so that they don't have to be calculated every time they are needed. They updated automatically based on the reactive values they use.
Your computed does not use any reactive items, so there's no point in its being a computed. It returns a Promise now (assuming the usual behavior of then).
It's not entirely clear what you want to achieve, but my best guess is that you should create a data item to hold response.data, and make your api.get call in the created hook. Something like
export default {
data() {
return {
//...
messages: []
};
},
created() {
api.get(`/users/${this.value.username}/message/`, {
'headers': {
'Authorization': 'JWT ...'
}
})
.then(response => this.messages = response.data);
}
}

es7 makes doing this quite trivial by using async and await in conjunction with axios' returned promise. You'll need the vue-async-computed package.
export default {
asyncComputed: {
async myResolvedValue() {
return await api.get(`/users/${this.value.username}/message/`, {'headers': { 'Authorization': 'JWT ...' }})
.then(response => response.data)
}
}
}

I bumped on a similar case where I need to re-run the computed function to fetch from a server every time a data or a props changes.
Without installing any extra package (vue-async-computed or vue3-async-computed as pointed by other answers), you can force a data to reload by creating a "virtual" computed method.
Lets say you want to fetch data from the server every time the user types their username, and depending on what was typed, you want to show a given message from the server.
From the example below, username and messages in this case are both reactive data, but there is no direct connection between them, so lets create a computed that depends on username by returning its value, which will force it to be called every time username is changed. Now you just need to call a function that can be async and will update messages after fetching from the server.
In the example below, I use ":dummy" just to force a call to my computed function.
<template>
<input v-model="username">
<div class="msg_content" :dummy="force_react">
{{messages}}
</div>
</template>
<script>
export default {
data: function () {
return {
messages: "",
username: "",
};
},
computed: {
force_react: function() {
this.get(); // called every time that this.username updates
return this.username; // becase its result depends on username
}
},
methods: {
async get() { // get's called every time that this.username updates
console.log("Got called");
let response = await api.get(`/users/${this.username}/message/`, {'headers': { 'Authorization': 'JWT ...' }});
this.messages = response.data;
}
},
}
</script>
You can see a working example here in Vue SFC playground

Why not using a watcher? Vue Documentation
You can use async function in the callback of the watcher.

Related

Vue prefetch data from separate backend

I have some queries from an API-Server that returns a json object that will be static over a user session, but not static forever.
It's a one-pager with Vue router.
How can I achieve that I:
can access this.myGlobals (or similar eg window.myGlobals) in all components, where my prefetched json-data from API-Server is stored.
My approach that is already working is to embed help.js via a mixin.
Oddly enough, I get hundreds of calls to this query. At first I thought that it only happened in the frontend and is chached, but the requests are actually sent hundreds of times to the server. I think it is a mistake of my thinking, or a systematic mistake.
i think the problem is, that the helper.js is not static living on the vue instance
main.js:
import helpers from './helpers'
Vue.mixin(helpers)
helpers.js:
export default {
data: function () {
return {
globals: {},
}
}, methods: {
//some global helper funktions
},
}, mounted() {
let url1 = window.datahost + "/myDataToStore"
this.$http.get(url1).then(response => {
console.log("call")
this.globals.myData = response.data
});
}
}
log in console:
call
SomeOtherStuff
(31) call
SomeOtherStuff
(2) call
....
log on server:
call
call
call (pew pew)
My next idea would be to learn vuex, but since its a easy problem, im not sure if i really need that bomb ?
You can use plugin to achieve this.
// my-plugin.js
export default {
install (Vue, options) {
// start fetching data right after install
let url1 = window.datahost + "/myDataToStore"
let myData
Vue.$http.get(url1).then(response => {
console.log("call")
myData = response.data
})
// inject via global mixin
Vue.mixin({
computed: {
myData () {
return myData
}
}
})
// or inject via instance property
Vue.prototype.$myData = myData
// or if you want to wait until myData is available
Vue.prototype.$myData = Vue.$http.get(url1)
.then(response => {
console.log("call")
myData = response.data
})
}
}
and use it:
Vue.use(VueResource)
Vue.use(myPlugin)

Vue v-if resolve Promise - implementing Laravel Gate in Vue

I'm trying to implement Laravel's authorization & policy in Vue, by implementing a mixin which sends a GET request to a controller in the backend.
The problem is the v-if directive is receiving a Promise, which obviously does not resolve
Below is a very simplified version of what I'm trying to do:
The global mixin, auth.js
import axios from "axios"
export default {
methods: {
async $can (permission, $model_id) {
let isAuthorized = false;
await axios.get(`/authorization?${permission}&${model_id}`)
.then(function (response) {
isAuthorized = response.data.isAuthorized
})
.catch((error) => {
isAuthorized = false;
});
return isAuthorized;
}
}
}
The main entry file, app.js
import Auth from '#/auth';
Vue.mixin(Auth);
...
new Vue({...})
Component.vue
<template>
<div>
<div v-if="$can('do-this', 12)">
Show Me
</div>
</div>
</template>
<script>
export default {}
</script>
Is there any way to 'await' the async $can operation in v-if? Or am I approaching this from a totally wrong direction?
You don't need async/await there because axios returns a promise. I think you can call that function from the created hook. Instead of returning a value, change the related data attribute, and use it in v-if like so:
<div v-if="permissions['do-this__12']">
data() {
return {
permissions: {
'do-this__12': false,
'or-this__13': false,
},
}
}
methods: {
getPermissions() {
for (const key in this.permissions) {
this.can(key.split('__')[0], key.split('__')[1])
}
},
can(permission, model_id) {
axios.get(`/authorization?${permission}&${model_id}`)
.then(response => {
this.permissions[`${permission}__${model_id}`] = response.data.isAuthorized
})
.catch(error => {
this.permissions[`${permission}__${model_id}`] = false;
});
},
}
created() {
this.getPermissions();
}
I didn't try my code, let me know if it fails. BTW, extracting this implementation to a mixin will be a better idea. If you like to do that, just leave "permissions" object in the component and move everything else to the mixin.
But that approach isn't effective when you need multiple API calls for permissions. That's why I think you should pass the whole permissions object to the backend and make the work in the server:
iPreferThisBecauseOfSingleAPICall() {
axios.get(`/authorization`, this.permissions)
.then(({ data }) => this.permissions = data)
}
// AuthorizationController
public function index(Request, $request)
{
$permissions = [];
foreach($request->all() as $permission) {
// run your backend code here
}
return $permissions;
}
One final note, instead of asking for permission each time, loading all permissions at one can be the best idea.

getting a restful Api using the fetch() in Vuex

im really going through hard times trying to figure out how to get my API data through Vuex, is there some body whom has accurate bibliography of how to do this step by step, or even better help me with this code?
Formerly without using Vuex , but Vue all request worked perfectly, but now i dont understand clearly what i should do, here sharing part of my code:
data() {
return {
testArray: []
};
methods: {
getJsonData() {
fetch(
"https://app.ticketmaster.com/discovery/v2/events.json?countryCode=" +
this.countriesDrop +
"&apikey=xxxxxxxxxxxxxxxxxxxxxxxx",
{
method: "GET"
}
)
.then(response => {
return response.json();
})
.then(test => {console.log(this.testArray)
this.testArray = test._embedded.events;
})
.catch(err => {
console.log(err);
});
},
watch: {
countriesDrop: function(val) {
this.getJsonData();
}
},
As you can see in the request also is included an external element which make it changes attuning with the watcher and the value the user might asign.
I already got set Vuex and all else pluggins...just dont know how to act like , thus would appreciate an accurate link or tutorial either help with this basic problem resolved on detail step by step, .....thanks!
In your code there's nothing with Vuex. I guessed you want to set the state so that the getJsonData() method is called according to what's in the store.
Here's a snippet as an example of handling async in a Vuex environment.
const store = new Vuex.Store({
state: {
testArray: []
},
mutations: {
setTestArray(state, data) {
state.testArray = data
}
},
actions: {
getJsonData({
commit
}, countriesDrop) {
if (countriesDrop && countriesDrop !== '') {
fetch(`https://jsonplaceholder.typicode.com/${countriesDrop}`, {
method: "GET"
})
.then(response => {
return response.json();
})
.then(json => {
commit('setTestArray', json)
})
.catch(err => {
console.log(err);
});
}
}
}
})
new Vue({
el: "#app",
store,
computed: {
getDataFromStore() {
return this.$store.state.testArray
}
},
methods: {
getData(countriesDrop) {
this.$store.dispatch('getJsonData', countriesDrop)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/es6-promise#4/dist/es6-promise.auto.js"></script>
<script src="https://unpkg.com/vuex#3.1.2/dist/vuex.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="getData('todos')">GET TODOS</button>
<button #click="getData('albums')">GET ALBUMS</button>
<ol>
<li v-for="data in getDataFromStore">{{data.title}}</li>
</ol>
</div>
The point is that Vuex is a central element in a Vue-Vuex application. You can store app state, handle async and sync functions (actions, mutations) with it, and all your Vue components can rely on the state - that should be the "single source of truth".
So, you get your input from a component (the Vue instance in this snippet), and dispatch an action that is available in the Vuex store. If the action needs to modify the state, then you call a mutation to do that. With this flow you keep reactivity for all your components that use that state.
I used a computed to get data from the Vuex store, but getters can be set also.
This way you don't "pollute" your components with functions and data that should be in the store.

VueJS, Vuex, Getter is showing as an empty array, but console.log shows it's an object with all the values

This is the method I'm using, pretty simple.
DailyCountTest: function (){
this.$store.dispatch("DailyCountAction")
let NewPatientTest = this.$store.getters.NewPatientCountGET
console.log(NewPatientTest)
}
The getter gets that data from a simple action that calls a django backend API.
I'm attempting to do some charting with the data so I need to assign them to variables. The only problem is I can't access the variables.
This is what the console looks like
And this is what it looks like expanded.
You can see the contents, but I also see empty brackets. Would anyone know how I could access those values? I've tried a bunch of map.(Object) examples and couldn't get any success with them.
Would anyone have any recommendation on how I can manipulate this array to get the contents?
Thanks!
Here is the Vuex path for the API data
Action:
DailyCountAction ({ commit }) {
axios({
method: "get",
url: "http://127.0.0.1:8000/MonthlyCountByDay/",
auth: {
username: "test",
password: "test"
}
}).then(response => {
commit('DailyCountMutation', response.data)
})
},
Mutation:
DailyCountMutation(state, DailyCount) {
const NewPatientMap = new Map(Object.entries(DailyCount));
NewPatientMap.forEach((value, key) => {
var NewPatientCycle = value['Current_Cycle_Date']
state.DailyCount.push(NewPatientCycle)
});
}
Getter:
NewPatientCountGET : state => {
return state.DailyCount
}
State:
DailyCount: []
This particular description of your problem caught my eye:
The getter gets that data from a simple action that calls a django backend API
That, to me, implies an asynchronous action and you might be getting a race condition. Would you be able to post a sample of your getter function to confirm my suspicion?
If that getter does indeed rely on an action to populate its contents, perhaps something to the effect of the following might do?
DailyCountTest: async () => {
await this.$store.dispatch('DailyCountAction')
await this.$store.dispatch('ActionThatPopulatesNewPatientCount')
let NewPatientTest = this.$store.getters.NewPatientCountGET
// ... do whatever with resulting array
}
You can also try with a computer property. You can import mapGetters
import { mapGetters } from 'vuex'
and later in computed properties:
computed: {
...mapGetters(['NewPatientCountGET'])
}
then you can use your NewPatientCountGET and it will update whenever the value changes in the store. (for example when the api returns a new value)
Hope that makes sense

React - TypeError: this.props.courses.map is not a function

I have created a react-redux application. Currently what it does is load courses from the server(api), and displays them to the course component. This works perfectly. I'm trying to add a feature where you can create a course by posting it to the server, the server would then true an a success object. However, when i post to the server i get the following error(see below). I think this is due to my connect statement listening for the load courses action. Clearly its thinking it should be getting a list of something, instead of a success object. I have tried a few thing for it to listen for both courses and the success response, but to save you the time of reading all the strange thing i have done, i could not get it to work. Dose anyone know how to fix this issue ?
error
TypeError: this.props.courses.map is not a function
course.component.js
onSave(){
// this.props.createCourse(this.state.course);
this.props.actions.createCourse(this.state.course);
}
render() {
return (
<div>
<div className="row">
<h2>Couses</h2>
{this.props.courses.map(this.courseRow)}
<input
type="text"
onChange={this.onTitleChange}
value={this.state.course.title} />
<input
type="submit"
onClick={this.onSave}
value="Save" />
</div>
</div>
);
}
}
// Error occurs here
function mapStateToProps(state, ownProps) {
return {
courses: state.courses
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(courseActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Course);
course.actions.js
export function loadCourse(response) {
return {
type: REQUEST_POSTS,
response
};
}
export function fetchCourses() {
return dispatch => {
return fetch('http://localhost:3000/api/test')
.then(data => data.json())
.then(data => {
dispatch(loadCourse(data));
}).catch(error => {
throw (error);
});
};
}
export function createCourse(response) {
return dispatch => {
return fetch('http://localhost:3000/api/json', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
response: response
})
})
.then(data => data.json())
.then(data => {
dispatch(loadCourse(data));
}).catch(error => {
throw (error);
});
};
}
course.reducer.js
export default function courseReducer(state = [], action) {
switch (action.type) {
case 'REQUEST_POSTS':
return action.response;
default:
return state;
}
}
server.js
router.get('/test', function(req, res, next) {
res.json(courses);
});
router.post('/json', function(req, res, next) {
console.log(req.body);
res.json({response: 200});
});
i have tried added a response to the state, and listening for it in the map state to props, but still for some reason react is trying to map response to courses. Do i need a second connect method?
function mapStateToProps(state, ownProps) {
return {
courses: state.courses,
resposne: state.resposne
};
}
As you can see from the pictures response is getting mapped as courses and not as response.
Picture
Assumptions:
state.courses is initially an empty array - from course.reducer.js
You don't call fetchCourses() action the first time you are rendering your view
Even if you call fetchCourses() there is no problem as long as courses in server.js is an array (the array in the response replaces the initial state.courses)
Flow:
Now I assume the first render is successful and React displays the <input type="text"> and submit button. Now when you enter the title and click on the submit button, the onSave() method triggers the createCourse() action with parameter that is more or less similar to { title: 'something' }.
Then you serialize the above mentioned param and send to the server (in course.actions.js -> createCourse()) which in turn returns a response that looks like {response: 200} (in server.js). Response field is an integer and not an array! Going further you call loadCourses() with the object {response: 200} which triggers the courseReducer in course.reducer.js
The courseReducer() replaces state.courses (which is [] acc. to assumption) with an integer. And this state update triggers a re-render and you end up calling map() on an integer and not on an array, thus resulting in TypeError: this.props.courses.map is not a function.
Possible Solution:
Return a valid response from serve.js (i.e. return the course object the endpoint is called with), or
Update your reducer to add the new course object into the existing state.courses array, like, return [...state, action.response]
Update:
Based on OP's comment, if what you want to do is send the new course object to the server, validate it and send success (or error) and based on response add the same course object to the previous list of courses, then you can simply call loadData() with the same course object you called createCourse() with and (as mentioned above) inside your reducer, instead of replacing or mutating the old array create a new array and append the course object to it, in es6 you can do something like, return [...state, course].
Update 2:
I suggest you go through Redux's Doc. Quoting from Redux Actions' Doc
Actions are payloads of information that send data from your application to your store. They are the only source of information for the store.
The createCourse() action is called with a payload which is more-or-less like,
{title: 'Thing you entered in Text Field'}, then you call your server with an AJAX-request and pass the payload to the server, which then validates the payload and sends a success (or error) response based on your logic. The server response looks like, {response: 200}. This is end of the createCourse()action. Now you dispatch() loadCourses() action from within createCorse(), with the response you received from the server, which is not what you want (based on your comments). So, instead try dispatch()ing the action like this (try renaming response param, it's a bit confusing)
//.....
.then(data => {
dispatch(loadCourse(response)); // the same payload you called createCourse with
})
//.....
Now, loadCourse() is a very basic action and it simply forwards the arguments, which Redux uses to call your reducer. Now, in case you followed the previous discussion and updates how you call loadCourse(), then the return from loadCourse() looks like
{
type: REQUEST_POSTS,
response: {
title: 'Thing you entered in Text Field',
}
}
which is then passed onto your reducer, specifically your courseReducer().
Again quoting from Redux Reducers' Doc
Actions describe the fact that something happened, but don't specify how the application's state changes in response. This is the job of reducers.
The reducer must define the logic on how the action should impact the data inside the store.
In your courseReducer(), you simply returns the response field inside the action object and [expect] Redux to auto-magically mutate your state! Unfortunately this is not what happens :(
Whatever you return from the reducer, completely replaces whatever thing/object was there before, like, if your state looks like this
{ courses: [{...}, {...}, {...}] }
and you return something like this from your reducer
{ title: 'Thing you entered in Text Field'}
then redux will update the state to look like
{ courses: { title: 'Thing you entered in Text Field'} }
state.courses is no longer an Array!
Solution:
Change your reducer to something like this
export default function courseReducer(state = [], action) {
switch (action.type) {
case 'REQUEST_POSTS':
return [...state, action.response]
default:
return state
}
}
Side Note: This is may be confusing at times, so just for the sake of record, state inside courseReducer() is not the complete state but a property on the state that the reducer manages. You can read more about this here
--Edit after reading a comment of you in a different answer, I've scraped my previous answer--
What you're currently doing with your actions and reducers, is that you're calling loadCourse when you fetched the initial courses. And when you created a new course, you call loadCourse too.
In your reducer you're directly returning the response of your API call. So when you fetch all the courses, you get a whole list of all your courses. But if you create a new one you currently receive an object saying response: 200. Objects don't have the map function, which explains your error.
I would suggest to use res.status(200).json() on your API and switching the response status in your front-end (or using then and catch if you can validate the response status, axios has this functionality (validateStatus)).
Next I would create a separate action-type for creating posts and dispatch that whenever it's successful.
I would change your reducer to something like
let initialState = {
courses: [],
createdCourse: {},
}
export default (state = initialState, action) => {
switch (action.type) {
case 'REQUEST_POSTS':
return {
...state,
courses: action.response
}
case 'CREATE_COURSE_SUCCESS':
return {
...state,
createdCourse: action.response,
}
default: return state;
}
}
I wouldn't mind looking into your project and giving you some feedback on how to improve some things (ES6'ing, best practices, general stuff)
Based on the questions & answers so far, it looks like you need to do something like this:
1) Add a new action and dispatch this from your createCourse function
export function courseAdded(course, response) {
return {
type: 'COURSE_ADDED',
course
response
};
}
export function createCourse(course) {
return dispatch => {
return fetch('http://localhost:3000/api/json', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
course
})
})
.then(response => response.json())
.then(response => {
dispatch(courseAdded(course, response));
}).catch(error => {
throw (error);
});
};
}
2) Change your reducers to handle both fetching courses and adding a new course (we're using combineReducers to handle this here)
import { combineReducers } from "redux";
function response(state = null, action) {
switch (action.type) {
case 'COURSE_ADDED':
return action.response;
default:
return state;
}
}
function courses(state = [], action) {
switch (action.type) {
case 'COURSE_ADDED':
return [...state, action.course];
case 'REQUEST_POSTS':
return action.response;
default:
return state;
}
}
export default combineReducers({
courses,
response
});
3) Hook up to the new response state in your connect component
function mapStateToProps(state, ownProps) {
return {
courses: state.courses,
response: state.response
};
}
4) Do something with this new response prop in your component if you want to show it e.g.
// this is assuming response is a string
<span className="create-course-response">
Create Course Response - {this.props.response}
</span>
UPDATE
I've added support for adding the new course to the end of the existing course list, as well as handling the response. How you shape the state is completely up to you and it can be re-jigged accordingly.
In order for this code to work, you will need to add support for the spread operator. If you are using babel it can be done like this. Creating a new object is important to ensure that you don't mutate the existing state. It will also mean react-redux knows the state has changed. Spread operator isn't essential and this can be done with Object.assign, but that syntax is ugly IMO.

Categories