Jak przewinąć do dołu w reakcji?

127

Chcę zbudować system czatu i automatycznie przewijać do dołu po wejściu do okna i kiedy przychodzą nowe wiadomości. Jak automatycznie przewijać do dołu kontenera w Reakcie?

helsont
źródło

Odpowiedzi:

222

Jak wspomniał Tushar, na dole czatu możesz umieścić fikcyjny element 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>
  );
}

a następnie przewiń do niego przy każdej aktualizacji komponentu (tj. stan aktualizowany po dodaniu nowych wiadomości):

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

componentDidMount() {
  this.scrollToBottom();
}

componentDidUpdate() {
  this.scrollToBottom();
}

Używam tutaj standardowej metody Element.scrollIntoView .

metakermit
źródło
3
ostrzeżenie z dokumentacji: "findDOMNode nie może być używany na komponentach funkcjonalnych."
Tomasz Mularczyk
1
this.messagesEnd.scrollIntoView()działało dobrze dla mnie. Nie było potrzeby używania findDOMNode().
Rajat Saxena
zmieniono funkcję, scrollToBottom(){this.scrollBottom.scrollIntoView({ behavior: 'smooth' })}aby działała w nowszych wersjach
Kunok
2
Ok, usunąłem findDOMNode. Jeśli to nie zadziała, możesz sprawdzić historię edycji odpowiedzi.
metakermit
7
Mam błąd, że scrollIntoView jest TypeError: Nie można odczytać właściwości „scrollIntoView” niezdefiniowanej. Co robić?
Feruza,
90

Chcę tylko zaktualizować odpowiedź, aby pasowała do nowej React.createRef() metody , ale w zasadzie jest taka sama, pamiętaj tylko o currentwłaściwości w utworzonym ref:

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

AKTUALIZACJA:

Teraz, gdy hooki są dostępne, aktualizuję odpowiedź, dodając użycie haków useRefi useEffect, prawdziwa rzecz, która robi magię (refs React i scrollIntoViewmetoda DOM) pozostaje taka sama:

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

Stworzyłem również (bardzo podstawowe) pole kodów, jeśli chcesz sprawdzić zachowanie https://codesandbox.io/s/scrolltobottomexample-f90lz

Diego Lara
źródło
2
componentDidUpdate może wywoływać wiele razy w cyklu życia React. Powinniśmy więc sprawdzić, czy ref this.messagesEnd.current istnieje lub nie ma w funkcji scrollToBottom. Jeśli this.messagesEnd.current nie istnieje, w komunikacie o błędzie zostanie wyświetlony błąd TypeError: Cannot read property „scrollIntoView” of null. Więc dodaj ten warunek if również scrollToBottom = () => {if (this.messagesEnd.current) {this.messagesEnd.current.scrollIntoView ({zachowanie: 'smooth'})}}
Arpit
componentDidUpdate zawsze ma miejsce po pierwszym renderowaniu ( actjs.org/docs/react-component.html#the-component-lifecycle ). W tym przykładzie nie powinno być błędów i this.messagesEnd.currentzawsze istnieje. Niemniej jednak ważne jest, aby zauważyć, że wywołanie this.messagesEnd.currentprzed pierwszym renderowaniem spowoduje wskazany błąd. Thnx.
Diego Lara
co jest this.messagesEndw Twoim pierwszym przykładzie w metodzie scrollTo?
dcsan
@dcsan to ref React, są one używane do śledzenia elementu DOM nawet po ponownym renderowaniu. actjs.org/docs/refs-and-the-dom.html#creating-refs
Diego Lara
1
Drugi przykładowy kod nie działa. useEffectSposób muszą być umieszczone z () => {scrollToBottom()}. W każdym razie bardzo dziękuję
Gaspar
36

Nie używać findDOMNode

Komponenty klasy z 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; }} />
  }
}

Elementy funkcyjne z haczykami:

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

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

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

  return <div ref={divRef} />;
}
tgdn
źródło
2
Czy możesz wyjaśnić, dlaczego nie powinieneś używać findDOMNode?
jeden stevy boi,
2
@steviekins Ponieważ „blokuje pewne ulepszenia w React” i prawdopodobnie zostanie wycofane github.com/yannickcr/eslint-plugin-react/issues/ ...
tgdn
2
Powinna być pisownia amerykańska behavior(nie można edytować, ponieważ „edycja musi mieć co najmniej 6 znaków”, westchnij).
Joe Freeman,
1
wsparcie dla usługi scrollIntoViewwith smoothjest obecnie bardzo słabe.
Andreykul
@Andreykul, wydaje mi się, że widzę podobne wyniki przy użyciu „smooth”. To nie jest spójne.
flimflam57
18

Dzięki @enlitement

powinniśmy unikać używania findDOMNode, możemy użyć refsdo śledzenia komponentów

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

odniesienie:

jk2K
źródło
Uważam to rozwiązanie najwłaściwsze, ponieważ nie dodaje nowych elementów (obojętne) do DOM, ale zajmuje dosłownie z istniejących, dzięki jk2k
devplayer
7

Możesz użyć refs do śledzenia komponentów.

Jeśli wiesz, jak ustawić refpojedynczy komponent (ostatni), napisz!

Oto, co okazało się dla mnie skuteczne:

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
źródło
7

