For the translation, this paper has obtained the original author’s permission, the original address: scottdomes.com/blog/ou…
When I first started writing React, I discovered that there were as many ways to write the React component as there were React tutorials. Although frameworks are mature today, there is no “right” way to write components.
During our year at MuseFind, our team wrote a number of React components. We are constantly refining the React component.
This article describes our team’s best practices for writing React components. We hope this article will be useful to you, whether you are a beginner or an experienced one.
Before we begin, a few points:
-
Our team uses ES6 and ES7 grammars.
-
If the difference between Presentational components and Container Components is unclear, we recommend reading this article first.
-
If you have any suggestions, questions or feedback, please let us know in the comments.
Class-based components
Class based Components contain states and methods. We should replace them with function-based Components whenever possible. But for now, let’s talk about how to write class-based components.
Let’s build our component line by line.
The introduction of CSS
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'Copy the code
I think the ideal CSS would be CSS in JavaScript. But this is still a new idea, and no mature solution has emerged. So, for now, we’ll use the method of importing CSS files into each React component.
Our team will first import dependent files (files in node_modules), then empty a line, and then import local files.
Initialization state
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
export default class ProfileContainer extends Component {
state = { expanded: false }Copy the code
You can use the old method of initializing state in constructor. You can also use ES7’s simple new method of initializing the state. Read more here.
propTypes and defaultProps
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
export default class ProfileContainer extends Component {
state = { expanded: false }
static propTypes = {
model: object.isRequired,
title: string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}Copy the code
PropTypes and defaultProps are static properties, and it is best to write them near the front of the component in component code. When other developers look at the code for this component, they should immediately see propTypes and defaultProps as if they were documentation for this component. (See this article for details on the order in which components are written.)
If React 15.3.0 or later is used, use prop-types instead of React.PropTypes. When using prop-types, they should be deconstructed.
All components should have propTypes.
Methods
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
export default class ProfileContainer extends Component {
state = { expanded: false }
static propTypes = {
model: object.isRequired,
title: string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
handleSubmit = (e) = > {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) = > {
this.props.model.changeName(e.target.value)
}
handleExpand = (e) = > {
e.preventDefault()
this.setState({ expanded:!this.state.expanded })
}Copy the code
When using class-based components, when you pass a method to a component, you must ensure that the method is called with the correct context, this. A common approach is to do this by passing this.handlesubmit.bind (this) to a child component.
In our opinion, the above method is simpler and more direct. Automatically bind the correct context via the ES6 arrow function.
tosetState
Passing a function
In the example above, we do this:
this.setState({ expanded:!this.state.expanded })Copy the code
Because setState it’s actually asynchronous. React updates the state in batches for performance reasons, so the state may not change immediately after setState is called.
This means that when calling setState, you shouldn’t rely on the current state, because you can’t be sure what that state is!
The solution: Pass a function to setState instead of a normal object. The first argument to the function is the previous state.
this.setState(prevState= > ({ expanded: !prevState.expanded }))Copy the code
Deconstruction Props
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
export default class ProfileContainer extends Component {
state = { expanded: false }
static propTypes = {
model: object.isRequired,
title: string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
handleSubmit = (e) = > {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) = > {
this.props.model.changeName(e.target.value)
}
handleExpand = (e) = > {
e.preventDefault()
this.setState(prevState= > ({ expanded: !prevState.expanded }))
}
render() {
const {
model,
title
} = this.props
return (
<ExpandableForm
onSubmit={this.handleSubmit}
expanded={this.state.expanded}
onExpand={this.handleExpand}>
<div>
<h1>{title}</h1>
<input
type="text"
value={model.name}
onChange={this.handleNameChange}
placeholder="Your Name"/>
</div>
</ExpandableForm>)}}Copy the code
As above, when a component has multiple props values, each prop should occupy a separate line.
A decorator
@observer
export default class ProfileContainer extends Component {Copy the code
If you use Mobx, use decorators. The essence is to pass a decorator component to a function.
Use decorators in a more flexible and readable way. Our team uses a lot of decorators when using Mobx and our own Mox-Models library.
If you don’t want to use decorators, you can do it as follows:
class ProfileContainer extends Component {
// Component code
}
export default observer(ProfileContainer)Copy the code
closure
Avoid passing a new closure to child components like the following:
<input
type="text"
value={model.name}
// onChange={(e) => { model.name = e.target.value }}
// ^ The top is wrong. Use the following method:
onChange={this.handleChange}
placeholder="Your Name"/>Copy the code
Why is that? Because every time the parent component renders, a new function is created.
If you pass this new function to a React component, it will cause it to re-render, regardless of whether the other props of the component are actually changed.
Reconciliation is the most performance-intensive part of React. Therefore, avoid passing new closure writing, and don’t make harmonization more performance costly! In addition, the intermediate form of the method passing the class is easier to read, debug, and change.
Here is our entire component:
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'
// Separate local imports from dependencies
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
// Use decorators if needed
@observer
export default class ProfileContainer extends Component {
state = { expanded: false }
// Initialize state here (ES7) or in a constructor method (ES6)
// Declare propTypes as static properties as early as possible
static propTypes = {
model: object.isRequired,
title: string
}
// Default props below propTypes
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
// Use fat arrow functions for methods to preserve context (this will thus be the component instance)
handleSubmit = (e) = > {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) = > {
this.props.model.name = e.target.value
}
handleExpand = (e) = > {
e.preventDefault()
this.setState(prevState= > ({ expanded: !prevState.expanded }))
}
render() {
// Destructure props for readability
const {
model,
title
} = this.props
return (
<ExpandableForm
onSubmit={this.handleSubmit}
expanded={this.state.expanded}
onExpand={this.handleExpand}>
// Newline props if there are more than two
<div>
<h1>{title}</h1>
<input
type="text"
value={model.name}
// onChange={(e)= > { model.name = e.target.value }}
// Avoid creating new closures in the render method- use methods like below
onChange={this.handleNameChange}
placeholder="Your Name"/>
</div>
</ExpandableForm>)}}Copy the code
Function-based components
Functional Components have no state and no methods. They are pure and readable. Use them whenever possible.
propTypes
import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
import './styles/Form.css'
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool
}
// Component declarationCopy the code
Define propTypes for components before declaring them, so they can be seen immediately. We can do this because JavaScript has a function promotion appeal.
Deconstruct Props and defaultProps
import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
import './styles/Form.css'
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool,
onExpand: func.isRequired
}
function ExpandableForm(props) {
const formStyle = props.expanded ? {height: 'auto'}, {height: 0}
return (
<form style={formStyle} onSubmit={props.onSubmit}>
{props.children}
<button onClick={props.onExpand}>Expand</button>
</form>)}Copy the code
Our component is a function, and the arguments to the function are the props of the component. We can use the method of deconstructing parameters:
import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
import './styles/Form.css'
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool,
onExpand: func.isRequired
}
function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
const formStyle = expanded ? {height: 'auto'}, {height: 0}
return (
<form style={formStyle} onSubmit={onSubmit}>
{children}
<button onClick={onExpand}>Expand</button>
</form>)}Copy the code
Note that we can also use the default parameter as defaultProps, which is more readable. If expanded is not defined, set it to false. (This avoids errors like ‘Cannot read
Avoid using function expressions to define components as follows:
const ExpandableForm = ({ onExpand, expanded, children })= > {Copy the code
This looks pretty cool, but in this case, functions defined by function expressions are anonymous functions.
If Bable doesn’t do the naming, the error stack doesn’t tell you which component failed, just <<anonymous>>. This makes debugging very bad.
Anonymous functions can also cause problems with the React test library Jest. Because of these potential pitfalls, we recommend using function declarations rather than function expressions.
Package function
Since function-based components cannot use modifiers, you should pass the function-based component as an argument to the corresponding function:
import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
import './styles/Form.css'
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool,
onExpand: func.isRequired
}
function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
const formStyle = expanded ? {height: 'auto'}, {height: 0}
return (
<form style={formStyle} onSubmit={onSubmit}>
{children}
<button onClick={onExpand}>Expand</button>
</form>)}export default observer(ExpandableForm)Copy the code
The full code is as follows:
import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
// Separate local imports from dependencies
import './styles/Form.css'
// Declare propTypes here, before the component (taking advantage of JS function hoisting)
// You want these to be as visible as possible
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool,
onExpand: func.isRequired
}
// Destructure props like so, and use default arguments as a way of setting defaultProps
function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
const formStyle = expanded ? { height: 'auto'}, {height: 0 }
return (
<form style={formStyle} onSubmit={onSubmit}>
{children}
<button onClick={onExpand}>Expand</button>
</form>)}// Wrap the component instead of decorating it
export default observer(ExpandableForm)Copy the code
Conditional expressions in JSX
Chances are you’ll do a lot of conditional rendering. Here’s what you want to avoid:
No, triplet nesting is not a good idea.
There are some libraries that solve this problem (jsX-Control Statementments), but to introduce another dependent library, we solve this problem using complex conditional expressions:
Wrap an immediate execute function (IIFE) with curly braces, then put your if statement inside it and return whatever you want to render. Note that IIFE like this can cause some performance drain, but in most cases readability is more important.
Update: Many commentators have suggested extracting this logic into the different buttons returned by the sub-components. That’s right, split components as much as possible.
Also, you should not do this when you have Boolean judgments on render elements:
{
isTrue
? <p>True!</p>
: <none/>
}Copy the code
Short-circuit operation should be used:
{
isTrue &&
<p>True!</p>
}Copy the code