Select Next/Previous list item in React without re-rendering - javascript

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...

Related

React-Native Flatlist all Flatlist Rerender on one Item Selection

I'm building a React-Native app and trying to optimize it, i runned into the case of my Flatlist.
So this Flatlist basically renders few elements and each of these elements are selectable.
The issue i'm facing is that selecting one single item rerenders the whole Flatlist, and thus all items it contains.
I've seen a lot of solutions online already, and tried them without any success.
Here is my code :
Class component containing the Flatlist
const keyExtractor = (item) => item.id
export default class OrderedList extends Component {
state = {
selected: null,
}
onPressSelect = (id) => {
console.log(this.state.selected)
if(this.state.selected === id) {
this.setState({ selected: null})
}
else {
this.setState({ selected: id})
}
}
renderItemOrdered = ({item}) => {
const { group, wording, description, id: uniqueID } = item
const { id, name } = group
return (
<CategoryCard
type="ordered"
// item={item}
uniqueID={uniqueID}
groupName={name}
groupID={id}
description={description}
title={wording}
selected={this.state.selected}
onPressSelect={() => this.onPressSelect(item.id)}
/>
)
}
render() {
return (
<FlatList
initialNumToRender={10}
maxToRenderPerBatch={10}
data={this.props.data}
renderItem={this.renderItemOrdered}
keyExtractor={keyExtractor}
extraData={this.state.selected} ---> Tried with and without it
/>
)
}
}
Class component containing the renderItem method
export default class CategoryCard extends Component {
shouldComponentUpdate = (nextProps, nextState) => {
return nextProps.selected !== this.props.selected &&
nextProps.onPressSelect !== this.props.onPressSelect
}
render(){
if(this.props.type === 'ordered') {
return (
<Pressable style={this.props.selected === this.props.uniqueID ? styles.cardContainerSelected : styles.cardContainer} onPressIn={this.props.onPressSelect}>
<View style={[styles.cardHeader, backgroundTitleColor(this.props.groupID)]}>
<Text style={[styles.cardGroupName, textTitleColor(this.props.groupID)]}>{this.props.groupName}</Text>
</View>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{this.props.wording}</Text>
<Text style={styles.cardDescription} numberOfLines={3} ellipsizeMode="tail">{this.props.description}</Text>
</View>
</Pressable>
)
}
}
}
What i already tried :
At first my components were functional components so i changed them into class components in order to make things works. Before that, i tried to use React.memo, also to manually add a function areEqual to it, to tell it when it should rerender, depending on props.
It didn't give me what i wanted.
I also tried to put all anonymous functions outside return statements, made use of useCallback, played around the ShouldComponentUpdate (like adding and removing all the props, the onPress prop, selected props)... None of that worked.
I must be missing something somewhere.. If you can help me with it, it would be a big help !

React Native not rendering on prop change

