A list of resources for writing good, understandable, and reusable React components for Paragon.
Highlights
Scott Domes encourages us to always conceptualize our components as functions, even if they are written as class components. Then addressing the question, “what makes a good function?” the article walks through the five factors outlined in Robert Martin’s Clean Code as they apply to React component design. The five factors and some quotes:
Small - “Your component should be small”
The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. — Clean Code
50 lines is a good rule of thumb for the body of your component (for class components, that is the render method). If looking at the total lines of the file is easier, most component files should not exceed 250 lines. Under 100 is ideal.
Does one thing - “Your component should do one thing”
Your components should have only one main responsibility: one reason to change.
Split your UI into tiny chunks that each handle one thing.
One level of abstraction - “Your component should have one level of abstraction”
Less than three arguments - “Your component should have only a few arguments (props)”
The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.. — Clean Code
Here’s a more relaxed rule of thumb. Three props is fine. Five props is a code smell. More than seven props is a crisis.
Descriptive name - “Your component should have a descriptive name”
“Having a hard time naming your component is a sign it’s doing too much. The answer to ‘what does this component do?’ should be simple, and lend itself to a descriptive name.”
All component properties should be annotated with associated prop-types from the react prop-types
library.
https://reactjs.org/docs/typechecking-with-proptypes.html
“One of the many great parts of React is how it makes you think about apps as you build them. In this document, we’ll walk you through the thought process of building a searchable product data table using React.”
Start With A Mock
Step 1: Break The UI Into A Component Hierarchy
Step 2: Build A Static Version in React (no state)
Step 3: Identify The Minimal (but complete) Representation Of UI State
Step 4: Identify Where Your State Should Live
Step 5: Add Inverse Data Flow
Don’t let JSX syntax fool you. The <Component />
jsx syntax is transpiled into React.createElement(Component, props, ...children)
. Example from the React docs:
With JSX
class Hello extends React.Component { render() { return <div>Hello {this.props.toWhat}</div>; } } ReactDOM.render( <Hello toWhat="World" />, document.getElementById('root') ); |
No JSX
class Hello extends React.Component { render() { return React.createElement('div', null, `Hello ${this.props.toWhat}`); } } ReactDOM.render( React.createElement(Hello, {toWhat: 'World'}, null), document.getElementById('root') ); |
Consider writing a React component as a function. Writing a good component shares a lot in common with writing a good function. Robert Martin’s Clean Code (also wow) outlines some good rules of thumb for functions:
Small!
Do One Thing
One Level of Abstraction per Function
Use Descriptive Names.
“Functions should hardly ever be 20 lines long.” Less than 50 lines is a good rule of thumb for the body of your component (for class components, that is the render method).
Aggressively split apart your components to reduce the number of jobs they have. Good related article.
Aim for fewer levels of abstraction
todo
Move lists into separate components
// Don't write loops in a component with other jobs const CourseResults = ({resultCount, courses}) => ( <div> <h1>Results: {resultCount}</h1> <Filters /> { courses.map(course => <CourseCard course={course} />) } <Pagination /> </div> ); // Move it into a list component const CourseResults = ({resultCount, courses}) => ( <div> <h1>Results: {resultCount}</h1> <Filters /> <CourseCardList courses={courses} /> <Pagination /> </div> ); |
Avoid nesting render functions
// A nested render func const Header = ({ navItems }) => { const renderNav = (navItem) => { return <button>...</button> }; return <div>{navItems.map(renderNav)}</div> } // Use multiple components component const NavItem = ({ navItem }) => ( <button>...</button> ) const NavItems = ({ navItems }) => { return navItems.map((navItem) => <NavItem navItem={navItem} />) } const Header = () => { return <div><NavItems navItems={navItems} /></div> } |
“The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic).”
Consider enumerating variants or state, or creating discrete components:
<Button primary> Primary </Button> <Button secondary> Secondary </Button> <Button danger> Danger </Button> /* What about this? */ <Button danger primary> Danger </Button> |
<Button variant="primary"> Primary </Button> /* Variant is one of - primary - danger - secondary */ |
Good, but what if your buttons would vary significantly? (Maybe “danger” always gets a stop icon)
<ButtonPrimary> Primary </ButtonPrimary> <ButtonSecondary> Secondary </ButtonSecondary> <ButtonTertiary> Tertiary </ButtonTertiary> |
Clearest, though requires abstracting common behavior and definitions to a base button component or object.
Nope |
function MyApp({ isCreateMode }) { return ( <Form isCreateForm={isCreateMode} /> ); } |
Meh |
function MyApp({ isCreateMode }) { if (isCreateMode) { return <CreateForm />; } return <ReadOnlyForm />; } |
Create two components, don’t make the Form component do two things.
Yup |
function MyApp() { return ( <> <Route path="/view" component={<ReadOnlyForm />} /> <Route path="/create" component={<CreateForm />} /> </> ); } |
No booleans needed!
“When a function seems to need more than two or three arguments it is likely that some of those arguments ought to be wrapped into a class of their own”
<UserProfileListItem profileImg="..." username="..." role="..." /> |
Better |
<UserProfileListItem user={userObj} /> |
Benefits:
If the user object has new properties added, this component doesn’t need a refactor
It’s clear what the concept of a “user” is.
“Don’t be afraid to make a name long. A long descriptive name is better than a short enigmatic name. A long descriptive name is better than a long descriptive comment.”
Booleans
is
(isVisible, isActive)
has
(hasCancelButton, hasIcon)
can
(canToggle, canSubmit)
Number
num__
(numItems, numRows)
__Count
(itemCount, rowCount)
__Index
(itemIndex, rowIndex)
Array
Use plural nouns (rows, items, users)
Object
Use the appropriate noun (item, user)
Node
__Node
(headerNode, titleNode)
Element
__Element
(headerElement, titleElement)
Describe what your component is or does, not why. It helps to ask yourself, “If I used this component in a totally different context, would these names make sense?”
Nope |
<CreateUserForm isMobileScreen={browser.isMobileScreen} isAdmin={user.isAdmin} /> |
Yup |
<CreateUserForm isCompactLayout={browser.isMobileScreen} canSubmit={user.isAdmin} /> |
Use the on__
prefix for event handler props.
Use handle
prefix for event handlers that get passed in (generally)
<Button onClick={handleClick} />
<UserItem user={user} onSelect={handleUserSelect} />
Render Props
sending in the type
Example A/B test data.