Comment faire défiler vers le bas pour réagir?

127

Je souhaite créer un système de chat et faire défiler automatiquement vers le bas lorsque vous entrez dans la fenêtre et lorsque de nouveaux messages arrivent. Comment faire défiler automatiquement vers le bas d'un conteneur dans React?

helsont
la source

Réponses:

222

Comme Tushar l'a mentionné, vous pouvez garder un div factice au bas de votre discussion:

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

puis faites défiler jusqu'à chaque fois que votre composant est mis à jour (c'est-à-dire que l'état est mis à jour lorsque de nouveaux messages sont ajoutés):

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

componentDidMount() {
  this.scrollToBottom();
}

componentDidUpdate() {
  this.scrollToBottom();
}

J'utilise la norme Element.scrollIntoView méthode ici.

metakermit
la source
3
avertissement de la documentation: "findDOMNode ne peut pas être utilisé sur les composants fonctionnels."
Tomasz Mularczyk
1
this.messagesEnd.scrollIntoView()a bien fonctionné pour moi. Il n'y avait aucun besoin d'utiliser findDOMNode().
Rajat Saxena
fonction modifiée scrollToBottom(){this.scrollBottom.scrollIntoView({ behavior: 'smooth' })}pour la faire fonctionner dans les versions plus récentes
Kunok
2
Ok, j'ai supprimé findDOMNode. Si cela ne fonctionne pas pour quelqu'un, vous pouvez consulter l'historique des modifications de la réponse.
metakermit
7
J'ai une erreur indiquant que scrollIntoView est TypeError: Impossible de lire la propriété 'scrollIntoView' d'undefined. Que faire?
Feruza
90

Je veux juste mettre à jour la réponse pour qu'elle corresponde à la nouvelle React.createRef() méthode , mais c'est fondamentalement la même chose, gardez simplement à l'esprit la currentpropriété dans la référence créée:

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

METTRE À JOUR:

Maintenant que les hooks sont disponibles, je mets à jour la réponse pour ajouter l'utilisation des hooks useRefet useEffect, la vraie chose à faire de la magie (Refs React et scrollIntoViewméthode DOM) reste la même:

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

Également créé un code et une boîte (très basique) si vous voulez vérifier le comportement https://codesandbox.io/s/scrolltobottomexample-f90lz

Diego Lara
la source
2
componentDidUpdate peut être appelé plusieurs fois dans le cycle de vie de React. Donc, nous devrions vérifier que ref this.messagesEnd.current existe ou non dans la fonction scrollToBottom. Si this.messagesEnd.current n'existe pas, le message d'erreur affichera TypeError: Impossible de lire la propriété 'scrollIntoView' de null. Donc, ajoutez cette condition if également scrollToBottom = () => {if (this.messagesEnd.current) {this.messagesEnd.current.scrollIntoView ({behavior: 'smooth'})}}
Arpit
componentDidUpdate se produit toujours après le premier rendu ( reactjs.org/docs/react-component.html#the-component-lifecycle ). Dans cet exemple, il ne doit y avoir aucune erreur et this.messagesEnd.currentexiste toujours. Néanmoins, il est important de noter que l'appel this.messagesEnd.currentavant le premier rendu entraînera l'erreur que vous avez signalée. Thnx.
Diego Lara
que this.messagesEndcontient votre premier exemple de la méthode scrollTo?
dcsan
@dcsan c'est une référence React, celles-ci sont utilisées pour garder une trace d'un élément DOM même après les rerenders. reactjs.org/docs/refs-and-the-dom.html#creating-refs
Diego Lara
1
Le deuxième exemple de code ne fonctionne pas. La useEffectméthode doit être placée avec () => {scrollToBottom()}. Merci beaucoup quand même
Gaspar
36

Ne pas utiliser findDOMNode

Composants de classe avec 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; }} />
  }
}

Composants fonctionnels avec crochets:

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

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

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

  return <div ref={divRef} />;
}
tgdn
la source
2
Pouvez-vous expliquer pourquoi vous ne devriez pas utiliser findDOMNode?
one stevy boi
2
@steviekins Parce que "cela bloque certaines améliorations dans React" et sera probablement obsolète github.com/yannickcr/eslint-plugin-react/issues/…
tgdn
2
Doit être l'orthographe américaine de behavior(impossible de modifier car "les modifications doivent comporter au moins 6 caractères", soupir).
Joe Freeman
1
le support pour scrollIntoViewavec smoothest très faible pour le moment.
Andreykul
@Andreykul, je semble voir des résultats similaires avec l'utilisation de «smooth». Ce n'est pas cohérent.
flimflam57
18

Merci à @enlitement

nous devrions éviter d'utiliser findDOMNode, nous pouvons utiliser refspour garder une trace des composants

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

référence:

jk2K
la source
Je trouve cette solution la plus appropriée, car elle n'ajoute pas de nouveaux éléments (factices) au DOM, mais traite littéralement de l'existant, merci jk2k
devplayer
7