I have created the following component:
type ToggleButtonProps = { title: string, selected: boolean }
export default class ToggleButton extends Component<ToggleButtonProps>{
render(){
return (
<TouchableWithoutFeedback {...this.props}>
<View style={[style.button, this.props.selected ? style.buttonSelected : style.buttonDeselected]}>
<Text style={[style.buttonText, this.props.selected ? style.buttonTextSelected : style.buttonTextDeselected]}>{this.props.title}</Text>
</View>
</TouchableWithoutFeedback>
);
}
}
The styles are simple color definitions that would visually indicate whether a button is selected or not. From the parent component I call (item is my object):
item.props.selected = true;
I've put a breakpoint and I verify that it gets hit, item.props is indeed my item's props with a selected property, and it really changes from false to true.
However, nothing changes visually, neither do I get render() or componentDidUpdate called on the child.
What should I do to make the child render when its props change? (I am on React Native 0.59.3)
You can't update the child component by literally assigning to props like this:
item.props.selected = true;
However, there are many ways to re-render the child components. But I think the solution below would be the easiest one.
You want to have a container or smart component which will keep the states or data of each toggle buttons in one place. Because mostly likely, this component will potentially need to call an api to send or process that data.
If the number of toggle buttons is fixed you can simply have the state like so:
state = {
buttonOne: {
id: `buttonOneId`,
selected: false,
title: 'title1'
},
buttonTwo: {
id: `buttonTwoId`,
selected: false,
title: 'title2'
},
}
Then create a method in the parent which will be called by each child components action onPress:
onButtonPress = (buttonId) => {
this.setState({
[buttonId]: !this.state[buttonId].selected // toggles the value
}); // calls re-render of each child
}
pass the corresponding values to each child as their props in the render method:
render() {
return (
<View>
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonOne} />
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonTwo} />
...
finally each child can use the props:
...
<TouchableWithoutFeedback onPress={() => this.props.onPressFromParent(this.props.dataFromParent.id)}>
<View style={[style.button, this.props.dataFromParent.selected ? style.buttonSelected : style.buttonDeselected]}>
...
I left the title field intentionally for you to try and implement.
P.S: You should be able to follow the code as these are just JS or JSX.
I hope this helps :)
Because children do not rerender if the props of the parent change, but if its STATE changes :)
Update child to have attribute 'key' equal to "selected" (example based on reactjs tho')
Child {
render() {
return <div key={this.props.selected}></div>
}
}

Dynamically change styled component based on state AND index

So I have a list that was returned from an API request (not important)
lets call it list = [1,2,3,4,5,6,7];
Now, inside render(), I have something like the following
render(){
<Wrapper>
{list.map((i) => {
return (<Button id = {i} onClick = {this.customFunc.bind(this, i)} />)
}
</Wrapper>
}
Now, I have another list, lets call it list_check = [false...] (for all 7 elements listed above)
Assume that customFunc changes the respective button id in list_check from false to true.
e.g. if I clicked button 1 (id = 1) then list_check becomes [false, true, false...]
** Now my problem is: I have 2 styled components, Button and Button_Selected,
how can i dynamically change between the 2 styled components so that if that unique button is clicked (change list_check[index] = true), the element becomes Button_Selected instead of Button (The entire list is initalized as Button since all elements are false)
Just to make it clear:
Both arrays are located in this.state and by 2 styled components I mean there exists
const Button = styled.div`
//styling here
`;
and
const Button_Selected = Button.extend`
//Small styling change to differentiate between selected and not selected
`;
edit: all answers are great! too bad I can only select one :(
You could just save the component to another variable.
this.state.list_check.map((item, i) => {
const NecessaryButton = item ? SelectedButton : Button;
return <NecessaryButton onClick={() => this.makeSelected(i)}>Normal Button</NecessaryButton>
})
You can see a live example here.
Although you can return 2 buttons based on conditional rendering .You can also pass props to your styled Button so that based on props you can change your styles.
render(){
<Wrapper>
{list.map((i) => {
return (<Button id = {i} isSelected={this.state.list_check[i]} onClick = {this.customFunc.bind(this, i)} />)
}
</Wrapper>
}
And in your styled Button:
const Button = styled.div`
styleName: ${props => props.isSelected ? 'styling_if_Selected' : 'styling_if_not_selected'};
`;
The easiest approach would be to have a single Button component and handle in the state if it was selected. Depending on this state you could switch classes.
Example:
class Button extends React.Component {
state = {
isSelected: false
};
handleClick() {
//Here we will set the state change and call the onClick function passed by props
this.setState({isSelected: !this.state.isSelected}, () => { this.props.onClick(); });
}
render() {
return (
<button
class={this.state.isButtonSelected ? "classBtnSelected" : "classBtnDefault"}
onClick={this.handleClick}
/>
);
}
}
Still, if you want to switch components you can use state to control if it was selected and do a conditional rendering. Example:
render(){
<Wrapper>
{list.map((i) => {
return (this.state.isSelected
? <Button id={i} onClick = {this.customFunc.bind(this, i)} />
: <Button_Selected id={i} onClick = {this.customFunc.bind(this, i)} />)
}
</Wrapper>
}

Disable buttons from parent

I am creating a Quiz app for the sake of learning React Native.
I want that when a user presses an answer, all buttons should be disabled. I have no idea how to do this, I have tried all different approaches, like changing props of the buttons from the parent, setting state from the parent etc. I just can't figure it out. I can make the clicked button disabled, but that doesn't help since the other buttons are still clickable.
Parent
class Container extends Component {
state = { currentQuestion: questions[0] }
buttons = new Array();
componentWillMount() {
this.makeButtons();
}
makeButtons() {
for (let i = 0; i < 4; i++) {
const isCorrect = (i === 0); //the first answer is correct, this is how I keep track
const btn = (
<Button
key={i}
title={this.state.currentQuestion[i]}
isCorrect={isCorrect}
/>
);
this.buttons.push(btn);
}
shuffle(this.buttons);
}
render() {
return (
<View style={containerStyle}>
<Text style={textStyle}>
{this.state.currentQuestion.title}
</Text>
{this.buttons}
</View>
);
}
}
Button
class Button extends Component {
state = { color: "rgb(0,208,196)" };
handleEvent() {
const newColor = (this.props.isCorrect) ? "green" : "red";
this.setState({ color: newColor });
this.props.onPress();
}
renderButton() {
return (
<TouchableOpacity
style={buttonStyle}
onPress={this.handleEvent.bind(this)}
disabled={this.props.disabled}
>
<Text style={textStyle}>
{this.props.title}
</Text>
</TouchableOpacity>
);
}
render() {
return this.renderButton();
}
}
You are creating your button components within an instance variable once when the parent component loads, but never re-rendering them. This is an anti-pattern of React. Ideally, your components should all be rendered within render(), and their props should be computed from state, so you only need to worry about updating the state correctly and all your components render properly.
In this case, you should construct the data for your buttons at component load, save your button data within state, and render your buttons within render(). Add a "disabled" state to your Button component, and when a user presses one of the buttons, use a callback to set "disabled" state in the parent component, and all your buttons will re-render to be properly disabled.

Rendering a list of items in React with shared state

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>

Categories

Resources