8
\$\begingroup\$

I made this as a practice application to learn React. It is simply a box where someone can enter their name and a comment and submit it, and display other comments. This animated GIF illustrates what it looks like:

comments

Note: Please don't be too critical of the looks, I haven't spent a lot of time styling it with CSS. Also those small vertical lines shown with the typing were introduced by imgur when I uploaded the gif to it, those don't appear on the actual page.


There is not a data persistence server/layer to speak of, as it is not meant to be actually deployed, so I simply keep the data into a regular array of objects, like so:

var comments = [
  {
    id: 1,
    author: "John Q. Commenter",
    text: "This is the first comment.",
    timestamp: new Date()
  },
  {
    id: 2,
    author: "Jane A. Opinionated",
    text: "This is the second comment.",
    timestamp: new Date()
  },
  /* Test for empty comment */
  {
    id: 3
  }
]

I went with a Component/Container design approach, where the container is responsible for the business logic, and the container is only responsible for rendering the appearance of the objects on the page.

There are 4 components together in this hierarchy:

CommentBox
|- CommentList
   |- Comment
|- CommentForm

The data flow for comments is essentially: CommentForm -> comments (array) -> CommentList -> Comment.

I will focus on the React files only, but if you are curious about the rest you can look at the repository on GitHub.

Note that I left out the import React from 'react' and export default lines because every file begins and ends with those, respectively.


index.jsx

This is the entry point:

import CommentBox from './containers/CommentBoxContainer'

class App extends React.Component {
  render() {
    return (
      <div>
        <CommentBox/>
      </div>

    )
  }
}

ReactDOM.render(
  <App/>,
  document.getElementById('app')
)

CommentBox

This component is meant to hold all the other elements together:

import CommentListContainer from '../containers/CommentListContainer'
import CommentFormContainer from '../containers/CommentFormContainer'

const CommentBox = (props) => (
  <div className="commentBox">
    <h1 className="commentBoxHeader">
      Comments
    </h1>
    <CommentListContainer data={props.data}/>
    <CommentFormContainer />
  </div>
)

CommentBox.propTypes = {
  data: React.PropTypes.object.isRequired
}

CommentBoxContainer

This container doesn't contain any business logic.

import CommentBox from '../components/CommentBox'

class CommentBoxContainer extends React.Component {

  constructor(props) {
    super(props)
  }

  render() {
    return (
      <CommentBox/>
    )
  }
}

CommentList

This component has a function to map the fields contained in each object in the comments array to be displayed each into a Comment component, which are then injected into a <div>.

import CommentContainer from '../containers/CommentContainer'

const CommentList = (props) => {
  // Parse comment data into a list of comments to be rendered:
  let commentNodes = props.data.map(function(comment) {
    return (
      <CommentContainer
        id={comment.id}
        author={comment.author}
        timestamp={comment.timestamp}
      >
        {comment.text}
      </CommentContainer>
    )
  })
  return (
    <div className="commentList">
      {commentNodes}
    </div>
  )
}

CommentList.propTypes = {
  data: React.PropTypes.array
}

CommentListContainer

The container is responsible for polling the comments array for data changes at a specific interval (I set it for 2 seconds), and if changes are detected it will render an updated CommentList to the page.

Note that I am aware that this would be better done with websockets or some other mechanism, I just didn't get around to implementing that yet

import CommentList from '../components/CommentList'
import comments from '../data/comments'

class CommentListContainer extends React.Component {

  constructor(props) {
    super(props)
    this.state = {
      data: [],
      numComments: 0
    }
    this.POLL_INTERVAL = 2000
    this.updateComments = this.updateComments.bind(this)
    this.poll = this.poll.bind(this)
  }

  componentDidMount() {
    this._timer = setInterval(this.poll, this.POLL_INTERVAL)
    if (comments) {
      this.updateComments()
    }
  }

  componentWillUnmount() {
    if (this._timer) {
      clearInterval(this._timer)
      this._timer = null
    }
  }

  updateComments() {
    this.setState({
      data: comments,
      numComments: this.state.data.length
    })
  }

  poll() {
    if (comments && comments.length != this.state.numComments) {
      this.updateComments()
    }
  }

  render() {
    if (this.state.data !== []) {
      return (
        <CommentList data={this.state.data}/>
      )
    }
  }
}

Comment

This renders the actual comment formatting and content onto the page, after the data is passed from the CommentList. Each className attribute maps to a CSS class. I added some defaultProps to deal with missing fields in the objects, as can be seen in the 3rd comment in the screenshot.

I elected not to set a default value for timestamp and instead replace it with the string "unknown date" if it was missing/undefined.

import formatTimestamp from '../utils/formatTimestamp'

const Comment = (props) => (
  <div className="comment">
    <p className="commentText">
      {props.children}
    </p>
    <span className="commentAuthor">
      - {props.author}
    </span>
    <span className="commentTimestamp">
      , posted on {props.timestamp ? formatTimestamp(props.timestamp) : "unknown date"}
    </span>
  </div>
)

Comment.defaultProps = {
  author:   <em>(author unknown)</em>,
  children: <em>(empty)</em>
}

