We will create a simple but realistic comment module for the blog, a simplified analogue of the real-time comment module offered by resources such as Disqus, LiveFyre and Facebook.
We will provide:
Will also be implemented:
Before proceeding to the manual, we need to start the server. It is a simple API that we will use to receive and store data. We have already written it for you in several interpreted languages, it has the minimum necessary functionality. You can read the source code or download a zip archive containing everything you need.
In this guide, we will try to implement everything as simple as possible. In the archive that we mentioned above, you will find an HTML file in which we will continue to work. Open the public / index.html file in your code editor. It should look like this:
<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>React Tutorial</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react-dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script> </head> <body> <div id="content"></div> <script type="text/babel" src="scripts/example.js"></script> <!-- --> <script type="text/babel"> // script, scripts/example.js // . </script> </body> </html>
All the JavaScript code from the manual we will write in the script tag. Since we have not implemented live-reloading, you will have to refresh the project page in the browser every time after saving changes. You can track your results by opening the http: // localhost: 3000 link in your browser (after starting the server). When you open the link for the first time, without any code changes, you will see the final version of our comment module. In order to get started, you must remove the first script tag that loads the code for the final version of the project "scripts / example.js" .
Note: We use jQuery in our project to simplify the code of our future ajax requests, but this is NOT a required library for React to work.
React is a modular, composable framework. Our project consists of several components located in the following structure:
Create a CommentBox component that will be a regular <div>
tag at the output:
// tutorial1.js var CommentBox = React.createClass({ render: function() { return ( <div className="commentBox"> Hello, world! I am a CommentBox. </div> ); } }); ReactDOM.render( <CommentBox />, document.getElementById('content') );
Note that the names of HTML elements begin with a small letter, while the names of React classes are large.
The first thing that catches your eye is the XML-like syntax in the submitted JavaScript code. We use a simple precompiler that produces pure javascript at the output:
// tutorial1-raw.js var CommentBox = React.createClass({displayName: 'CommentBox', render: function() { return ( React.createElement('div', {className: "commentBox"}, "Hello, world! I am a CommentBox." ) ); } }); ReactDOM.render( React.createElement(CommentBox, null), document.getElementById('content') );
It is not necessary to use precompilers; you can write in pure JavaScript, but in our guide we will use the JSX syntax, in our opinion, it is simpler and clearer. You can read more about it on the page of the JSX Syntax article .
We pass a JavaScript object with several methods to React.createClass () to create a new React component. The most important of the passed methods is called render , it returns the React tree of components, which will eventually be converted to HTML.
<div>
tags are not real DOM nodes, this is an implementation of React <div>
components. You can consider them as markers or pieces of data that React knows how to process. React is safe in terms of XSS vulnerabilities.
You do not need to return HTML code. You can return the component tree that you (or someone else) have created. This approach makes React composable: a key sign of a frontend's maintainable and well-designed architecture.
ReactDOM.render () creates an instance of the root component, runs the framework, and inserts the markup into the DOM element passed in with the second argument.
The ReactDom object contains methods for working with the DOM, while the React object contains the root methods used in other libraries, such as React Native .
The call to ReactDOM.render should be made after the declaration of all components. It is important.
Create a skeleton for CommentList and CommentForm , which will be the usual <div>
. Add these two components to your file, leaving CommentBox and ReactDOM.render in the previous example in place:
// tutorial2.js var CommentList = React.createClass({ render: function() { return ( <div className="commentList"> Hello, world! I am a CommentList. </div> ); } }); var CommentForm = React.createClass({ render: function() { return ( <div className="commentForm"> Hello, world! I am a CommentForm. </div> ); } });
Next, make changes to the CommentBox component to use our new components (lines marked "// new"):
// tutorial3.js var CommentBox = React.createClass({ render: function() { return ( <div className="commentBox"> //new start <h1>Comments</h1> <CommentList /> <CommentForm /> //new end </div> ); } });
Notice how we mix the HTML tags and components that we created. HTML components are standard React components, as well as those that we announced, but with only one difference. The JSX preprocessor will automatically rewrite HTML tags in React.createElement (tagName) expressions and leave everything else alone. This is necessary to prevent clogging of the global namespace.
Create a Comment component that will depend on the data passed by the parent component. The data transferred from the parent is available as a property in the child component. Access to properties through this.props . Using the details, we can read the data transferred to the Comment from CommentList , and display the markup:
// tutorial4.js var Comment = React.createClass({ render: function() { return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author} </h2> {this.props.children} </div> ); } });
By enclosing a javascript expression in braces inside JSX, you can add text or React components to the tree. We get access to the named attributes passed to the component as keys in this.props and to any nested elements, for example this.props.children .
Now that we have the Comments component announced, we will pass the name of the author and the text of the comment to it. This will allow us to reuse the same code for each comment. Now add some comments to the CommentList component:
// tutorial5.js var CommentList = React.createClass({ render: function() { return ( <div className="commentList"> //new start <Comment author="Pete Hunt">This is one comment</Comment> <Comment author="Jordan Walke">This is *another* comment</Comment> //new end </div> ); } });
Notice how we passed the data from the parent CommentList component to the child Comment components. For example, we passed Pete Hunt (via an attribute) and This is one connent (via an XML-like child node) on the first Comment . As mentioned earlier, the Comment component accesses these properties through this.props.author and this.props.children .
Markdown is a convenient way to format text. For example, text wrapped in asterisks will be underlined at the exit.
In this tutorial, we use the third-party marked library, which converts Markdown markup to pure HTML. We have already connected this library earlier in our HTML file, so that we can start using it. Let's convert the comment text with the markdown markup and display it:
// tutorial6.js var Comment = React.createClass({ render: function() { return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author} </h2> {marked(this.props.children.toString())} // new </div> ); } });
All we have done here is called the marked library. Now it is necessary to convert this.props.children from a React-like text to a regular string that the marked will understand, so we specifically call the toString () function for this.
But there is a problem! Our processed components look like this in the browser: " <p>
This is <em>
another </em>
comment </p>
". We need to convert all tags into markup for HTML text.
So React protects you from XSS attacks. Below is a way to get around this:
// tutorial7.js //new start var Comment = React.createClass({ rawMarkup: function() { var rawMarkup = marked(this.props.children.toString(), {sanitize: true}); return { __html: rawMarkup }; }, //new end render: function() { return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author} </h2> <span dangerouslySetInnerHTML={this.rawMarkup()} /> //new </div> ); } });
This is a special API that intentionally complicates working with pure HTML, but we will make an exception for marked .
Attention !: using similar exceptions, you completely rely on the security of the marked library. To do this, we pass the second argument senitize: true , which includes clearing any HTML tags.
Up to this point, we inserted comments directly from the code. Now we will try to convert the JSON object into a sheet of comments. Next we will take them from the server, but for now we will add these lines to our code:
// tutorial8.js var data = [ {id: 1, author: "Pete Hunt", text: "This is one comment"}, {id: 2, author: "Jordan Walke", text: "This is *another* comment"} ];
Now we need to pass this object to CommentList , while respecting modularity. Let's change CommentBox and RenderDOM.render () to transfer data to the CommentList component using the props method:
// tutorial9.js var CommentBox = React.createClass({ render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.props.data} /> //new <CommentForm /> </div> ); } }); ReactDOM.render( <CommentBox data={data} />, //new document.getElementById('content') );
Now the data is available in the CommentList component, let's try to dynamically display comments:
// tutorial10.js var CommentList = React.createClass({ render: function() { //new start var commentNodes = this.props.data.map(function(comment) { return ( <Comment author={comment.author} key={comment.id}> {comment.text} </Comment> ); }); //new end return ( <div className="commentList"> {commentNodes} // new </div> ); } });
Done!
Let's replace the comments wired in the code with the data from the server. To do this, replace the data attribute with a url , as shown below:
// tutorial11.js ReactDOM.render( <CommentBox url="/api/comments" />, // new document.getElementById('content') );
Attention. At this stage, the code does not work.
Until now, based on their parameters, each component has drawn itself once, props are unchanged - this means that they are transmitted from the parent and it remains their owner. To organize the interaction, we add a variable property to the component. this.state is private to the component and can be changed by calling this.setState () . After updating the property, the component will redraw itself.
The render () methods are written declaratively, like the functions of this.props and this.state .
React ensures that the data on the server and in the user interface.
When the server sends data, we need to change the comments in the interface. Add a component with comments to the CommentBox component as a separate parameter:
// tutorial12.js var CommentBox = React.createClass({ //new start getInitialState: function() { return {data: []}; }, //new end render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> // new <CommentForm /> </div> ); } });
getInitialState () is executed once during the component's life cycle and sets the initial state of the component.
After creating the component, we want to get JSON from the server and update the data in the component to display it in the interface. For asynchronous requests to the server, we will use jQuery. The data is already on the server (stored in comments.json) that you started at the very beginning. When data is received from the server, this.state.data will contain:
[ {"id": "1", "author": "Pete Hunt", "text": "This is one comment"}, {"id": "2", "author": "Jordan Walke", "text": "This is *another* comment"} ]
// tutorial13.js var CommentBox = React.createClass({ getInitialState: function() { return {data: []}; }, //new start componentDidMount: function() { $.ajax({ url: this.props.url, dataType: 'json', cache: false, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, //new end render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm /> </div> ); } });
The componentDidMount method is automatically called by React after the component is initially drawn. The this.setState () method is responsible for the dynamic update. We will replace the old comments array with a new one from the server and our interface will automatically update itself. Because of this, we will need to make minor edits to add a real-time update. For simplicity, we will use the technology of polling (Frequent requests), but in the future you can easily use WebSockets or any other technology.
// tutorial14.js var CommentBox = React.createClass({ loadCommentsFromServer: function() { // new $.ajax({ url: this.props.url, dataType: 'json', cache: false, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, // new getInitialState: function() { return {data: []}; }, componentDidMount: function() { //new start this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); //new end }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm /> </div> ); } }); ReactDOM.render( <CommentBox url="/api/comments" pollInterval={2000} />, // new document.getElementById('content') );
Here we moved the AJAX request to a separate method and initiate its call after the component is first loaded and every 2 seconds after. Now try to open our comments page in the browser and make changes to the comments.json file (in the root directory of your server); within 2 seconds you will see the changes on the page.
Now it's time to create a comment form. Our CommentForm component should prompt the user for the name and text of the comment, then send a request to the server to further save the comment.
// tutorial15.js var CommentForm = React.createClass({ render: function() { return ( //new start <form className="commentForm"> <input type="text" placeholder="Your name" /> <input type="text" placeholder="Say something..." /> <input type="submit" value="Post" /> </form> //new end ); } });
In a traditional DOM, the input element is drawn and then the browser already sets its value. As a result, the DOM value will be different from the component value. It's bad when the value in the view is different from the value in the component. In React, a component must always correspond to the representation and not only at the moment of its initialization.
Therefore, we will use this.state to store user input. We declare the initial state with two properties, author and text, and assign them the value of the empty string. In our <input>
element, we assign the value of the state value to the value parameter, and hang the onChange handler on it. This <input>
element with the value attribute set is called a monitored component. You can read more about the controlled components in the Forms article .
// tutorial16.js var CommentForm = React.createClass({ //new start getInitialState: function() { return {author: '', text: ''}; }, handleAuthorChange: function(e) { this.setState({author: e.target.value}); }, handleTextChange: function(e) { this.setState({text: e.target.value}); }, //new end render: function() { return ( <form className="commentForm"> //new start <input type="text" placeholder="Your name" value={this.state.author} onChange={this.handleAuthorChange} /> <input type="text" placeholder="Say something..." value={this.state.text} onChange={this.handleTextChange} /> //new end <input type="submit" value="Post" /> </form> ); } });
React event handlers use the camelCase naming convention. We hung onChange handlers on two <input>
elements. Now that the user has entered data in the <input>
field, the event handler makes a callback and modifies the value of the component. Subsequently, the input value will be updated to reflect the current value of the component.
Let's make the form interactive. After the user submits the form, we need to clear it, send a request to the server and update the list of comments. To get started, get the form data and clean it up.
// tutorial17.js var CommentForm = React.createClass({ getInitialState: function() { return {author: '', text: ''}; }, handleAuthorChange: function(e) { this.setState({author: e.target.value}); }, handleTextChange: function(e) { this.setState({text: e.target.value}); }, //new start handleSubmit: function(e) { e.preventDefault(); var author = this.state.author.trim(); var text = this.state.text.trim(); if (!text || !author) { return; } // TODO: this.setState({author: '', text: ''}); }, //new end render: function() { return ( <form className="commentForm" onSubmit={this.handleSubmit}> // new <input type="text" placeholder="Your name" value={this.state.author} onChange={this.handleAuthorChange} /> <input type="text" placeholder="Say something..." value={this.state.text} onChange={this.handleTextChange} /> <input type="submit" value="Post" /> </form> ); } });
We hang the onSubmit handler on the form, which will clear it when the form is filled with the correct data and sent.
Call preventDefault () to prevent the browser from submitting a default form.
When a user sends a comment, we need to update the comment sheet to add a new one. It makes sense to implement all this logic in the CommentBox , since CommentBox manages the list of comments.
We need to pass from the child component to the parent. We will do this through our parent render method, passing a new callback ( handleCommentSubmit ) to the child, associating it with the child component's onCommentSubmit event. Each time an event occurs, the callback function is called:
// tutorial18.js var CommentBox = React.createClass({ loadCommentsFromServer: function() { $.ajax({ url: this.props.url, dataType: 'json', cache: false, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, //new start handleCommentSubmit: function(comment) { // TODO: }, //new end getInitialState: function() { return {data: []}; }, componentDidMount: function() { this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> // new </div> ); } });
Now, when the CommentBox component has provided access to the callback function of the CommentForm component via the onCommentSubmit parameter, the CommentForm component can call the callback function when the user submits the form:
// tutorial19.js var CommentForm = React.createClass({ getInitialState: function() { return {author: '', text: ''}; }, handleAuthorChange: function(e) { this.setState({author: e.target.value}); }, handleTextChange: function(e) { this.setState({text: e.target.value}); }, handleSubmit: function(e) { e.preventDefault(); var author = this.state.author.trim(); var text = this.state.text.trim(); if (!text || !author) { return; } this.props.onCommentSubmit({author: author, text: text}); // new this.setState({author: '', text: ''}); }, render: function() { return ( <form className="commentForm" onSubmit={this.handleSubmit}> <input type="text" placeholder="Your name" value={this.state.author} onChange={this.handleAuthorChange} /> <input type="text" placeholder="Say something..." value={this.state.text} onChange={this.handleTextChange} /> <input type="submit" value="Post" /> </form> ); } });
Now that we have a callback function, we just have to send the data to the server and update the comment sheet:
// tutorial20.js var CommentBox = React.createClass({ loadCommentsFromServer: function() { $.ajax({ url: this.props.url, dataType: 'json', cache: false, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, handleCommentSubmit: function(comment) { //new start $.ajax({ url: this.props.url, dataType: 'json', type: 'POST', data: comment, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); //new end }, getInitialState: function() { return {data: []}; }, componentDidMount: function() { this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } });
Our application is ready, but waiting for the completion of the request to the server and the appearance of your comment on the page makes it visually slow. We can immediately add our comment to the list, not waiting for the server request to complete, and this will occur almost instantly.
// tutorial21.js var CommentBox = React.createClass({ loadCommentsFromServer: function() { $.ajax({ url: this.props.url, dataType: 'json', cache: false, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, handleCommentSubmit: function(comment) { //new start var comments = this.state.data; // Optimistically set an id on the new comment. It will be replaced by an // id generated by the server. In a production application you would likely // not use Date.now() for this and would have a more robust system in place. comment.id = Date.now(); var newComments = comments.concat([comment]); this.setState({data: newComments}); //new end $.ajax({ url: this.props.url, dataType: 'json', type: 'POST', data: comment, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { this.setState({data: comments}); // new console.error(this.props.url, status, err.toString()); }.bind(this) }); }, getInitialState: function() { return {data: []}; }, componentDidMount: function() { this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } });
You have created a comment module in a few simple steps. Learn more about why using React , or go straight to learning about the API and start writing code! Good luck!
Source: https://habr.com/ru/post/282874/