Vous pouvez utiliser refs pour suivre les composants.

Si vous connaissez un moyen de définir le refcomposant d'un composant individuel (le dernier), veuillez poster!

Voici ce que j'ai trouvé a fonctionné pour moi:

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
la source
7

react-scrollable-feed fait automatiquement défiler jusqu'au dernier élément si l'utilisateur était déjà en bas de la section déroulante. Sinon, il laissera l'utilisateur dans la même position. Je pense que c'est assez utile pour les composants de chat :)

Je pense que les autres réponses ici forceront le défilement à chaque fois, peu importe où se trouvait la barre de défilement. L'autre problème avec scrollIntoViewest qu'il fera défiler toute la page si votre div à défilement n'était pas visible.

Il peut être utilisé comme ceci:

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

Assurez-vous simplement d'avoir un composant wrapper avec un heightoumax-height

Clause de non-responsabilité: je suis le propriétaire du colis

Gabriel Bourgault
la source
Merci, j'ai utilisé votre contrôle. Remarque: j'ai dû utiliser forceScroll = true, alors faites-le fonctionner comme vous le souhaitez, pour une raison quelconque, il ne défilait pas automatiquement vers le haut lorsque la barre de défilement a commencé à apparaître.
Patric
6
  1. Faites référence à votre conteneur de messages.

    <div ref={(el) => { this.messagesContainer = el; }}> YOUR MESSAGES </div>
  2. Trouvez votre conteneur de messages et rendez son scrollTopattribut égal scrollHeight:

    scrollToBottom = () => {
        const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    };
  3. Evoquez la méthode ci-dessus sur componentDidMountet componentDidUpdate.

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

Voici comment j'utilise ceci dans mon code:

 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
la source
6

J'ai créé un élément vide à la fin des messages et j'ai fait défiler jusqu'à cet élément. Pas besoin de garder une trace des références.

Tushar Agarwal
la source
comment as-tu fait
Pedro JR
@mmla Quel était le problème que vous avez rencontré avec Safari? Ne défile-t-il pas de manière fiable?
Tushar Agarwal
5

Si vous voulez faire cela avec React Hooks, cette méthode peut être suivie. Pour un div factice a été placé au bas de la discussion. useRef Hook est utilisé ici.

Référence de l'API 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
la source
5

Le moyen le plus simple et le meilleur que je recommanderais est.

Ma version ReactJS: 16.12.0


Structure HTML à l' intérieur de la render()fonction

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

scrollToBottom()fonction qui obtiendra la référence de l'élément. et faites défiler selon la scrollIntoView()fonction.

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

et appelez la fonction ci-dessus à l'intérieur componentDidMount()etcomponentDidUpdate()

pour plus d'explications sur la Element.scrollIntoView()visite developer.mozilla.org

Ahamed Rasheed
la source
La référence doit en fait être déclarée dans le message divs, pas dans le conteneur
toing_toing
4

Je n'ai pu obtenir aucune des réponses ci-dessous, mais de simples js ont fait l'affaire pour moi:

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

Exemple de travail:

Vous pouvez utiliser la scrollIntoViewméthode DOM pour rendre un composant visible dans la vue.

Pour cela, lors du rendu du composant, donnez simplement un ID de référence pour l'élément DOM à l'aide de l' refattribut. Ensuite, utilisez la méthode scrollIntoViewsur le componentDidMountcycle de vie. Je mets juste un exemple de code fonctionnel pour cette solution. Ce qui suit est un composant rendu à chaque fois qu'un message est reçu. Vous devez écrire du code / des méthodes pour rendre ce composant.

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

Voici this.props.message.MessageIdl'ID unique du message de discussion particulier passé commeprops

Sherin José
la source
Incroyable Sherin bhai, il fonctionne comme un gâteau.Merci
Mohammed Sarfaraz
@MohammedSarfaraz Heureux de pouvoir aider :)
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
la source
1

J'aime le faire de la manière suivante.

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
la source
1

merci 'metakermit' pour sa bonne réponse, mais je pense que nous pouvons l'améliorer un peu, pour faire défiler vers le bas, nous devrions utiliser ceci:

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

mais si vous voulez faire défiler vers le haut, vous devez utiliser ceci:

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

et ces codes sont communs:

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
la source
0

En utilisant 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
la source
0

Voici comment résoudre ce problème dans TypeScript (en utilisant la référence vers un élément ciblé vers lequel vous faites défiler):

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

Pour plus d'informations sur l'utilisation de ref avec React et Typescript, vous pouvez trouver un excellent article ici .

Daniel Budick
la source
-1

Version complète (Typescript):

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
la source
cela donne toutes sortes d'erreurs pour moi: Property 'scrollIntoView' does not exist on type 'RefObject<unknown>'. et Type 'HTMLDivElement | null' is not assignable to type 'RefObject<unknown>'. Type 'null' is not assignable to type 'RefObject<unknown>'. donc ...
dcsan
Version de ReactJS pls? J'utilise 1.16.0
TechTurtle