There are two rules to abide by when building a React component. These rules:
- Promote code re-use because there are no side-effects (ie. no extra dependencies and nothing done that you don't know about)
- Make it easier to debug
- Make it easier to maintain
- Prevent bugs
Any data that is displayed in a component must come from its props.
- Make API requests in the top level controller
- Pass data to a component in its props
- Document all props as strictly as possible in the component's propTypes
- Making API requests inside a component to get data. Instead, make the API request in the controller and then pass data to the component through its props.
- Loading data inside a component using global methods like
Policy.getCurrent()
. Pass the policy as a prop instead.
Any UI interaction that needs to trigger business logic (ie. saving a form value) must be passed up the component chain via prop callbacks to communicate with the parent component and ultimately the controller.
- Communicating with something that is not the parent component (eg. API, global libs)
- Putting business logic in a component.
The business logic is the job of the controller. In React only apps there will ideally be high-level components designated as page controllers in charge of the business logic.
Note on using PubSub: PubSub is allowed in some extreme cases. These should be the exception, not the rule. Including and probably limited to:
- Global libs like the site loader, error messages, alerts, growls
- When two components need to communicate with each other and the chain of nested components is more than 3-4 components deep in each path. This is an exception SOLEY because it can be fairly tedious to drill props down like this in deep ways. However, please never resort to this as your plan A.
When using PubSub, try to use the withPubSub
higher order component to make it easy to subscribe and unsubscribe to
events in a React component.
- Pass callback handlers to a component's props so that the parent knows when the child component has done something.
(eg. if the child component is a form, then pass a prop called
onSubmit
which is triggered when the form is submitted, and pass all the form values to that method) - If an action in a component will be accompanied with a re-rendering of the view, it's best to let the parent handle this by passing updated props back down to the child (see below about controlled and uncontrolled components)
- Prop callbacks should be named for what has happened, not for what is going to happen. Components should never assume anything about how they will be used (that's the job of whatever is implementing it).
// BAD
propTypes: {
// A callback to call when we want to save the form
onSaveForm: React.PropTypes.func.isRequired,
}
// GOOD
propTypes: {
// A callback to call when the form has been submitted
onFormSubmitted: React.PropTypes.func.isRequired,
}
- Avoid public methods on components.
// BAD
class MyComponent extends React.Component {
/**
* Refresh the data in our component by calling `MyComponent.refreshData()`
*
* @public
* @params {Object[]} newData
*/
refreshData(newData) {
this.setState({data: newData});
}
}
ReactDOM.render(<MyComponent ref={el => this.component = el}>, $('#container)[0]);
fetchData().done(newData => this.component.refreshData(newData));
// GOOD
class MyComponent extends React.Component {
static get propTypes {
// The data that will be displayed in our component
data: React.PropTypes.array.isRequired,
}
}
function renderComponent(data = []) {
ReactDOM.render(<MyComponent data={data}>, $('#container)[0]);
}
fetchData().done(newData => renderComponent(newData));
- Do not use underscores when naming private methods.
- Do not add method documentation for the built-in lifecycle methods of a component.
- Add descriptions to all propTypes using a single line comment above the definition. No need to document the types, but add some context for each property so that other developers understand the intended use.
// Bad
propTypes: {
currency: React.PropTypes.string.isRequired,
amount: React.PropTypes.number.isRequired,
isIgnored: React.PropTypes.bool.isRequired
}
// Bad
propTypes: {
/**
* The currency that the reward is in
*/
currency: React.PropTypes.string.isRequired,
/**
* The amount of reward
*/
amount: React.PropTypes.number.isRequired,
/**
* If the reward has been ignored or not
*/
isIgnored: React.PropTypes.bool.isRequired
}
// Good
propTypes: {
// The currency that the reward is in
currency: React.PropTypes.string.isRequired,
// The amount of reward
amount: React.PropTypes.number.isRequired,
// If the reward has been ignored or not
isIgnored: React.PropTypes.bool.isRequired
}
- Use inline ternary statements when rendering optional pieces of templates. Notice the white space and formatting of the ternary.
// Bad
{
render() {
const optionalTitle = this.props.title ? <div className="title">{this.props.title}</div> : null;
return (
<div>
{optionalTitle}
<div className="body">This is the body</div>
</div>
);
}
}
// Good
{
render() {
return (
<div>
{this.props.title ?
<div className="title">{this.props.title}</div>
: null}
<div className="body">This is the body</div>
</div>
);
}
}
// Good
{
render() {
return (
<div>
{this.props.title ?
<div className="title">{this.props.title}</div>
:
<div className="title">Default Title</div>
}
<div className="body">This is the body</body>
</div>
);
}
}
// Best
{
render() {
return (
<div>
{this.props.title && <div className="title">{this.props.title}</div>}
{!this.props.title && <div className="title">Default Title</div>}
<div className="body">This is the body</body>
</div>
);
}
}
In React Native, one must not attempt to falsey-check a string for an inline ternary. Even if it's in curly braces,
React Native will try to render it as a <Text>
node and most likely throw an error about trying to render text outside
of a <Text>
component.
// Bad! This will cause a breaking an error on native platforms
{
render() {
return (
<View>
{this.props.title ?
<View style={styles.title}>{this.props.title}</View>
: null}
<View style={styles.body}>This is the body</View>
</View>
);
}
}
// Good
{
render() {
return (
<View>
{!_.isEmpty(this.props.title) ?
<View style={styles.title}>{this.props.title}</View>
: null}
<View style={styles.body}>This is the body</View>
</View>
);
}
}
When writing a stateless component you must:
- Define
propTypes
anddefaultProps
at the top of the file in variables calledpropTypes
anddefaultProps
. - Assign those variables to the component at the bottom of the file.
- ALWAYS add a
displayName
property and give it the same value as the name of the component (this is so it appears properly in the React dev tools). - When writing a stateless component in non-modularized code (eg.
web-expensify/site
, or anytime you are adding something toReact.v
orReact.c
), you must wrap the definition of the component in an IIFE to remove as much from the global scope as you can.
Good example:
/* global React, PersonalDetail */
React.c.AvatarPersonalDetail = (() => {
const propTypes = {
// ID to provide in case we are creating more than one
id: window.PropTypes.string,
// The text to display for the label
labelText: window.PropTypes.string,
// PersonalDetail object to pull values from
personalDetail: window.PropTypes.instanceOf(PersonalDetail)
};
const defaultProps = {
id: '',
labelText: '',
personalDetail: null,
};
const AvatarPersonalDetail = props => (
<li className="notPersonal marginTop">
<label htmlFor={props.id}>{props.labelText}</label>
<span className="marginRight5 gravatar size-input-lg round">
<img alt={props.personalDetail.getDisplayName()} src={props.personalDetail.getAvatarUrl()} />
</span>
<span id={props.id} className="marginRight lineHeightInput-lg inlineBlock vAlignTop">
{props.personalDetail.getDisplayName(true)}
</span>
</li>
);
AvatarPersonalDetail.propTypes = propTypes;
AvatarPersonalDetail.defaultProps = defaultProps;
AvatarPersonalDetail.displayName = 'AvatarPersonalDetail';
return AvatarPersonalDetail;
})();
Stateless components vs Pure Components vs Class based components vs Render Props - When to use what?
1. Stateless components: Used when you don't need to maintain state or use lifecycle methods.
In many cases we create components that do not need to have a state, lifecycle hooks or any internal variables. In other words just a dumb component that takes props and renders something presentational. But often times we write them as class based components which come with a lot of cruft ( for e.g., it has a state, lifecycle hooks and it is a javascript class which means that React creates instances of it ) that is unnecessary in this situation.
A quote from the React documentation:
These components must not retain internal state, do not have backing instances, and do not have the component lifecycle methods. They are pure functional transforms of their input, with zero boilerplate. However, you may still specify
.propTypes
and.defaultProps
by setting them as properties on the function, just as you would set them on an ES6 class.
- Here is an example from our codebase of a stateless component.
const StepProgressBar = ({steps, currentStep}) => {
const currentStepIndex = Math.max(0, _.findIndex(steps, step => step.id === currentStep));
return (
<div id="js_steps_progress" className="progress-wrapper">
...
2. Pure components: Use to improve performance where a component does not need to be rendered too often.
IF YOU ARE NOT SURE ABOUT USING React.PureComponent, USE React.Component INSTEAD. It's very important that you understand the differences.
By default, a plain React.Component has shouldComponentUpdate
set to always return true. This is good because it means
React errs on the side of always updating the component in case there's any new data to show. However, it is bad because
it means React might trigger unnecessary re-renders.
React.PureComponent has a default implementation of shouldComponentUpdate
that does a shallow comparison of props and
state to determine if it should re-render or not.
Read the React Docs to understand this more.
// Internal react code for pure component looks like this w.r.t shouldComponentUpdate
if (type.prototype && type.prototype.isPureReactComponent) {
shouldUpdate = !shallowEqual(oldProps, props) ||
!shallowEqual(oldState, state);
}
A common pitfall when converting from Component to PureComponent is to forget that the children need to re-render too. As with all React - if the parent doesn't re-render the children won't either. So if you have a PureComponent with children, those children can only update if the parent's state or props are shallowly different (causing the parent to re-render). You can only have a PureComponent parent if you know none of the children should re-render if the parent doesn't re-render.
3. Class based components: Use it when you need to maintain state and use lifecycle methods.
Always extend from React.Component
.
If the component needs some data which cannot be passed as a prop, use class components to get the data. Another rule of thumb is you should always fetch data which involves API calls, any ajax calls in the lifecycle methods. Never do this in render!
4. Render Props: Use for Cross-Cutting Concerns and Code Reuse
Let's say you have two separate components that render two different things, but you want them both to be based on the
same state. You can pass a render
property to a component to accomplish this. You can read more about how this works
and when to use it in the React Docs.
Exception: If a third party library is being implemented and the only interface they provide is via hooks, then it's OK to use hooks.
Hooks have been avoided until we have established clear best practices. We were bitten by this previously when we were an early adopter of React mixins. After a couple of years, React decided that mixins had many downfalls and they moved away from using them which left us with a lot of technical debt.
They are also avoided so that we can maintain consistent code styles everywhere and it doesn't come as a surprise on how something should be built. This decreases the barrier of entry to our codebase because you don't need to be an expert in React in order to work on it.
The conversation about using hooks is not final, but as for now, we choose not to have them in our code.
// Bad
// Extends from our custom components
class AComponent extends CComponent, React.Component { ... }
class BComponent extends CComponent, React.Component { ... }
// Good
const AEnhancedComponent = higherOrderComponent(CComponent);
// Good
// Only extends from React's component, not from our own components
class AComponent extends React.Component { ... }
class BComposedComponent extends React.Component
{
...
render() {
return (
<AComponent {...props}>
<div>
{this.state.whatever}
</div>
</AComponent>
)
}
...
}
From React's documentation:
Props and composition give you all the flexibility you need to customize a componentts look and behavior in an explicit and safe way. Remember that components may accept arbitrary props, including primitive values, React elements, or functions. If you want to reuse non-UI functionality between components, we suggest extracting it into a separate JavaScript module. The components may import it and use that function, object, or a class, without extending it.
Use Higher order components if you find a use case where you need inheritance.
React's documentation explains refs in detail. It's important to understand when to use them and how to use them to avoid bugs and hard to maintain code.
A common mistake with refs is using them to pass data back to a parent component higher up the chain. In most cases, you can try lifting state up to solve this.
There are several ways to use and declare refs and we prefer the callback method.
There are a few cases where jQuery should be used in a React component. There are also times when you should not use jQuery at all.
- Do use jQuery for global event listeners e.g. on
document
orwindow
(sparingly). jQuery offers some conveniences around global event binding that are more useful and less verbose than their native JS equivalents such as namespaced events. - Do use jQuery in a component that requires jQuery UI or Bootstrap plugins e.g. modals, date pickers, tooltips, etc. These all necessarily have dependencies on the global version of jQuery.
- Do use jQuery when you need to modify or interact with some content that has been set via
dangerouslySetInnerHTML
. Where possible, try to find a better way to do whatever you are trying to do. But if we must usedangerouslySetInnerHTML
then React has no way to interact with the DOM at that point. jQuery is likely our best option. - Do use jQuery to trigger global animations like page scrolls.
- Do not target an HTML element with a jQuery
id
orclass
selector that a React component knows about. Use aref
instead. - Do not access HTML elements that exist outside of a React component's immediate control or child view heirarchy
(e.g. to access a
<div>
rendered by a parent component from a child component). Move logic into whatever component owns the element you are trying to access and/or useReact.forwardRef
if you need to access an element owned by a child. - Do not call
addClass
,toggleClass
, orremoveClass
on a React component's HTML elements. Use theclassnames
library instead and set or unset the class based on props or state. - Do not add or remove HTML attributes to rendered elements. If you want an attribute to optionally not appear simply
default it to
undefined
in those cases. - Do not use jQuery to attach event listeners to DOM elements that React knows about. Use the built-in events instead
e.g.
onChange
event. - Do not import another instance of jQuery into a modular JS file if that file has a jQuery UI dependency. This can
overwrite
window.$
with an instance of jQuery that is missing the various plugins and dependencies it needs. - Avoid using jQuery to perform animations that are easily replaced by CSS. Animations can be triggered by adding and removing classes or by using React Transition Group.
- Do not get or set an uncontrolled input's value via
$.val()
. Use the input'sref
instead e.g.this.input.value = newValue
.
Never put CSS in a react file. Always use classes and put the styles in CSS files.
// Bad
<input style={{height: '25px'}} />
// Good
<input className="tallInput" />