0

I'm trying to control the scroll position of a textarea using a React component's state, but it doesn't move when I call setState().

Here's an example where clicking on the link should scroll to the top of the textarea, but it doesn't move. Scrolling with the scroll bar can log the positions, and the field moves. Clicking on the link writes a message to the console, but the scroll position doesn't move. I based this approach on the controlled components described in the ReactJS documentation.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {scrollTop: 0};
    this.handleScroll = this.handleScroll.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleScroll() {
    let scrollTop = this.refs.content.scrollTop;
    console.log(scrollTop);
    this.setState({scrollTop: scrollTop});
  }
  
  handleClick(e) {
    e.preventDefault();
    console.log('Clicked.');
    this.setState({scrollTop: 0});
  }
  
  render() {
    let text = 'a\n\nb\n\nc\n\nd\n\ne\n\nf\n\ng';
    return <p><textarea
        ref="content"
        value={text}
        rows="10"
        cols="30"
        scrollTop={this.state.scrollTop}
        onScroll={this.handleScroll}/>
      <a href="#" onClick={this.handleClick}>
        Scroll to top
      </a></p>;
  }
}

ReactDOM.render(
  <App/>,
  document.getElementById('root')
);
.as-console-wrapper { max-height: 10% !important; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

I can control the scroll position by using the ref and setting its scrollTop, but I want to control it through the component state or the component properties. (Eventually, I want to synchronize the scrolling of two textareas.) I also tried using the react-scroll-sync library, but it doesn't work well with table cells.

Here's an example that controls the position using the ref and its scrollTop:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.handleScroll = this.handleScroll.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleScroll() {
    let scrollTop = this.refs.content.scrollTop;
    console.log(scrollTop);
  }
  
  handleClick(e) {
    e.preventDefault();
    console.log('Clicked.');
    this.refs.content.scrollTop = 0;
  }
  
  render() {
    let text = 'a\n\nb\n\nc\n\nd\n\ne\n\nf\n\ng';
    return <p><textarea
        ref="content"
        value={text}
        rows="10"
        cols="30"
        onScroll={this.handleScroll}/>
      <a href="#" onClick={this.handleClick}>
        Scroll to top
      </a></p>;
  }
}

ReactDOM.render(
  <App/>,
  document.getElementById('root')
);
.as-console-wrapper { max-height: 10% !important; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Don Kirkby
  • 53,582
  • 27
  • 205
  • 286
  • 2
    I do not believe that scrollTop is a valid HTML attribute. It seems like it is a DOM property with no corresponding HTML attribute, which is why it works when you use it as a property on a ref, but not when you use it to change the HTML attribute. Looking at the list of valid HTML attributes I do not see it, https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes. I know a lot of times attributes have 1:1 mappings to properties but this does not seem to be the case with scrollTop. – Tommy May Mar 26 '19 at 03:58

1 Answers1

1

Tommy May explained in a comment that scrollTop isn't an HTML attribute, so I can't link it directly to the component state or properties. However, I found that the componentDidUpdate() method can be used to handle changes to the state or properties.

Here's an updated example that uses componentDidUpdate():

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {scrollTop: 0};
    this.content = React.createRef();
    this.handleScroll = this.handleScroll.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleScroll() {
    let scrollTop = this.content.current.scrollTop;
    console.log(scrollTop);
    this.setState({scrollTop: scrollTop});
  }
  
  handleClick(e) {
    e.preventDefault();
    console.log('Clicked.');
    this.setState({scrollTop: 0});
  }
  
  componentDidUpdate() {
    this.content.current.scrollTop = this.state.scrollTop;
  }
  
  render() {
    let text = 'a\n\nb\n\nc\n\nd\n\ne\n\nf\n\ng';
    return <p><textarea
        ref={this.content}
        value={text}
        rows="10"
        cols="30"
        onScroll={this.handleScroll}/>
      <a href="#" onClick={this.handleClick}>
        Scroll to top
      </a></p>;
  }
}

ReactDOM.render(
  <App/>,
  document.getElementById('root')
);
.as-console-wrapper { max-height: 10% !important; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

This isn't particularly useful in this example, but it is useful when you want to use properties instead of local state, as in this answer I posted for my original goal of synchronized scrolling.

Don Kirkby
  • 53,582
  • 27
  • 205
  • 286