reakcja-przewijalne-źródło automatycznie przewija w dół do ostatniego elementu, jeśli użytkownik był już na dole przewijalnej sekcji. W przeciwnym razie pozostawi użytkownika w tej samej pozycji. Myślę, że jest to całkiem przydatne w przypadku komponentów czatu :)

Myślę, że inne odpowiedzi tutaj wymuszą przewijanie za każdym razem, bez względu na to, gdzie był pasek przewijania. Innym problemem scrollIntoViewjest to, że przewija całą stronę, jeśli nie widać przewijalnego elementu div.

Można go używać w następujący sposób:

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

Po prostu upewnij się, że masz komponent opakowania z określonym heightlubmax-height

Zastrzeżenie: jestem właścicielem pakietu

Gabriel Bourgault
źródło
Dzięki, użyłem twojej kontroli. Uwaga: musiałem użyć forceScroll = true, więc spraw, aby działało zgodnie z oczekiwaniami, z jakiegoś powodu nie przewijało się automatycznie do góry, gdy zaczął się pojawiać pasek przewijania.
Patric
6
  1. Odwołaj się do kontenera wiadomości.

    <div ref={(el) => { this.messagesContainer = el; }}> YOUR MESSAGES </div>
  2. Znajdź kontener wiadomości i wyrównaj jego scrollTopatrybut scrollHeight:

    scrollToBottom = () => {
        const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    };
  3. Wywołaj powyższą metodę na componentDidMounti componentDidUpdate.

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

Oto jak używam tego w moim kodzie:

 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
źródło
6

Utworzyłem pusty element na końcu wiadomości i przewinąłem do tego elementu. Nie ma potrzeby śledzenia referencji.

Tushar Agarwal
źródło
jak to zrobiłeś
Pedro JR
@mmla Jaki był problem, który napotkałeś podczas korzystania z Safari? Czy przewija się niezawodnie?
Tushar Agarwal
5

Jeśli chcesz to zrobić z React hookami, możesz zastosować tę metodę. Fikcyjny div został umieszczony na dole czatu. useRef Hook jest używany tutaj.

Dokumentacja interfejsu API hooków: 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
źródło
5

Najłatwiejszy i najlepszy sposób, który bym polecił, to.

Moja wersja ReactJS: 16.12.0


Struktura HTML wewnątrz render()funkcji

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

scrollToBottom()funkcja, która uzyska odniesienie do elementu. i przewijaj zgodnie z scrollIntoView()funkcją.

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

i wywołaj powyższą funkcję wewnątrz componentDidMount()icomponentDidUpdate()

aby uzyskać więcej informacji na temat Element.scrollIntoView()odwiedź developer.mozilla.org

Ahamed Rasheed
źródło
Odnośnik powinien być faktycznie zadeklarowany w div wiadomości, a nie w kontenerze
toing_toing
4

Nie udało mi się uzyskać żadnej z poniższych odpowiedzi, ale proste js załatwiły sprawę:

  window.scrollTo({
  top: document.body.scrollHeight,
  left: 0,
  behavior: 'smooth'
});
rilar
źródło
3

Przykład roboczy:

Możesz użyć scrollIntoViewmetody DOM , aby wyświetlić komponent w widoku.

W tym celu podczas renderowania komponentu po prostu podaj identyfikator odniesienia dla elementu DOM za pomocą refatrybutu. Następnie użyć metody scrollIntoVieww componentDidMountcyklu życia. Właśnie umieszczam działający przykładowy kod tego rozwiązania. Poniżej przedstawiono komponent renderujący za każdym razem, gdy otrzymana wiadomość. Powinieneś napisać kod / metody renderowania tego komponentu.

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

Oto this.props.message.MessageIdunikalny identyfikator konkretnej wiadomości czatu przekazanej jakoprops

Sherin Jose
źródło
Niesamowity Sherin bhai działa jak ciasto. Dziękuję
Mohammed Sarfaraz
@MohammedSarfaraz Cieszę się, że mogłem pomóc :)
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
źródło
1

Lubię to robić w następujący sposób.

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

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

render(){
  return(
    <div ref={`thing`}>
      <ManyThings things={}>
    </div>
  )
}
estrro
źródło
1

dziękuję „metakermit” za jego dobrą odpowiedź, ale myślę, że możemy to trochę ulepszyć, aby przewinąć do dołu, powinniśmy użyć tego:

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

ale jeśli chcesz przewijać do góry, użyj tego:

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

i te kody są wspólne:

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
źródło
0

Jako kolejną opcję warto przyjrzeć się komponentowi React scroll .

Jan
źródło
0

Za pomocą 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
źródło
0

Oto jak można to rozwiązać w TypeScript (używając ref do docelowego elementu, do którego przewijasz):

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

Aby uzyskać więcej informacji na temat używania ref z React i Typescript, możesz znaleźć świetny artykuł tutaj .

Daniel Budick
źródło
-1

Pełna wersja (maszynopis):

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
źródło
to daje mi wszelkiego rodzaju błędy: Property 'scrollIntoView' does not exist on type 'RefObject<unknown>'. a Type 'HTMLDivElement | null' is not assignable to type 'RefObject<unknown>'. Type 'null' is not assignable to type 'RefObject<unknown>'. więc ...
dcsan
Wersja ReactJS pls? Używam 1.16.0
TechTurtle,