Why watch doesn't invoke when I usePromise? - vue.js

I'm using #vue/composition-api and usePromise from vue-composable.
I watch on computed value that return from a promise. the promise is resolve after 12000ms.
For the first time the computed and watch are invoked, but after 12000ms none of them is invoke. so the watch callback doesn't work. even the api.result is changed.
I can see that value changed in the template by printing: {{items}}.
How to solve this unexpected behavior? how to call the watch callback when the value change?
here is the output:
in computed
in watch
expected:
in computed
in watch
in computed <-- data is resolved the api.result changed (has new value).
in watch <--- same.
The code:
setup() {
function timeout(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const api = usePromise(() => Promise.all([timeout(12000).then(() => ({ some: 'data'}))]));
const items = computed(() => {
console.log('in computed');
return api.result;
});
watch(items, (v) => {
console.log('in watch');
})
api.exec();
return { items }
This is works:
watch(api.result, (v) => {
console.log('in watch api.result');
})
But why computed doesn't work? how make it recalculate so the watch on computed will work?
The code on codesandbox

Related

Why this navigation.goBack is not working as I intended ? How can I get the Last Active State?

I got 3 pages
homepage, productList and productDetails
When going from homepage to productList I pass a route param,
navigation.navigate('productList', { showCategory: 'productListA'} )
InitialProcess when component mounted
Inside the productList page when the component is mounted. I am declaring use state like this.
const {showCateory} = route.params;
const [activeTab, setActiveTab] = useState(showCateory);
and calling api using that activeTab
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
try {
await dispatch(
fetchProductList(
activeTab,
),
);
} catch (err) {
console.log(err);
}
}
fetchData();
});
return unsubscribe;
}, []);
User Interaction
But I also add the button in the productList so that user can change the current active tab
<TouchableOpacity onPress={() => changeTab()}></TouchableOpacity>
const changeTab = async () => {
await setActiveTab('productListB'),
await dispatch(fetchProductList(activeTab)
}
Take note that right now active tab and data coming from api is different from when the component is start mounted.
Navigation Change again
When use goes from productList to productDetails. All thing is fine.
But inside the product details I am going back to productList with this.
navigation.goBack().
When I am back in productList page The activeTab is change back to productListA and the data is change back to when component is mounted
Can I pass or change the route params when calling navigation.goBack()?
add activeTab in useEffect depedineces.
as docs say
The array of dependencies is not passed as arguments to the effect function. Conceptually, though, that’s what they represent: every value referenced inside the effect function should also appear in the dependencies array. In the future, a sufficiently advanced compiler could create this array automatically.
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
try {
await dispatch(
fetchProductList(
//this value will always updated when activeTab change
activeTab,
),
);
} catch (err) {
console.log(err);
}
}
fetchData();
});
return unsubscribe;
}, [activeTab]); //<<<<< here
also you need to know setState() does not always immediately update the component. see here
so change this
const changeTab = async () => {
//await setActiveTab('productListB'),
//await dispatch(fetchProductList(activeTab)
setActiveTab('productListB')
dispatch(fetchProductList('productListB'))
}
This might be happening because route.params is still set to { showCategory: 'productListA'} when you are coming back to the screen.
If this is the case, you can fix it by Changing params object in changeTab() like
navigation.setParams({
showCategory: 'productListB',
});
I hope this will fix your problem.
This happens because the callback function inside the focus listener uses the initial value of the state when the function was defined (at initial page render) . Throughout the lifespan of listener the callback function uses this stale state value.You can read more about this behaviour in this answer
Although the answer by Ahmed Gaber works in this case as the listener is cleared and redefined after each state change.Another common work-around is to use an useRef instead of useEffect.A ref is basically a recipe that provides a mutable object that can be passed by reference.
In your case you can initialise activeTab with navigation param value using useRef hook as :
const activeTab = useRef(showCateory);
and the focus listener callback function should be changed to use the Reference current value as
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
try {
await dispatch(
fetchProductList(
activeTab.current, //<<<<<<---------here
),
);
} catch (err) {
console.log(err);
}
}
fetchData();
});
return unsubscribe;
}, []);
and the changeTab function can directly update reference current value
const changeTab = async () => {
setActiveTab.current = 'productListB';
dispatch(fetchProductList('productListB'))
}

