Как прокрутить вниз в React?

128

Я хочу создать систему чата и автоматически прокручивать вниз при входе в окно и при поступлении новых сообщений. Как вы автоматически прокручиваете до конца контейнера в React?

helsont
источник

Ответы:

222

Как упоминал Тушар, вы можете оставить фиктивный div внизу чата:

render () {
  return (
    <div>
      <div className="MessageContainer" >
        <div className="MessagesList">
          {this.renderMessages()}
        </div>
        <div style={{ float:"left", clear: "both" }}
             ref={(el) => { this.messagesEnd = el; }}>
        </div>
      </div>
    </div>
  );
}

а затем прокрутите до него всякий раз, когда ваш компонент обновляется (т.е. состояние обновляется по мере добавления новых сообщений):

scrollToBottom = () => {
  this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}

componentDidMount() {
  this.scrollToBottom();
}

componentDidUpdate() {
  this.scrollToBottom();
}

Здесь я использую стандартный метод Element.scrollIntoView .

metakermit
источник
3
предупреждение из документации: «findDOMNode не может использоваться в функциональных компонентах».
Tomasz
1
this.messagesEnd.scrollIntoView()у меня работал нормально. В использовании не было необходимости findDOMNode().
Rajat Saxena
изменена функция, чтобы scrollToBottom(){this.scrollBottom.scrollIntoView({ behavior: 'smooth' })}она работала в более новых версиях
Kunok
2
Хорошо, я удалил findDOMNode. Если у кого-то это не сработает, вы можете проверить историю редактирования ответа.
metakermit
7
У меня есть ошибка, что scrollIntoView имеет значение TypeError: Cannot read property 'scrollIntoView' of undefined. Что делать?
Feruza
90

Я просто хочу обновить ответ, чтобы он соответствовал новому React.createRef() методу , но в основном он такой же, просто помните о currentсвойстве в созданной ссылке:

class Messages extends React.Component {

  const messagesEndRef = React.createRef()

  componentDidMount () {
    this.scrollToBottom()
  }
  componentDidUpdate () {
    this.scrollToBottom()
  }
  scrollToBottom = () => {
    this.messagesEnd.current.scrollIntoView({ behavior: 'smooth' })
  }
  render () {
    const { messages } = this.props
    return (
      <div>
        {messages.map(message => <Message key={message.id} {...message} />)}
        <div ref={this.messagesEndRef} />
      </div>
    )
  }
}

ОБНОВИТЬ:

Теперь, когда крючки доступны, я обновляю ответ , чтобы добавить использование useRefи useEffectкрючков, реальная вещь делает магию (React рефов и scrollIntoViewметод DOM) остается тем же:

import React, { useEffect, useRef } from 'react'

const Messages = ({ messages }) => {

  const messagesEndRef = useRef(null)

  const scrollToBottom = () => {
    messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
  }

  useEffect(scrollToBottom, [messages]);

  return (
    <div>
      {messages.map(message => <Message key={message.id} {...message} />)}
      <div ref={messagesEndRef} />
    </div>
  )
}

Также сделал (очень простой) codeandbox, если вы хотите проверить поведение https://codesandbox.io/s/scrolltobottomexample-f90lz

