VueJS Delay on Update Props - javascript

So I have a simple component, let's say Transaction.vue, and inside it there is a child component named TransactionEditModal.vue.
In the Transaction.vue component, I call the method "openTransactionEditModal" by clicking a button.
The flow of the method is that I am editing my child property "this.chosenTransactionId" first before opening the modal using "showModal()".
// Transaction.vue
<button ref="editTransactionButton" v-on:click="openTransactionEditModal($event)" class="btn btn-primary">
<i style="pointer-events: none" class="far fa-edit"></i>
</button>
<TransactionEditModal ref="transactionEditModal" v-bind:transactionId="chosenTransactionId" />
<script>
data: function () {
return {
chosenTransactionId: "",
}
}
methods: {
openTransactionEditModal(event: MouseEvent) {
if (event.currentTarget instanceof HTMLButtonElement) {
this.chosenTransactionId = event.currentTarget.id;
console.log("Chosen Transaction Id is Updated", event.currentTarget.id);
}
var transactionEditModal: any = this.$refs.transactionEditModal; transactionEditModal.initializeExistingValues(this.transactions[6]);
transactionEditModal.showModal();
}
}
</script>
// TransactionEditModal.vue
<script>
props: {
transactionId: String,
},
methods: {
showModal() {
console.log("Child component props should have been updated, is it? ", this.transactionId);
var reportApproveModal: JQuery<HTMLDivElement> = $('#transactionEditModal');
reportApproveModal.modal('show');
},
}
</script>
But why is it that the props is only updated after second click?
Result:
// First Click
Chosen Transaction Id is Updated 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 Transaction.vue:365
Child component props should have been updated, is it? TransactionEditModal.vue:36
// Second Click
Chosen Transaction Id is Updated 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 Transaction.vue:365
Child component props should have been updated, is it? 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 TransactionEditModal.vue:36
UPDATE 1
After using watch functionality in the child component I get this result:
Chosen Transaction Id is Updated 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 Transaction.vue:365
Child component props should have been updated, is it? TransactionEditModal.vue:36
Means updated after watch 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 TransactionEditModal.vue:42
And I am inferring that it is updated after showModal() is called, which I actually found it to be weird, maybe updating props are asynchronous?
// TransactionEdit.vue
<script>
watch: {
transactionId(newVal, oldVal) {
console.log('Means updated after watch', newVal, oldVal);
},
},
</script>

The reason why the props is only updated after second click is because Vue uses an asynchronous update queue to update the component, which means that when you update the props in the openTransactionEditModal method, it doesn't take effect immediately. Instead, it is added to the update queue and processed after the current event loop. When you click the button the first time, the props are updated and added to the update queue, but the showModal method is called before the update is processed, so the child component still has the old props. When you click the button the second time, the update is already processed and the child component now has the updated props.
Using the watch functionality in the child component you can check the updated value of the props and check the exact time it was updated.
You can refactor the code to make sure the child component receives the updated props before the showModal method is called:
// Transaction.vue
<button v-on:click="openTransactionEditModal($event)" class="btn btn-primary">
<i class="far fa-edit"></i>
</button>
<TransactionEditModal v-bind:transactionId="chosenTransactionId" #modal-open="showModal" ref="transactionEditModal" />
<script>
data: function () {
return {
chosenTransactionId: "",
}
},
methods: {
openTransactionEditModal(event: MouseEvent) {
if (event.currentTarget instanceof HTMLButtonElement) {
this.chosenTransactionId = event.currentTarget.id;
}
this.$nextTick(() => {
this.$refs.transactionEditModal.initializeExistingValues(this.transactions[6]);
this.$refs.transactionEditModal.$emit('modal-open');
});
},
showModal() {
var reportApproveModal: JQuery<HTMLDivElement> = $('#transactionEditModal');
reportApproveModal.modal('show');
},
}
</script>
// TransactionEditModal.vue
<script>
props: {
transactionId: String,
},
</script>
I have added a custom event 'modal-open' which is emitted from the parent component after updating the chosenTransactionId and it triggers the showModal() method. This way, we ensure that the child component has already received the updated props before the modal is opened.
In addition, I have also wrapped the initializeExistingValues() and $emit('modal-open') function inside this.$nextTick(() =>{}) to ensure that the value has been updated before calling the function.
Also, I have removed the unnecessary ref attributes and the type casting of HTMLButtonElement.