How to return promise result from Vuex action instead of the promise itself?

I have an action in my vuex store:
export const actions = {
myaction() {
return 'foo'
}
}
Can I get the promise result (here foo) in the mounted() life cycle hook and display it in the console ? If yes, how ?
I tried this:
mounted() {
console.log(
this.$store
.dispatch('myaction')
.then(res => res)
)
}
But it returns the promise instead of the promise result I'm expecting.
Either of these should work:
Using .then():
mounted() {
this.$store
.dispatch('myaction')
.then(res => console.log(res));
}
Or if you're using ES2017 or later (or some compatibility tool like Babel), then you can use async/ await:
async mounted() {
const res = await this.$store.dispatch('myaction');
console.log(res);
}
As #Dan mentions below, whilst this will return you the value of the Promise, this is not the intended usage of Vuex, which prefers all data to be saved to and accessed from Vuex's state.
It would be better to mutate the store in your action to save the value and then use a computed property in your component to retrieve it from state, either directly or through a getter. This can be done very cleanly with the mapGetters Vuex helper function.

reactive object not updating on event emitted from watch

i'm building a complex form using this reactive obj
const formData = reactive({})
provide('formData', formData)
inside the form one of the components is rendered like this:
<ComboZone
v-model:municipality_p="formData.registry.municipality"
v-model:province_p="formData.registry.province"
v-model:region_p="formData.registry.region"
/>
this is the ComboZone render function:
setup(props: any, { emit }) {
const { t } = useI18n()
const { getters } = useStore()
const municipalities = getters['registry/municipalities']
const _provinces = getters['registry/provinces']
const _regions = getters['registry/regions']
const municipality = useModelWrapper(props, emit, 'municipality_p')
const province = useModelWrapper(props, emit, 'province_p')
const region = useModelWrapper(props, emit, 'region_p')
const updateConnectedField = (key: string, collection: ComputedRef<any>) => {
if (collection.value && collection.value.length === 1) {
console.log(`update:${key} => ${collection.value[0].id}`)
emit(`update:${key}`, collection.value[0].id)
} else {
console.log(`update:${key} =>undefined`)
emit(`update:${key}`, undefined)
}
}
const provinces = computed(() => (municipality.value ? _provinces[municipality.value] : []))
const regions = computed(() => (province.value ? _regions[province.value] : []))
watch(municipality, () => updateConnectedField('province_p', provinces))
watch(province, () => updateConnectedField('region_p', regions))
return { t, municipality, province, region, municipalities, provinces, regions }
}
useModelWrapper :
import { computed, WritableComputedRef } from 'vue'
export default function useModelWrapper(props: any, emit: any, name = 'modelValue'): WritableComputedRef<any> {
return computed({
get: () => props[name],
set: (value) => {
console.log(`useModelWrapper update:${name} => ${value}`)
emit(`update:${name}`, value)
}
})
}
problem is that the events emitted from useModelWrapper update the formData in the parent template correctly, the events emitted from inside the watch function are delayed by one render....
TL;DR;
Use watchEffect instead of watch
...with the caveat that I haven't tried to reproduce, my guess is that you're running into this issue because you're using watch which runs lazily.
The lazy nature is a result of deferring execution, which is likely why you're seeing it trigger on the next cycle.
watchEffect
Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.
watch
Compared to watchEffect, watch allows us to:
Perform the side effect lazily;
Be more specific about what state should trigger the watcher to re-run;
Access both the previous and current value of the watched state.
found a solution, watchEffect wasn't the way.
Looks like was an issue with multiple events of update in the same tick, worked for me handling the flush of the watch with { flush: 'post' } as option of the watch function.
Try to use the key: prop in components. I think it will solve the issue.

Unit test jest enzyme throws error on Formik 'resetForm'