Диего Лара
источник
2
componentDidUpdate может вызывать много раз в жизненном цикле React. Итак, мы должны проверить, существует ли ref this.messagesEnd.current в функции scrollToBottom. Если this.messagesEnd.current не существует, в сообщении об ошибке будет отображаться TypeError: Cannot read property 'scrollIntoView' of null. Итак, добавьте это условие if также scrollToBottom = () => {if (this.messagesEnd.current) {this.messagesEnd.current.scrollIntoView ({behavior: 'smooth'})}}
Arpit
componentDidUpdate всегда происходит после первого рендеринга ( reactjs.org/docs/react-component.html#the-component-lifecycle ). В этом примере не должно быть ошибок и this.messagesEnd.currentвсегда есть. Тем не менее важно заметить, что вызов this.messagesEnd.currentперед первым рендерингом приведет к указанной вами ошибке. Thnx.
Диего Лара
что this.messagesEndв вашем первом примере в методе scrollTo?
dcsan
@dcsan - это ссылка React, они используются для отслеживания элемента DOM даже после повторных рендеров. reactjs.org/docs/refs-and-the-dom.html#creating-refs
Диего Лара
1
Второй пример кода не работает. useEffectПотребность метод , который будет размещен с () => {scrollToBottom()}. В любом случае большое спасибо
Гаспар
37

Не используй findDOMNode

Компоненты класса с ref

class MyComponent extends Component {
  componentDidMount() {
    this.scrollToBottom();
  }

  componentDidUpdate() {
    this.scrollToBottom();
  }

  scrollToBottom() {
    this.el.scrollIntoView({ behavior: 'smooth' });
  }

  render() {
    return <div ref={el => { this.el = el; }} />
  }
}

Функциональные компоненты с крючками:

import React, { useRef, useEffect } from 'react';

const MyComponent = () => {
  const divRref = useRef(null);

  useEffect(() => {
    divRef.current.scrollIntoView({ behavior: 'smooth' });
  });

  return <div ref={divRef} />;
}
tgdn
источник
2
Можете ли вы объяснить, почему вам не следует использовать findDOMNode?
one stevy boi 05
2
@steviekins Потому что «он блокирует определенные улучшения в React» и, вероятно, будет устаревшим github.com/yannickcr/eslint-plugin-react/issues/…
tgdn
2
Должно быть написано в американском стиле behavior(нельзя редактировать, потому что «редактирование должно содержать не менее 6 символов», вздох).
Джо Фриман
1
поддержка scrollIntoViewwith smoothна данный момент очень слабая.
Andreykul 01
@Andreykul, похоже, я вижу аналогичные результаты с использованием «сглаживания». Это непоследовательно.
flimflam57
18

Спасибо @enlitement

мы должны избегать использования findDOMNode, мы можем использовать refsдля отслеживания компонентов

render() {
  ...

  return (
    <div>
      <div
        className="MessageList"
        ref={(div) => {
          this.messageList = div;
        }}
      >
        { messageListContent }
      </div>
    </div>
  );
}



scrollToBottom() {
  const scrollHeight = this.messageList.scrollHeight;
  const height = this.messageList.clientHeight;
  const maxScrollTop = scrollHeight - height;
  this.messageList.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
}

componentDidUpdate() {
  this.scrollToBottom();
}

ссылка:

jk2K
источник
Я считаю это решение наиболее подходящим, потому что оно не добавляет новые (фиктивные) элементы в DOM, а работает буквально с существующими, спасибо jk2k
devplayer
7

Ты можешь использовать ref s, чтобы отслеживать компоненты.

Если вы знаете, как установить refодин отдельный компонент (последний), пожалуйста, опубликуйте!

Вот что я обнаружил:

class ChatContainer extends React.Component {
  render() {
    const {
      messages
    } = this.props;

    var messageBubbles = messages.map((message, idx) => (
      <MessageBubble
        key={message.id}
        message={message.body}
        ref={(ref) => this['_div' + idx] = ref}
      />
    ));

    return (
      <div>
        {messageBubbles}
      </div>
    );
  }

  componentDidMount() {
    this.handleResize();

    // Scroll to the bottom on initialization
    var len = this.props.messages.length - 1;
    const node = ReactDOM.findDOMNode(this['_div' + len]);
    if (node) {
      node.scrollIntoView();
    }
  }

  componentDidUpdate() {
    // Scroll as new elements come along
    var len = this.props.messages.length - 1;
    const node = ReactDOM.findDOMNode(this['_div' + len]);
    if (node) {
      node.scrollIntoView();
    }
  }
}
helsont
источник
7

response-scrollable-feed автоматически прокручивается до последнего элемента, если пользователь уже находился внизу прокручиваемого раздела. В противном случае он оставит пользователя на том же месте. Думаю, это очень полезно для компонентов чата :)

Я думаю, что другие ответы здесь заставят прокрутку каждый раз, независимо от того, где была полоса прокрутки. Другая проблема scrollIntoViewзаключается в том, что он будет прокручивать всю страницу, если ваш прокручиваемый div не был в поле зрения.

Его можно использовать так:

import * as React from 'react'

import ScrollableFeed from 'react-scrollable-feed'

class App extends React.Component {
  render() {
    const messages = ['Item 1', 'Item 2'];

    return (
      <ScrollableFeed>
        {messages.map((message, i) => <div key={i}>{message}</div>)}
      </ScrollableFeed>
    );
  }
}

Просто убедитесь, что у вас есть компонент-оболочка с определенным height илиmax-height

Отказ от ответственности: я являюсь владельцем пакета

