¿Cómo desplazarse hacia abajo en Reaccionar?

127

Quiero crear un sistema de chat y desplazarme automáticamente hacia la parte inferior cuando ingrese a la ventana y cuando entren nuevos mensajes. ¿Cómo se desplaza automáticamente hasta la parte inferior de un contenedor en React?

helsont
fuente

Respuestas:

222

Como mencionó Tushar, puedes mantener un div ficticio en la parte inferior de tu chat:

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>
  );
}

y luego desplácese hasta él cada vez que se actualice su componente (es decir, estado actualizado a medida que se agregan nuevos mensajes):

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

componentDidMount() {
  this.scrollToBottom();
}

componentDidUpdate() {
  this.scrollToBottom();
}

Estoy usando el método estándar Element.scrollIntoView aquí.

metakermit
fuente
3
advertencia de la documentación: "findDOMNode no se puede utilizar en componentes funcionales".
Tomasz Mularczyk
1
this.messagesEnd.scrollIntoView()funcionó bien para mí. No hubo necesidad de usar findDOMNode().
Rajat Saxena
función cambiada para scrollToBottom(){this.scrollBottom.scrollIntoView({ behavior: 'smooth' })}que funcione en versiones más nuevas
Kunok
2
Ok, eliminé findDOMNode. Si esto no funciona para alguien, puede consultar el historial de edición de la respuesta.
metakermit
7
Tengo un error de que scrollIntoView es TypeError: no se puede leer la propiedad 'scrollIntoView' de undefined. ¿Qué hacer?
Feruza
90

Solo quiero actualizar la respuesta para que coincida con el nuevo React.createRef() método , pero es básicamente lo mismo, solo tenga en cuenta la currentpropiedad en la referencia creada:

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>
    )
  }
}

ACTUALIZAR:

Ahora que los ganchos están disponibles, estoy actualizando la respuesta para agregar el uso de los ganchos useRefy useEffect, lo real que hace la magia (las referencias de React y el scrollIntoViewmétodo DOM) sigue siendo el mismo:

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>
  )
}

También hizo un codeandbox (muy básico) si desea verificar el comportamiento https://codesandbox.io/s/scrolltobottomexample-f90lz

Diego Lara
fuente
2
componentDidUpdate puede llamar muchas veces en el ciclo de vida de React. Por lo tanto, deberíamos comprobar que ref this.messagesEnd.current existe o no en la función scrollToBottom. Si this.messagesEnd.current no existe, el mensaje de error mostrará TypeError: No se puede leer la propiedad 'scrollIntoView' de null. Por lo tanto, agregue esta condición if también scrollToBottom = () => {if (this.messagesEnd.current) {this.messagesEnd.current.scrollIntoView ({behavior: 'smooth'})}}
Arpit
componentDidUpdate siempre ocurre después del primer renderizado ( reactjs.org/docs/react-component.html#the-component-lifecycle ). En este ejemplo, no debería haber errores y this.messagesEnd.currentsiempre existe. Sin embargo, es importante notar que llamar this.messagesEnd.currentantes del primer render resultará en el error que señaló. Thnx.
Diego Lara
¿Qué hay this.messagesEnden su primer ejemplo en el método scrollTo?
Dcsan
@dcsan es una referencia de React, se utilizan para realizar un seguimiento de un elemento DOM incluso después de la reproducción. reactjs.org/docs/refs-and-the-dom.html#creating-refs
Diego Lara
1
El segundo código de ejemplo no funciona. El useEffectmétodo debe colocarse con () => {scrollToBottom()}. Muchas gracias de todos modos
Gaspar
36

No utilice findDOMNode

Componentes de clase con 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; }} />
  }
}

Componentes funcionales con ganchos:

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

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

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

  return <div ref={divRef} />;
}
tgdn
fuente
2
¿Puede explicar por qué no debería usar findDOMNode?
un stevy boi
2
@steviekins Porque "bloquea ciertas mejoras en React" y probablemente quedará obsoleto github.com/yannickcr/eslint-plugin-react/issues/…
tgdn
2
Debe ser la ortografía estadounidense de behavior(no se puede editar porque "las ediciones deben tener al menos 6 caracteres", suspiro).
Joe Freeman
1
El soporte para scrollIntoViewcon smoothes muy pobre en este momento.
Andreykul
@Andreykul, parece que veo resultados similares con el uso de 'suave'. No es consistente.
flimflam57
18

Gracias a @enlitement

debemos evitar usar findDOMNode, podemos usar refspara realizar un seguimiento de los componentes

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();
}

referencia:

jk2K
fuente
Encuentro esta solución más apropiada, porque no agrega elementos nuevos (ficticios) al DOM, sino que trata literalmente con los existentes, gracias jk2k
devplayer
7

Puedes usar ref s para realizar un seguimiento de los componentes.

Si conoce una forma de establecer el valor refde un componente individual (el último), ¡publíquelo!

Esto es lo que encontré que funcionó para mí:

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

