#vue-composition / How can I use asynchronous methods in setup() - javascript

<script lang="ts">
import { createComponent } from "#vue/composition-api";
import { SplashPage } from "../../lib/vue-viewmodels";
export default createComponent({
async setup(props, context) {
await SplashPage.init(2000, context.root.$router, "plan", "login");
}
});
</script>
ERROR: "setup" must return a "Object" or a "Function", got "Promise"

The setup function must be synchronous can be async using Suspense.
How to avoid using async setup (obsolete answer)
An onMounted hook can be used with an async callback:
import { onMounted } from "#vue/composition-api";
// …
export default createComponent({
setup(props, context) {
onMounted(async () => {
await SplashPage.init(2000, context.root.$router, "plan", "login");
)};
}
});
Or, it is always possible to call an asynchronous function without to await it:
SplashPage.init(2000, context.root.$router, "plan", "login")
.catch(console.log);
In both cases, you'll have to take into account that the component will be rendered before the execution of the asynchronous function. A simple way to not display something that would depends on it is to use v-if in your template.

I have another solution that works in my use-case. Maybe it will help. It is a bit like the lifestyle hook approach, but without needing it. It also doesn't need the <Suspense> tag which was "overkill" in my use-case.
The idea is to return a default value (the empty array in this case, but could be a "Loading..." splash page). Then, after the async has resolved, update the reactive prop (menuItems array here, but it could be the actual splash page contents or html or whatever).
I know this might not suit all use-cases but it is another approach that works.
Simplified code:
setup () {
const menuItems = ref([])
const buildMenuItems = async () => {
// eg get items from server, return an array of formatted items...
}
/* setTimeout(async () => {
menuItems.value = await buildMenuItems()
}, 0) */
// or..
;(async () => {
menuItems.value = await buildMenuItems()
})()
return {
menuItems
}
}
I tested it by making buildMenuItems() take 2 seconds and it all works fine.
EDIT: And then I discovered other ways (even for non TypeScript): How can I use async/await in the Vue 3.0 setup() function using Typescript
Cheers,
Murray

Related

Javascript async-mutex does not seem to lock properly

