Now we have three Dumb components, a reducer that controls comments. What are we missing? Someone needs to go to LocalStorage to load data, to control adding and deleting comments, and to save data to LocalStorage. Before, we put these logics scattered in each component (mainly CommentApp
components), that is because at that time we didn't have the understanding of the type of Dumb and Smart components, and there was no such clear distinction between the state and the view.
And now we know that these logic should be placed in the Smart component:
Students who understand the MVC and MVP architectural patterns should be able to compare to the past. The Dumb component is the View (responsible for rendering), the Smart component is the Controller (Presenter), and the State is actually a bit similar to the Model. In fact, it cannot be completely compared to the past, and there are still many differences between them. But in essence, it still divides things into three layers, so it is true that the front end likes to fry the concept that others have already played badly. Without further ado, we will now extract these application logics into Smart components.
Smart CommentList
For the CommentList
component, you can see that it accepts two parameters: comments
and onDeleteComment
. The description needs a Smart component that is responsible for passing comments
data to it and also responding to its requests to delete comments. We create a new Smart component src/containers/CommentList.js
to do these things:
import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import CommentList from '../components/CommentList' import { initComments, deleteComment } from '../reducers/comments' // CommentListContainer // A Smart component responsible for loading, initializing, and deleting comments of comment list data // Communicate CommentList and state class CommentListContainer extends Component { static propTypes = { comments: PropTypes.array, initComments: PropTypes.func, onDeleteComment: PropTypes.func } componentWillMount () { // Initialize comments in componentWillMount lifecycle this ._loadComments() } _loadComments () { // Load comments from LocalStorage let comments = localStorage.getItem('comments' ) comments = comments ? JSON.parse(comments) : [] // this.props.initComments is passed in from connect // It can help us initialize data into state this .props.initComments(comments) } handleDeleteComment (index) { const { comments } = this .props // props cannot be changed, so here is a new list of comments with a specific subscript removed const newComments = [ ...comments.slice(0, index), ...comments.slice(index + 1) ] // Save the latest comment list to LocalStorage localStorage.setItem('comments' , JSON.stringify(newComments)) if ( this .props.onDeleteComment) { // this.props.onDeleteComment is passed in from connect // will dispatch one action to delete the comment this .props.onDeleteComment(index) } } render () { return ( <CommentList comments={this.props.comments} onDeleteComment={this.handleDeleteComment.bind(this)} /> ) } } // The list of comments is obtained from state.comments const mapStateToProps = (state) => { return { comments: state.comments } } const mapDispatchToProps = (dispatch) => { return { // provide to CommentListContainer // this method will be used after loading the comment list from LocalStorage // initialize the comment list into state initComments: (comments) => { dispatch (initComments (comments)) }, // Delete the comment onDeleteComment: (commentIndex) => { dispatch(deleteComment(commentIndex)) } } } // Connect CommentListContainer to store // Pass comments, initComments, onDeleteComment to CommentListContainer export default connect( mapStateToProps, mapDispatchToProps )(CommentListContainer)
The code is a bit long, and you should understand the basic logic of this component through comments. One thing to note is that what we initially passed to CommentListContainer
was props.comments
the empty array initialized in the reducer comments
, because the data has not been fetched from LocalStorage.
The CommentListContainer
internal comments
data is loaded from LocalStorage, and then the call this.props.initComments(comments)
will cause the initialization to be dispatch
actually loaded from LocalStorage into the state.comments
Because dispatch
the packaged components inside go to the state to get the latest one connect
and then re-render, only at this time can the data be obtained .Connect
comments
CommentListContainer
props.comments
The logic here is a bit confusing, you can review what we have implemented before react-redux.js
to experience it.
Smart CommentInput
For the CommentInput
component, we can see that it has three parameters: username
, onSubmit
, onUserNameInputBlur
. We need a Smart component to manage the loading and saving of user names in LocalStorage; the user may also click the "Publish" button, so we also need to deal with the logic of comment publishing. We create a new Smart component src/containers/CommentInput.js
to do these things:
import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import CommentInput from '../components/CommentInput' import { addComment } from '../reducers/comments' // CommentInputContainer // Responsible for loading and saving user names and publishing comments class CommentInputContainer extends Component { static propTypes = { comments: PropTypes.array, onSubmit: PropTypes.func } constructor () { super() this.state = { username: '' } } componentWillMount () { // Initialize username in componentWillMount lifecycle this ._loadUsername() } _loadUsername () { // Load username from LocalStorage // Then it can be passed to CommentInput in the render method const username = localStorage.getItem('username' ) if (username) { this .setState({ username }) } } _saveUsername (username) { // Look at the onUserNameInputBlur of the render method // This method will be called when the username input box is blurred and save the username localStorage.setItem('username' , username) } handleSubmitComment (comment) { // Verification of comment data if (!comment) return if (!comment.username) return alert('Please enter the username' ) if (!comment.content) return alert('Please enter the comment content' ) // Added Comments are saved to LocalStorage const { comments } = this .props const newComments = [...comments, comment] localStorage.setItem( 'comments' , JSON.stringify(newComments)) // this.props.onSubmit is passed in from connect // will dispatch an action to add a comment if ( this .props.onSubmit) { this .props. onSubmit(comment) } } render () { return ( <CommentInput username={this.state.username} onUserNameInputBlur={this._saveUsername.bind(this)} onSubmit={this.handleSubmitComment.bind(this)} /> ) } } const mapStateToProps = (state) => { return { comments: state.comments } } const mapDispatchToProps = (dispatch) => { return { onSubmit: (comment) => { dispatch(addComment(comment)) } } } export default connect( mapStateToProps, mapDispatchToProps )(CommentInputContainer)
Likewise, explanations of the code are placed in comments. This builds a Smart one CommentInput
.
Smart CommentApp
The next thing is very simple, we use CommentApp
to combine these two Smart components, src/CommentApp.js
move to src/containers/CommentApp.js
, and replace the content inside with:
import React, { Component } from 'react' import CommentInput from './CommentInput' import CommentList from './CommentList' export default class CommentApp extends Component { render() { return ( <div className='wrapper'> <CommentInput /> <CommentList /> </div> ) } }
What was originally complicated CommentApp
has become extremely simple, because its logic is separated into two Smart components. The original one CommentApp
does carry too many responsibilities that it shouldn't. Separating this logic will also bring benefits to the maintenance and management of our code.
The last step, modify src/index.js
:
import React from 'react' import ReactDOM from 'react-dom' import { createStore } from 'redux' import { Provider } from 'react-redux' import CommentApp from './containers/CommentApp' import commentsReducer from './reducers/comments' import './index.css' const store = createStore(commentsReducer) ReactDOM.render( <Provider store={store}> <CommentApp /> </Provider>, document.getElementById('root') );
By commentsReducer
building one store
, and then letting Provider
pass it down, we're done with the final refactoring.
Our final component tree looks like this:
File Directory:
src
├── components
│ ├── Comment.js
│ ├── CommentInput.js
│ └── CommentList.js
├── containers
│ ├── CommentApp.js
│ ├── CommentInput.js
│ └── CommentList.js
│ reducers
│ └── comments.js
├── index.css
└── index.js
All code can be found here: comment-app3 .