Comment.propTypes = {
  author:     React.PropTypes.string.isRequired,
  children:   React.PropTypes.any.isRequired,
  timestamp:  React.PropTypes.string.isRequired
}

It also uses a simple formatting function that I made, called formatTimestamp, to display it as US-formatted locale like "Sunday, Oct 23, 2016, 10:53 PM".

function formatTimestamp(timestamp) {
  var options = {
    weekday: "long",
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit"
  }
  return timestamp.toLocaleTimeString("en-us", options)
}

CommentContainer

This container also has no business logic in it.

import Comment from '../components/Comment'

class CommentContainer extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    return (
      <Comment
        author={this.props.author}
        timestamp={this.props.timestamp}
      >
        {this.props.children}
      </Comment>
    )
  }
}

CommentForm

This component renders 2 text inputs, a submit button, and a Twitter-style character counter. I passed it 3 callback functions from CommentFormContainer to handle onSubmit and onChange events and thus overriding the browser's automatic handling of the inputs.

const CommentForm = (props) => (
  <form className="commentForm" onSubmit={props.onSubmit}>
    <h3 className="commentFormHeader">Post a comment:</h3>
    <div>
      <label htmlFor="author">Name: </label>
      <input
        type="text"
        name="author"
        placeholder="Your name"
        value={props.author}
        onChange={props.onAuthorChange}
      />
    </div>
    <div>
      <label htmlFor="comment">Comment: </label>
      <br />
      <textarea
        placeholder="What's on your mind?"
        name="comment"
        value={props.comment}
        onChange={props.onCommentChange}
      />
    </div>
    <button type="submit">Post</button>
    <p>{props.charsLeft}/{props.charLimit}</p>
  </form>
)

CommentForm.defaultProps = {
  author: '',
  comment: '',
  charsLeft: 0,
  charLimit: 0
}

CommentForm.propTypes = {
  author:           React.PropTypes.string.isRequired,
  comment:          React.PropTypes.string.isRequired,
  charsLeft:        React.PropTypes.number.isRequired,
  onAuthorChange:   React.PropTypes.func.isRequired,
  onCommentChange:  React.PropTypes.func.isRequired
}

CommentFormContainer

This container is responsible for a few things:

  • Handle onChange and onSubmit events passed from CommentForm

  • Set and enforce the character limit

  • Build and push new comment objects into the comments array

  • Future: give each entry a unique ID

Its state contains the data that is entered on the form as well as the count of characters that are in the <textarea> input.

import CommentForm from '../components/CommentForm'
import comments from '../data/comments'

const CHARACTER_LIMIT = 200

class CommentFormContainer extends React.Component {

  constructor(props) {
    super(props)
    this.state = {
      author: '',
      comment: '',
      charsLeft: CHARACTER_LIMIT
    }
    this.handleAuthorChange = this.handleAuthorChange.bind(this)
    this.handleCommentChange = this.handleCommentChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
  }

  handleAuthorChange(event) {
    this.setState({ author: event.target.value })
  }

  handleCommentChange(event) {
    var input = event.target.value
    var length = input.length
    if (length <= CHARACTER_LIMIT) {
      this.setState({
        comment: input,
        charsLeft: CHARACTER_LIMIT - length
      })
    }
  }

  handleSubmit(event) {
    event.preventDefault()
    let author = this.state.author
    let comment = this.state.comment
    let timestamp = new Date()
    if (!author || !comment) {
      return
    }
    // TODO generate an actual ID, such as GUID/UUID
    comments.push({
      id: 42,
      author: this.state.author,
      text: this.state.comment,
      timestamp: timestamp
    })
    this.setState({
      author: '',
      comment: '',
      charsLeft: CHARACTER_LIMIT
    })
  }

  render() {
    return (
      <CommentForm
        author={this.state.author}
        comment={this.state.comment}
        charsLeft={this.state.charsLeft}
        charLimit={CHARACTER_LIMIT}
        onAuthorChange={this.handleAuthorChange}
        onCommentChange={this.handleCommentChange}
        onSubmit={this.handleSubmit}
      />
    )
  }
}
\$\endgroup\$
4
\$\begingroup\$

I see no particularly compelling reason to make CommentBoxContainer a class instead of a Stateless Functional Component, like most of the rest of the items here.

CommentContainer also could be a SFC.


In CommentListContainer, I would set:

this.state = { data: this.props.data || [], numComments: this.props.data && this.props.data.length || 0 }

However, that is probably likely exactly the same as setting the data initially in componentDidMount, I've just not currently used it like that, as all of my components tend to not hang around, so constructor is always called right before componentDidMount().

I'd also consider moving the poll query up to CommentBox, considering that's where you start using the data var in your chain.


I also see that CommentBox has a required prop for data, but you're not actually using it. So that can probably be removed.


I'm a bit confused by the decision to have a separate CommentForm and CommentFormContainer -- I mean, I guess if you were planning to support getting data from some source other than React State, that might make sense.. but otherwise, it seems a bit convoluted to have a component and then have it's state kept in another component.

\$\endgroup\$
  • \$\begingroup\$ Thanks, Phrancis, I haven't been around here much and completely forgot about formatting. Much appreciated, and I'll try to do better next time. \$\endgroup\$ – Eric Blade Nov 2 '16 at 4:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.