简体   繁体   中英

ReactDOM renders only one component

I want to create an app with comments feature. I am trying with the code like this:

response.data.forEach((el, idx, arr) => {
  const newMessage = <CommentMessage username={el.username} message={el.message}/>
  ReactDOM.render(newMessage, this.commentListRef.current)
})

I am using MySQL. Axios for HTTP Requests. And Next.js for the framework.

Full code:

import React from 'react'
import ReactDOM from 'react-dom'
import styles from './comments-list.module.css'

class CommentMessage extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return (<div>
            <b>{this.props.username}</b>
            <span>: </span>
            <span>{this.props.message}</span>
        </div>)
    }
}

class CommentsList extends React.Component {
    constructor(props) {
        super(props)

        this.commentListRef = React.createRef()

        const comments = []
    }
    loadComments() {
        const axios = require('axios')
        axios.get('/api/getcomments')
            .then(response => {
                response.data.forEach((el, idx, arr) => {
                    const newMessage = <CommentMessage username={el.username} message={el.message}/>
                    ReactDOM.render(newMessage, this.commentListRef.current)
                })
            })
            .catch(err => {
                console.log(err)
            })
    }
    render() {
        return (<div ref={this.commentListRef} onLoad={this.loadComments()}>

        </div>)
    }
}

export default CommentsList

But it only render this:

在此处输入图像描述

Expected this:

在此处输入图像描述

You're going about this pretty strangely; I don't know if that's on purpose or not. Regardless, the recommended approach would be to store the comments as part of your component's state, and update the state when you get the comments.

Like this:

class CommentsList extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
          comments: []
        };

        this.commentListRef = React.createRef()

        const comments = []
    }
    loadComments() {
        const axios = require('axios')
        axios.get('/api/getcomments')
            .then(response => {
              this.setState({
                comments: response.data
              });
            })
            .catch(err => {
                console.log(err)
            })
    }
    
    componentDidMount(){
      this.loadComments();
    }
    
    render() {
        return (<div ref={this.commentListRef}>
            (this.state.comments.map(comment => (
                <CommentMessage username={comment.username} message={comment.message}/>
            )))
        </div>)
    }
}

Also, your onLoad wasn't working as you had expected. It will call loadComments every time the component renders, and I don't even know if onLoad is a proper event on a div.

At any rate, if you absolutely wanted to do it the way you did it, you would have to mount each node into its own container. As you have it right now, each comment is overwriting the contents of commentListRef. So you'd have to create a new element, append that to commentListRef, and mount the react component to that:


   loadComments() {
       const axios = require('axios')
       axios.get('/api/getcomments')
           .then(response => {
               response.data.forEach((el, idx, arr) => {
                   const element = document.createElement('div');
                   this.commentListRef.current.appendChild(element);
                   const newMessage = <CommentMessage username={el.username} message={el.message}/>
                   ReactDOM.render(newMessage, element)
               })
           })
           .catch(err => {
               console.log(err)
           })
   }

ReactDOM.render will only render one component for a given container. From the docs :

Any existing DOM elements inside are replaced when first called. Later calls use React's DOM diffing algorithm for efficient updates.

Basically when you call ReactDOM.render in a loop, React is treating each given component as an update to the previous component, rather than rendering each individually.

Best practice is to render a single component at the root container (usually called <App> ). However it seems you've already done this as these ReactDOM.render calls are happening within another component. Generally, you should only need to use ReactDOM.render once within an app.

Instead you can store the data in the CommentsList component's state and just return the children components from the parent's render method.

For example:

class CommentsList extends React.Component {
    constructor(props) {
        super(props)
        
        this.state = {
            comments: [],
        }
    }

    loadComments = () => {
        const axios = require('axios')
        axios.get('/api/getcomments')
            .then(response => {
                this.setState(prev => ({...prev, comments: response.data}));
            })
            .catch(err => {
                console.log(err)
            })
    }
    
    render() {
        const { comments } = this.state;
        
        return (
            <React.Fragment>
                {comments.map(e => (
                    <CommentMessage key={e.id} username={e.username} message={e.message}/>
                ))}
            </React.Fragment>
        )
    }
}

Note: I've also passed a key to the CommentMessage component to give each child a stable identity (see docs for more info). Had to guess, but I assume a comment would have an id value, if not, you can choose a different unique value for the comment to use as a key.

Also I'd recommend moving to React Hooks over class-based components—a lot easier to work with once you get a grasp on hooks.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM