Let's take the following scenario where WelcomePage (parent) uses LoginForm (children) with a custom event #submit:
// WelcomePage.vue
<LoginForm #submit="handleLogin">Login<Button>
Then, the component LoginForm has the following code:
// LoginForm
<form #submit.prevent="handleSubmit"> ... </form>
handleSubmit() {
// do some stuff...
// following Vue docs to create custom events.
this.$emit('submit', 'some_data')
// OR... we can also use $listeners.
this.$listeners.submit('some_data')
// do other stuf...
}
Is there any con of using this.$listeners.submit() instead of this.$emit('submit')?
One advantage of using this.$listeners is it can be used with await, which is a limitation of $emit that force us to use the done() callback approach. And it's useful when we wanna update some state (this.isLoading) after the custom event is finished.
Using $emit with callback:
// LoginForm.vue
async handleSubmit() {
this.isLoading = true
this.$emit('submit', 'some_data', () => {
this.isLoading = false
})
}
// WelcomePage.vue
async handleLogin(data, done) {
// await for stuff related to "data"...
done();
}
Using $listeners with await:
// LoginForm.vue
async handleSubmit() {
this.isLoading = true
await this.$listeners.submit('some_data') // no need to use done callback
this.isLoading = false
}
So, my question is: Is it okay to use this.$listeners? What's the purpose / advantage of this.$emit?
UPDATE:
Passing a prop isLoading from the parent to the children would be the first (obvious) option, instead of using $emit.
But that would require to set and update this.isLoading = true | false on handleSubmit every time we use the children component (and it's used a lot). So I'm looking for a solution where the parent doesn't need to worry about isLoading when #submit gets called.
The way I see it is that $emit helps you keep the FLUX architecture.
You can easily see the data flow, it helps you debug.
While using $listeners on the other hand is considered as a bad practice. It can be done but it can break one way data flow. The same is with a $root, you still have access to it, but that doesn't mean you should be using (modifing) it ;-)
Still, what to use as always depends on a context and your need. Just be careful, once broken one way data flow is very hard to debug.
Edit after comment: This is just my point of view on it.
When using props and $emit as recommended way for communication between components. You have a clear data flow. Plus Vue dev-tools helps you track every $emit, so you know exactly what happend step by step.
When using "collbackFunc" as a props and call this callback in a child component that will still work. And that is still a good way to go. The downside of it is that it is not a recommended usage.
Imagin you pass that "callbackFunc" to many childs components. When something goes wrong it is very hard to track from where it was fired.
The same applies to calling directly method on $listeners. Suddenly your state is changed, and you don't know exactly from where. Which and when component has fired it.
Using props instead of callbacks from $emit
const LoginForm = {
name: 'Login-Form',
props: {
loading: {
type: Boolean,
default: false,
}
},
template: `
<button :disabled="loading" #click="$emit('some_data')">
<template v-if="loading">Logging you in</template>
<template v-else>login</template>
</button>
`,
};
new Vue({
el: '#app',
components: {
LoginForm,
},
data() {
return {
loadingLoginForm: false,
};
},
methods: {
handleSubmit() {
// Set loading state before async
this.loadingLoginForm = true;
// Some async shish
setTimeout(() => this.loadingLoginForm = false, 1500)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<login-Form :loading="loadingLoginForm" #some_data="handleSubmit" />
</div>
Related
Given the code below, my child component alerts trigger before any of the code in the Parent mounted function.
As a result it appears the child has already finished initialization before the data is ready and therefor won't display the data until it is reloaded.
The data itself comes back fine from the API as the raw JSON displays inside the v-card in the layout.
My question is how can I make sure the data requested in the Parent is ready BEFORE the child component loads? Anything I have found focuses on static data passed in using props, but it seems this completely fails when the data must be fetched first.
Inside the mounted() of the Parent I have this code which is retrieves the data.
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray).then(() => {
console.log('DATA ...') // fires after the log in Notes component
this.checkAttendanceForPreviousTwoWeeks().then(()=>{
this.getCurrentParticipants().then((results) => {
this.currentP = results
this.notesArr = this.notes // see getter below
})
The getter that retrieves the data in the parent
get notes() {
const newNotes = eventsModule.getNotes
return newNotes
}
My component in the parent template:
<v-card light elevation="">
{{ notes }} // Raw JSON displays correctly here
// Passing the dynamic data to the component via prop
<Notes v-if="notes.length" :notesArr="notes"/>
</v-card>
The Child component:
...
// Pickingn up prop passed to child
#Prop({ type: Array, required: true })
notesArr!: object[]
constructor()
{
super();
alert(`Notes : ${this.notesArr}`) // nothing here
this.getNotes(this.notesArr)
}
async getNotes(eventNotes){
// THIS ALERT FIRES BEFORE PROMISES IN PARENT ARE COMPLETED
alert(`Notes.getNotes CALL.. ${eventNotes}`) // eventNotes = undefined
this.eventChanges = await eventNotes.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
...
I am relatively new to Vue so forgive me if I am overlooking something basic. I have been trying to fix it for a couple of days now and can't figure it out so any help is much appreciated!
If you are new to Vue, I really recommend reading the entire documentation of it and the tools you are using - vue-class-component (which is Vue plugin adding API for declaring Vue components as classes)
Caveats of Class Component - Always use lifecycle hooks instead of constructor
So instead of using constructor() you should move your code to created() lifecycle hook
This should be enough to fix your code in this case BUT only because the usage of the Notes component is guarded by v-if="notes.length" in the Parent - the component will get created only after notes is not empty array
This is not enough in many cases!
created() lifecycle hook (and data() function/hook) is executed only once for each component. The code inside is one time initialization. So when/if parent component changes the content of notesArr prop (sometimes in the future), the eventChanges will not get updated. Even if you know that parent will never update the prop, note that for performance reasons Vue tend to reuse existing component instances when possible when rendering lists with v-for or switching between components of the same type with v-if/v-else - instead of destroying existing and creating new components, Vue just updates the props. App suddenly looks broken for no reason...
This is a mistake many unexperienced users do. You can find many questions here on SO like "my component is not reactive" or "how to force my component re-render" with many answers suggesting using :key hack or using a watcher ....which sometimes work but is almost always much more complicated then the right solution
Right solution is to write your components (if you can - sometimes it is not possible) as pure components (article is for React but the principles still apply). Very important tool for achieving this in Vue are computed propeties
So instead of introducing eventChanges data property (which might or might not be reactive - this is not clear from your code), you should make it computed property which is using notesArr prop directly:
get eventChanges() {
return this.notesArr.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
Now whenever notesArr prop is changed by the parent, eventChanges is updated and the component will re-render
Notes:
You are overusing async. Your getNotes function does not execute any asynchronous code so just remove it.
also do not mix async and then - it is confusing
Either:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray)
await this.checkAttendanceForPreviousTwoWeeks()
const results = await this.getCurrentParticipants()
this.currentP = results
this.notesArr = this.notes
or:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
Promise.all(promisesArray)
.then(() => this.checkAttendanceForPreviousTwoWeeks())
.then(() => this.getCurrentParticipants())
.then((results) => {
this.currentP = results
this.notesArr = this.notes
})
Great learning resource
I have a component which needs to initialize data at the beginning, it does some transformations to the data (based on some values which are saved in localstorage to mark those data as selected).
Using Vue 2.5.
// component.vue
import Vue from 'vue'
export default Vue.extend({
data() {
fetchedData: [],
},
mounted() {
this.getStuff()
window.Bus.$on('localStorageRefreshed', this.getStuff)
},
computed: {
selectedData() {
return this.fetchedData.filter(data => data.selected)
}
},
methods: {
getStuff() {
const doTransformations = function (res, existing) {
// blabla
}
axios.get('/endpoint/for/stuff/').then(result => {
doTransformations(result, this.fetchedData) // not exactly what happens, but I think this is unneeded to solve my problem. mostly there to illustrate how this all fits together.
window.Bus.$emit('stuffFetched', this.selectedData)
})
},
}
})
So window.Bus is a Vue instance which is just posing as a global event handler. Within my getStuff-method, the event never gets emitted, and I have no idea why. I've console logged everywhere in order to figure out if the bus is just not initialized (it is, because I have tons of components which works perfectly with it). The event is just never emitted. I've tried wrapping the emitting in $nextTick but that doesn't work either (I also tried doing this directly in the mounted-lifecycle method, because the computed property updates in Vue devtools like it should and contains all the right stuff).
I'm really at a loss of what to do here, everything seems to work like it should, but the event is not even registered in Vue devtools.
The reason I need to fire this event is in order to do some price calculations for another component which exists outside of the scope of this child and it's parent. Just emitting in the child scope (this.$emit('dataChanged')) doesn't emit an event either using this approach.
Anyone have an idea of what my ******* brain is doing to me here?
Did you try to use async await?
That i'll probably make the timeout job, something like:
async mounted() {
await this.getStuff()
window.Bus.$on('localStorageRefreshed', this.getStuff)
}
and then do that on your getStuff too:
async getStuff() {
const doTransformations = function (res, existing) {
// blabla
}
await axios.get('/endpoint/for/stuff/').then(result => {
doTransformations(result, this.fetchedData) // not exactly what happens,
but I think this is unneeded to solve my problem. mostly there to
illustrate how this all fits together.
window.Bus.$emit('stuffFetched', this.selectedData)
})
}
I have App component, which loads JSON data from the server.
And after data is loaded, I update state of child component.
Now my function looks like this:
componentDidMount() {
setTimeout(()=> {
if (this.state.users.length !== this.props.users.length) {
this.setState({users: this.props.users});
this.setState({tasks: this.getTasksArray()});
}, 500);
}
I use setTimeout to wait if data is loaded and sent to child. But I'm sure, it is not the best way
May be, it's better to use redux instead of setTimeout.
Parent component loads data:
componentWillMount() {
var _url = "/api/getusers/";
fetch(_url)
.then((response) => response.json())
.then(users => {
this.setState({ users });
console.log("Loaded data:", users);
});
}
Parent sends props with:
<AllTasks users={this.state.users} />
So, my question is: what is the best way to watch changes in child component?
I mean in this particular situation.
Yes, this is not the correct way because api calls will be asynchronous and we don't know how much time it will take.
So instead of using setTimeout, use componentWillReceiveProps lifecycle method in child component, it will get called whenever you change props values (state of parent component).
Like this:
componentWillReceiveProps(newProps){
this.setState({
users: newProps.users,
tasks: this.getTasksArray()
})
}
One more thing, don't call setState multiple times within a function because setState will trigger re-rendering so first do all the calculations then do setState in the last and update all the values in one call.
As per DOC:
componentWillReceiveProps() is invoked before a mounted component
receives new props. If you need to update the state in response to
prop changes (for example, to reset it), you may compare this.props
and nextProps and perform state transitions using this.setState() in
this method.
Update:
You are calling a method from cWRP method and using the props values inside that method, this.props will have the updated values after this lifecycle method only. So you need to pass the newProps values as a parameter in this function and use that instead of this.props.
Like this:
componentWillReceiveProps(newProps){
this.setState({
users: newProps.users,
tasks: this.getTasksArray(newProps)
})
}
getTasksArray(newProps){
//here use newProps instead of this.props
}
Check this answer for more details: componentWillRecieveProps method is not working properly: ReactJS
I found the problem.
I have getTasksArray() function that depends of props and uses this.props.users.
So, when I update state like this:
this.setState({
users: newProps.users,
tasks: this.getTasksArray()
})
getTasksArray() function uses empty array.
but when I split it to 2 lines and add setTimeout(fn, 0) like this:
this.setState({users: newProps.users});
setTimeout(()=> { this.setState({ tasks: this.getTasksArray() }, 0)
getTasksArray() function uses array that is already updated.
setTimeout(fn, 0) makes getTasksArray() to run after all other (even if I set timeout to 0 ms).
Here is the screenshot of console.log's without setTimeout:
And here is the screenshot with setTimeout:
Problem
I'm setting a react ref using an inline function definition
render = () => {
return (
<div className="drawer" ref={drawer => this.drawerRef = drawer}>
then in componentDidMount the DOM reference is not set
componentDidMount = () => {
// this.drawerRef is not defined
My understanding is the ref callback should be run during mount, however adding console.log statements reveals componentDidMount is called before the ref callback function.
Other code samples I've looked at for example this discussion on github indicate the same assumption, componentDidMount should be called after any ref callbacks defined in render, it's even stated in the conversation
So componentDidMount is fired off after all the ref callbacks have
been executed?
Yes.
I'm using react 15.4.1
Something else I've tried
To verify the ref function was being called, I tried defining it on the class as such
setDrawerRef = (drawer) => {
this.drawerRef = drawer;
}
then in render
<div className="drawer" ref={this.setDrawerRef}>
Console logging in this case reveals the callback is indeed being called after componentDidMount
Short answer:
React guarantees that refs are set before componentDidMount or componentDidUpdate hooks. But only for children that actually got rendered.
componentDidMount() {
// can use any refs here
}
componentDidUpdate() {
// can use any refs here
}
render() {
// as long as those refs were rendered!
return <div ref={/* ... */} />;
}
Note this doesn’t mean “React always sets all refs before these hooks run”.
Let’s look at some examples where the refs don’t get set.
Refs don’t get set for elements that weren’t rendered
React will only call ref callbacks for elements that you actually returned from render.
This means that if your code looks like
render() {
if (this.state.isLoading) {
return <h1>Loading</h1>;
}
return <div ref={this._setRef} />;
}
and initially this.state.isLoading is true, you should not expect this._setRef to be called before componentDidMount.
This should make sense: if your first render returned <h1>Loading</h1>, there's no possible way for React to know that under some other condition it returns something else that needs a ref to be attached. There is also nothing to set the ref to: the <div> element was not created because the render() method said it shouldn’t be rendered.
So with this example, only componentDidMount will fire. However, when this.state.loading changes to false, you will see this._setRef attached first, and then componentDidUpdate will fire.
Watch out for other components
Note that if you pass children with refs down to other components there is a chance they’re doing something that prevents rendering (and causes the issue).
For example, this:
<MyPanel>
<div ref={this.setRef} />
</MyPanel>
wouldn't work if MyPanel did not include props.children in its output:
function MyPanel(props) {
// ignore props.children
return <h1>Oops, no refs for you today!</h1>;
}
Again, it’s not a bug: there would be nothing for React to set the ref to because the DOM element was not created.
Refs don’t get set before lifecycles if they’re passed to a nested ReactDOM.render()
Similar to the previous section, if you pass a child with a ref to another component, it’s possible that this component may do something that prevents attaching the ref in time.
For example, maybe it’s not returning the child from render(), and instead is calling ReactDOM.render() in a lifecycle hook. You can find an example of this here. In that example, we render:
<MyModal>
<div ref={this.setRef} />
</MyModal>
But MyModal performs a ReactDOM.render() call in its componentDidUpdate lifecycle method:
componentDidUpdate() {
ReactDOM.render(this.props.children, this.targetEl);
}
render() {
return null;
}
Since React 16, such top-level render calls during a lifecycle will be delayed until lifecycles have run for the whole tree. This would explain why you’re not seeing the refs attached in time.
The solution to this problem is to use
portals instead of nested ReactDOM.render calls:
render() {
return ReactDOM.createPortal(this.props.children, this.targetEl);
}
This way our <div> with a ref is actually included in the render output.
So if you encounter this issue, you need to verify there’s nothing between your component and the ref that might delay rendering children.
Don't use setState to store refs
Make sure you are not using setState to store the ref in ref callback, as it's asynchronous and before it's "finished", componentDidMount will be executed first.
Still an Issue?
If none of the tips above help, file an issue in React and we will take a look.
A different observation of the problem.
I've realised that the issue only occurred while in development mode.
After more investigation, I found that disabling react-hot-loader in my Webpack config prevents this problem.
I am using
"react-hot-loader": "3.1.3"
"webpack": "4.10.2",
And it's an electron app.
My partial Webpack development config
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
module.exports = merge(baseConfig, {
entry: [
// REMOVED THIS -> 'react-hot-loader/patch',
`webpack-hot-middleware/client?path=http://localhost:${port}/__webpack_hmr`,
'#babel/polyfill',
'./app/index'
],
...
})
It became suspicious when I saw that using inline function in render () was working, but using a bound method was crashing.
Works in any case
class MyComponent {
render () {
return (
<input ref={(el) => {this.inputField = el}}/>
)
}
}
Crash with react-hot-loader (ref is undefined in componentDidMount)
class MyComponent {
constructor (props) {
super(props)
this.inputRef = this.inputRef.bind(this)
}
inputRef (input) {
this.inputField = input
}
render () {
return (
<input ref={this.inputRef}/>
)
}
}
To be honest, hot reload has often been problematic to get "right". With dev tools updating fast, every project has a different config.
Maybe my particular config could be fixed. I'll let you know here if that's the case.
The issue can also arise when you try to use a ref of a unmounted component like using a ref in setinterval and do not clear set interval during component unmount.
componentDidMount(){
interval_holder = setInterval(() => {
this.myref = "something";//accessing ref of a component
}, 2000);
}
always clear interval like for example,
componentWillUnmount(){
clearInterval(interval_holder)
}
What is the most concise way to trigger route changes based on a change to a state store, using Fluxible and react router?
An example component might take some user input and call an Action on a click event (shortened for brevity)
class NameInput extends React.Component {
constructor (props) {
super(props);
this.state = props.state;
this.handleClick = this.handleClick.bind(this);
}
handleClick (event) {
this.context.executeAction(setName, {name:'Some User Value'});
}
render () {
return (
<div>
<input type="button" value="Set Name" onClick={this.handleClick} />
</div>
);
}
}
export default Home;
The handleClick method executes an Action which can update a Store with our new value.
But what if I also want this to trigger a navigation after the Store is updated? I could add the router context type and directly call the transition method after executing the Action:
this.context.executeAction(setName, {name:'Some User Value'});
this.context.router.transitionTo('some-route');
But this assumes that the setName Action is synchronous. Is this conceptually safe, on the assumption that the new route will re-render once the Action is completed and the Store is updated?
Alternatively, should the original Component listen for Store Changes and start the route transition based on some assessment of the store state?
Using the Fluxible, connectToStores implementation, I can listen for discreet changes to Store state:
NameInput = connectToStores(NameInput, [SomeStore], function (stores, props) {
return {
name: stores.SomeStore.getState().name
}
});
How a could a Store listener of this type be used to initiate a Route change?
I've noticed in my own application that for these kinds of flows it's usually safer to let actions do all the hard work. Annoying thing here is that the router isn't accessible from actions.
So, this is what I'd do:
Create a meta-action that does both: setNameAndNavigate. As payload you use something like this:
{
name: 'Value',
destination: {to: 'some-route', params: []},
router: this.context.router
}
Then, in your action, do the navigating when the setName completes.
It's not ideal, especially the passing of the Router to the action. There's probably some way to attach the Router to the action context, but that's not as simple as I had hoped. This is probably a good starting point though.
Extra reading:
Why do everything in actions? It's risky to execute actions in components in response to store changes. Since Fluxible 0.4 you can no longer let actions dispatch inside another dispatch. This happens a lot faster than you think, for example, executing an action in response to a change in a store, without delaying it with setImmediate or setTimeout, will kill your application since store changes happen synchronously during a dispatch.
Inside actions however, you can easily execute actions and dispatches, and wait for them to complete before executing the next one.
The end result of working this way is that most of your application logic has moved to actions, and your components turn into simple views that set-and-forget actions only in response to user interactions (clicks/scrolling/hover/..., as long as it's not in response to a store change).
The Best way is to create a new action as #ambroos suggested,
setNameAndNavigate. For navigation though, use the navigateAction
https://github.com/yahoo/fluxible/blob/master/packages/fluxible-router/lib/navigateAction.js, you would only have to give the url as argument.
Something like this,
import async from 'async';
import setName from 'some/path/setName';
export default function setNameAndNavigate(context, params, done) {
async.waterfall([
callback => {
setName(context, params, callback);
},
(callback) => {
navigate(context, {
url: '/someNewUrl',
}, callback);
}
], done);
}
let your actions be the main workers as much as possible.