Update React component when state changes, using a reference to itself - javascript

In a React component, I define how the component handles state change in a callback to setState(). This seems wrong / against flux convention. Where is the correct place to define how an element should behave given a new state?
I'm assuming it's in the render() method or the didMount() method.
In this situation, I need to call a JS method on the DOM element:
if (this.state.play) {
document.querySelector(".VideoPlayer-Video").play();
} else {
document.querySelect(".VideoPlayer-Video").pause();
}
I can't do this before the component has rendered. So how & where should this be done?
Example Details:
This is a very simple react component. It has a <video> element, and a button that pauses or plays it.
The state only has one attribute, "play", which is "true" if the video is playing or false if the video is paused.
When the "play" button is clicked, I flip the state, then do a DOM query to get the video element, and call a method on it.
This seems unconventional in React, since I am telling the component how to respond to a state change inside of a click handler. I would expect to define how the component responds to state change elsewhere, but I'm not sure where.
Where is the conventional place to tell the video element to play or not in response to state change? How can I reference the DOM element there?
Code:
var React = require('react');
module.exports = React.createClass({
getInitialState: function(){
return {
play: true
}
},
render: function(){
return <div className="VideoPlayer">
<video className="VideoPlayer-Video" src="/videos/my-video.mov"></video>
<button className="VideoPlayer-PlayButton" onClick={this.handlePlayButtonClick}>
{this.state.play ? "Pause" : "Play"}
</button>
</div>
},
handlePlayButtonClick: function(){
this.setState({
play: !this.state.play
}), function(){
var video = document.querySelector(".VideoPlayer-Video");
if (this.state.play) {
video.play();
} else {
video.pause();
}
}
}
});

Using componentDidUpdate seems appropriate.
1.Click
2. Change state -> trigger - rerender
3. Just after your component is mounted call the right function for video
componentDidUpdate: function() {
if (this.state.play) {
this.refs.MyVideo.play();
} else {
this.refs.MyVideo.pause();
}
}
If you need to control those function for the very first rendering use componentDidMount.
Simple and clean in my opinion.
Edit: I edited my code using ref, I think this is indeed the right way to go
https://facebook.github.io/react/docs/more-about-refs.html
Just place a ref in your video component:
<video ref="MyVideo"> </video>
Updated after mark's comment

Flux is good consideration for long term components, yes it's very big and requires some breaking changes, as François Richard noticed. But still worth it.
If you have a big component, then here's some tips:
You change state of that video component to playing, ONLY when video component receives $event, otherwise it doesn't make sense, as flux heavily uses nodejs eventEmitter, note video components must be explicitly separated, so when you expand your functionality it won't hurt your component.
Simple demo repo.

Methods performed on the DOM should be called within componentDidUpdate, as it is called once react has finished its updates to the DOM.

Related

Vue.JS - listen to click on component

I'm fairly new to Vue.JS and currently having an issue listening to a click event on a component.
JS:
Vue.component('photo-option', {
data: function () {
return {
count: 0
}
},
props: ['src'],
template: `
<img :src=src v-on:click="$emit('my-event')" />
`
});
HTML:
<photo-option :src=uri v-for='uri in aboutUsPhotos' v-on:my-event="foo" />
...where foo is a method on my main Vue instance.
The above is based on the Vue.JS docs for handling component events, and I can't see what I'm doing wrong. The alert doesn't fire, and there's no errors in the console.
Before I found those docs, I also tried simply adding v-on:click='...' to both the JS (i.e. the template) and the HTML, each with no success.
What am I doing wrong?
[EDIT]
This is happening because the code is picked up by a lightbox script and has its DOM position changed. So presumably the binding/event attachment is being lost.
Does Vue have any way of allowing for this, perhaps by 'reinitialising' itself on an element, or something?

Responding to a click on a Vue element programmatically

