2

I am trying to pass the ref of my Table.row into a click handler. Perhaps I want to select a row and highlight it when selected by changing the className. However it always logs null.

Attempt:

<Table.Body>
    {_.map(data, ({ Name, Version, Type, Modified }) => (
        <Table.Row
          ref={(ref) => { this.row = ref; }}
          className=""
          key={Version}
          onClick={() => this.handleClick(this.row, Name, Version, Type, Modified)}
        >
            <Table.Cell>{Name}</Table.Cell>
            <Table.Cell>{Version}</Table.Cell>
            <Table.Cell>{Type}</Table.Cell>
            <Table.Cell>{Modified}</Table.Cell>
        </Table.Row>
    ))}
</Table.Body>

handleClick(row, Name, Version, Type, Modified) {
    const me = this;
    log.info('logging row', row); //null
    log.info('logging row data', Name, Version, Type, Modified); //works
}

Does anybody know why ref is not working properly here?

5
  • It should work, though what you are doing wrong is reassigning the this.ref on each iteration through your map. As such, the last Table.Row should be referenced by this.ref, the other rows should have no ref. Perhaps the error is related? Commented Jan 16, 2018 at 22:42
  • you are overriding the ref by the way Commented Jan 16, 2018 at 22:43
  • another thing, i don't see any support for refs in their docs. you might find this pattern helpful. Commented Jan 16, 2018 at 22:48
  • i have a better and more react'ish solution for you. i'll write it as an answer Commented Jan 16, 2018 at 23:16
  • that would be amazing - still stuck here Commented Jan 16, 2018 at 23:20

2 Answers 2

2

As i mentioned in my comments, I don't see any support for refs in their docs.
And as for your ref implementation, you are overriding this.row on each iteration.

Anyway there is a more react'ish solution for this in my opinion.

If you want to keep track of which rows were selected you will need an object in your state that will get updated on each click of a row, a kind of a lookup table object.

To create such lookup table, you will need id's for each row (you can use an id from the data object or an index of the array).

In order to be able pass down the id to the Table.Row and back up to the parent as a parameter of the onClick you can wrap the Table.Row with a class component that will handle the clicks and the data that it returns.

For example MyRow will expect to get a rowId, a onClick event and of course the active props that we will pass down to Table.Row.

when MyRow will get clicked, it will pass the this.props.rowId as a parameter to the parent, and the parent will update the lookup table.

Here is a running example of this use case:

const { Table } = semanticUIReact; // --> import { Table } from 'semantic-ui-react'

const usersFromServer = [
  { name: 'john', age: 25, gender: 'male', id: 1 },
  { name: 'jane', age: 22, gender: 'female', id: 2 },
  { name: 'david', age: 31, gender: 'male', id: 3 },
  { name: 'jain', age: 29, gender: 'female', id: 4 }
];


class MyRow extends React.Component {
  onClick = (e) => {
    const { onClick, rowId } = this.props;
    onClick(rowId, e);
  }

  render() {
    const { data, active } = this.props;
    return (
      <Table.Row onClick={this.onClick} active={active}>
        <Table.Cell>{data.name}</Table.Cell>
        <Table.Cell>{data.age}</Table.Cell>
        <Table.Cell >{data.gender}</Table.Cell>
      </Table.Row>
    );
  }
}


class TableExample extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      activeRows: []
    }
  }

  onRowClick = (id, e) => {
    const { activeRows } = this.state;
    const nextRows = {
      ...activeRows,
      [id]: !activeRows[id]
    }
    this.setState({ activeRows: nextRows });
  }

  render() {
    const { activeRows } = this.state;
    return (
      <Table unstackable>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell>Name</Table.HeaderCell>
            <Table.HeaderCell>Age</Table.HeaderCell>
            <Table.HeaderCell>Gender</Table.HeaderCell>
          </Table.Row>
        </Table.Header>

        <Table.Body>
          {
            usersFromServer.map((u) => {
              const isActive = activeRows[u.id];
              return (
                <MyRow
                  active={isActive}
                  key={u.id}
                  rowId={u.id}
                  data={u}
                  onClick={this.onRowClick}
                />
              );
            })
          }

        </Table.Body>
      </Table>
    )
  }
}

