I am building a simple timer as React practice. Right now I am just focusing on getting the seconds to work. When a user inputs a selection in baseSeconds, the timer will stop at that second.
It works if you hardcode a number as a prop instead of passing the state. and I know the props are changing in the component based on the {this.props.baseSeconds} I've outputted as a test. But when I put this.state.baseSeconds as props, the timer keeps on going.
Parent component of Settings:
const baseMin = [];
for (var i=0; i <= 60; i++) {
baseMin.push(i);
}
const baseSec = [];
for (var i=0; i <= 60; i++) {
baseSec.push(i);
}
const displayMinutes = baseMin.map((minute) =>
<option value={minute}>{minute}</option>
)
const displaySeconds = baseSec.map((second) =>
<option value={second}>{second}</option>
)
class Settings extends React.Component {
constructor(props) {
super();
this.state = {
baseMinutes: 0,
baseSeconds: 0,
varMinutes: 0,
varSeconds: 0
};
this.updateBaseMin = this.updateBaseMin.bind(this);
this.updateBaseSec = this.updateBaseSec.bind(this);
this.updateVarMin = this.updateVarMin.bind(this);
this.updateVarSec = this.updateVarSec.bind(this);
this.renderTimer = this.renderTimer.bind(this);
}
updateBaseMin(event) {
this.setState({ baseMinutes: event.target.value });
}
updateBaseSec(event) {
this.setState({ baseSeconds: event.target.value });
}
updateVarMin(event) {
this.setState({ varMinutes: event.target.value });
}
updateVarSec(event) {
this.setState({ varSeconds: event.target.value });
}
renderTimer(e) {
e.preventDefault();
const { baseMinutes, baseSeconds, varMinutes, varSeconds } = this.state;
return(
<Timer baseMinutes={baseMinutes} baseSeconds={baseSeconds} varMinutes={varMinutes} varSeconds={varSeconds}/>
)
}
render() {
const varMin = [];
for (var i=0; i <= this.state.baseMinutes; i++) {
varMin.push(i);
}
const displayBaseMin = varMin.map((minute) =>
<option value={minute}>{minute}</option>
)
const varSec = [];
for (var i=0; i <= this.state.baseSeconds; i++) {
varSec.push(i);
}
const displayBaseSec = varSec.map((second) =>
<option value={second}>{second}</option>
)
const { baseMinutes, baseSeconds, varMinutes, varSeconds } = this.state;
return (
<Container>
Settings
<form onSubmit={this.renderTimer}>BaseTime
<select onChange={this.updateBaseMin}>
{displayMinutes}
</select>
:
<select onChange={this.updateBaseSec}>
{displaySeconds}
</select>
VarTime +-
<select onChange={this.updateVarMin}>
{displayBaseMin}
</select>
:
<select onChange={this.updateVarSec}>
{displayBaseSec}
</select>
<input type="submit" value="Submit" />
</form>
<p>{this.state.baseMinutes}, {this.state.baseSeconds}, {this.state.varMinutes}, {this.state.varSeconds}</p>
<div>{this.renderTimer}</div>
<Timer baseMinutes={baseMinutes} baseSeconds={this.state.baseSeconds} varMinutes={varMinutes} varSeconds={varSeconds}/>
</Container >
)
}
}
export default Settings
child component of Timer:
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {
minutes: 0,
seconds: 0,
baseSeconds: this.props.baseSeconds
}
this.go = this.go.bind(this);
this.stop = this.stop.bind(this);
this.reset = this.reset.bind(this);
}
go = () => {
this.timer = setInterval(() => {
if ((this.state.seconds) === (this.props.baseSeconds)) {
clearInterval(this.timer);
} else {
this.setState({ seconds: this.state.seconds + 1 })
console.log(this.state.baseSeconds)
}
}, 1000)
}
stop = () => {
clearInterval(this.timer);
}
reset = () => {
this.setState({ minutes: 0, seconds: 0 })
}
render() {
return (
<div>
<button onClick={this.go}>start</button>
<button onClick={this.stop}>stop</button>
<button onClick={this.reset}>reset</button>
<p>{this.props.baseMinutes}:{this.props.baseSeconds}</p>
<p>{this.state.minutes}:{this.state.seconds}</p>
</div>
)
}
}
export default Timer
Yes, A change in props causes a re-render by default. BUT in your case, in the child component, the initial state (baseSeconds) is based on a prop (this.props.baseSeconds) which is not recommended. The constructor runs only once ( when the component mounts ) and any update on the baseSeconds props after that won't be detected as a result. You can use the props directly inside render without using the local state. if you must update the local state using props, the recommended approach would be to use getDerivedStateFromProps lifecycle method.
Related
React Newbie here,
import React, { Component } from "react";
class AudioList extends Component {
constructor(props) {
super(props);
this.audios = [];
this.buttonText = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios.push(new Audio(this.props.songs[i].song_url));
this.buttonText.push(String(i));
}
this.state = {
songs: "",
buttonText: this.buttonText
};
}
componentWillMount() {
const songs = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios[i].addEventListener("play", () => {
let stateArray = [...this.state.buttonText];
let stateArrayElement = { ...stateArray[i] };
stateArrayElement = "playing";
stateArray[i] = stateArrayElement;
console.log(stateArray);
this.setState({ buttonText: stateArray });
console.log(this.state.buttonText[i]);
});
songs.push(
<div className="song-preview">
<button
className="preview"
onClick={() => this.toggle(this.audios[i])}
>
{this.state.buttonText[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 came up with this implementation where the button text is based on an array in the state called buttonText. The buttons are rendered correctly on startup, but when the event listener picks up the event and changes the state, the text in the button is not re-rendering or changing, even though it is based on an element in an array in the state that is changing.
Does anyone have any suggestions about why it may be failing to re-render?
EDIT: Changing an individual array element in the state is based on React: how to update state.item[1] in state using setState?
I have restructured your code a bit (but it's untested it's tested now):
const songs = [
{
title: "small airplane long flyby - Mike_Koenig",
song_url: "http://soundbible.com/mp3/small_airplane_long_flyby-Mike_Koenig-806755389.mp3"
},
{
title: "Female Counts To Ten",
song_url: "http://soundbible.com/mp3/Female%20Counts%20To%20Ten-SoundBible.com-1947090250.mp3"
},
];
class AudioList extends React.Component {
audios = new Map();
state = {
audio: null,
song: null
};
componentDidUpdate() {
// stop playing if the song has been removed from props.songs
if (this.state.song && !this.props.songs.some(song => song.song_url === this.state.song.song_url)) {
this.toggle(this.state.audio, this.state.song);
}
}
componentWillUnmount() {
if (this.state.audio) {
this.state.audio.pause();
}
this.audios.clear();
}
toggle(audio, song) {
this.setState(state => {
if (audio !== state.audio) {
if (state.audio) {
state.audio.pause();
}
audio.play();
return { audio, song };
}
audio.pause();
return { audio: null, song: null };
});
}
getAudio(song) {
let audio = this.audios.get(song.song_url);
if (!audio) {
this.audios.set(song.song_url, audio = new Audio(song.song_url));
}
return audio;
}
render() {
return <div className="song-list">{
this.props.songs.map((song, i) => {
const audio = this.getAudio(song);
const playing = audio === this.state.audio;
return <div className="song-preview">
<button
className="preview"
onClick={this.toggle.bind(this, audio, song)}
>
{playing ? "playing" : (song.title || i)}
</button>
</div>
})
}</div>;
}
}
ReactDOM.render(<AudioList songs={songs} />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Edit: added a title to the song-objects and display them on the buttons
I simply moved all of the code from componentWillMount() to render(). I also removed 'songs' as a state variable and set it to a variable that exists only in render as songs is simply just a set of divs.
import React, { Component } from "react";
const audio1 =
"http://soundbible.com/mp3/small_airplane_long_flyby-Mike_Koenig-806755389.mp3";
const audio2 =
"http://soundbible.com/mp3/Female%20Counts%20To%20Ten-SoundBible.com-1947090250.mp3";
class AudioList extends Component {
constructor(props) {
super(props);
this.audios = [];
this.buttonText = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios.push(new Audio(this.props.songs[i]));
this.buttonText.push(String(i));
}
this.state = {
buttonText: this.buttonText
};
}
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() {
const songs = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios[i].addEventListener("play", () => {
console.log("playing");
let stateArray = [...this.state.buttonText];
let stateArrayElement = { ...stateArray[i] };
stateArrayElement = "playing";
stateArray[i] = stateArrayElement;
console.log(stateArray);
this.setState({ buttonText: stateArray });
console.log(this.state.buttonText);
});
songs.push(
<div className="song-preview">
<button
className="preview"
onClick={() => this.toggle(this.audios[i])}
>
{this.state.buttonText[i]}
</button>
</div>
);
}
return (
<div>{songs}</div>
)
}
}
export default () => <AudioList songs={[audio1, audio2]} />;
The code now runs as expected.
i am new to react and was making a basic counter. Logic might not be perfect but for now i want my counter to increase until the input value. Currently it decreases until 0 and increases unlimited. how can implement or bind my onChange function to onIncrease function?
import React from 'react';
import './App.css';
class App extends React.Component{
constructor(){
super();
this.state = {
counter: 0,
inputText: '',
}
}
// TODO: i should be able to increase only up to given input number
onChange = (event) => {
let inputText = event.target.value;
this.setState({inputText});
}
onIncrease = () => {
let counter = this.state.counter +1;
this.setState({counter})
}
onDecrease = () => {
if (this.state.counter > 0){
let counter = this.state.counter -1;
this.setState({counter})
}
}
render() {
console.log(this.state.counter);
return (
<div className="App">
<h2 id='s'>Counter: {this.state.counter}</h2>
<input onChange= {this.onChange}/>
<button onClick={this.onIncrease}>Increase</button>
<button onClick={this.onDecrease}>Decrease</button>
</div>
)
}
}
export default App;
In your decrease function, you check to see that the current counter is larger than zero, before decreasing. You should implement a similar check in the increase function. E.g., check that the current counter is below the value from the input field.
A couple of more things I would do to reduce the amount of logic in the increase function to a minimum:
In the initial state, set a "maxValue" property to Infinity
parse the input value an int in the onChange function, and use it to update the maxValue property of the state
Use the maxValue from the state in the increase function
// in constructor:
this.state = {
...
maxValue: Infinity
};
// in onChange:
this.setState({
maxValue: parseInt(e.target.value)
});
// in increase function:
if (this.state.counter < this.state.maxValue) {
//increase here
}
You can put a similar check as you have put in onDecrease method.
And I think one more check should be put incase the input value was more that the current counter value. May be we can change the counter to the new input value.
onChange = event => {
let inputText = event.target.value;
if (this.state.counter > Number(inputText)) {
this.setState({
counter: Number(inputText),
inputText
});
} else {
this.setState({ inputText });
}
};
onIncrease = () => {
if (this.state.counter < Number(this.state.inputText)) {
let counter = this.state.counter + 1;
this.setState({ counter });
}
};
You can add checks to user inputs too, like if a user inputs some string.
You want to cap the min and max values of the count:
onChange = event => {
let inputText = Number(event.target.value);
this.setState({ inputText });
};
onIncrease = () => {
let counter = Math.min(this.state.counter + 1, this.state.inputText);
this.setState({ counter });
};
onDecrease = () => {
let counter = Math.max(0, this.state.counter - 1);
this.setState({ counter });
};
import React from 'react';
import './App.css';
class App extends React.Component{
constructor(){
super();
this.state = {
counter: 0,
inputValue: 0,
}
}
// TODO: i should be able to increase only up to given input number
onChange = (event) => {
let inputValue = event.target.value;
this.setState({inputValue});
}
onIncrease = () => {
if (this.state.counter < this.state.inputValue){
let counter = this.state.counter +1;
this.setState({counter})
}
onDecrease = () => {
if (this.state.counter > 0){
let counter = this.state.counter -1;
this.setState({counter})
}
}
render() {
console.log(this.state.counter);
return (
<div className="App">
<h2 id='s'>Counter: {this.state.counter}</h2>
<input onChange= {this.onChange}/>
<button onClick={this.onIncrease}>Increase</button>
<button onClick={this.onDecrease}>Decrease</button>
</div>
)
}
}
export default App;
I'm lifting state up from child to parent and trying to render select elements with data from HTTP request. The select tag values within the Inv child component will show previously selected values, but I'm getting this error:
Warning: Can't call setState (or forceUpdate) on an
unmounted component. This is a no-op, but it indicates a memory leak
in your application. To fix, cancel all subscriptions and asynchronous
tasks in the componentWillUnmount method.
It looks like the error is coming from the Inv child component that is being passed props with this.state.identInvsData. That is the only time I get the error. The child components in the else statements that don't get the identInvsData don't get that error message. The parent component does not re-render, or "mount" the children.
Any ideas as to what I'm doing wrong?
Edit: The child components are populating the data from HTTP endpoint, but onChange it
doesn't change the values in setState, only when I select the button Add Inv. It adds another child but also allows the previous children to change.
Identity Parent Component
class Identity extends React.Component {
state = {
invIds: [],
inTypes: [],
invSeqIds: [],
numChildren: 0,
addInvBtnPressed: false
};
handleAddInv = () => {
this.setState({
numChildren: this.state.numChildren + 1,
addInvBtnPressed: true
});
};
onChangeInv = (e, index, sId) => {
const invIdsArry = this.state.invIds;
const invSeqIdsArry = this.state.invSeqIds;
const newId = [
...invIdsArry.slice(0, index),
(invIdsArry[index] = e.target.value),
...invIdsArry.slice(index + 1)
];
if (sId) {
const newSeqId = [
...invSeqIdsArry.slice(0, index),
(invSeqIdsArry[index] = sId),
...invSeqIdsArry.slice(index + 1)
];
this.setState({ invIds: newId, invSeqIds: newSeqId });
} else {
this.setState({ invIds: newId });
}
};
onChangeType = (e, index) => {
const invTypesArry = this.state.invTypes;
const newTypes = [
...invTypesArry.slice(0, index),
(invTypesArry[index] = e.target.value),
...invTypesArry.slice(index + 1)
];
this.setState({ invTypes: newTypes });
};
render() {
const children = [];
var i;
if (this.state.identInvsData && !this.state.addInvBtnPressed) {
for (i = 0; i < this.state.numChildren; i += 1) {
children.push(
<Inv
key={i}
invKey={i}
onChangeInv={this.onChangeInv.bind(this)}
onChangeType={this.onChangeType.bind(this)}
invId={this.state.identInvsData[i].invId}
invType={this.state.identInvsData[i].invTypeCd}
seqId={this.state.identInvsData[i].seqId}
invData={this.state.identInvsData[i]}
/>
);
}
} else if (this.state.identInvsData && this.state.addInvBtnPressed) {
for (i = 0; i < this.state.numChildren; i += 1) {
children.push(
<Inv
key={i}
invKey={i}
onChangeInv={this.onChangeInv.bind(this)}
onChangeType={this.onChangeType.bind(this)}
/>
);
}
} else {
for (i = 0; i < this.state.numChildren; i += 1) {
children.push(
<Inv
key={i}
invKey={i}
onChangeInv={this.onChangeInv.bind(this)}
onChangeType={this.onChangeType.bind(this)}
/>
);
}
}
return (
<div>
<button
type="button"
className="btn btn-info"
onClick={this.handleAddInv}
>
Add Inv
</button>
<div>{children}</div>
</div>
)
}
}
export default Identity;
Inv Child Component
class Inv extends React.Component {
state = {
kddLookupData: "",
invData: ""
};
componentDidMount = () => {
kddlookups_getAll().then(resp => {
this.setState({
kddLookupData: resp.data.item
});
});
invs_getAll().then(resp => {
this.setState({
invData: resp.data.items
});
});
};
handleInvestigatorChange = e => {
this.props.onChangeInv(e, this.props.invKey, this.props.seqId);
};
handleInvestigatorTypeChange = e => {
this.props.onChangeType(e, this.props.invKey);
};
render() {
return (
<div>
<select
value={this.props.invId}
name={this.props.invKey}
onChange={this.handleInvChange.bind(this)}
>
<option className="blank">Select inv name:</option>
{this.state.invData &&
this.state.invData.map(inv => {
return (
<option key={inv.userId} value={inv.userId}>
{inv.invName}
</option>
);
})}
</select>
<select
value={this.props.invType}
name={this.props.invKey}
onChange={this.handleInvTypeChange.bind(this)}
>
<option className="blank">Select inv type:</option>
{this.state.kddData &&
this.state.kddData.kdd_inv_type.map(inv => {
return (
<option key={inv.inv_type_cd} value={inv.inv_type_cd}>
{inv.inv_type_name}
</option>
);
})}
</select>
</div>
)
}
}
export default Inv;
It doesn't look like you are returning anything in your render function in the child component. Try:
render() {
return (
<div>
<select
value={this.props.invId}
name={this.props.invKey}
onChange={this.handleInvChange.bind(this)}
>
<option className="blank">Select inv name:</option>
{this.state.invData &&
this.state.invData.map(inv => {
return (
<option key={inv.userId} value={inv.userId}>
{inv.invName}
</option>
);
})}
</select>
<select
value={this.props.invType}
name={this.props.invKey}
onChange={this.handleInvTypeChange.bind(this)}
>
<option className="blank">Select inv type:</option>
{this.state.kddData &&
this.state.kddData.kdd_inv_type.map(inv => {
return (
<option key={inv.inv_type_cd} value={inv.inv_type_cd}>
{inv.inv_type_name}
</option>
);
})}
</select>
</div>
);
}
I'm new to react and programming in general, I have searched and only found solutions for js not react specific.
Having trouble displaying next or previous item in an array passed via props. When Next button is clicked I only see the same item in the array being returned not the next item, I understand previous will return null as displaying first item on load.
import React, { Component } from 'react'
import VideoPlayer from './Video'
import axios from 'axios'
export default class App extends Component {
constructor(props) {
super(props);
this._TogglePrev = this._TogglePrev.bind(this);
this._ToggleNext = this._ToggleNext.bind(this);
// app state
this.state = {
videos: [],
selectedVideo: null
}
}
componentDidMount() {
axios.get('http://localhost:5000/v1/video?id=287948764917205')
.then((result)=> {
var videos = result.data.payload
this.setState({
videos: videos,
selectedVideo: videos[0]
});
})
}
componentWillUnmount() {
this.serverRequest.abort()
}
// State transitions
_ToggleNext() {
console.log("something worked");
// take a copy of our state
const selectedVideo = this.state.selectedVideo;
// next video
var i = 0,
max = selectedVideo.length;
for (i; i < max; i += 1) {
if (selectedVideo[i]) {
return selectedVideo[i + 1];
}
}
//set our state
this.setState( selectedVideo );
console.log(selectedVideo)
}
_TogglePrev() {
console.log("something worked");
var current = this.state.selectedVideo;
var prev = current - 1;
if (prev < 0) {
prev = this.state.videos.length - 1;
}
// update our state
this.setState({ prev });
}
render() {
return (
<div className="App" style={{width: '100%', height: '100%'}}>
<div className="controls">
<button className="toggle toggle--prev" onClick={this._TogglePrev}>Prev</button>
<button className="toggle toggle--next" onClick={this._ToggleNext}>Next</button>
</div>
<VideoPlayer video={this.state.selectedVideo} />
</div>
)
}
}
The returned data
[
{ eventId: "287948764917205"
userName: "Jon Doe"
videoLink: "https://"https:s3.amazonaws.com/...""
userPhotoLink: "https://"https:s3.amazonaws.com/...""
},
{ eventId: "287948764917205"
userName: "Jane Thompson"
videoLink: "https://"https:s3.amazonaws.com/...""
userPhotoLink: "https://"https:s3.amazonaws.com/...""
}
]
Mistakes:
1. If you use return keyword inside for loop it will not only break the loop, it will return from that function also, so in these lines:
for (i; i < max; i += 1) {
if (selectedVideo[i]) {
return selectedVideo[i + 1];
}
}
this.setState( selectedVideo );
....
If if(selectedVideo[i]) will return true then it will break the loop and return from the function, so the lines after this for loop will never executes because of that return statement.
2. setState is a function and we need to pass an object (key-value pair, key will be the state variable names) in this, so you need to write it like this:
this.setState({ selectedVideo }); or this.setState({ selectedVideo: selectedVideo }); //both are same
Another way of writing the code by maintaining index:
1. Instead of maintaining selectedVideo in state variable maintain the index only, index of item of the array.
2. On click of next and prev button, increase or decrease the value of index and use that index to pass specific object of the state videos array to child component.
Like this:
import React, { Component } from 'react'
import VideoPlayer from './Video'
import axios from 'axios'
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
videos: [],
selectedIndex: 0
}
this._TogglePrev = this._TogglePrev.bind(this);
this._ToggleNext = this._ToggleNext.bind(this);
}
componentDidMount() {
axios.get('http://localhost:5000/v1/video?id=287948764917205')
.then((result)=> {
var videos = result.data.payload
this.setState({
videos: videos,
selectedIndex: 0
});
})
}
componentWillUnmount() {
this.serverRequest.abort()
}
_ToggleNext() {
if(this.state.selectedIndex == this.state.videos.length - 1)
return;
this.setState(prevState => ({
selectedIndex: prevState.selectedIndex + 1
}))
}
_TogglePrev() {
if(this.state.selectedIndex == 0)
return;
this.setState(prevState => ({
selectedIndex: prevState.selectedIndex - 1
}))
}
render() {
let {selectedIndex, videos} = this.state;
return (
<div className="App" style={{width: '100%', height: '100%'}}>
<div className="controls">
<button className="toggle toggle--prev" onClick={this._TogglePrev}>Prev</button>
<button className="toggle toggle--next" onClick={this._ToggleNext}>Next</button>
</div>
<VideoPlayer video={videos[selectedIndex]} />
</div>
)
}
}
Use document.activeElement in order to get the currently focused element. Then, use nextElementSibling on order to get the next element then focus() just like thisdocument.activeElement.nextElementSibling.focus()
Full example:
export default function TextField() {
return (
<div
onKeyDown={(e:any)=>{
if (e.keyCode==13){
const active:any = document.activeElement
active.nextElementSibling.focus()
}
}}
>
<input/>
<input/>
<input/>
</div>
);
};
It's better to write in the constructor:
constructor(props) {
super(props);
this._TogglePrev.bind(this);
this._ToggleNext.bind(this);
// app state
this.state = {
videos: [],
selectedVideo: null,
selectedVideoIndex:0
}
}
and also change
_ToggleNext() {
console.log("something worked");
// take a copy of our state
const selectedVideo = this.state.selectedVideo;
// next video
var selectedVideoIndex = this.state.selectedVideoIndex; //i always starts with zero ????? you need also to save the index
max = selectedVideo.length;
for (selectedVideoIndex; selectedVideoIndex < max; selectedVideoIndex++) {
if (selectedVideo[selectedVideoIndex]) {
const retval = selectedVideo[selectedVideoIndex + 1];
this.setState( selectedVideoIndex+1 );
this.setState(retval );
return retval;
}
}
console.log(selectedVideo)
}
I'm running stuck on a problem in React (files provided below). I'm trying to generate a set of child components and would like to be able to remove these. I have a state parameter this.state.lipids which contains an array of objects, using these I generate several <DataListCoupledInput /> components inside <RatioCoupledDataLists />. The components are generated and do what their supposed to do with the exception of refusing to be removed.
When I alter my this.state.lipids array by removing an element via removeLipid(index) it triggers a re-render that for some reason magically generates the same <DataListCoupledInput /> component that I just removed.
I tried moving the responsibility up to the parent level of <RatioCoupledDataLists /> but the problem remains.
Please help me.
RatioCoupledDataLists.jsx
class RatioCoupledDataLists extends React.Component {
constructor(props) {
super(props)
this.state = {
lipids: [{value: null, upperLeaf: 1, lowerLeaf: 1}]
}
this.registerLipid = this.registerLipid.bind(this);
this.addLipid = this.addLipid.bind(this);
this.removeLipid = this.removeLipid.bind(this);
}
registerLipid(index, lipid) {
var lipids = [];
lipids = lipids.concat(this.state.lipids);
lipids[index] = lipid;
this.setState({lipids: lipids});
this.state.lipids[index] = lipid;
}
addLipid() {
var mapping = {value: null, upperLeaf: 1, lowerLeaf: 1};
this.setState({lipids: this.state.lipids.concat(mapping)});
}
removeLipid(index) {
var lipids = [];
lipids = lipids.concat(this.state.lipids);
lipids.splice(index, 1);
this.setState({lipids: lipids});
}
render() {
var itemIsLast = function(index, len) {
if (index + 1 === len) {
// item is last
return true;
}
return false;
}
var makeLipidSelects = function() {
var allLipids = [];
for (var i = 0; i < this.state.lipids.length; i++) {
// Add RatioCoupledDataList
allLipids.push(
<DataListCoupledInput
fr={this.registerLipid}
list="lipids"
description="Membrane lipids"
add={this.addLipid}
remove={this.removeLipid}
key={i}
id={i}
isFinal={itemIsLast(i, this.state.lipids.length)}
lipid={this.state.lipids[i]} />);
}
return allLipids;
}
makeLipidSelects = makeLipidSelects.bind(this);
return <div className="category">
<div className="title">Custom Membrane</div>
<DataList data={lipids} id="lipids" />
{makeLipidSelects()}
</div>
}
}
DataListCoupledInput.jsx
class DataListCoupledInput extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.lipid.value,
active: this.props.isFinal,
upperLeaf: this.props.lipid.upperLeaf,
lowerLeaf: this.props.lipid.lowerLeaf
}
this.handleChange = this.handleChange.bind(this);
this.registerLeafletValue = this.registerLeafletValue.bind(this);
this.registerLipid = this.registerLipid.bind(this);
}
registerLipid(lipid) {
this.props.fr(this.props.id, lipid);
}
handleChange(event) {
this.registerLipid({value: event.target.value, upperLeaf: this.state.upperLeaf, lowerLeaf: this.state.lowerLeaf});
this.setState({value: event.target.value});
}
registerLeafletValue(leaflet) {
if (leaflet.name === "upperRatio") {
this.registerLipid({value: this.state.value, upperLeaf: leaflet.value, lowerLeaf: this.state.lowerLeaf});
this.setState({upperLeaf: leaflet.value});
}
if (leaflet.name === "lowerRatio") {
this.registerLipid({value: this.state.value, upperLeaf: this.state.upperLeaf, lowerLeaf: leaflet.value});
this.setState({lowerLeaf: leaflet.value});
}
}
render() {
var canEdit = function() {
if (this.props.isFinal === true) {
return <input onBlur={this.handleChange} list={this.props.list} />;
} else {
return <div className="description">{this.state.value}</div>
}
}
canEdit = canEdit.bind(this);
var addOrRemove = function() {
if (this.props.isFinal !== true) {
return <div className="remove" onClick={() => {this.props.remove(this.props.id)}}><div>-</div></div>;
} else {
return <div className="add" onClick={this.props.add}><div>+</div></div>;
}
}
addOrRemove = addOrRemove.bind(this);
return (
<div className="input-wrap">
<div className="input">
{canEdit()}
</div>
<div className="abundance">
<DynamicNumber
min={this.props.min}
max={this.props.max}
step={this.props.step}
name="lowerRatio"
value={this.state.upperLeaf}
fr={this.registerLeafletValue}
type="linked" />
<div className="to">:</div>
<DynamicNumber
min={this.props.min}
max={this.props.max}
step={this.props.step}
name="upperRatio"
value={this.state.lowerLeaf}
fr={this.registerLeafletValue}
type="linked" />
</div>
{addOrRemove()}
<div className="description">{this.props.description}</div>
</div>
)
}
}