I am trying to run unit test (enzyme) throws error on Formik 'resetForm'.
TypeError: Cannot read property 'resetForm' of undefined
FormikForm.js
_handleSubmitPress = (values, { resetForm }) => {
const { onSubmit } = this.props;
if (onSubmit) {
onSubmit({ ...values, resetForm });
}
};
UnitTest.js:
it('Should fire formik form submit', () => {
const UpdateButtonPressMock = jest.fn();
const component = Component({
onSubmit: UpdateButtonPressMock,
});
expect(component.find(Formik)).toHaveLength(1);
component.find(Formik)
.first()
.simulate('Submit');
expect(UpdateButtonPressMock).toHaveBeenCalled();
});
I couldn't find any solution for this error.
Could someone help me on the above? I would really appreciate any help.
According to official docs for simulate, the function signature accepts an optional mock event.
The code you are testing uses properties that are not included in the default SyntheticEvent object that ReactWrapper passes to your event handler by default, for instance event.resetForm.
One way to do this is by triggering Formik's onSubmit directly like so:
// UnitTest.js
.simulate("submit", { resetForm: whateverYourMockResetFormValueShouldBe })
component.find(Formik)
.first()
.prop('onSubmit')(valuesMock, { resetForm: UpdateButtonPressMock });
expect(UpdateButtonPressMock).toHaveBeenCalled();
I haven't tested this, but you should be able to pass the event along with simulate as well.
// UnitTest.js
component.find(Formik)
.first()
.simulate("submit", { resetForm: UpdateButtonPressMock })
expect(UpdateButtonPressMock).toHaveBeenCalled();

vue jest spyOn not working when calculating watcher method calls

I'm getting familiar with jest and vue and I wanted to see how to make sure a method fired when a prop changed. In this particular scenario it's trivial and this seemed straight forward. But it's not working.
Components Watcher
#Watch("id")
public async idChanged() {
this.calculateStatus();
}
beforeEach - this initilizes the wrapper for each test
beforeEach(async () => {
var httpClient = new PemHttpClient(vue);
var v3ReferenceDatumService = new V3ReferenceDatumService(httpClient, "");
var contractService = new V3ContractService(httpClient, "", v3ReferenceDatumService);
wrapper = mount(AmendmentIdDisplay, {
provide: {
v3ContractService: contractService,
},
propsData: {
id: "82.5.1"
}
});
await wrapper.vm.$nextTick();
})
Jest Test
let calculateFired = jest.spyOn(wrapper.vm, "calculateStatus");
wrapper.setProps({
...wrapper.props(),
id: "1"
})
await wrapper.vm.$nextTick();
expect(calculateFired).toBeCalled();
I would expect the spy to have incremented the call counter but it does not. It remains at zero. If I manually call wrapper.vm.calculateStatus(), the spy works correctly. So the setProps is either not firing the watcher at all, or some weird reference thing is happening which is causing the method that is called within the watcher, to not be the method I'm spying on. I'm not sure which.
I hope it is not too late. Yes there is a problem with jest.spyOn() and vue watchers. I have a trick that patch the problem for now (tested on sync function only) :
const insertSpyWatcher = (vueInstance: any, watcherExpression: string, spyInstance: jest.SpyInstance) => {
let oldWatcherIndex = -1;
let deep = false; // pass the deep option value from the original watcher to the spyInstance
// find the corresponding watcher
vueInstance._watchers.forEach((watcher: any, index: number) => {
if (watcher.expression === watcherExpression) {
oldWatcherIndex = index;
deep = watcher.deep;
}
});
// remove the existing watcher
if (oldWatcherIndex >= 0) {
vueInstance._watchers.splice(oldWatcherIndex, 1);
} else {
throw new Error(`No watchers found with name ${watcherExpression}`);
}
// replace it with our watcher
const unwatch = vueInstance.$watch(watcherExpression, spyInstance, { deep });
return unwatch;
};
Then in your test :
it('test the watcher call', () => {
let calculateFired = jest.spyOn(wrapper.vm, "calculateStatus");
insertSpyWatcher(wrapper.vm, "id", calculateFired) // Yes 'id' is the name of the watched property
wrapper.setProps({
...wrapper.props(),
id: "1"
})
await wrapper.vm.$nextTick();
expect(calculateFired).toBeCalled();
});
If the immmediate property is needed, you can always add it as argument of insertSpyWatcher. I did not find a way to get the immediateproperty of the original watcher.