react-scrollable-feed se desplaza automáticamente hacia abajo hasta el último elemento si el usuario ya estaba en la parte inferior de la sección desplazable. De lo contrario, dejará al usuario en la misma posición. Creo que esto es bastante útil para los componentes de chat :)

Creo que las otras respuestas aquí forzarán el desplazamiento cada vez, sin importar dónde esté la barra de desplazamiento. El otro problema scrollIntoViewes que se desplazará por toda la página si su div desplazable no estaba a la vista.

Se puede usar así:

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>
    );
  }
}

Sólo asegúrese de tener un componente contenedor con una específica heightomax-height

Descargo de responsabilidad: soy el propietario del paquete

Gabriel Bourgault
fuente
Gracias, usé tu control. Nota: tuve que usar forceScroll = true, así que haz que funcione como lo deseas, por alguna razón no se desplazó automáticamente hacia la parte superior cuando la barra de desplazamiento comenzó a aparecer.
Patric hace
6
  1. Haga referencia a su contenedor de mensajes.

    <div ref={(el) => { this.messagesContainer = el; }}> YOUR MESSAGES </div>
  2. Encuentra tu contenedor de mensajes y haz que su scrollTopatributo sea igual scrollHeight:

    scrollToBottom = () => {
        const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    };
  3. Evocar el método anterior en componentDidMounty componentDidUpdate.

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

Así es como estoy usando esto en mi código:

 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>
        );
    }
}
Marcin Rapacz
fuente
6

Creé un elemento vacío al final de los mensajes y me desplacé hasta ese elemento. No es necesario realizar un seguimiento de las referencias.

Tushar Agarwal
fuente
cómo lo hiciste
Pedro JR
@mmla ¿Cuál fue el problema que enfrentó con Safari? ¿No se desplaza de forma fiable?
Tushar Agarwal
5

Si desea hacer esto con React Hooks, puede seguir este método. Porque se ha colocado un div ficticio en la parte inferior del chat. useRef Hook se utiliza aquí.

Referencia de la API de Hooks: 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>
  );
}
Chathurika Senani
fuente
5

La mejor y más fácil forma que recomendaría es.

Mi versión de ReactJS: 16.12.0


Estructura HTML dentro de la render()función

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

scrollToBottom()función que obtendrá la referencia del elemento. y desplácese según la scrollIntoView()función.

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

y llama a la función anterior dentro componentDidMount()ycomponentDidUpdate()

para obtener más explicaciones sobre la Element.scrollIntoView()visita developer.mozilla.org

Ahamed Rasheed
fuente
La referencia debe declararse en los divs del mensaje, no en el contenedor
toing_toing
4

No pude hacer que ninguna de las respuestas a continuación funcionara, pero simples js me hicieron el truco:

  window.scrollTo({
  top: document.body.scrollHeight,
  left: 0,
  behavior: 'smooth'
});
rilar
fuente
3

Ejemplo de trabajo:

Puede utilizar el scrollIntoViewmétodo DOM para hacer que un componente sea visible en la vista.

Para esto, mientras renderiza el componente, simplemente proporcione una ID de referencia para el elemento DOM usando el refatributo. Luego usa el método scrollIntoViewdel componentDidMountciclo de vida. Solo estoy poniendo un código de muestra funcional para esta solución. El siguiente es un componente que se representa cada vez que se recibe un mensaje. Debe escribir código / métodos para renderizar este componente.

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>
        );
    }
}

Aquí this.props.message.MessageIdestá el ID único del mensaje de chat en particular pasado comoprops

Sherin José
fuente
Increíble Sherin Bhai está funcionando como un pastel.Gracias
Mohammed Sarfaraz
@MohammedSarfaraz Me alegro de haber podido ayudar :)
Sherin Jose
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>
        );
    }
}
Pavan Garre
fuente
1

Me gusta hacerlo de la siguiente manera.

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
fuente
1

gracias 'metakermit' por su buena respuesta, pero creo que podemos hacerlo un poco mejor, para desplazarse hacia abajo, deberíamos usar esto:

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

pero si desea desplazarse hacia arriba, debe usar esto:

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

y estos códigos son comunes:

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>
  );
}
Abolfazl Miadian
fuente
0

Utilizando 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>
                    )
        }
}
Akash
fuente
0

Así es como resolvería esto en TypeScript (usando la referencia a un elemento de destino al que se desplaza):

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

Para obtener más información sobre el uso de ref con React y Typescript, puede encontrar un excelente artículo aquí .

Daniel Budick
fuente
-1

Versión completa (mecanografiado):

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
fuente
esto me da todo tipo de errores: Property 'scrollIntoView' does not exist on type 'RefObject<unknown>'. y Type 'HTMLDivElement | null' is not assignable to type 'RefObject<unknown>'. Type 'null' is not assignable to type 'RefObject<unknown>'. así ...
dcsan
Versión de ReactJS pls? Estoy usando 1.16.0
TechTurtle