Matteo Mazzarolo Matteo Mazzarolo - 3 months ago 27
React JSX Question

Re-rendering a single row of a list without re-rendering the entire list

we're trying to implement a contact list that works just like the new Material Design Google Contacts (you must enable the material design skin to see it) using material-ui.

Specifically we're trying to show a checkbox instead of the avatar on row hover.

We'd like to catch and re-render only the interested row (when hovered) and show the avatar/checkbox accordingly... this seems an easy task but we're not able to isolate the render to the hovered row (instead of re-rendering the entire list)
Do you have any suggestion on how to do something like this?

Our temporary solution uses a container component that handles the table:
When a row is hovered we capture it from

onRowHover
of the
Table
component and save it in the container state. This triggers a re-render of the entire list with really poor perfomance.

You can see a video of the issue here.

Here is a code sample:

import React from 'react'
import Avatar from 'material-ui/lib/avatar'
import Checkbox from 'material-ui/lib/checkbox'
import Table from 'material-ui/lib/table/table'
import TableHeaderColumn from 'material-ui/lib/table/table-header-column'
import TableRow from 'material-ui/lib/table/table-row'
import TableHeader from 'material-ui/lib/table/table-header'
import TableRowColumn from 'material-ui/lib/table/table-row-column'
import TableBody from 'material-ui/lib/table/table-body'
import R from 'ramda'

export default class ContactsList extends React.Component {

constructor (props) {
super(props)
this.state = { hoveredRow: 0 }
this.contacts = require('json!../../public/contacts.json').map((e) => e.user) // Our contact list array
}

_handleRowHover = (hoveredRow) => this.setState({ hoveredRow })

_renderTableRow = ({ hovered, username, email, picture }) => {
const checkBox = <Checkbox style={{ marginLeft: 8 }} />
const avatar = <Avatar src={picture} />
return (
<TableRow key={username}>
<TableRowColumn style={{ width: 24 }}>
{hovered ? checkBox : avatar}
</TableRowColumn>
<TableRowColumn>{username}</TableRowColumn>
<TableRowColumn>{email}</TableRowColumn>
</TableRow>
)
}

render = () =>
<Table
height='800px'
fixedHeader
multiSelectable
onRowHover={this._handleRowHover}
>
<TableHeader displaySelectAll enableSelectAll>
<TableRow>
<TableHeaderColumn>Nome</TableHeaderColumn>
<TableHeaderColumn>Email</TableHeaderColumn>
<TableHeaderColumn>Telefono</TableHeaderColumn>
</TableRow>
</TableHeader>
<TableBody displayRowCheckbox={false} showRowHover>
{this.contacts.map((contact, index) => this._renderTableRow({
hovered: index === this.state.hoveredRow,
...contact }))
}
</TableBody>
</Table>
}


Thank you in advance.

Answer

You could wrap your rows into a new component implementing shouldComponentUpdate like so :

class ContactRow extends Component {
    shouldComponentUpdate(nextProps) {
        return this.props.hovered !== nextProps.hovered || ...; // check all props here
    }
    render() {
        const { username, email, ...otherProps } = this.props;
        return (
            <TableRow { ...otherProps } >
                <TableRowColumn style={{ width: 24 }}>
                    {this.props.hovered ? checkBox : avatar}
                </TableRowColumn>
                <TableRowColumn>{this.props.username}</TableRowColumn>
                <TableRowColumn>{this.props.email}</TableRowColumn>
             </TableRow>
        );
    }
}

Then you can use it in your ContactList component like so :

this.contacts.map((contact, index) => <ContactRow key={contact.username} {...contact} hovered={index === this.state.hoveredRow} />)

If you don't want to manually implement shouldComponentUpdate, you can use React's PureRenderMixin or check a lib like recompose which provides useful helpers like pure to do so.

EDIT

As pointed out by @Denis, the approach above doesn't play well with some features of the Table component. Specifically, TableBody does some manipulation on its children's children. A better approach would be to define your ContactRow component like so:

class ContactRow extends Component
    shouldComponentUpdate(nextProps) {
        // do your custom checks here
        return true;
    }
    render() {
        const { username, email, ...otherProps } = this.props;
        return <TableRow { ...otherProps } />;
    }
end

and then to use it like this

<ContactRow { ...myProps }>
    <TableRowColumn>...</TableRowColumn>
</ContactRow>

But I guess having TableRow re-render only when necessary is a feature everyone would benefit from, so maybe a PR would be in order :)

Comments