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:
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
andonSubmit
events passed fromCommentForm
Set and enforce the character limit
Build and push new comment objects into the
comments
arrayFuture: 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}
/>
)
}
}