Are you sure you have made the correct transactionId reactive?
In Transaction.vue you have defined a reactive variable called transactionId.
But then in the code of that component you refer to a variable called chosenTransactionId.
I suggest you rationalise it to just transactionId for both.
Or, if you really need two separate variables, then add chosenTransationId to your data function in Transaction.vue.
How about only allowing the modal to exist, once the transactionId is set?
You could change
<TransactionEditModal ref="transactionEditModal" v-bind:transactionId="chosenTransactionId" />
to
<TransactionEditModal
v-if="chosenTransactionId"
ref="transactionEditModal"
v-bind:transactionId="chosenTransactionId"
/>
This is the more conventional approach. You could even send the initialization information in a v-bind parameter to the child. That would avoid the parent component having to directly call a function within the Modal.
In general it is better to avoid the pattern of calling functions in a child. Instead, make the child exist (with v-if) only when all the necessary information is available and ready to be passed to it.

Related

PointerEvent object being returned instead of child data on emit

I am working on creating a vue component library. I have build a button component that has data of it's width and left position. I'm trying to emit that data to the parent (a tabs component) when it's clicked. I have troubleshooted quite a bit, and have narrowed down most of the problem. My child component (button) is emitting the correct thing, but it looks like the parent component (tabs) is receiving the value of the click/pointerevent object instead of the data passed on the emit. I'm certain this is some issue in my parent click handle method, but can't pinpoint what exactly. I've included code snippets for the components and their click handler methods.
This is pared down, but essentially, I want to emit the width (and eventually left position) of the child button to the parent tab upon clicking the child/button. I want to assign that emitted width/left position to the slider to move some reactive underlining whenever a button is clicked in the tabs. I built in a console log statement on the click event that returns the emitted value from the child, and then returns the received value from the parent. Right now, the child is emitting the correct value when button is clicked, but parent is receiving and trying to assign a PointerEvent object. Thanks for any feedback!
Child (button) template and relevant script:
<template>
<div class="button #click="click" ref="button">
<slot />
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'Button',
emits: [
'click'
],
data () {
return {
width: '',
left: ''
}
},
setup() {
const button = ref(null)
return {
button
}
},
mounted () {
this.$nextTick(() => {
this.left = Math.ceil(this.button.getBoundingClientRect().left)
this.width = Math.ceil(this.button.getBoundingClientRect().width)
})
},
methods: {
click () {
this.$emit('click', this.width)
console.log(`${this.width} has been emitted to the tabs component`)
}
}
}
</script>
Parent (tab) template and relevant script:
<template>
<div class="tabs" #click="updateSliderWidth">
slot
</div>
<div class="slider" :style="sliderWidth">
</template>
<script>
import Button from './Button.vue'
export default {
name: 'Tabs',
components: {
Button
},
methods: {
updateSliderWidth (value) {
this.sliderWidth = value
console.log(`${value} has been received and assigned by parent`)
}
},
data () {
return {
sliderWidth: ''
}
}
}
</script>
I can't see any problems with your code, except that you don't use the Button component in the parent component. Instead you are using a div. This would explain, why you're getting a PointerEvent. This Event is passed as first parameter to the event, if you don't pass anything explicitly.
Here a demo: https://stackblitz.com/edit/vue-opruyd?file=src%2FApp.vue

In react, if I set shouldComponentUpdate to false, will any state I send to children be updated?