Габриэль Бурго
источник
Спасибо, я использовал твой контроль. Примечание: мне пришлось использовать forceScroll = true, чтобы заставить его работать по своему желанию, по какой-то причине он не прокручивался автоматически вверх, когда начала появляться полоса прокрутки.
Патрик
@Patric, если бы вы могли открыть проблему на GitHub с некоторыми подробностями о вашей настройке, может быть, мы сможем выяснить, что не так?
Габриэль Бурго
6
  1. Ссылка на свой контейнер сообщений.

    <div ref={(el) => { this.messagesContainer = el; }}> YOUR MESSAGES </div>
  2. Найдите свой контейнер сообщений и сделайте его scrollTopатрибут равным scrollHeight:

    scrollToBottom = () => {
        const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    };
  3. Вызов метода выше на componentDidMountи componentDidUpdate.

    componentDidMount() {
         this.scrollToBottom();
    }
    
    componentDidUpdate() {
         this.scrollToBottom();
    }

Вот как я использую это в своем коде:

 export default class StoryView extends Component {

    constructor(props) {
        super(props);
        this.scrollToBottom = this.scrollToBottom.bind(this);
    }

    scrollToBottom = () => {
        const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    };

    componentDidMount() {
        this.scrollToBottom();
    }

    componentDidUpdate() {
        this.scrollToBottom();
    }

    render() {
        return (
            <div>
                <Grid className="storyView">
                    <Row>
                        <div className="codeView">
                            <Col md={8} mdOffset={2}>
                                <div ref={(el) => { this.messagesContainer = el; }} 
                                     className="chat">
                                    {
                                        this.props.messages.map(function (message, i) {
                                            return (
                                                <div key={i}>
                                                    <div className="bubble" >
                                                        {message.body}
                                                    </div>
                                                </div>
                                            );
                                        }, this)
                                    }
                                </div>
                            </Col>
                        </div>
                    </Row>
                </Grid>
            </div>
        );
    }
}
Марцин Рапач
источник
6

Я создал пустой элемент в конце сообщений и перешел к нему. Нет необходимости отслеживать реф.

Тушар Агарвал
источник
как вы это сделали
Педро младший
@mmla С какой проблемой вы столкнулись в Safari? Не скроллирует надежно?
Тушар Агарвал,
5

Если вы хотите сделать это с помощью React Hooks, можно воспользоваться этим методом. В нижней части чата размещен фиктивный div. Здесь используется useRef Hook.

Справочник по API хуков: https://reactjs.org/docs/hooks-reference.html#useref

import React, { useEffect, useRef } from 'react';

const ChatView = ({ ...props }) => {
const el = useRef(null);

useEffect(() => {
    el.current.scrollIntoView({ block: 'end', behavior: 'smooth' });
});

 return (
   <div>
     <div className="MessageContainer" >
       <div className="MessagesList">
         {this.renderMessages()}
       </div>
       <div id={'el'} ref={el}>
       </div>
     </div>
    </div>
  );
}
Чатурика Сенани
источник
5

Я бы порекомендовал самый простой и лучший способ.

Моя версия ReactJS: 16.12.0


Структура HTML внутри render()функции

    render()
        return(
            <body>
                <div ref="messageList">
                    <div>Message 1</div>
                    <div>Message 2</div>
                    <div>Message 3</div>
                </div>
            </body>
        )
    )

scrollToBottom()функция, которая получит ссылку на элемент. и прокрутите в соответствии с scrollIntoView()функцией.

  scrollToBottom = () => {
    const { messageList } = this.refs;
    messageList.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
  }

и вызовите указанную выше функцию внутри componentDidMount()иcomponentDidUpdate()

для получения дополнительной информации Element.scrollIntoView()посетите developer.mozilla.org

Ахамед Рашид
источник
На самом деле ref должен быть объявлен в блоках сообщений, а не в контейнере
toing_toing
4

Мне не удалось получить ни один из следующих ответов, но простой js помог мне:

  window.scrollTo({
  top: document.body.scrollHeight,
  left: 0,
  behavior: 'smooth'
});
rilar
источник
3

Рабочий пример:

Вы можете использовать scrollIntoViewметод DOM, чтобы сделать компонент видимым в представлении.

Для этого при рендеринге компонента просто укажите ссылочный идентификатор для элемента DOM с помощью refатрибута. Затем с помощью метода scrollIntoViewна componentDidMountжизненном цикле. Я просто помещаю рабочий образец кода для этого решения. Ниже приведен компонент, отображающий каждый раз получение сообщения. Вы должны написать код / ​​методы для рендеринга этого компонента.

class ChatMessage extends Component {
    scrollToBottom = (ref) => {
        this.refs[ref].scrollIntoView({ behavior: "smooth" });
    }

    componentDidMount() {
        this.scrollToBottom(this.props.message.MessageId);
    }

    render() {
        return(
            <div ref={this.props.message.MessageId}>
                <div>Message content here...</div>
            </div>
        );
    }
}

Вот this.props.message.MessageIdуникальный идентификатор конкретного сообщения чата, переданного какprops

