Newbie React question here on show hide functionality.
I have a state of 'show' that I set to false:
this.state = {
show: false,
};
Then I use the following function to toggle
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
And my display is
{this.state.show && <xxxxxx> }
This all works fine. However I want to apply the function it to multiple cases (similar to accordion, without the closing of other children. So I change my constructor to
this.state = {
show: [false,false,false,false,false,false]
};
and this to recognise there are 6 different 'shows'.
{this.state.show[0] && <xxxxxx> }
{this.state.show[1] && <xxxxxx> } etc
But where I get stuck is how to account for them in my toggleDiv function. How do I insert the square bracket reference to the index of show (if this is my problem)?
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
Thanks for looking.
First of all I'd suggest you not to rely on current state in setState function, but to use the callback option to be 100% sure that you are addressing to the newest state:
this.setState((prevState) => ({ show: !prevState.show }));
How to deal with multiple elements?
You'll have to pass the index of currently clicked element.
{yourElements.map((elem, i) => <YourElem onClick={this.toggleDiv(i)} />)}
and then inside your toggleDiv function:
toggleDiv = (i) => () => {
this.setState((prevState) => {
const r = [...prevState.show]; // create a copy to avoid state mutation
r[i] = !prevState.show[i];
return {
show: r,
}
}
}
Use an array instead of a single value. In your toggle div function make a copy of the state array make necessary changes and push the entire array back up to state at the end.
This is some simplified code showing the workflow I described above
export default class myClass extends React.Component{
constructor(props){
super(props);
this.state = { show: new Array(2).fill(false) };
}
//you need a index or id to use this method
toggleDiv = (index) => {
var clone = Object.assign( {}, this.state.show ); //ES6 Clones Object
switch(clone[index]){
case false:
clone[index] = true
break;
case true:
clone[index] = false
break;
}
this.setState({ show: clone });
}
render(){
return(
<div>
{ this.state.show[0] && <div> First Div </div> }
{ this.state.show[1] && <div> Second Div </div> }
{ this.state.show[2] && <div> Third Div </div> }
</div>
)
}
}
Related
I have some elements inside an array that shares a same state. I need to update only the clicked one in order to add one more item to my shopping cart. How can i do this without changing the others?
My initial state looks like this:
class ShoppingCart extends Component {
constructor() {
super();
this.state = {
isEmpty: true,
cartItems: [],
count: 0,
};
this.getStoredProducts = this.getStoredProducts.bind(this);
this.handleButtonIncrease = this.handleButtonIncrease.bind(this);
}
componentDidMount() {
this.getStoredProducts();
}
handleButtonIncrease() {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
}
getStoredProducts() {
const getFromStorage = JSON.parse(localStorage.getItem('cartItem'));
if (getFromStorage !== null) {
this.setState({
cartItems: getFromStorage,
}, () => {
const { cartItems } = this.state;
if (cartItems.length) {
this.setState({ isEmpty: false });
}
});
}
}
render() {
const { isEmpty, cartItems, count } = this.state;
const emptyMsg = (
<p data-testid="shopping-cart-empty-message">Seu carrinho está vazio</p>
);
return (
<div>
{ isEmpty ? (emptyMsg) : (cartItems.map((item) => (
<ShoppingCartProduct
key={ item.id }
id={ item.id }
count={ count }
cartItems={ item }
handleButtonIncrease={ this.handleButtonIncrease }
/>
)))}
</div>
);
}
}
It seems like this should be ShoppingCartProduct's responsibility. If you remove this count and setCount logic from your ShoppingCart component and create it inside of the ShoppingCartProducts component, each one of the items will have their own count state that can be updated independently.
One other way of seeing this is directly mutating each cartItem, but since you didn't specify their format there's no way of knowing if they're storing any information about quantity so I would go with the first approach.
handleButtonIncrease can accept item.id as parameter so that it can update the state.cartItems.
handleButtonIncrease(itemId) {
const cartItems = this.state.cartItems.map(item => {
return item.id === itemId
? {
// apply changes here for the item with itemId
}
: item
});
this.setState((prevState) => ({
cartItems,
count: prevState.count + 1,
}));
}
After that, update your callback as well:
handleButtonIncrease={ () => this.handleButtonIncrease(item.id) }
I'm using a bootstrap form slider from 1-5 to describe the condition of something from good - bad. On the slider change, I want it to envoke two functions; one to change the number in the state, and then one to identify the correct text to go with the number and display it as the user slides the slider. I've tried a few different variations of this, but nothing seems to work. Would also welcome other ways of forming the functions or state all together. Thanks in advance.
class ConditionSlider extends Component {
constructor (props) {
super(props);
this.state = {
condition: 1
};
};
render() {
const slideCalls = (e) => {
slideChangeState(e);
conditionText(e)
};
const slideChangeState = (e) => {
this.setState({
condition: e
})
}
let spanText;
const conditionText = (e) => {
if(e === '1' ) {
spanText = <div>Very Poor</div>
} else if (e === '2') {
spanText = <div>Poor</div>
} else if (e === '3') {
spanText = <div>Okay</div>
} else if (e === '4') {
spanText = <div>Good</div>
} else if (e === '5') {
spanText = <div>Excellent</div>
}
}
console.log(this.state.condition)
return (
<div className="slide-class">
<RangeSlider
value={this.state.condition}
onChange={e => slideCalls(e.target.value)}
min={1}
max={5}
step={1}
size='sm'
tooltip='off'
variant='primary'
/>
<p>{this.spanText}</p>
<p>{spanText}</p>
</div>
)}
};
You need to modify several things first to make it work. First, the render function should not contain those two functions. Then, you could have an object that contains the numbers as keys (1 to 5) and the string that correlates with that number (Poor, excelent, etc.) so you don't fallback to a large if...else statement.
So leaving it as this:
const RANGES = {
1: "Very Poor",
2: "Poor",
3: "Okay",
4: "Good",
5: "Excellent",
}
class ConditionSlider extends Component {
constructor (props) {
super(props);
this.state = {
condition: 1
};
};
slideCalls(e) {
slideChangeState(e);
conditionText(e)
};
slideChangeState(e) {
this.setState({
condition: e
})
}
render() {
return (
<div className="slide-class">
<RangeSlider
value={this.state.condition}
onChange={e => slideCalls(e.target.value)}
min={1}
max={5}
step={1}
size='sm'
tooltip='off'
variant='primary'
/>
<p>{RANGES[this.state.condition]}</p> // removed <div> here since you cant have a div inside a <p> element. I mean, you could but it would display an error in the console
<p>{spanText}</p>
</div>
)}
};
I'm coding this blind, but this is how I'd handle this situation. If you're using state to manage the value of the slider then you ,ight as well use state to hold the text value too.
I've moved the functions outside of the render method, and as slideChangeState requires access to this I've bound it in the constructor.
I hope this helps - never used this control before
class ConditionSlider extends Component {
constructor(props) {
super(props);
this.state = {
condition: 1,
spanText: "Very Poor"
};
this.slideChangeState = this.slideChangeState.bind(this);
};
slideChangeState(value) {
var spanText = conditionText(value);
this.setState({
condition: value,
spanText: spanText
})
};
conditionText(value) {
switch (value) {
case 1:
return "Very Poor";
case 2:
return "Poor";
case 3:
return "Okay";
case 4:
return "Good";
case 5:
return "Excellent"
}
}
render() {
return (
<div className="slide-class">
<RangeSlider
value={this.state.condition}
onChange={e => slideCalls(e.target.value)}
min={1}
max={5}
step={1}
size='sm'
tooltip='off'
variant='primary'
/>
<p>{this.state.spanText}</p>
</div>
)
}
};
I am extremely new to react and am building a simple todo list app. I am trying to edit data from my child component and send it back to my parent. When I am printing console logs the parent state seems to be getting set correctly, but the child elements are not refreshing. Am I doing something conceptually wrong here?
I have tried to share the entire code as I am not sure whether it is correct conceptually. I am new to JS. When the handleSave() and handleComplete() are called i can see correct values getting returned and set to my PArent State, but there is no refresh of the child components.
Below is my Parent class code.
class App extends Component {
state = {
taskList: [
]
};
saveEventHandler = data => {
console.log("I am in saveEventHandler");
var uniqid = Date.now();
const taskList = [...this.state.taskList];
taskList.push({
id: uniqid,
taskDescText: data,
isFinished: false
});
console.log(taskList);
this.setState({'taskList':taskList});
};
deleteEventHandler = (index) => {
const taskList = [...this.state.taskList];
taskList.splice(index,1)
this.setState({'taskList':taskList});
}
editEventHandler = (index,data) => {
var uniqid = Date.now();
console.log("In edit event handler")
console.log(data)
console.log(index)
const taskList = [...this.state.taskList];
taskList[index] = {
id: uniqid,
taskDescText: data,
isFinished: false
}
this.setState({'taskList':taskList});
console.log(this.state.taskList)
}
handleComplete = (index) => {
console.log("In complete event handler")
const taskList = [...this.state.taskList];
const taskDescriptionOnEditIndex = taskList[index]
taskDescriptionOnEditIndex.isFinished = true
taskList[index] = taskDescriptionOnEditIndex
this.setState({'taskList':taskList});
console.log(this.state.taskList)
}
render() {
return (
<div className="App">
<h1>A Basic Task Listing App </h1>
<CreateTask taskDescription={this.saveEventHandler} />
{this.state.taskList.map((task, index) => {
return (
<Task
taskDescText={task.taskDescText}
taskCompleted={task.isFinished}
deleteTask={() => this.deleteEventHandler(index)}
editTask={(editTask) => this.editEventHandler(index,editTask)}
handleComplete={() => this.handleComplete(index)}
editing='false'
/>
);
})}
</div>
);
}
}
export default App;
and my child class code
export default class Task extends React.Component {
state = {
editing : false
}
notCompleted = {color: 'red'}
completed = {color: 'green'}
textInput = React.createRef();
constructor(props) {
super(props);
this.state = props
}
handleEdit = () => {
this.setState({editing:true});
}
handleSave = () => {
this.props.editTask(this.textInput.current.value);
this.setState({editing:false});
};
editingDiv = (<div className = 'DisplayTask'>
<span className='TaskDisplayText' style={!this.props.taskCompleted ? this.notCompleted: this.completed}>{this.props.taskDescText} </span>
<button label='Complete' className='TaskButton' onClick={this.props.handleComplete}> Complete</button>
<button label='Edit' className='TaskButton' onClick={this.handleEdit}> Edit Task</button>
<button label='Delete' className='TaskButton' onClick={this.props.deleteTask}> Delete Task</button>
</div> );
nonEditingDiv = ( <div className = 'DisplayTask'>
<input className='TaskDescEditInput' ref={this.textInput}/>
<button label='Save' className='TaskButton' onClick={this.handleSave} > Save Task</button>
<button label='Delete' className='TaskButton' onClick={this.props.deleteTask}> Delete Task</button>
</div>);
render() {
return (
!this.state.editing ? this.editingDiv : this.nonEditingDiv
)
};
}
Move your editingDiv and nonEditingDiv definitions inside render() method. Since you're defining them as instance variables, they're initialized once, and never get re-rendered again with new prop values.
By moving them to render() method, render() which is called every time when there's a prop update will pick up the new prop values.
I'm trying to create my own pagination (without using a package), but I can't get it to work.
I'm wondering if it has something to do with how I'm copying my arrays, but I'm not really sure.
class InsightSearchResults extends Component {
state = {
start: 0,
end: 2,
insightsArrayOriginal: [],
copiedArr: []
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.insightList[0]) {
this.setState({
insightsArrayOriginal: nextProps.insightList[0].insights,
copiedArr: nextProps.insightList[0].insights.splice(this.state.start, this.state.end)
})
}
}
clickNext = () => {
let copied = [...this.state.insightsArrayOriginal];
this.setState({
start: this.state.start + 2,
end: this.state.end + 2
}, () => {
this.setState({
copiedArr: copied.splice(this.state.start, this.state.end)
})
})
}
clickPrev = () => {
this.setState({
start: this.state.start - 2 < 0 ? 0 : this.state.start - 2,
end: this.state.end - 2
})
}
render() {
const { copiedArr } = this.state;
return (
<div style={{padding: "1.5rem"}}>
{copiedArr ? copiedArr.map(insight => (
<div>
<Grid className="insight_result_block">
<Col className="insight_results_col2" span="10">
<div>
<h4>Hello</h4>
<p>{insight.insightDesc}</p>
</div>
</Col>
</Grid>
<hr className="bottom_hr_insight" />
</div>
)) : <p>loading...</p> }
<button onClick={this.clickPrev}>Prev</button>
<button onClick={this.clickNext}>Next</button>
</div>
)
}
}
I haven't really worked on the "prev" part yet. I'm just trying to get the "next" to work for now...
There are two problems:
UNSAFE_componentWillReceiveProps is not called on initial render. From the docs:
React doesn’t call UNSAFE_componentWillReceiveProps() with initial
props during mounting. It only calls this method if some of
component’s props may update. Calling this.setState() generally
doesn’t trigger UNSAFE_componentWillReceiveProps().
splice mutates the original array, use slice instead. See this question.
So you can move the content of UNSAFE_componentWillReceiveProps to componentDidMount and componentDidUpdate
componentDidMount() {
this.updateState();
}
componentDidUpdate() {
// check if a change in props has caused the rerender
// or you will get infinite rerenders if one state update causes the next one
if (
this.props.insightList[0] &&
this.props.insightList[0].insights !== this.state.insightsArrayOriginal
) {
this.updateState();
}
}
These functions don't receive a parameter: replace nextProps parameter with this.props; and change all splice occurrences with slice.
updateState() {
if (this.props.insightList[0]) {
this.setState({
insightsArrayOriginal: this.props.insightList[0].insights,
copiedArr: this.props.insightList[0].insights.slice( . // <-here
this.state.start,
this.state.end
)
});
}
}
clickNext = () => {
let copied = [...this.state.insightsArrayOriginal];
this.setState({ start: this.state.start + 2, end: this.state.end + 2 },
() => {
this.setState({
copiedArr: copied.slice(this.state.start, this.state.end) // <- and here
});
}
);
};
Also, based on this code sample alone, you could entirely remove insightsArrayOriginal from your state and use it from props, but this may change if you plan to expand the functionality.
I want to be able to trigger a load animation in React and perform a GET request, and then stop the loading animation after the get request completed. This is how I laid out my code.
export default class Dialog extends Component {
state = {
domIdx: 0
}
loadData = () => {
this.setState({
domIdex: 1
}, $.getJSON('http://google.com', () => {
this.setState({
domIdx: 2
})
})
}
render() {
let arr = [<UploadFile/>, <LoadAnimation/>, <Done/>]
return (
<div>
{arr[this.state.domIdx]}
</div>
)
}
}
However, the above code is not working after the loading animation is triggered. The loading animation is shown, the GET request is completed, but the view doesn't change after calling setState again.
How can I achieve the intended action?
In your component state, you can define a loading piece of state:
state = { loading: false }
Then, when you make your request, flip it to to true:
loadData = () => {
this.setState({
loading: true
}, $.getJSON('http://google.com', () => {
this.setState({
loading: false
})
})
}
Then in your render:
render() {
return (
<div>
{this.state.loading ? (<SomeSpinner />):(<Completed />)}
</div>
)
}