iTranslated by AI
(2016) Tips for Building Maintainable React Components
A comprehensive collection of small techniques, centered around things that worked well for me personally.
What this covers
- Topics regarding React.Component
- Likely aimed at intermediate users
- Aimed at production code
- Goals/Objectives
- More designer-friendly components
- More testable components
- Component reusability (within a narrow scope, such as a single product)
What this [mostly] does NOT cover
- State management topics like Flux or Redux
- Overall project structure
- Component topics for libraries or broad reusability
- Performance
- Performance is an important item, but it won't be the main focus this time.
Write as much as possible using Stateless Functional Components
In recent versions of React, components can be written as functions.
These are called Stateless Functional Components.
(Hereafter abbreviated as SFC)
const Foo = (props) => {
return <div>props.baz</div>
}
In my experience, I felt that about 80% of components can be written as SFCs.
By using SFCs, there are several pros and cons:
-
Pros
- Since they are pure functions, readability tends to be higher.
- Fewer things to think about.
- You can separate the essential view layer from the layer involving logic.
- It becomes much easier to communicate with designers when they say, "Tweak this part!"
- It's easier to display in Storybook and perform snapshot tests (more on snapshot testing later).
- Performance optimizations can be expected in the future.
- Since they are pure functions, readability tends to be higher.
-
Cons (Things you can't use without a
class Component)- Lifecycle methods (e.g., componentDidMount)
- State management
- Context
- refs
- Instance management for third-party libraries
Even when using class Component, I try to build while keeping the following in mind:
- You don't need to cram everything into a
class Component. How about making parts of it an SFC? - Can you think of it in terms of parent and child components?
- Would it be better if the parent focuses on managing state and lifecycle, while the child focuses on rendering?
- This pattern often works out very well.
- Can the component be broken down into even smaller pieces?
- Would it be better if the parent focuses on managing state and lifecycle, while the child focuses on rendering?
- Can it be solved simply by writing it as a Higher Order Component?
- Would recompose provide a nice solution?
Create components that simply pass all values through using props spreading ({...props})
This is a method also documented in the official guide.
A technique for expanding an object with the {...props} syntax and passing down props to child components.
It’s quite handy once you remember it.
For example, it's useful in the following cases:
- When you want to add just a class to a standard DOM element.
- When you want to pass contents as-is when using wrapper components or Higher-Order Components.
You can use it simply in combination with Stateless Functions.
const MyInput = (props) => (<input className="my-input-class" {...props} />)
const SomeComponent = () => {
return <MyInput onChange={(e) => {foo() }} />
}
It is also possible to process only some of the values using the rest operator.
const MyInput = ({ someValue, ...rest}) => {
const cls = someValue === "foo" ? "baz" : "bar"
return <input className={cls} {...props} />
}
Unpack props and state into local variables instead of passing them around directly within functions
This is a matter of preference, but it’s a method I personally like.
Rather than passing props or state as they are, I often find it easier to understand and maintain if you break them down using destructuring at the beginning of the function.
This also makes it easier to convert them to SFCs later on.
const Foo = ({some, values}) => {
return <div>{some} Hello {values}</div>
}
class Foo extends Component{
render(){
const {some, values} = this.props
const {foo, baz, bar} = this.state
const {hoge, fuga} = this.context
return <div>{some} Hello {values} {foo}</div>
}
}
A disadvantage exists where you cannot use this if state and props share the same name.
In that case, however, I take it as a sign that the component might already be getting too bloated and consider whether it should be broken down.
If you absolutely must handle state and props with the same name, you can assign them to different names using destructuring.
const {foo : fooProps } = this.props
const {foo : fooState } = this.state
const {foo : fooContext } = this.context
Break down components as finely as possible into meaningful units
This is not limited to React, but a general principle of template languages like Slim: if there's a unit you can give a name to, it's often cleaner to separate it into its own component.
const SomeForm = ({name, onChangeName, mail, onChangeMail onSubmit}) => {
return <from>
<div>
<label>
Name: <input onChange={onChangeName} name={name} />
</label>
<label>
Email: <input onChange={onChangeMail} name={mail} />
</label>
</div>
<input type={onSubmit} value={"send"} />
</from>
}
For example, with a component like this, you can break it down as follows:
const SomeInput = ({name, onChangeName, mail, onChangeMail}) => {
return <div>
{/* You can further separate or unify the Name and Email inputs. */}
<label>
Name: <input onChange={onChangeName} name={name} />
</label>
<label>
Email: <input onChange={onChangeMail} name={mail} />
</label>
</div>
}
const SomeSendButton = ({onSubmit}) => {
return <input type={onSubmit} value={"send"} />
}
const SomeForm = (props) => {
return <from>
<SomeInput {...props} />
<SomeSendButton {...props} />
</from>
}
Passing unnecessary props via {...props} is a bit of a shortcut. While it doesn't cause much of a problem if the child components are thin, you should filter and pass only the necessary values if it concerns you.
In React, the cost of separating components is low because you can do it just by separating variables. Therefore, I think it's fine to divide them into units that simply "make sense."
This not only improves readability but also makes it easier to clearly identify units for modification when designs change, making them easier to discard or rebuild.
However, doing this too early when requirements are not yet solidified can make things more difficult, so it's also a good idea to refactor once things have settled to some extent.
If separating with conditional branching in an SFC, separate each state into a child SFC
For example, in cases like displaying something when an item does not exist.
// Common pattern
const SomeList = ({someItem}) => {
if(someItem === undefined){
return <div>No items found</div>
}
return <div>{someItem}</div>
}
It's easy to write it like this, but it can quickly become bloated and difficult to manage.
// Example of it becoming difficult
const SomeList = ({someItem}) => {
if(someItem === undefined){
return <div>
<Mecha>
<Sugoi>
<Decoration>
No items found
</Decoration>
</Sugoi>
</Mecha>
</div>
}
if(someItem === "fuga"){
return <div>Item is fuga</div>
}
return <div>
<Sugoi>
<Mendokusai>
<Decoration>
{someItem}
</Decoration>
</Mendokusai>
</Sugoi>
</div>
}
In my case, I try to separate components based on conditional branches as much as possible.
// Pattern for breaking down components finely
const ItemNotFound = () => <div>No items found</div>
const Item = ({someItem}) => <div>{someItem}</div>
const SomeList = ({someItem}) => {
if(someItem === undefined){
return <ItemNotFound />
}
return <Item someItem={someItem} />
}
Doing this provides the following benefits:
- You have fewer things to worry about regarding conditional branching when designing.
- It's easier to test in Storybook and similar tools.
- It's easier to handle bloat.
Utilize children in the React way
When things like child elements are strings, people tend to pass values through props, but using children instead of custom properties can often lead to cleaner code.
Let's look at a common pattern first.
const FooButton = ({label}) => {
return <button>{label}</button>
}
This is fine for displaying a label, but it quickly falls apart in typical scenarios like "I want to customize the label content!"
{/* Can only be defined via props */}
<FooButton label="foo" />
{/* If you want to set a color, it ends up like this */}
<FooButton label="foo" color="red"/>
{/* A pattern where extensions keep increasing and lead to hell */ }
<FooButton label="foo" color="red" weight="bold" img="dog" animate="false"/>
Patterns using children can solve this to some extent.
const GoodButton = ({children}) => {
return <button>{children}</button>
}
Child elements wrapped between tags are automatically assigned to children. It's not that "wrapping is mandatory" but rather "wrapping is possible," so you can also specify children as a prop.
Even if the decoration of internal elements increases, it's easy to offload the responsibility for decoration.
{/* Both work */}
<GoodButton children="foo" />
<GoodButton>foo</GoodLabel>
{/* Dependency on various decorations can be delegated to separate components */}
<GoodButton><FooDecorate>foo</FooDecorate></GoodLabel>
Old content regarding PropTypes
Additionally, if you set up PropTypes, using node allows you to limit it to renderable items.
GoodLabel.propTypes = {
children: React.PropTypes.node,
}
While children is recommended, the same can be achieved with the first example <FooButton> by passing a node to label. It's a matter of choosing the right tool for the job.
<FooButton label={<div>item</div>} />
For more complex cases of handling children, I've written about it here:
If you're unsure about Component implementation, practice with create-react-app.
Starting to build components heavily from scratch can be quite difficult. If things "just aren't working (or don't look like they will)," it's best to practice.
However, if you've already made some progress, it's often quite difficult to try things out on an existing project.
So, to start from a clean slate, using create-react-app is a good idea.
Personally, I'm a bit skeptical about using create-react-app for production purposes with long lifecycles (for now), but it's an excellent boilerplate tool for prototyping purposes.
$ npm install -g create-react-app
$ create-react-app my-prototype
$ cd my-prototype/
The entry point is src/App.js. You can write here, but since I often want to clear my head, I personally prefer to replace the contents entirely.
import React, { Component } from 'react';
// Write things to try in src/SomePrototypeComponent.js
import SomePrototypeComponent from './SomePrototypeComponent'
class App extends Component {
render() {
return (
<div className="App">
<SomePrototypeComponent />
</div>
);
}
}
export default App;
And then start:
$ npm start
You might also consider introducing Storybook and prototyping on top of it, but since Storybook leans more towards style guides than prototyping, it might not be suited for that kind of practice.
Manage components to be testable
It's a bit of a difficult task to consider how much effort should be put into testing components, but my personal stance is that "it's good to do it even if only lightly."
Recently, Snapshot Testing has emerged as a good fit for that level of effort.
There are several ways to use it:
- Use the snapshot mode of jest
- It is described in detail in the snapshot-testing section.
- Use Storybook + StoryShots
- You can use it as a style guide while also utilizing it for testing.
- Use react-test-renderer directly
- This is what jest and storyshots use internally. It is provided by Facebook. (Be careful as there are packages with similar names like react-test-render.)
- If for some reason you cannot (or do not want to) depend on other testing tools, using this directly is an option.
Personally, I often use Storybook + StoryShots.
The test commands end up being separate, but I like the part where visual confirmation becomes easy.
Keep a good distance from PropTypes
Past content
There are arguments that PropTypes is dead and you should use Flow or TypeScript, but both currently have a high barrier to entry that's hard to recommend to everyone.
For now, I approach it with these thoughts:
- Don't be too rigid; use it loosely where it seems helpful.
- It's convenient to have, but I feel there's no point in being too obsessed with it.
- If you're writing a library, I think it's fine to be strict.
- For regular production use, writing it for everything can sometimes have poor cost-performance.
- If you want to "write everything" in production, introducing Flow or similar might be more effective considering future potential.
- Aim to write them after the requirements have solidified as much as possible.
- Views with unsolidified requirements often end up being rewritten anyway.
- It's often more rewarding to write them after requirements are settled.
- Proactively write them for function handler types like onXXX.
- There are management methods to consolidate the same PropTypes in one place, so use them if necessary.
Discussion