Шерин Хосе
источник
Удивительный Шерин Бхай, он работает как торт. Спасибо
Мохаммед Сарфараз
@MohammedSarfaraz Рад, что смог помочь :)
Шерин Хосе
2
import React, {Component} from 'react';

export default class ChatOutPut extends Component {

    constructor(props) {
        super(props);
        this.state = {
            messages: props.chatmessages
        };
    }
    componentDidUpdate = (previousProps, previousState) => {
        if (this.refs.chatoutput != null) {
            this.refs.chatoutput.scrollTop = this.refs.chatoutput.scrollHeight;
        }
    }
    renderMessage(data) {
        return (
            <div key={data.key}>
                {data.message}
            </div>
        );
    }
    render() {
        return (
            <div ref='chatoutput' className={classes.chatoutputcontainer}>
                {this.state.messages.map(this.renderMessage, this)}
            </div>
        );
    }
}
Паван Гарре
источник
1

Мне нравится делать это следующим образом.

componentDidUpdate(prevProps, prevState){
  this.scrollToBottom();
}

scrollToBottom() {
  const {thing} = this.refs;
  thing.scrollTop = thing.scrollHeight - thing.clientHeight;
}

render(){
  return(
    <div ref={`thing`}>
      <ManyThings things={}>
    </div>
  )
}
aestrro
источник
1

спасибо 'metakermit' за его хороший ответ, но я думаю, что мы можем сделать его немного лучше, для прокрутки вниз мы должны использовать это:

scrollToBottom = () => {
   this.messagesEnd.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
}

но если вы хотите прокрутить вверх, вы должны использовать это:

scrollToTop = () => {
   this.messagesEnd.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
}   

и эти коды общие:

componentDidMount() {
  this.scrollToBottom();
}

componentDidUpdate() {
  this.scrollToBottom();
}


render () {
  return (
    <div>
      <div className="MessageContainer" >
        <div className="MessagesList">
          {this.renderMessages()}
        </div>
        <div style={{ float:"left", clear: "both" }}
             ref={(el) => { this.messagesEnd = el; }}>
        </div>
      </div>
    </div>
  );
}
Абольфазл Миадиан
источник
0

В качестве другого варианта стоит рассмотреть компонент React scroll .

Джон
источник
0

С помощью React.createRef()

class MessageBox extends Component {
        constructor(props) {
            super(props)
            this.boxRef = React.createRef()
        }

        scrollToBottom = () => {
            this.boxRef.current.scrollTop = this.boxRef.current.scrollHeight
        }

        componentDidUpdate = () => {
            this.scrollToBottom()
        }

        render() {
            return (
                        <div ref={this.boxRef}></div>
                    )
        }
}
Акаша
источник
0

Вот как вы могли бы решить эту проблему в TypeScript (используя ссылку на целевой элемент, к которому вы прокручиваете):

class Chat extends Component <TextChatPropsType, TextChatStateType> {
  private scrollTarget = React.createRef<HTMLDivElement>();
  componentDidMount() {
    this.scrollToBottom();//scroll to bottom on mount
  }

  componentDidUpdate() {
    this.scrollToBottom();//scroll to bottom when new message was added
  }

  scrollToBottom = () => {
    const node: HTMLDivElement | null = this.scrollTarget.current; //get the element via ref

    if (node) { //current ref can be null, so we have to check
        node.scrollIntoView({behavior: 'smooth'}); //scroll to the targeted element
    }
  };

  render <div>
    {message.map((m: Message) => <ChatMessage key={`chat--${m.id}`} message={m}/>}
     <div ref={this.scrollTarget} data-explanation="This is where we scroll to"></div>
   </div>
}

Для получения дополнительной информации об использовании ref с React и Typescript вы можете найти отличную статью здесь .

Дэниел Будик
источник
-1

Полная версия (Машинопись):

import * as React from 'react'

export class DivWithScrollHere extends React.Component<any, any> {

  loading:any = React.createRef();

  componentDidMount() {
    this.loading.scrollIntoView(false);
  }

  render() {

    return (
      <div ref={e => { this.loading = e; }}> <LoadingTile /> </div>
    )
  }
}

TechTurtle
источник
это дает мне всевозможные ошибки: Property 'scrollIntoView' does not exist on type 'RefObject<unknown>'. и Type 'HTMLDivElement | null' is not assignable to type 'RefObject<unknown>'. Type 'null' is not assignable to type 'RefObject<unknown>'. так ...
dcsan 02
Версия ReactJS, пожалуйста? Я использую 1.16.0
TechTurtle 08