I am using [async-mutex](https://github.com/DirtyHairy/async-mutex because there is a race condition in my code from concurrent requests returning. And upon all of the concurrent requests resolving, they each need to add something from the response into a state array.
I have included a codesandbox replicating this issue: https://codesandbox.io/s/runtime-haze-2407uy
I will also post the code here for reference:
import Uppy from "#uppy/core";
import XHRUpload from "#uppy/xhr-upload";
import { DragDrop, ProgressBar } from "#uppy/react";
import { Mutex } from "async-mutex";
import { useEffect, useMemo, useRef, useState } from "react";
export default function App() {
const [stuff, setStuff] = useState([]);
const uppy = new Uppy({
meta: { type: "file" },
autoProceed: true
});
uppy.use(XHRUpload, {
endpoint: `google.com/upload`
});
const mutex = new Mutex();
uppy.on("upload-error", (_, response) => {
mutex.acquire().then((release) => {
let joined = stuff.concat("test");
setStuff(joined);
console.log(stuff);
release();
});
});
return (
<div className="App">
<DragDrop
uppy={uppy}
locale={{
strings: {
// Text to show on the droppable area.
// `%{browse}` is replaced with a link that opens the system file selection dialog.
dropHereOr: "Drop here or %{browse}",
// Used as the label for the link that opens the system file selection dialog.
browse: "browse"
}
}}
/>
</div>
);
}
I expect, when uploading two files (the upload server is bogus but that is intended, because all requests (one per file) will trigger the upload-error event) that the stuff array will end up like ['test', 'test']. However, that does not happen:
The reason for this is because the "state" (no pun intended) of the stuff state is uncertain at the time of running setState, due to the nature of setStuff and state setters in general being asynchronous.
The solution is to
a) use await because in any case the mutex lock acquisition is a promise
b) pass a lambda function into setStuff that guarantees the state of stuff will be up to date, as opposed to just assuming stuff will be up to date (which it won't be)
uppy.on('upload-error', async (_, response) => {
await mutex.acquire().then((release) => {
setStuff((prevState => {
return prevState.concat('test');
}));
release();
});
})
For more information, check out https://stackoverflow.com/a/44726537/8652920

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.

Jest: Mock a _HOC_ or _curried_ function

Given the following function:
./http.js
const http = {
refetch() {
return (component) => component;
}
}
I would like to mock the function in a test as follows:
./__tests__/someTest.js
import { refetch } from './http';
jest.mock('./http', () => {
return {
refetch: jest.fn();
}
}
refetch.mockImplementation((component) => {
// doing some stuff
})
But I'm receiving the error
TypeError: _http.refetch.mockImplementation is not a function
How can I mock the refetch function in the given example?
update:
When I modify the mock function slightly to:
jest.mock(
'../http',
() => ({ refetch: jest.fn() }),
);
I get a different error:
TypeError: (0 , _http.refetch)(...) is not a function
My guess it's something with the syntax where the curried function (or HOC function) is not mapped properly. But I don't know how to solve it.
Some of the real code I'm trying to test.
Note: The example is a bit sloppy. It works in the application. The example given is to give an idea of the workings.
./SettingsContainer
// ...some code
return (
<FormComponent
settingsFetch={settingsFetch}
settingsPutResponse={settingsPutResponse}
/>
);
}
const ConnectedSettingsContainer = refetch(
({
match: { params: { someId } },
}) => ({
settingsFetch: {
url: 'https://some-url.com/api/v1/f',
},
settingsPut: (data) => ({
settingsPutResponse: {
url: 'https://some-url.com/api/v1/p',
}
}),
}),
)(SettingsContainer);
export default ConnectedSettingsContainer;
Then in my component I am getting the settingsPutResponse via the props which react-refetch does.
I want to test if the user can re-submit a form after the server has responded once or twice with a 500 until a 204 is given back.
./FormComponent
// ...code
const FormComp = ({ settingsResponse }) => {
const [success, setSuccess] = useState(false);
useEffect(() => {
if (settingsResponse && settingsResponse.fulfilled) {
setSuccess(true);
}
}, [settingsResponse]);
if (success) {
// state of the form wil be reset
}
return (
<form>
<label htmlFor"username">
<input type="text" id="username" />
<button type="submit">Save</button>
</form>
)
};
The first question to ask yourself about mocking is "do I really need to mock this?" The most straightforward solution here is to test "component" directly instead of trying to fake out an http HOC wrapper around it.
I generally avoid trying to unit test things related to I/O. Those things are best handled with functional or integration tests. You can accomplish that by making sure that, given same props, component always renders the same output. Then, it becomes trivial to unit test component with no mocks required.
Then use functional and/or integration tests to ensure that the actual http I/O happens correctly
To more directly answer you question though, jest.fn is not a component, but React is expecting one. If you want the mock to work, you must give it a real component.
Your sample code here doesn't make sense because every part of your example is fake code. Which real code are you trying to test? I've seen gigantic test files that never actually exercize any real code - they were just testing an elaborate system of mocks. Be careful not to fall into that trap.

How to reload localStorage VueJS after using this.$router.go(-1);

This is my Login.vue:
mounted() {
if (localStorage.login) this.$router.go(-1);
},
methods: {
axios.post(ApiUrl + "/login") {
...
}
then(response => {
...
localStorage.login = true;
this.$router.go(0); /* Reload local storage */
})
}
App.vue:
mounted() {
axios
.get("/user")
.then(response => {
localStorage.user_id = response.data.user.id;
localStorage.package_id = response.data.user.package_id;
})
},
Project.vue:
mounted() {
this.user_id = localStorage.user_id
this.package_id = localStorage.package_id
}
With that above code, I cannot get localStorage.user_id and localStorage.package_id as I expected. But if I change like the follow, it worked.
mounted() {
const self = this
setTimeout(function () {
self.user_id = localStorage.user_id
self.package_id = localStorage.package_id
self.getProject();
},1000)
}
But I think setTimeout not good in that case. Is there any way to refactor this code?
Thank you!
Try this: in your root component (it's usually const app = new Vue({ ... })) write the following:
import {localStorage} from 'localStorage'; // import your module if necessary
// this is relative to the way you manage your dependencies.
const app = new Vue({
//...
data: function() {
return {
localStorage: localStorage;
}
}
})
Now whenever you want to use localStorage, access it from root component like this:
this.$root.localStorage
Hope this solves your problem.
Don't know your project structure ,but I guess it's probably an async issue. You got the user information async, so when Project.vue mounted, the request is not complete yet. As a result, the localstorage is empty at the monent.
There are two solutions for this:
Make sure Project.vue is not rendered before userinfo is complete. For example, things like <project v-if="userinfo.user_id" /> should works.
Use some data binding libary like vuex to bind userinfo to Project.vue instead of assign it in lifecycle like mounted or created.
Hope it helps.

jest.mock is not a function in react?

Could you please tell me how to test componentDidMount function using enzyme.I am fetching data from the server in componentDidMount which work perfectly.Now I want to test this function.
here is my code
https://codesandbox.io/s/oq7kwzrnj5
componentDidMount(){
axios
.get('https://*******/getlist')
.then(res => {
this.setState({
items : res.data.data
})
})
.catch(err => console.log(err))
}
I try like this
it("check ajax call", () => {
const componentDidMountSpy = jest.spyOn(List.prototype, 'componentDidMount');
const wrapper = shallow(<List />);
});
see updated code
https://codesandbox.io/s/oq7kwzrnj5
it("check ajax call", () => {
jest.mock('axios', () => {
const exampleArticles:any = {
data :{
data:['A','B','c']
}
}
return {
get: jest.fn(() => Promise.resolve(exampleArticles)),
};
});
expect(axios.get).toHaveBeenCalled();
});
error
You look like you're almost there. Just add the expect():
expect(componentDidMountSpy).toHaveBeenCalled();
If you need to check if it was called multiple times, you can use toHaveBeenCalledTimes(count).
Also, be sure to mockRestore() the mock at the end to make it unmocked for other tests.
List.prototype.componentDidMount.restore();
To mock axios (or any node_modules package), create a folder named __mocks__ in the same directory as node_modules, like:
--- project root
|-- node_modules
|-- __mocks__
Inside of there, make a file named <package_name>.js (so axios.js).
Inside of there, you'll create your mocked version.
If you just need to mock .get(), it can be as simple as:
export default { get: jest.fn() }
Then in your code, near the top (after imports), add:
import axios from 'axios';
jest.mock('axios');
In your test, add a call to axios.get.mockImplementation() to specify what it'll return:
axios.get.mockImplementation(() => Promise.resolve({ data: { data: [1, 2, 3] } });
This will then make axios.get() return whatever you gave it (in this case, a Promise that resolves to that object).
You can then do whatever tests you need to do.
Finally, end the test with:
axios.get.mockReset();
to reset it to it's default mocked implementation.

Categories