If I run this code:
const state = reactive({
title: '',
})
watchEffect(() => {
console.log(state.title)
})
watchEffect is triggered and the console is outputting an empty string:
""
If I want to assign a new value to state.title, watchEffect is triggered twice. Is this behaviour expected or am I doing something wrong?
According to the documentation, watchEffect
Runs a function immediately [emphasis added] while reactively tracking its dependencies
and re-runs it whenever the dependencies are changed.
So it is expected that it should run twice in this situation: once when it is first defined, and then again when the value changes.
As #MykWillis pointed out and is clearly stated in the docs, watchEffect runs immediately and on subsequent changes, exactly as watch with { immediate: true } would.
If you do not want the effect to run initially, don't use watchEffect, use watch instead:
const { createApp, watchEffect, reactive, toRefs, watch } = Vue;
createApp({
setup() {
const state = reactive({
title: 'test',
});
watchEffect(() => {
console.log('watchEffect', state.title)
});
watch(() => state.title, () => {
console.log('watch', state.title)
});
watch(() => state.title, () => {
console.log('watch.immediate', state.title)
}, { immediate: true })
return { ...toRefs(state) }
}
}).mount('#app')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="app">
<input v-model="title">
</div>
Related
I am facing the following problem with Realm and React Native.
Inside my component I want to listen to changes of the Realm, which does work. But inside the listener, my state is always undefined - also after setting the state inside the listener, the useEffect hook does not trigger. It looks like everything inside the listener doesn't have access to my state objects.
I need to access the states inside the listener in order to set the state correctly. How can I do this?
edit: The state seems to be always outdated. After hot reloading, the state is correct, but still lags behind 1 edit always.
const [premium, setPremium] = useState<boolean>(true);
const [settings, setSettings] = useState<any>({loggedIn: true, userName: 'XYZ'});
useEffect(() => {
console.log('updated settings'); // never gets called
}, [settings]);
useEffect(() => {
const tmp: any = settingsRealm.objects(MyRealm.schema.name)[0];
tmp.addListener(() => {
console.log(premium, settings); // both return undefined
if (premium) {
setSettings(tmp);
}
// for demonstration purposes
setSettings(tmp);
})
}, []);
For anyone having the same problem:
Adding a useRef referencing the state, and updating both(!) at the same time (state and ref) will solve the problem. Now all instances (inside the listener) of "premium" will have to be changed to "premiumRef.current".
https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559
const [premium, _setPremium] = useState<boolean>(true);
const [settings, setSettings] = useState<any>({loggedIn: true, userName: 'XYZ'});
const premiumRef = useRef<boolean>(premium);
const setPremium = (data) => {
premiumRef.current = data;
_setPremium(data);
}
useEffect(() => {
const tmp: any = settingsRealm.objects(MyRealm.schema.name)[0];
tmp.addListener(() => {
if (premiumRef.current) {
setSettings(tmp);
}
})
}, []);
[1]: https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559
I am trying run a function be it computed or watch or watcheffect after the template has mounted.
the watcheffect only executes once, computed of course runs before mounted.
I have tried flush: 'post' in watcheffect and flush in watch, I am rather stuck.
looking at the docs it should work as expected:
https://v3.vuejs.org/guide/composition-api-template-refs.html#watching-template-refs
Therefore, watchers that use template refs should be defined with the flush: 'post' option. This will run the effect after the DOM has been updated and ensure that the template ref stays in sync with the DOM and references the correct element.
app.vue
<template>
<div ref="target">
<h1>my title</h1>
<p>Tenetur libero aliquam at distinctio.</p>
<h1>hello</h1>
<p class="fuckyeah yolo">quia nam voluptatem illum ratione ipsum.</p>
<img src="img.jpg" />
" title="hello" alt />
<h2>hello</h2>
<ol>
<li>hello inital</li>
<li v-for="i in inc">hello</li>
</ol>
</div>
<div>
<button #click="inc++">inc</button>
</div>
<pre>
<code>
{{ toJson }}
</code>
</pre>
</template>
<script>
import { ref } from '#vue/reactivity'
import { templateRef } from '#vueuse/core'
import { useParser } from './markcomposable.js'
import { onMounted, computed, watchEffect } from '#vue/runtime-core';
export default {
setup() {
const inc = ref(0);
const target = ref(null);
const { toJson } = useParser(target);
return {
inc, target, toJson
}
}
}
</script>
//composable.js
import { parse, validate } from "fast-xml-parser"
import { ref, reactive, watchEffect, toRef, nextTick } from 'vue'
const useParser = (target) => {
const toJson = ref(null);
const jsonOptions = reactive({
//defaults
attributeNamePrefix: "",
ignoreAttributes: false,
textNodeName: "text",
arrayMode: true
})
const dumpJson = (target, options) =>
validate(target.outerHTML) ? parse(target.outerHTML, options) : isValid.value;
watchEffect(() => {
if (target.value) {
toJson.value = dumpJson(target.value, jsonOptions)
console.log(toJson.value)
}
}, {
flush: 'post',
})
return {
target,
toJson,
}
}
export { useParser }
If I understand correctly, you're trying to observe the outerHTML of the template ref, and you're expecting the template-ref watcher to be invoked whenever you insert nodes (via the button callback), but it's only ever invoked once.
This happens because the watcher effectively only watches the template ref and not its properties. The watcher would only be invoked when the template ref is initialized with the component/element reference. Template refs cannot be reassigned, so the watcher would not be invoked again. Moreover, the template ref's properties are not reactive, so the watcher would not be invoked if the target node's HTML changed.
Solution
Instead of the watcher, use a MutationObserver to observe the changes made to the target node, including the outerHTML.
Create a function that uses a MutationObserver to invoke a callback:
const observeMutations = (targetNode, callback) => {
const config = { attributes: true, childList: true, subtree: true }
const observer = new MutationObserver(callback)
observer.observe(targetNode, config)
return observer
}
In an onMounted hook, use that function to observe the template ref in target.value, passing a callback that sets toJson.value:
let observer = null
onMounted(() => {
toJson.value = dumpJson(target.value.outerHTML, jsonOptions)
observer = observeMutations(target.value, () => {
toJson.value = dumpJson(target.value.outerHTML, jsonOptions)
})
})
In an onUnmounted hook, disconnect the observer as cleanup:
onUnmounted(() => observer?.disconnect())
demo
Just to clarify:
You're trying to pass a template ref to your function but it always turns out as null when you execute the logic, right?
You could simply use a onMounted(() => {}) hook in your composable.js file or you could implement templateRef (which you already tried to include from the looks of it) and until(https://vueuse.org/shared/until/#usage).
So instead of const target = ref(null) you'll do const target = templateRef('target', null) and pass that to your composable.js.
There you'll watch until the ref is truthy.
So before your actual logic you'll do this:
await until(unrefElement(target)).toBeTruthy()
Afterwards the ref should provide an actual element (use unrefElement to get the element of the templateRef) and you can start applying your actual logic to it.
I have a event binding from the window object on scroll. It gets properly fired everytime I scroll. Until now everything works fine. But the setNavState function (this is my setState-function) does not update my state properties.
export default function TabBar() {
const [navState, setNavState] = React.useState(
{
showNavLogo: true,
lastScrollPos: 0
});
function handleScroll(e: any) {
const currScrollPos = e.path[1].scrollY;
const { lastScrollPos, showNavLogo } = navState;
console.log('currScrollPos: ', currScrollPos); // updates accordingly to the scroll pos
console.log('lastScrollPos: ', lastScrollPos); // last scroll keeps beeing 0
if (currScrollPos > lastScrollPos) {
setNavState({showNavLogo: false, lastScrollPos: currScrollPos});
} else {
setNavState({showNavLogo: true, lastScrollPos: currScrollPos});
}
}
useEffect(() => {
window.addEventListener('scroll', handleScroll.bind(this));
}, []);
...
}
So my question is how do I update my state properties with react hooks in this example accordingly?
it's because how closure works. See, on initial render you're declaring handleScroll that has access to initial navState and setNavState through closure. Then you're subscribing for scroll with this #1 version of handleScroll.
Next render your code creates version #2 of handleScroll that points onto up to date navState through closure. But you never use that version for handling scroll.
See, actually it's not your handler "did not update state" but rather it updated it with outdated value.
Option 1
Re-subscribing on each render
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
});
Option 2
Utilizing useCallback to re-create handler only when data is changed and re-subscribe only if callback has been recreated
const handleScroll = useCallback(() => { ... }, [navState]);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);
Looks slightly more efficient but more messy/less readable. So I'd prefer first option.
You may wonder why I include navState into dependencies but not setNavState. The reason is - setter(callback returned from useState) is guaranteed to be referentially same on each render.
[UPD] forgot about functional version of setter. It will definitely work fine while we don't want to refer data from another useState. So don't miss up-voting answer by giorgim
Just add dependency and cleanup for useEffect
function TabBar() {
const [navState, setNavState] = React.useState(
{
showNavLogo: true,
lastScrollPos: 0
});
function handleScroll(e) {
const currScrollPos = e.path[1].scrollY;
const { lastScrollPos, showNavLogo } = navState;
console.log('showNavLogo: ', showNavLogo);
console.log('lastScrollPos: ', lastScrollPos);
if (currScrollPos > lastScrollPos) {
setNavState({showNavLogo: false, lastScrollPos: currScrollPos});
} else {
setNavState({showNavLogo: true, lastScrollPos: currScrollPos});
}
}
React.useEffect(() => {
window.addEventListener('scroll', handleScroll.bind(this));
return () => {
window.removeEventListener('scroll', handleScroll.bind(this));
}
}, [navState]);
return (<h1>scroll example</h1>)
}
ReactDOM.render(<TabBar />, document.body)
h1 {
height: 1000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
I think your problem could be that you registered that listener once (e.g. like componendDidMount), now every time that listener function gets called due to scroll, you are referring to the same value of navState because of the closure.
Putting this in your listener function instead, should give you access to current state:
setNavState(ps => {
if (currScrollPos > ps.lastScrollPos) {
return { showNavLogo: false, lastScrollPos: currScrollPos };
} else {
return { showNavLogo: true, lastScrollPos: currScrollPos };
}
});
Code
let basicProps = {
inCart: false,
quantity: 0,
min: 1,
max: 10,
productData: mockProductData
};
const mockData = {
inCart: true,
quantity: 1,
min: 1
};
const mockOnAddToCart = jest.fn(async (data, props) => {
const newProps = await mockAxios(data).post()
const updatedProps = { ...props, newProps };
return updatedProps;
});
test('if clicking the "ADD" button, renders the "increment" button',() => {
let newProps;
async function onClickEvent () {
newProps = await mockOnAddToCart();
};
const { getByText, rerender } = render(
<CTAWrapper
{...basicProps}
onAddToCart={onClickEvent}
/>
);
// CLICK ADD BUTTON
fireEvent.click(getByText('Add'));
console.log({...newProps}); // log - {}
// RE-RENDER PRODUCT CARD COUNTER
// rerender(<CTAWrapper {...newProps}/>);
// TEST IF THE INCREMENT BUTTON IS THERE
// expect(getByText('+')).toBeInTheDocument();
});
__mocks__/axios.js
export default function(data) {
return {
get: jest.fn(() => Promise.resolve(data)),
post: jest.fn(() => Promise.resolve(data))
};
}
Problem
This code doesn't seem to work properly. AFAIK, it is because when I fire the onClick event, it runs onClickEvent asynchronously, which updates the value of newProps after it's being logged to the console. The value of newProps is needed to re-render the component for further assertion testing.
Objective
Want to mock the onclick function
The onclick function should make a mock axios call
The axios call should return a promise
The resolved value of that promise should be the data that is passed to the mock axios function
The component should re-render with new props
Stack
jest
#testing-library/react
jest-dom
Any help is much appreciated.
It would be useful to see the code for your components. Judging by your test the problem is that the logic to update CTAWrapper lives in its parent component. If you render that component after the asynchronous code is executed the DOM should update accordingly.
I'm new to vuejs and I'm trying to build a simple single file component for testing purpose.
This component simply displays a bool and a button that change the bool value.
It also listen for a "customEvent" that also changes the bool value
<template>
{{ mybool }}
<button v-on:click="test">test</button>
</template>
<script>
ipcRenderer.on('customEvent', () => {
console.log('event received');
this.mybool = !this.mybool;
});
export default {
data() {
return {
mybool: true,
};
},
methods: {
test: () => {
console.log(mybool);
mybool = !mybool;
},
},
};
</script>
The button works fine. when I click on it the value changes.
but when I receive my event, the 'event received' is displayed in the console but my bool doesn't change.
Is there a way to access the components data from my code?
Thanks and regards,
Eric
You can move ipcRenderer.on(...) into vuejs's lifecycle hooks like created.
See: https://v2.vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks
You are setting up the event listener outside of the component's options which you export by using
export default{ //... options }
Set up the event listener inside the vue options so the vue instance has control over it, in your case modifying dara property
As choasia suggested move the event listener to `created() life cycle hook:
<template>
{{ mybool }}
<button v-on:click="test">test</button>
</template>
<script>
export default {
data() {
return {
mybool: true,
};
},
methods: {
test: () => {
console.log(mybool);
mybool = !mybool;
},
},
created(){
ipcRenderer.on('customEvent', () => {
console.log('event received');
this.mybool = !this.mybool;
});
}
};
</script>
Now you component will starting listening for that particular even when the component is created