Essentially, I want my Vue instance to respond to a click on an uploaded thumbnail.
I'm using the FineUploader Vue package with the template layout per the docs (see end of the question). Upon uploading an image, a tree like this is outputted:
<Root>
<Gallery>
<Thumbnail>
</Gallery>
</Root>
Coming from a jQuery background I really have no idea about the 'correct' way to go about this given that the Thumbnail Template is defined by the package already, and so I'm not creating my own Thumbnail template. I know that I can access elements like this:
let thumb = this.$el.querySelector('.vue-fine-uploader-thumbnail');
And perhaps a listener
thumb.addEventListener('click', function() {
alert('I got clicked');
});
But dealing with the Vue instance being re-rendered etc. I'm not familiar with.
Vue Template:
<template>
<Gallery :uploader="uploader" />
</template>
<script>
import FineUploaderTraditional from 'fine-uploader-wrappers'
import Gallery from 'vue-fineuploader/gallery'
export default {
components: {
Gallery
},
data () {
const uploader = new FineUploaderTraditional({
options: {/*snip*/}
})
return {
uploader
}
}
}
</script>
In order to respond to click events you add the v-on:click (or it's short form: #click) to whatever tag you want.
If you have elements that are nested that respond to the click event you might experience that a click on a child triggers a parents click event. To prevent this you add #click.stop instead, so that it doesn't trigger the parents click.
So you would have something along the lines of:
<Gallery #click="myFunction" />

Better React component hierarchy for rendering progress bar each second?

I am writing a simple react app that is a music player. It has an album art frame, play/pause button, and a progress bar. The current component structure is which renders and .
The state representation:
{
tracks: //array of audio urls
current: new Audio(tracks[0].url),
artistName: //str
trackName: //str
albumArt: // image url
playing: true,
progress: 0,
duration: 0,
}
However, this presents a problem. We need the progress bar to re-render every second to show the track progress. But right now, inside, , I have an event listener:
currentTrack.addEventListener('timeupdate', () => {
this.setState({
progress: this.state.currentTrack.currentTime
});
});
But this will cause every component to re-render, even though only progress has changed in the state.
I thought about putting currentTrack inside the <Controls> component as its own state, but then the entire controls component will re-render every second. The only other abstraction I can think of is make the progress bar its own component and put currentTrack inside that, but that feels weird. What would be an optimal component structure?
Create a ProgressBar React Component and set the state.progress as property. So every time your player update the state.progress your ProgressBar Component will render.
I control that way my file upload progress and it work well.
Look into shouldComponentUpdate
https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate
do you have an example you can post where a bit more example is given?

Angular2 - Expression has changed after it was checked - Binding to div width with resize events

I have done some reading and investigation on this error, but not sure what the correct answer is for my situation. I understand that in dev mode, change detection runs twice, but I am reluctant to use enableProdMode() to mask the issue.
Here is a simple example where the number of cells in the table should increase as the width of the div expands. (Note that the width of the div is not a function of just the screen width, so #Media cannot easily be applied)
My HTML looks as follows (widget.template.html):
<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
<td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>
This on its own does nothing. I'm guessing this is because nothing is causing change detection to occur. However, when I change the first line to the following, and create an empty function to receive the call, it starts working, but occasionally I get the 'Expression has changed after it was checked error'
<div #widgetParentDiv class="Content">
gets replaced with
<div #widgetParentDiv (window:resize)=parentResize(10) class="Content">
My best guess is that with this modification, change detection is triggered and everything starts responding, however, when the width changes rapidly the exception is thrown because the previous iteration of change detection took longer to complete than changing the width of the div.
Is there a better approach to triggering the change detection?
Should I be capturing the resize event through a function to ensure
change detection occurs?
Is using #widthParentDiv to access the
width of the div acceptable?
Is there a better overall solution?
For more details on my project please see this similar question.
Thanks
To solve your issue, you simply need to get and store the size of the div in a component property after each resize event, and use that property in the template. This way, the value will stay constant when the 2nd round of change detection runs in dev mode.
I also recommend using #HostListener rather than adding (window:resize) to your template. We'll use #ViewChild to get a reference to the div. And we'll use lifecycle hook ngAfterViewInit() to set the initial value.
import {Component, ViewChild, HostListener} from '#angular/core';
#Component({
selector: 'my-app',
template: `<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="divWidth > 350">Value2</td>
<td *ngIf="divWidth > 700">Value3</td>
</tr>
</table>`,
})
export class AppComponent {
divWidth = 0;
#ViewChild('widgetParentDiv') parentDiv:ElementRef;
#HostListener('window:resize') onResize() {
// guard against resize before view is rendered
if(this.parentDiv) {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
ngAfterViewInit() {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
Too bad that doesn't work. We get
Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.
The error is complaining about our NgIf expressions -- the first time it runs, divWidth is 0, then ngAfterViewInit() runs and changes the value to something other than 0, then the 2nd round of change detection runs (in dev mode). Thankfully, there is an easy/known solution, and this is a one-time only issue, not a continuing issue like in the OP:
ngAfterViewInit() {
// wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
}
Note that this technique, of waiting one tick is documented here: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child
Often, in ngAfterViewInit() and ngAfterViewChecked() we'll need to employ the setTimeout() trick because these methods are called after the component's view is composed.
Here's a working plunker.
We can make this better. I think we should throttle the resize events such that Angular change detection only runs, say, every 100-250ms, rather then every time a resize event occurs. This should prevent the app from getting sluggish when the user is resizing the window, because right now, every resize event causes change detection to run (twice in dev mode). You can verify this by adding the following method to the previous plunker:
ngDoCheck() {
console.log('change detection');
}
Observables can easily throttle events, so instead of using #HostListener to bind to the resize event, we'll create an observable:
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );
This works, but... while experimenting with that, I discovered something very interesting... even though we throttle the resize event, Angular change detection still runs every time there is a resize event. I.e., the throttling does not affect how often change detection runs. (Tobias Bosch confirmed this:
https://github.com/angular/angular/issues/1773#issuecomment-102078250.)
I only want change detection to run if the event passes the throttle time. And I only need change detection to run on this component. The solution is to create the observable outside the Angular zone, then manually call change detection inside the subscription callback:
constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
// set initial value, but wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
this.ngzone.runOutsideAngular( () =>
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
this.cdref.detectChanges();
})
);
}
Here's a working plunker.
In the plunker I added a counter that I increment every change detection cycle using lifecycle hook ngDoCheck(). You can see that this method is not being called – the counter value does not change on resize events.
detectChanges() will run change detection on this component and its children. If you would rather run change detection from the root component (i.e., run a full change detection check) then use ApplicationRef.tick() instead (this is commented out in the plunker). Note that tick() will cause ngDoCheck() to be called.
This is a great question. I spent a lot of time trying out different solutions and I learned a lot. Thank you for posting this question.
Other way that i used to resolve this:
import { Component, ChangeDetectorRef } from '#angular/core';
#Component({
selector: 'your-seelctor',
template: 'your-template',
})
export class YourComponent{
constructor(public cdRef:ChangeDetectorRef) { }
ngAfterViewInit() {
this.cdRef.detectChanges();
}
}
Simply use
setTimeout(() => {
//Your expression to change if state
});
The best solution is to use setTimeout or delay on the services.
https://blog.angular-university.io/angular-debugging/
Mark Rajcok gave a great answer. The simpler version (without throttling) would be:
ngAfterViewInit(): void {
this.windowResizeSubscription = fromEvent(window, 'resize').subscribe(() => this.onResize())
this.onResize() // to initialize before any change
}
onResize() {
this.width = this.elementRef.nativeElement.getBoundingClientRect().width;
this.changeDetector.detectChanges();
}

How independent can a component be before it becomes an anti-pattern?

I understand that React apps should follow the following principle:
Data flows down, events bubble up
However, I find it quite cumbersome to send events like mouse-clicks from child components all the way up to the root and then send the new state all the way back down. I understand this method makes sense if multiple components are affected by the new state - but consider the following example:
var PSMenu = React.createClass({
getInitialState: function() {
return {
isOpen: false
}
},
handleMenuClick: function(){
console.log("hello worldd");
this.setState({isOpen: !this.state.isOpen});
},
render: function() {
return (
<div className={this.state.isOpen ? "ps-menu ps-menu-open" : "ps-menu"}>
<div className="ps-menu-button" onClick={this.handleMenuClick}>
</div>
</div>
)
}
});
Here, I have a menu class that has a button sticking out. The button is a toggle for opening and closing the menu. Is it okay that I handle the onClick of the ps-menu-button div within the PSMenu component? The root component has no idea this occurred but it doesn't matter since it shouldn't care.
If this example does indeed follow the React paradigm, consider the following. What if the div ps-menu-button was not a regular div but a React component called PSMenuButton nested inside PSMenu. Can I have just the PSMenu and PSMenuButton talk between each other without the parent knowing?
Thanks for any insight.

Categories

Resources