ReactDOM.render(<TableExample />, document.getElementById('root'));
<link href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.9/semantic.min.css" rel="stylesheet"/>
<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>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/semantic-ui-react.min.js"></script>
<div id="root"></div>

Sign up to request clarification or add additional context in comments.

3 Comments

thanks so much for your answer.. have not had time yet to test it but the solution I came up with is much simpler -- take a look what do you think
there are a lot of solutions for this challenge, but the best practice is to create another component and use the "component composition pattern".
@Sagivb.g what if we want to scroll to a given row? I'd expect to be able to take the ref and do ref.current.scrollIntoView().
0

Not a very React-ish solution but very simple: Instead of using refs I made a unique id for every row. Then in the clickHandler I can identify the row and mark it selected:

export default class TableExampleSortable extends Component {

    constructor(props) {
        super(props);

        this.state = {
            column: null,
            data: props.convo,
            direction: null,
        };

        this.handleClick = this.handleClick.bind(this);
    }


    handleSort = clickedColumn => () => {
        const { column, data, direction } = this.state;

        if (column !== clickedColumn) {
            this.setState({
                column: clickedColumn,
                data: _.sortBy(data, [clickedColumn]),
                direction: 'ascending',
            });

            return;
        }

        this.setState({
            data: data.reverse(),
            direction: direction === 'ascending' ? 'descending' : 'ascending',
        });
    }

    handleClick(Version) {
        const element = document.getElementById(Version);
        if (element.className === 'selected') {
            element.className = 'notSelected';
        } else {
            element.className = 'selected';
        }
    }

    render() {
        const { column, data, direction } = this.state;

        return (
            <div>
                <Table sortable selectable>
                    <Table.Header>
                        <Table.Row>
                            <Table.HeaderCell sorted={column === 'Name' ? direction : null} onClick={this.handleSort('Name')}>
                                Name
                            </Table.HeaderCell>
                            <Table.HeaderCell sorted={column === 'Version' ? direction : null} onClick={this.handleSort('Version')}>
                                Version
                            </Table.HeaderCell>
                            <Table.HeaderCell sorted={column === 'Type' ? direction : null} onClick={this.handleSort('Type')}>
                                Type
                            </Table.HeaderCell>
                            <Table.HeaderCell sorted={column === 'Modified' ? direction : null} onClick={this.handleSort('Modified')}>
                                Modified
                            </Table.HeaderCell>
                        </Table.Row>
                    </Table.Header>
                    <Table.Body>
                        {_.map(data, ({ Name, Version, Type, Modified }) => (
                            <Table.Row
                              id={Version}
                              className="notSelected"
                              key={Version}
                              onClick={() => this.handleClick(Version)}
                            >
                                <Table.Cell>{Name}</Table.Cell>
                                <Table.Cell>{Version}</Table.Cell>
                                <Table.Cell>{Type}</Table.Cell>
                                <Table.Cell>{Modified}</Table.Cell>
                            </Table.Row>
                        ))}
                    </Table.Body>
                </Table>
            </div>
        );
    }
}

4 Comments

this pattern may produce a performance hit. you are creating a new function reference on each iteration which will run on each render call. not to mention this is not much of a scalable nor testable solution. simple is not always better :) you should read this answer and this lint rule for the differences in the patterns
edited the full class -- my function is outside the render. take a look. It is NOT being called on every render
seems like you went with my answer (currying) from the link i provided. if you will look closely i wrote in the bottom of my answer: "Note that this approach doesn't solve the creation of a new instance on each render". you are still creating a new function because the outer function is returning a new function every time it gets invoked. there is no way to bypass this without creating another class component that will create an instance and let react do it's magic with the props and diffing algorithm
caught me with the arrow functions in the onClick.. thanks for you answer!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.