I have the following scenario:
1) There is a parent component "ModuleListContainer".
2) A module (in the module list, also a child component, but not interesting in this context) gets selected when hovering over it a module item in the list.
3) When hovering over a module, a menu should be shown in the corner of the module.
4) The whole parent component should NOT be updated when a module is selected, since it can be quite a long list of modules, that is why I set shouldComponentUpdate = false when updating which module should be selected.
5) The menu is loaded when the parent component loads, and only its position is updated when hovering over a module.
This is the parent component (simplified)...
class ModuleListContainer extends Component {
constructor(props) {
super(props);
this.state = {
selectingModule: false,
currentlySelectedModule: nextProps.currentModule
}
}
shouldComponentUpdate(nextProps, nextState) {
if (nextState.selectingModule === true) {
this.setState({
selectingModule: false,
currentlySelectedModule: null
})
return false;
}
return true;
}
mouseEnterModule = (e, moduleItem) => {
const menu = document.getElementById('StickyMenu');
const menuPosition = calculateModuleMenuPosition(e.currentTarget);
if (moduleItem.ModuleId !== this.props.currentModuleId) {
this.props.actions.selectModule(moduleItem);
this.setState({
selectingModule: true
});
}
menu.style.top = menuPosition.topPos + 'px';
menu.style.left = menuPosition.leftPos + 'px';
}
render() {
return (
<div>
<section id="module-listing">
{/* ... list of mapped modules with mouseEnterModule event */}
</section>
<ModuleMenu {... this.props} currentlySelectedModule={this.state.currentlySelectedModule} />
</div>
);
}
}
This is the menu component (simplified)...
class ModuleMenu extends Component {
constructor(props) {
super(props);
this.state = {
currentModule: this.props.currentlySelectedModule
};
}
clickMenuButton = () => {
console.log('CURRENT MODULE', this.state.currentModule);
}
render() {
return (
<div id="StickyMenu">
<button type="button" onClick={this.clickMenuButton}>
<span className="fa fa-pencil"></span>
</button>
</div>
);
}
}
When, in my menu component, I try to console.log the current module from the state, I keep getting null.
My question is if this is because...
I have set the shouldComponentUpdate to false and the menu's state does not get updated?
Or could it be because I do not re-render the whole component?
Or is it because I load the menu together with the parent component
and it does not get re-rendered when a module is selected?
Or is it possibly a combination of some of the above?
The react docs (https://reactjs.org/docs/react-component.html) says:
Returning false does not prevent child components from re-rendering
when their state changes.
Therefore, I am hoping that it is none of the above since I really don't want to have to re-render the entire component when selecting a module.
Your children state doesn't change in this case, you're only changing the state of the parent. What you should probably do is split the render method of your component into two components:
render() {
return (
<div>
<NoUpdateComponent someProps={this.props.someProps}/>
<ModuleMenu {... this.props} currentlySelectedModule={this.state.currentlySelectedModule} />
</div>
);
}
And then in your first costly component, use the shouldComponentUpdate method to prevent it from re rendering
I think that in your code there are other problems you need to solve before looking for a practical solution, starting from the use you make of shouldComponentUpdate().
Official doc says that:
Use shouldComponentUpdate() to let React know if a component’s output is not affected by the current change in state or props. The default behavior is to re-render on every state change, and in the vast majority of cases you should rely on the default behavior.
shouldComponentUpdate() is invoked before rendering when new props or state are being received. Defaults to true. This method is not called for the initial render or when forceUpdate() is used.
If you perform a setState() call inside the shouldComponentUpdate() function it might work but essentially you are telling React to start a new render cycle before knowing if in this execution it should render or not.
Also keep in mind that setState() is not guaranteed to be executed immediately:
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
Moreover (not very clear from the code, so I guess) you are separating Component and DOM object for ModuleMenu: its representation must be guided by state or props, here instead you are using HTMLElement.style.x syntax to set its representation properties.
I'd restructure ModuleListContainer to store in its state an object that represents ModuleMenu properties, that will be passed to ModuleMenu component as props, something like this:
moduleMenu {
currentModuleId: ... ,
top: ... ,
left: ...
}
And set the state in mouseEnterModule handler:
mouseEnterModule = (e, moduleItem) => {
const menuPosition = calculateModuleMenuPosition(e.currentTarget);
if (moduleItem.ModuleId !== this.props.currentModuleId) {
this.props.actions.selectModule(moduleItem);
this.setState({
moduleMenu: {
currentModuleId: moduleItem.ModuleId,
left: menuPosition.leftPos + 'px',
top: menuPosition.topPos + 'px'
}
});
}
}
Then ModuleMenu can get the new position like this:
<div id="StickyMenu">
style={{
left: this.props.left,
top: this.props.top
}}
>
...
</div>
Of course you can still use shouldComponentUpdate() to determine which modules in the list should be updated but this time returning just a boolean after a comparison of (once again, I guess) ids of items; avoiding too many calls to setState() method.
Hope this may help you!

If render functions should be pure, how the view can get changed based on state?

I am new to react and while going through the tutorials I found this ,
"The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it's invoked, and it does not directly interact with the browser." - https://facebook.github.io/react/docs/react-component.html#reference
I am little confused with this. If render function should return same result each time how can I modify the display based on states ?
For example I have text with edit button. When I click on edit, text-box should appear with content of text and edit button changes to save.
"The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it's invoked, and it does not directly interact with the browser." - https://facebook.github.io/react/docs/react-component.html#reference
is absolutely a correct statement in react. Changing a state cannot be done can't inside a render method. You can access the state inside the render but change. To change the state we use setState function in callback method which set the new state and ready to change anywhere in your component.
Note: setState expects the state to be initialized first. getInitialState is the function for the same and given in example below
eg.
var firstName = 'Anand';
var testExample = React.createClass({
getDefaultProps: function () {
return {
name: 'React',
message: 'This is the default message!'
};
},
getInitialState: function () {
return {
name: this.props.name
};
},
handleNewName: function (name) {
this.setState({
name: name
});
},
render: function () {
var name = this.state.name;
var message = this.props.message;
return (
<div>
<h1>{message}</h1>
<h2>{name}</h2>
</div>
);
}
});
Inside the handleNewName function we have changed the state which then used inside render. Hope this helps

vuejs: v-if directive for event?

Can I react to an event in a vue template? Say a child component dispatches an event $dispatch('userAdded'), could I do something like this in the parent component:
<div class="alert alert-info" v-if="userAdded">
User was created!
</div>
or, if not, can I access variables of the child component?
<div class="alert alert-info" v-if="$refs.addView.form.successful">
User was created!
</div>
I tried both without success.
Also, while I'm here, is there an expressive way to hide elements after a certain amount of time? Something like (to hide after 2s):
<div class="alert alert-info" v-if="$refs.addView.form.successful" hide-after="2000">
User was created!
</div>
Thanks!
edit: wrote my own hide-after directive:
Vue.directive('hide-after', {
update: function(value) {
setTimeout(() => this.el.remove(), value);
}
});
<div class="alert alert-info" v-hide-after="2000">
This will be shown for 2 seconds
</div>
Yes you can but you need to take this approach.
Create a child that dispatches an event
In the parent component create an event listener for the event and also a data property that the event listener will set locally on the component instance
In the parent bind your v-if to the local data component
The code would look something like
parent
HTML
<div v-if="showAlert"></div>
Js
events: {
'alert.show': function () {
this.showAlert = true
},
'alert.hide': function () {
this.showAlert = false
}
},
data () {
return {
showAlert: false
}
}
Child
Js
methods: {
showAlert (show) {
show ? this.$dispatch('alert.show') : this.$dispatch('alert.hide')
}
}
The reason you should avoid using the $child and $parent is that it makes that component always depend on the fact that the parent will have the alert property and makes the child component lest modular
Since dispatch goes up until it hits a listener you can have several nested components in between the parent and child dispatching the alert control
UPDATE
Alternately, since you do not like the LOE of using events you can create a 2-way property on the child that either the parent or child can update
Example
Parent
HTML
<div v-if="showAlert"></div>
<child-component :show-alert.sync="showAlert"></child-component>
JS
data () {
return {
showAlert: false
}
}
Child
js
props: {
showAlert: {
type: Boolean,
twoWay: true
}
},
methods: {
showAlertInParent (show) {
this.$set('showAlert', show)
}
}
The whole idea of events is that you can react to them. But you want the reaction to pass by the model. You really don't want unrelated bits of markup listening and reacting 'independently'. $dispatch is deprecated. To do this now, do the following...
In the child component, emit an event as follows
this.$emit('didIt' {wasItAwful:'yep',wereYouScared:'absolutely'});
In the parent, you register the event listener with v-on, as an attribute of the child's tag...
<adventure-seeking-child v-on:did-it='myChildDidIt' />
Then, in the parent's methods, define your handler.
methods : { myChildDidIt : function(payload){ ... } }
Docs are here.

Refreshing children state from parent React

I have a table with some data and each element in the table is a React class component. It looks like this:
All i want is to have one checkbox for "check all" feature (top left checkbox). The thing is I don't know how to solve that because of props and state.
I have code like that in single element component:
getInitialState: function() {
return { component: this.props.data };
},
render: function() {
var data = this.state.component;
data = data.set('checked', this.props.data.get('checked'));
...
}
And I know I shouldn't get checked param from props but it is just temporary.
What I have problem with is: When I update checked param in parent it doesn't update state, because getInitialState isn't called after refresh (yep, i know it should be like that).
My question is: can I somehow update state of child component? Or it is better way to achieve that.
With functional components:
An easy way to refresh the children internal state when props provided by parent change is through useEffect():
In the children:
const [data, setData] = useState(props.data);
useEffect( () => {
setData(props.data);
}, [props.data]);
In this way everytime the props.data change the useEffect will be triggered and force to set a new status for some data and therefore the component will "refresh".
My approach is that you should have structure something like this in parent's render:
<ParentView>
{ this.props.rows.map(function(row) {
<ChildRow props={row.props} />
}).bind(this)
}
</ParentView>
And then on row.props you have the information whether current row item is checked or not. When parent checkbox is toggled, you populate all the row.props with the status.
On child, you will receive those with componentWillReceiveProps and you do the magic (e.g. set the correct state) when checkbox is toggled:
componentWillReceiveProps: function(props) {
this.setState({isChecked: props.isChecked});
}
(Info from the React's docs: Calling this.setState() within this function will not trigger an additional render.)
Child element's render would be something like:
<div>
<input type='checkbox' checked={this.state.isChecked} />
<label>{this.props.label}</label>
</div>
You can solve this by storing the checked state of all child elements in the parent only. The children set their checked status based on props only (they don't use state for this) and call a callback supplied by the parent to change this.
E.g., in the child:
render: function() {
//... not showing other components...
<input type="checkbox"
value={this.props.value}
checked={this.props.checked}
onClick={this.props.onClick}>
}
The parent supplies the onClick, which changes the checked status of the child in its state and passes this back to the child when it re-renders.
In the parent:
getInitialState: function() {
return {
allChecked: false,
childrenChecked: new Array(NUM_CHILDREN) // initialise this somewhere (from props?)
}
},
render: function() {
return <div>
<input type="checkbox" checked={this.state.allChecked}>
{children.map(function(child, i) {
return <Child checked={this.state.childrenChecked[i]}
onClick={function(index) {
return function() {
// update this.state.allChecked and this.state.childrenChecked[index]
}.bind(this)
}.bind(this)(i)}
/>
}).bind(this)}
</div>;
}
-- not checked for typos etc.
Please see the react documentation on Lifting State Up.
In your child component, you need to use the props. To update the prop, you need to provide an update function from the parent.

Categories

Resources