https://fiddle.jshell.net/q8b3vwv8/
this is jquery example, I wrote it within 15 seconds.
But I'm stuck in react for 30 minutes. I've tried this but the class won't stay.
https://fiddle.jshell.net/8wsr7xa1
constructor(props){
super(props)
this.state = {
active: null
}
}
onMouseEnter(item){
this.setState({active: item})
}
render(){
const items = [1,2,3,4,5];
return (
<div>
{items.map((obj,i) =>
<div
key={i}
style={this.state.active === obj ?
{backgroundColor: 'yellow'} : {}}
onMouseEnter={this.onMouseEnter.bind(this, obj)}>
{obj}
</div>
)}
</div>
);
}
Really need help guys!
Problem is that you are trying to set one state to several items while only one is active at time. You need somehow track state changes and I made simple solution to have individual states for each item.
this.state = {
active: null,
activeItems: [false,false,false,false,false]
}
Then on mouseEnter I'm setting the state:
onMouseEnter(item){
this.state.activeItems[item-1]=true;
this.setState({activeItems: this.state.activeItems});
}
and finally set color based on state:
style={this.state.activeItems[i] ?
{backgroundColor: 'yellow'} : {}}
onMouseEnter={this.onMouseEnter.bind(this, obj)}>
Fiddle is here: fiddle
If you don't have the control over number of items then you can try the following approach.
onMouseEnter(event){
event.target.style.backgroundColor = 'yellow';
}
render(){
const items = [1,2,3,4,5];
return (
<div>
{items.map((obj,i) =>
<div
key={i}
onMouseEnter={this.onMouseEnter}>
{obj}
</div>
)}
</div>
);
}
Here is the link to working fiddle: JSFiddle
Related
I'm learning React. Say that I have a ListContainer which renders several ListItems. I want to keep track of the currently selected ListItem, render it in another color, and be able to navigate up and down.
One way would be to store selectedItem as state in ListContainer, and send it down as a prop to ListItem. But if I do it this way, then every time I change selectedItem I will rerender all ListItems because they are dependent on selectedItem. (I should only have to re-render two ListItems, the one that gets deselected, and the one that gets selected).
Is there a way to implement next and previous function without re-rendering all items?
Note: I know that React doesn't re-render unnecessarily in the DOM, but I'm trying to optimize operations on virtual DOM also.
Edit: Here is my example in code. It renders a list, and when the user click one item it gets selected. We also see that "ListItem update" gets printed 100 times, each time we change selection, which happens regardless of PureComponent or React.memo.
let mylist = []
for (let i = 0; i < 100; i++) {
mylist.push({ text: "node:" + i, id: i })
}
window.mylist = mylist
const ListItem = React.memo (class extends Component {
componentDidUpdate() {
console.log('ListItem update')
}
render() {
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
return (
<li
style={{ backgroundColor }}
onMouseDown={() => this.props.setSelected(this.props.item.id)}
>
{this.props.item.text}
</li>
)
}
})
class ListContainer extends Component {
constructor(props) {
super(props)
this.state = {
selectedItem: 10
}
this.setSelected = this.setSelected.bind(this)
}
setSelected(id) {
this.setState({ selectedItem: id })
this.forceUpdate()
}
render() {
return (
<ul>
{this.props.list.map(item =>
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
</ul>
)
}
}
function App() {
return (
<ListContainer list={mylist} />
);
}
The state you suggesting is the right way to implement it...
The other problem with unnecessary renders of the list item can easily be soved by wrapping the export statement like this:
export default React.memo(ListItem)
This way the only elements that has changed their props will rerender.. be aware that overuse of This can cause memory leaks when using it unnecessarily...
UPDATE
according to your example in addition to the React.memo you can update the way you transfer props to avoid senfing the selected item in each item...
istead of:
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
do :
let backgroundColor = this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={item.id === this.state.selectedItem}
setSelected={this.setSelected}
/>)}
this way the react memo will prevent rerenders when it is possible...
I'm building a pomodoro timer with 4 sessions. When a 25-minute timer is up, 1 is added to a state variable sessionNumber.
I have four of these circle/unchecked checkboxes displayed when the pomodoro cycle starts:
<FontAwesomeIcon
icon={faCircle}
size="2x"
className="pomodoro-unchecked session-checkbox"
/>
Each time 1 is added to sessionNumber state, I would like to hide one and display a different icon component:
<FontAwesomeIcon
icon={faCheckCircle}
size="2x"
className="pomodoro-checked session-checkbox"
/>
The only ways I could think of doing this would take a whole lot of code -- for example if statements based on each session number, like with an if statement, if the session is 0, display 4 unchecked circles (with the code for all four components), and if the session is 1, display 1 checked circle and 3 unchecked circles, and so forth. The second way I considered would be to give each one a different class name and in the method that changes the session number, display and hide each one, based on which session number it is (that would be more complicated). Is there a simpler, more succinct method?
You could use an array in state. You could initialise the array with four "faCircle" and then use the array.fill() method on setState to fill up the icons with "faCheckCircle". Then you can map over the array to render the appropriate icon.
class SessionIcons extends React.Component {
constructor(props) {
super(props);
this.state = {
sessions: 0,
icons: ["faCircle", "faCircle", "faCircle", "faCircle"]
};
}
onAddSession = () => {
this.setState(state => ({
sessions: state.sessions + 1,
icons: state.icons.fill("faCheckCircle", 0, state.sessions + 1)
}));
};
render() {
return (
<div>
{this.state.icons.map((icon, index) => (
<FontAwesomeIcon
key={icon + index}
icon={icon === "faCircle" ? faCircle : faCheckCircle}
size="2x"
/>
))}
<button onClick={this.onAddSession}>Add session</button>
</div>
);
}
}
Demo: https://codesandbox.io/s/shy-tdd-qzs1n
class Timer extends React.Component{
state = { sessionNumber: 1}
checkTimer = () =>{
..your logic to check timer every 25 mins
this.setState({timerState:1})
}
render(){
Let font;
if( this.state.sessionNumber == 1)
{
font = <FontAwesomeIcon
icon={faCircle}
size="2x"
className="pomodoro-unchecked session-checkbox"
/>
}
else if(this.state.sessionNumber == 2)
{
font = FontAwesomeIcon
icon={faCheckCircle}
size="2x"
className="pomodoro-checked session-checkbox"
/>
return(
{font}
)
}
}
I am building a React JS application.
So, I want to print something over and over from an array, but it has only two elements. I am using a custom package called 'Typist' that enables me to give a 'Typing' kind of animation with whatever I type.
I am basically trying to type 'Hi', erase it and then type 'Ola' and then erase it and then start with 'Hi' again and keep repeating this pattern.
Here's what I have right now:
let greetings=["Hi","Ola"];
render() {
return(
<div className={"TypistExample-header"} >
<Typist className={"TypistExample"}>
<Typist.Delay ms={1000} />
{
greetings.map(i =>{
return <li><h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</li>
})
}
</Typist>
P.S I did find a way to do it a few times,still not infinite, by doing this:
let greetings=["Hi","Ola"];
var actualTyping= greetings.map(i =>{
return <li><h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</li>
});
var rows=[];
for(var i=0;i<10;i++){
rows.push(actualTyping)
}
return(
<div className={"TypistExample-header"} >
<Typist className={"TypistExample"}>
<Typist.Delay ms={1000} />
{rows}
</Typist>
</div>
);
You can use Typist's onTypingDone property to restart the animation. Pass the text array via state to Typist. When the typing is done, clear the state, which will remove the rendered Typist, then restore the original text, and it will be typed again (sandbox).
Note: setState is asynchronous, and it batches updates together or defers them to a later time. In this case we want to clear the text (set null), and only after the view is updated repopulate the text. To do so, we can use the 2nd param of setState, a callback that is fired only after the update (null) has been applied.
const greetings = ["Hi", "Ola"];
class ContType extends React.Component {
constructor(props) {
super(props);
this.state = {
text: props.text
};
}
onTypingDone = () => {
this.setState(
{
text: null
},
() =>
// run this callback after the state updates
this.setState({
text: this.props.text
})
);
};
render() {
const { text } = this.state;
return (
text && (
<Typist onTypingDone={this.onTypingDone}>
<Typist.Delay ms={1000} />
{text.map((i, index) => {
return (
<div key={index}>
<h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</div>
);
})}
</Typist>
)
);
}
}
render(<ContType text={greetings} />, document.getElementById("root"));
Better and simple solution would be creating a constant integer array of your choice and then mapping carried out using the value specified for integer.
const a = [1...10000]
let greetings = ["hi", "hello"]
render(){
return(
a.map( i => {
<h1>greeting[0] - greeting[1]</h1>
})
)
}
And always, keep in mind that infinite loop cause react engine to break down. Good practice is to specify an integer value for mapping for such cases.
Original Question
I'm trying to render a list of items using React. The key is that the items share a common state, which can be controlled by each item.
For the sake of simplicity, let's say we have an array of strings. We have a List component that maps over the array, and generates the Item components. Each Item has a button that when clicked, it changes the state of all the items in the list (I've included a code snippet to convey what I'm trying to do).
I'm storing the state at the List component, and passing down its value to each Item child via props. The issue I'm encountering is that the button click (within Item) is not changing the UI state at all. I believe the issue has to do with the fact that items is not changing upon clicking the button (rightfully so), so React doesn't re-render the list (I would have expected some kind of UI update given the fact that the prop isEditing passed onto Item changes when the List state changes).
How can I have React handle this scenario?
Note: there seems to be a script error when clicking the Edit button in the code snippet, but I don't run into it when I run it locally. Instead, no errors are thrown, but nothing in the UI gets updated either. When I debug it, I can see that the state change in List is not propagated to its children.
Edited Question
Given the original question was not clear enough, I'm rephrasing it below.
Goal
I want to render a list of items in React. Each item should show a word, and an Edit button. The user should only be able edit one item at a time.
Acceptance Criteria
Upon loading, the user sees a list of words with an Edit button next to each.
When clicking Edit for item 1, only item 1 becomes editable and the Edit button becomes a Save button. The rest of the items on the list should no longer show their corresponding Edit button.
Upon clicking Save for item 0, the new value is shown for that item. All the Edit buttons (for the rest of the items) should become visible again.
Problem
On my original implementation, I was storing an edit state in the parent component (List), but this state wasn't properly being propagated to its Item children.
NOTE: My original implementation is lacking on the state management logic, which I found out later was the main culprit (see my response below). It also has a bind bug as noted by #Zhang below. I'm leaving it here for future reference, although it's not really a good example.
Here's my original implementation:
const items = ['foo', 'bar'];
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
isEditing: false
};
}
toggleIsEditing() {
this.setState((prevState) => {
return {
isEditing: !prevState.isEditing
}
});
}
render() {
return (
<ul>
{items.map((val) => (
<Item value={val}
toggleIsEditing={this.toggleIsEditing}
isEditing={this.state.isEditing}/>
))}
</ul>
);
}
}
class Item extends React.Component {
render() {
return (
<li>
<div>
<span>{this.props.value}</span>
{ !this.props.isEditing &&
(<button onClick={this.props.toggleIsEditing}>
Edit
</button>)
}
{ this.props.isEditing &&
(<div>
<span>...Editing</span>
<button onClick={this.props.toggleIsEditing}>
Stop
</button>
</div>)
}
</div>
</li>
);
}
}
ReactDOM.render(<List />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<body>
<div id="app" />
</body>
you didn't bind the parent scope when passing toggleIsEditing to child component
<Item value={val}
toggleIsEditing={this.toggleIsEditing.bind(this)}
isEditing={this.state.isEditing}/>
I figured out the solution when I rephrased my question, by rethinking through my implementation. I had a few issues with my original implementation:
The this in the non-lifecycle methods in the List class were not bound to the class scope (as noted by #ZhangBruce in his answer).
The state management logic in List was lacking other properties to be able to handle the use case.
Also, I believe adding state to the Item component itself was important to properly propagate the updates. Specifically, adding state.val was key (from what I understand). There may be other ways (possibly simpler), in which case I'd be curious to know, but in the meantime here's my solution:
const items = ['foo', 'bar'];
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
editingFieldIndex: -1
};
}
setEdit = (index = -1) => {
this.setState({
editingFieldIndex: index
});
}
render() {
return (
<ul>
{items.map((val, index) => (
<Item val={val}
index={index}
setEdit={this.setEdit}
editingFieldIndex={this.state.editingFieldIndex} />
))}
</ul>
);
}
}
class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
val: props.val
};
}
save = (evt) => {
this.setState({
val: evt.target.value
});
}
render() {
const { index, setEdit, editingFieldIndex } = this.props;
const { val } = this.state;
const shouldShowEditableValue = editingFieldIndex === index;
const shouldShowSaveAction = editingFieldIndex === index;
const shouldHideActions =
editingFieldIndex !== -1 && editingFieldIndex !== index;
const editableValue = (
<input value={val} onChange={(evt) => this.save(evt)}/>
)
const readOnlyValue = (
<span>{val}</span>
)
const editAction = (
<button onClick={() => setEdit(index)}>
Edit
</button>
)
const saveAction = (
<button onClick={() => setEdit()}>
Save
</button>
)
return (
<li>
<div>
{ console.log(`index=${index}`) }
{ console.log(`editingFieldIndex=${editingFieldIndex}`) }
{ console.log(`shouldHideActions=${shouldHideActions}`) }
{
shouldShowEditableValue
? editableValue
: readOnlyValue
}
{
!shouldHideActions
? shouldShowSaveAction
? saveAction
: editAction
: ""
}
</div>
</li>
);
}
}
ReactDOM.render(<List />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<body>
<div id="app" />
</body>
I failed to apply a class to a Dom node, below code will apply class to every DOM node.
import { Component } from 'react';
export default class App extends Component {
constructor(props){
super(props)
this.state = {
active: false
}
}
onMouseEnter(){
this.setState({active:true})
}
render(){
const items = [1,2,3,4,5];
return (
<div>
{items.map((obj,i) => <div key={i} className={this.state.active ? 'active' : ''} onMouseEnter={this.onMouseEnter.bind(this)}>{obj}</div>)}
</div>
);
}
}
What has gone wrong here? Also, how to do onMouseLeave? Just set this.setState({active:false}) false?
You are close... What you want is something like assigning an "active index". Your onMouseEnter() function could be changed to take the index of the active item like this
onMouseEnter(index){
this.setState({active: index})
}
And your render function would look like this instead:
render(){
const items = [1,2,3,4,5];
return (
<div>
{items.map((obj,i) =>
<div key={i} className={this.state.active === i ? 'active' : ''} onMouseEnter={this.onMouseEnter.bind(this, i)}>{obj}</div>)}
</div>
);
}
The thing you did wrong in the example you posted is not differentiating between which item in the list is in fact active instead you applied the active class to every item.
Your comments on my answer to this question make no sense:
(as you can see my mouse is no longer hovering over the active item but it is still yellow)