ReactJS - Change Button Text For a Specific Button on an Event - javascript

React newbie here,
EDIT: Sorry, I forgot to add the eventListener code. Then render will just output {this.state.songs}. This is separate from the code I posted above and from a modified version of the code I posted above.
import React, { Component } from 'react';
class AudioList extends Component {
constructor (props) {
super(props);
this.state = {
songs: ''
}
this.audios = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios.push(new Audio(this.props.songs[i].song_url));
}
}
componentWillMount() {
const songs = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios[i].addEventListener('play', () => console.log('Play'));
songs.push(
<div className='song-preview'>
<button className='preview' onClick={() => this.toggle(this.audios[i])}>Preview {i}</button>
</div>
)
}
this.setState({
songs: songs
})
}
componentWillUnmount() {
for (let i = 0; i < this.props.songs.length; i++) {
this.audios[i].pause();
}
}
getCurrentAudio () {
return this.audios.find(audio => false === audio.paused);
}
toggle (nextAudio) {
const currentAudio = this.getCurrentAudio();
if (currentAudio && currentAudio !== nextAudio) {
currentAudio.pause();
nextAudio.play();
}
nextAudio.paused ? nextAudio.play() : nextAudio.pause();
}
render () {
if (this.state.songs) {
return (
<div className='song-list'>
{ this.state.songs }
</div>
)
} else {
return (
<div className='song-list'></div>
)
}
}
}
export default AudioList;
I am using this code from a previous solution that I found on Stackoverflow (https://stackoverflow.com/a/50595639). I was able to implement this solution to solve my own challenge of needing to have multiple audio sources with one audio player and multiple buttons. However, I am now faced with a new challenge - I want a specific button's text to change when an event is fired up.
I was thinking about implementing state that contains the text for the button that changes when an event fires up, but that wont work because every button will be reading off of the same variable in state and re-rendering, I only want one button to re-render.
I was also thinking about having multiple variables in state that control each specific button, but given that my modified implementation has an dynamic number of audio sources, I don't know the number of state variables I will need.
Does anyone have any suggestions?

Thomas from my secondary thread was kind enough to re-factor my code and essentially re-write parts of it to fix this issue.
You can read his solution here: https://stackoverflow.com/a/62769376/8888293

Related

How to return an HTML <div> tag from a javascript function in React?

I am working on a React application where I am trying to render text on the screen when a button is clicked. I have defined a function onButtonClick which gets triggered whenever the button is clicked. However, the HTML that I am returning from the function is not rendered on the screen. I am in the learning stages of React so please excuse me if the question seems silly.
class App extends Component {
constructor() {
super();
this.state = {
blockno:0
}
}
OnButtonClick = () => {
this.setState({blockno: this.state.blockno + 1})
return(
<div>
<h3>Some text</h3>
</div>
);
}
render() {
return(
<div>
<Button onButtonClick={this.OnButtonClick}/>
</div>
);
}
}
The value is being returned, but the framework/browser/etc. has no reason to do anything with that value.
Try thinking about this a different way, a "more React way". You don't want to return the value to be rendered, you want to update state. Something like this:
constructor() {
super();
this.state = {
blockno:0,
showDiv: false // <-- note the new property in state
}
}
OnButtonClick = () => {
this.setState({blockno: this.state.blockno + 1, showDiv: true})
}
Now you're not returning anything, but rather updating the state of the component. Then in your render method you conditionally render the UI based on the current state:
render() {
return(
<div>
<Button onButtonClick={this.OnButtonClick}/>
{
this.state.showDiv
?
<div>
<h3>Some text</h3>
</div>
: ''
}
</div>
);
}
The click handler doesn't modify the page, it just modifies the state of the component you're writing. The render method is responsible for rendering the UI based on that state. Any time state changes, render will be called again to re-render the output.
(Note: It's not 100% clear if this is exactly the functionality you're looking for in the UI, since it's not really clear what you're trying to build. But the point here is to illustrate how to update state and render output in React. Your logic can be tweaked as needed from there.)
You have to make a render based on your state. Please check the tutorial at the react docs to learn more about how React works. It's really good
Here is a version of your code that works. Hope it helps
class App extends Component {
constructor() {
super();
this.state = {
blockno: 0
};
}
OnButtonClick = () => {
//updates the states
this.setState({ blockno: this.state.blockno + 1 });
};
//remember: every time there is an update to the state the render functions re-runs
render() {
//variable holding the blocks in an array
let blocks = []
//if blockno is greater than 0, it checks everytime that there is a state change
if (this.state.blockno > 0) {
//for every block added
for (let index = 0; index < this.state.blockno; index++) {
//We`re going to add to the array of blocks a new div with the block number
blocks.push(
<div>
<h3>My block number is {index}</h3>
</div>
);
}
}
return (
<div>
<div>
{/**button that updates the state on every click */}
<button onClick={this.OnButtonClick}>
Click me to add a new div!
</button>
</div>
{/**This render the blocks variable that holds the divs */}
{blocks}
</div>
);
}
}
What I see is that you are trying to build a counter. The value that you're returning from the click handler function can't be rendered, instead you need to manage it in the render function as follow:
class App extends Component {
constructor() {
super();
this.state = {
blockno: 0
}
}
OnButtonClick = () => {
this.setState(prevState => ({ blockno: prevState.blockno + 1 }));
}
render() {
return(
<div>
{this.state.blockno > 0 && <div>some text {this.state.blockno}</div>}
<Button onButtonClick={this.OnButtonClick} />
</div>
);
}
}
Also note that the setState method is asynchronous, please read the documentation https://reactjs.org/docs/react-component.html#setstate

React-js won't recognize AmbientLightSensor

I'm trying to use the sensor's API with react and I can't seen to be able to make it work.
It gives me an error saying that AmbientLightSensor (in my case this sensor) is undefined
If I run the script outside react ( more exactly with an extension from VSCode "live server" ) it works fine ( just a html with some JS code in it ).
That's cool and all but in this case at least I want to run this script inside react and it just doesn't let me.
So far I've tried:
running this code in react as a class method called by componentDidMount and that (Simply i've put my JS code in there ^^ )
running this code with the tag hoping that maybe react isn't really using the JS that I know and that maybe running it inside html will change this ... No it didn't do the trick
So at this point I'm unsure what to even check to make this work
Here is my code, the js code I'm trying to run is inside the Did mount component
class App extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const details = document.getElementById("details");
// Feature detection
if (window.AmbientLightSensor) {
try {
const sensor = new AmbientLightSensor();
// Detect changes in the light
sensor.onreading = () => {
details.innerHTML = sensor.illuminance;
// Read the light levels in lux
// < 50 is dark room
if (sensor.illuminance < 50) {
document.body.className = "darkLight";
} else {
document.body.className = "brightLight";
}
};
// Has an error occured?
sensor.onerror = event =>
(document.getElementById("details").innerHTML =
event.error.message);
sensor.start();
} catch (err) {
details.innerHTML = err.message;
}
} else {
details.innerHTML =
"It looks like your browser doesnt support this feature";
}
}
render() {
return (
<div className="app">
<h1>Ambient Light Sensor</h1>
<p>Current Light Levels</p>
<div id="details"></div>
</div>
);
}
}
And also here is the working html
<head>
<meta charset="UTF-8" />
<title>Ambient Light Sensor</title>
</head>
<body class="brightLight">
<h1>Ambient Light Sensor</h1>
<p>Current Light Levels</p>
<div id="details"></div>
</body>
<script>
const details = document.getElementById("details");
// Feature detection
if (window.AmbientLightSensor) {
try {
const sensor = new AmbientLightSensor();
// Detect changes in the light
sensor.onreading = () => {
details.innerHTML = sensor.illuminance;
// Read the light levels in lux
// < 50 is dark room
if (sensor.illuminance < 50) {
document.body.className = "darkLight";
} else {
document.body.className = "brightLight";
}
};
// Has an error occured?
sensor.onerror = event =>
(document.getElementById("details").innerHTML =
event.error.message);
sensor.start();
} catch (err) {
details.innerHTML = err.message;
}
} else {
details.innerHTML =
"It looks like your browser doesnt support this feature";
}
</script>
</html>```
PS* for this to work you need to run this on a https server
That's not how React works...
I'd suggest looking into dangerouslySetInnerHTML link here: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
And createRef link here: https://reactjs.org/docs/refs-and-the-dom.html#creating-refs
Here is a simple example utilizing both to give you a better idea:
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.detailsRef = React.createRef();
}
createMarkup() {
return { __html: 'whatever you want...' };
}
componentDidMount() {
console.log(this.detailsRef.current.innerHTML);
}
render() {
return (
<div className="app">
<h1>Ambient Light Sensor</h1>
<p>Current Light Levels</p>
<div
ref={this.detailsRef}
dangerouslySetInnerHTML={this.createMarkup()}
/>
</div>
);
}
}
export default App;
Play around with it and read the links from the official docs to adapt to your specific use case...
Never used the AmbientLightSensorAPI before but: try something like this:
class App extends Component {
constructor(props) {
super(props);
this.state = {
details: ''
};
}
componentDidMount() {
if (window.AmbientLightSensor) {
try {
const sensor = new AmbientLightSensor();
sensor.onreading = () => {
this.setState({ details: sensor.illuminance });
if (sensor.illuminance < 50) {
document.body.className = 'darkLight';
} else {
document.body.className = 'brightLight';
}
};
sensor.onerror = event =>
this.setState({ details: event.error.message });
sensor.start();
} catch (err) {
this.setState({ details: err.message });
}
} else {
this.setState({
details:
'It looks like your browser doesnt support this feature'
});
}
}
render() {
return (
<div className="app">
<h1>Ambient Light Sensor</h1>
<p>Current Light Levels</p>
<div id="details">{this.state.details}</div>
</div>
);
}
}
Ok so I got it :D
The only working way I found to send a JavaScript file and run it is by declaring it in the index.html file inside the public folder ( if u made the app with create react app )
Normally react servers you this file and it adds it's components over it.
The reason why running the script inside a react class won't do ( as far as I managed to understand ) is that react isn't actually running JavaScript there but a language based on top of JavaScript ES6.
While this means that most functionalities you may be accustomed with are working in it there are exceptions too, mostly to new functionalities ( as the sensor's API, its pretty new ).
This may not be a problem in the future, but for the time being I guess this is one way to do it.
Edit * And #SakoBu's answer turned out to be the safe way of doing
this
( #Change my mind meme :3 )

React state is out of sync between render method and what is actually displayed on page

I need to access DOM elements outside of my React app, which may load slower than my app. Then I need to update my state to render a few different things. To do that I am polling for the DOM elements with a recursive function that gets kicked off from componentDidMount(). I'm seeing a weird issue where once the element is found and I've updated the state, things get out of sync. In the render function, my console.log() shows the updated state value, in React Developer Tools I see the updated state value, but on the actual rendered page I see still see the old state value.
Code:
// initially doesn't exist. Added to the DOM after 3 seconds
let slowElement = document.querySelector('.external-dom-element')
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
showFoundSlowElementMessage: false,
slowElementCheckMaxAttempts: 5,
slowElementCheckCount: 0,
}
this.checkForSlowElement = this.checkForSlowElement.bind(this)
}
componentDidMount () {
this.checkForSlowElement()
}
checkForSlowElement () {
slowElement = document.querySelector('.external-dom-element')
if (slowElement !== null) {
console.log('found') // element found, show message
this.setState({
showFoundSlowElementMessage: true
})
} else {
console.log('not found') // element not found, increment count and check again after delay
this.setState({
slowElementCheckCount: this.state.slowElementCheckCount + 1
}, () => {
if (this.state.slowElementCheckCount < this.state.slowElementCheckMaxAttempts) {
window.setTimeout(this.checkForSlowElement, 1000)
}
})
}
}
render() {
const foundSlowElement = this.state.showFoundSlowElementMessage
? <p>Found slow element</p>
: <p>No sign of slow element, checked {this.state.slowElementCheckCount} times</p>
// null until it is added to the page
console.log(foundSlowElement)
return (
<div>
<h1>Hello</h1>
{foundSlowElement}
</div>
);
}
}
}
ReactDOM.render(<App />, document.getElementById('react-target'));
// Simulate slow element by adding it to the DOM after 3 seconds
window.setTimeout(() => {
const root = document.getElementById('root');
const newElement = '<div class="external-dom-element">slow element</div>';
root.innerHTML += newElement;
}, 3000)
Working example on codepen
I figured this out myself. It has nothing to do with my component, it's the demo itself that is breaking it. When I simulate the slow element by appending the root element's inner html:
root.innerHTML += newElement;
It re-parses the entire element and React loses all of the event handlers, etc. that it had previously set up.
This thread helped me out

Deleting child component state from parent state results in duplicate child DOM among other problems

I am updating this post with a link to my full and broken code. Unfortunately it is breaking in jsbin for a reason I do not understand. But maybe someone will find it useful regardless.
I have a parent component that generates child components. The child components are removed when they animate out of the viewport.
When the child components are removed in the reverse order from their creation, everything works as expected. But when first created is also first removed, the remaining children experience DOM duplication that is not represented in the Parent's state.
As you review the below pseudocode, I am hoping to learn what in my code is causing the DOM duplication.
In the below, the deletion occurs by two distinct implementations, one using splice, one using filter. The problem existed just the same when they were both splice.
Heres the code:
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
childArray: [],
nextKey: 0,
animInterval: -1,
...other state...
}
}
addChild() {
let newInterval = this.state.animInterval,
if (newInterval === -1) {
newInterval = setInterval(this.updateAnimation.bind(this), this.state.animStep);
}
this.setState({
childArray: [
...childArray,
{
id: this.state.nextKey,
...other state...
}
],
nextKey: this.state.nextKey + 1,
interval: newInterval,
});
}
// this deletes when the child moves off the page,
// but it seems to cause duplication of child DOM as well
// as a halt to css transitions that smooth out the animation steps
updateAnimation() {
this.setState((prevState, props) => {
let updated = prevState.bubbleArr.slice()
remove = [];
for (let i = 0; i < updated.length; i++) {
if (!updated[i]) {
continue;
}
updated[i].pos.y += this.state.deltaY;
if (updated[i].pos.y >= 100+this.state.deltaY) {
remove.push(i);
}
}
return { bubbleArr: updated.filter((x,i) => !remove.includes(i))
};
});
}
render() {
let styles = {...various styles...};
return (
<div className="canvas">
<div
className="emitter"
style={styles}
onClick={this.onClick.bind(this)} />
{this.state.bubbleArr.map(child => (
<Child
key={child.id}
props={bubble} />
))}
</div>
); // above, we are passing the parents handleChildUnmount
// to the child to be called on click
}
} // class
class Child extends React.Component {
render() {
let p = this.props.props, // this is ugly to me, too, but its helping
classes = "bubble", // me not modify the parent's render all the time
styles = {...various styles to animate...};
return (
<div className={classes} style={styles} onClick={this.onClick.bind(this)}>
</div>
);
}
}
To recap, my question is:
Why does removing any child but the last cause that child's DOM to duplicate?
Thank you for your time.

React updating DOM in an unpredictable manner with setState and class names

I'm attempting to do an animation with React and CSS classes. I have created a live demo, if you visit it and click the Start button you will see the text fade in and up one by one. This is the desired animation that I am after.
However, there seems to be issues of consistency when you hit Start multiple times and I cannot pinpoint why.
The Issue: Below is a recording of the issue, you can see the number 1 is not behaving as expected.
live demo
The process: Clicking Start will cancel any previous requestAnimationFrame' and will reset the state to it's initial form. It then calls the showSegments() function with a clean state that has no classNames attached to it.
This function then maps through the state adding a isActive to each segment in the state. We then render out the dom with a map and apply the new state.
This should create a smooth segmented animation as each class gets dropped one by one. However when i test this in Chrome (Version 56.0.2924.87 (64-bit)) and also on iOS, it is very inconsistent, sometimes it works perfectly, other times the first DOM element won't animate, it will just stay in up and visible it's completed transitioned state with "isActive".
I tried to replicate this issue in safari but it worked perfectly fine, I'm quite new to react so i am not sure if this is the best way to go about things, hopefully someone can offer some insight as to why this is behaving quite erratic!
/* MotionText.js */
import React, { Component } from 'react';
import shortid from 'shortid';
class MotionText extends Component {
constructor(props) {
super(props);
this.showSegments = this.showSegments.bind(this);
this.handleClickStart = this.handleClickStart.bind(this);
this.handleClickStop = this.handleClickStop.bind(this);
this.initialState = () => { return {
curIndex: 0,
textSegments: [
...'123456789123456789123456789123456789'
].map(segment => ({
segment,
id: shortid.generate(),
className: null
}))
}};
this.state = this.initialState();
}
handleClickStop() {
cancelAnimationFrame(this.rafId);
}
handleClickStart(){
cancelAnimationFrame(this.rafId);
this.setState(this.initialState(), () => {
this.rafId = requestAnimationFrame(this.showSegments);
});
}
showSegments() {
this.rafId = requestAnimationFrame(this.showSegments);
const newState = Object.assign({}, this.state);
newState.textSegments[this.state.curIndex].className = 'isActive';
this.setState(
{
...newState,
curIndex: this.state.curIndex + 1
},
() => {
if (this.state.curIndex >= this.state.textSegments.length) {
cancelAnimationFrame(this.rafId);
}
}
);
}
render(){
const innerTree = this.state.textSegments.map((obj, key) => (
<span key={obj.id} className={obj.className}>{obj.segment}</span>
));
return (
<div>
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<hr />
<div className="MotionText">{innerTree}..</div>
</div>
)
}
}
export default MotionText;
Thank you for your time, If there any questions please ask
WebpackBin Demo
Changing the method to something like this works
render(){
let d = new Date();
const innerTree = this.state.textSegments.map((obj, key) => (
<span key={d.getMilliseconds() + obj.id} className={obj.className}>{obj.segment}</span>
));
return (
<div>
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<hr />
<div className="MotionText">{innerTree}..</div>
</div>
)
}
How this helps is that, the key becomes different than previously assigned key to first span being rendered. Any way by which you can make the key different than previous will help you have this animation. Otherwise React will not render it again and hence you will never see this in animation.

Categories