Originally published on https://smellycode.com/component-glossary/
Components are basic building blocks of modern web applications. They help web developers to break complex user interfaces into independent smaller blocks or pieces which can be reused and plugged with other pieces or components as is. In general, a component is
"a part or element of a larger whole, especially a part of a machine or vehicle." ~ Google
This article explains various types of components with words and code.
Function Components
Function Components are JavaScript functions which take inputs known as props and returns a React Element as output. Here's a simple Greetings
function component to greet.
function Greetings(props) {
return <h1>Hello {props.name}</h1>;
}
// With arrow function
// const Greetings = props => <h1>Hello {props.name}</h1>;
People often mix up function components with "Functional Components". Every component is a functional component if it is functioning or working fine. 😀
React does not instantiate function components. It means they can not be accessed with the ref attribute. Lack of instantiation also makes the life-cycle hooks inaccessible to function components.
Function components don't have any state unless they are hooked.
Class Components
Components created with ES6 Classes are known as Class Components. Class components extend the base class React.Component. Unlike function components, class components can have state and access the life-cycle methods. Class Components define a render
method which returns a react element as output. Here's a simple Clock
component to display time.
class Clock extends React.Component {
state = { now: new Date() };
intervalId = null;
updateTime = () => this.setState({ now: new Date() });
componentDidMount() {
this.intervalId = setInterval(() => this.updateTime(), 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
render() {
return <p>{this.state.now.toLocaleTimeString({}, { hour12: true })}</p>;
}
}
Instances of class components can be accessed with the ref attribute.
class App extends React.Component {
clockRef = React.createRef();
componentDidMount() {
// instance of the clock component
console.log(this.clockRef.current);
}
render() {
return <Clock ref={this.clockRef} />;
}
}
Pure Components
Let's discuss a simple Greetings
React.Component first.
class Greetings extends React.Component {
render() {
console.count('Greetings --> render');
return <p>Hello {this.props.name}!</p>;
}
}
It greets with a name
passed as props. An additional console.count statement is added to render
method to count executions.
The App
component below takes name
from a form input control and passes it to the Greetings
component.
class App extends React.Component {
state = { name: 'Sheldon', text: '' };
handleChange = event => {
this.setState({ text: event.target.value });
};
handleSubmit = event => {
event.preventDefault();
this.setState({ text: '', name: this.state.text });
};
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input
type="text"
value={this.state.text}
required
onChange={this.handleChange}
/>
<input type="submit" value="Greet" />
</form>
<Greetings name={this.state.name} />
</div>
);
}
}
When a user interacts with the input control, it updates the state of the App
component. React invokes the render
method--with the updated state and props--of the App
component and its children to create a new React Element tree for diffing. Although, the state and props of the Greetings
component are not changed, still React calls the render
method of the Greetings
component.
In large applications, such unnecessary executions of render
methods create performance issues and bog down user interfaces. The shouldComponentUpdate life-cycle method is used to avoid these unnecessary re-renderings of the component. By default, shouldComponentUpdate
return true, but its implementation can be easily overridden. Let's override shouldComponentUpdate
for the Greetings
component.
class Greetings extends React.Component {
shouldComponentUpdate(nextProps) {
// Re-render only when the `name` prop changes.
return this.props.name !== nextProps.name;
}
render() {
console.count('Greetings --> render');
return <p>Hello {this.props.name}!</p>;
}
}
After the very first render, Greetings
component is re-rendered only when the name
prop changes.
To solve the same problem, React introduces a variant of React.Component called React.PureComponent which implicitly implements shouldComponentUpdate
. The implicit implementation compares props and state by reference(shallow comparison). Let's write the pure version of Greetings
.
class PureGreetings extends React.PureComponent {
render() {
console.count('Pure Greetings --> render');
return <span>Hello {this.props.name}!</span>;
}
}
Here's the pen with full code.
Controlled/Uncontrolled Components
Working with form elements is a tad tedious. It requires a lot of malarky to get data from the form elements. That's because form elements maintain their own state internally. Developers have to throw a few lines to JavaScript to get the job done. Form elements in React are no exception. The way developers deal with a form element determines whether that element is a Controlled or Uncontrolled Element/Component. If the value of a form element is controlled by React then it's called a "Controlled Component" otherwise "Uncontrolled Component".
Controlled Components don't change their state on user interaction. State changes happen only when the parent component decides eg. the SubscriptionForm
component below doesn't honor user inputs (Codepen).
class SubscriptionForm extends React.Component {
handleSubmit = event => {
event.preventDefault();
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="email" value="smelly@smellycode.com" />
<input type="submit" value="Subscribe" />
</form>
);
}
}
Why changes are not honored? That's because the value
attribute for the email input is set to smelly@smellycode.com
. When React runs the diffing algorithm on the render tree. It always gets the email input as smelly@smellycode.com
so it ends up rendering the same value regardless of inputs entered by the user. Let's fix it by setting up an event listener which will update the state on change
event(Codepen).
class SubscriptionForm extends React.Component {
state = { email: '' };
handleSubmit = event => {
event.preventDefault();
console.log('Values --> ', this.state);
};
handleChange = event => this.setState({ email: event.target.value });
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type="email"
value={this.state.email}
onChange={this.handleChange}
/>
<input type="submit" value="Subscribe" />
</form>
);
}
}
Everything that goes into the input form elements is controlled by React here. That's why it is called "Controlled Component".
For "Uncontrolled Component", form data is not handled by React. DOM takes care of them. Here's an uncontrolled version of the SubscriptionForm
.
class SubscriptionForm extends React.Component {
inputRef = React.createRef();
handleSubmit = event => {
event.preventDefault();
console.log('Value -->', this.inputRef.current.value);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="email" ref={this.inputRef} />
<input type="submit" value="Subscribe" />
</form>
);
}
}
For elaborative comparison please refer the article.
Higher Order Components
Suppose there's an application which has a few malformed components--components whose elements/children are invalid react elements. Rendering of these components breaks the user interface.
// A sample malformed component.
class MalformedComponent extends React.Component {
render() {
// {new Date()} is not a valid react element. Rendering it will throw an error.
return <p>Now:{new Date()}</p>;
}
}
We need to implement an error handling mechanism to avoid crashes. React provides error boundary apis to handle such errors. So we refactor MalformedComponent
as:
class MalformedComponent extends React.Component {
state = {
error: null
};
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { error };
}
render() {
if (this.state.error) {
return (
<details>
<summary>Ouch! Things are messed up. We are sorry. 👾</summary>
<pre style={{ color: `red` }}>{this.state.error.stack}</pre>
</details>
);
}
return <WrappedComponent {...this.props} />;
}
}
Adding error boundaries only fixes the MalformedComponent
. We need to fix the other components too, means we need to add error boundaries to other components.
How do we do it? Hmm, One way is to add the error handling code in every malformed component the way we did above. But it will make our component a bit cumbersome to maintain and less DRY.
What if we write a function to fill in the error handling code? Well, we can write but we shouldn't because we'll be modifying the existing component which is not recommended and may lead to unexpected behavior.
What if we write a function which takes a malformed component and returns a new component which wraps the malformed component with error boundaries? Interesting! Only thing is, it will add a new wrapper component in our component tree, but we can live with it. Let's code it.
const withErrorBoundaries = WrappedComponent => props => {
return class extends React.Component {
state = {
error: null
};
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { error };
}
render() {
if (this.state.error) {
// Fallback ui.
return (
<details>
<summary>Ouch! Things are messed up. We are sorry. 👾</summary>
<pre style={{ color: `red` }}>{this.state.error.stack}</pre>
</details>
);
}
return <WrappedComponent {...this.props} />;
}
};
};
withErrorBoundaries
can be used with any malformed component.
const SafeComponent = withErrorBoundaries(MalformedComponent);
That's what precisely a higher order component is all about. It's a pattern which facilitates component logic reusability. You can think of a HOC as a function that takes a component and returns a new component. An in-depth explanation of HOCs is available here.
Dumb Components
Dumb components are also known as presentational or stateless components. They mostly contain HTML and styles. The purpose of dumb components is to render the DOM using props. Dumb Components don't load or mutate any data. Data required by dumb components are passed as input/props along with the actions. That's why dumb components don't have any state related to data. It makes them more reusable and manageable. Here is a very basic Greetings
dumb component :
function Greetings(props) {
return <h1>Hello {props.name}</h1>;
}
Smart/Container Components
Smart components are also known as Container Components. Smart Components know how to load and mutate data. Sometimes smart components mere act as a container and pass data to child components as props. Smart Components can also have state and logic to update the state. A simple Clock
component with state and logic.
class Clock extends React.Component {
state = { now: new Date() };
intervalId = null;
tick = () => this.setState({ now: new Date() });
componentDidMount() {
this.intervalId = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
render() {
return <p>{this.state.now.toLocaleTimeString()}</p>;
}
}
You can read more about Dumb Components and Smart components on Shade.codes.
Top comments (0)