Hello, React!
React.js is a library that premiered in 2013. Since then, React has been constantly developed due to its simplicity, ease of expansion and innovative approach. It is built around a large ecosystem of related technologies.
React itself is only a library used to build user interfaces. Typically, React based internet applications use many “helper” libraries which, in combination with React, can already be referred to as a framework or ecosystem.
When building an application based on React.js, we most often decide to use at least a few auxiliary libraries such as React Router, Redux and Redux Saga.
React Components
From a technical point of view, every element of React.js is a component. In a basic approach, components can be created using functions or ES6 classes.
Sample component built using the function:
import React from 'react';
function Header() {
return <div>Lorem Ipsum</div>
}
export default Header;
import React from 'react';
const Header = () => (<div>Lorem Ipsum</div>);
export default Header;
Sample component built using the ES6 class extended with React.Component:
import React, { Component } from 'react';
class Header extends Component {
render() {
return (
<div>Lorem Ipsum</div>
)
}
}
export default Header;
Until now, the basic difference between these types of components was the ability to handle state in class components. Functional components were used to present data and render the HTML code.
Due to differences in the operation of components, there is a division and design pattern that consists of splitting the application into two separate types of components with the appropriate division of responsibility.
- Presentation components – most often built using functions and used only for the presentation of data.
- Container components – to support more advanced business logic, usually built using class components.
Example container component:
import React, { Component, Fragment } from 'react';
import DisplayCounter from ‘./DisplayCounter.component.js’;
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
counterValue: 0,
}
}
incrementCounter() {
this.setState({count: this.state.counterValue + 1});
}
decrementCounter() {
this.setState({count: this.state.counterValue - 1});
}
render() {
return (
<Fragment>
<DisplayCounter couterValue={this.state.counterValue} />
<button onClick={this.incrementCounter.bind(this)}
INCREMENT COUTER
</button>
<button onClick={this.decrementCounter.bind(this)}
DECREMENT COUTER
</button>
</Fragment>
)
}
}
export default Counter;
Example presentation component:
import React from 'react';
const DisplayCounter = (props) => (
<section>
<h2>
Current counter value:
</h2>
<div>
{props.counterValue}
</div>
</section>
);
export default DisplayCounter;
The pattern of using container and presentation components often solves many architectural problems and makes it easier to divide the application and build optimal reusable components. Often, programmers encounter the division and choice of the type of component problem.
React on the Hooks
Simplified code of state components
State components code can be significantly simplified by using state support in functional components.
In functional components, state handling is performed using the useState function.
const [stateNumberValue, setNumberValue] = useState(0);
As a result of executing the above code, we will get the stateNumberValue constant and the setNumberValue() method which will be used to mutate the state of our component.
The state in stateNumberValue will be initialized with the value 0.
To get the functionality of the meter in the class component, most often we would create a similar solution:
import React, { Component } from 'react'
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
counterValue: 0,
}
}
incrementCounter()
this.setState({count: this.state.counterValue + 1});
}
decrementCounter() {
this.setState({count: this.state.counterValue - 1});
}
render() {
const { counterValue } = this.state;
return (
<div>
<div>Current counter value: {counterValue}</div>
<button onClick={this.incrementCounter.bind(this)}>
INCREMENT COUTER
</button>
<button onClick={this.decrementCounter.bind(this)}>
DECREMENT COUTER
</button>
</div>
)
}
}
export default Counter;
By choosing to use a functional component and handle state using the useState hook, the component could be significantly simplified:
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<div>Count: {count}</div>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
};
export default Counter;
You will notice a much simpler code structure and, finally, less final code needed to create the solution. This is not the only benefit of using the new approach.
One downside is that the code written in ES6 must be transposed to an older version of Javascript.
As a result of creating an identical class component, our application’s build will be enriched with 4.1kB of code. However, if we use a functional component with state support implemented using the useState hook, the size of the resulting code will be 1.9kB.
Conclusion:
- Component code has been simplified.
- We generated 54% less result code.
Easier creation of universal logic containing state
If we want to separate the logic of a state component so that it is easily reusable, we often decide to use the High Order Component pattern. This is a design pattern that involves creating higher-order components that contain specific logic which is all delivered to the main component using props. We create HOCs, whether by downloading data from the application’s store, linking actions to a component or creating universal logic to support localStorage.
HOC is a function that gets a component as a parameter, to which it provides all relevant data and functionalities to props object.
High Order Component for localStorage support:
import React, { Component } from 'react';
const withStorage = (WrappedComponent) => {
class HOC extends Component {
state = {
localStorageAvailable: false,
};
componentDidMount() {
this.checkLocalStorageExists();
}
checkLocalStorageExists() {
const testKey = 'test';
try {
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
this.setState({ localStorageAvailable: true });
} catch(e) {
this.setState({ localStorageAvailable: false });
}
}
save = (key, data) => {
if (this.state.localStorageAvailable) {
localStorage.setItem(key, data);
}
}
render() {
return (
<WrappedComponent save={this.save} {...this.props} />
);
}
}
return HOC;
}
export default withStorage;
Component wrapped by HOC:
import withStorage from 'components/withStorage';
const App = (props) => {
return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={e => props.save(e.target.value)}
/>
</div>
);
const WrappedComponent = withStorage(App);
export default WrappedComponent;
Typical problems with the use of High Order Components are:
- A high logical complexity of created components.
- Making the component tree difficult to read while debugging.
We can partially eliminate the problem by using the state in the functions provided in Hooks. This mechanism is called the Creation of Custom Hooks. The new approach is to create a function in which we can use the internal state created using the useState hook. The function can both return the value of its state and the methods used to mutate it.
Custom Hooks for localStorage support:
import { useState } from 'react';
export default function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = value => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
Component using localStorage customHook:
import React from 'react';
import useLocalStorage from './use-local-storage';
const App = () => {
const [name, setName] = useLocalStorage('name', 'Bob');
return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
);
}
export default App;
The biggest advantage of using Custom Hooks instead of High Order Components is they are easier to use inside the target component. In addition, another advantage is the simplification of debugging as a result of the non-existence of an additional component in the component structure.
After transposing for a solution with HOCs, we get about 5.5 kB of the resulting code. The Custom Hook solution for a similar functionality is about 2.8 kB.
Conclusion:
- Component code has been simplified.
- Custom hooks are an easier way to create shared logic.
- We generated 51% less result code.
Solution to the problem of distributed code, the same functionality.
The observation of component life cycles in functional components is carried out using special methods, if we want to execute the code while assembling the component we will use the componentDidMount() method. Changes in the application can be observed using the componentDidUpdate() method, while in the case of unmounting a component we can use the componentWillUnmount() method.
Using multiple methods to handle the state often leads to situations where the code responsible for the same functionality is distributed.
A class component that uses life cycles:
import React, { Component } from 'react';
class WindowResize extends Component {
state = {
height: window.innerHeight,
width: window.innerWidth,
};
componentDidMount() {
window.addEventListener("resize", this.listener);
}
componentWillUnmount() {
window.removeEventListener("resize", this.listener);
}
listener = () => {
this.setState({
height: window.innerHeight,
width: window.innerWidth,
})
};
render() {
const { height, width } = this.state;
return (
<div>
{width} x {height}
</div>
)
}
}
export default WindowResize;
In the above example, as part of the componentDidMount() method, eventListener is added and removed using the componentWillUnmount() method.
In functional components, the side effects support has been designed in such a way that it significantly eliminates the problem of code dispersibility.
A functional component with hook useEffect:
import React, { useState, useEffect } from 'react';
const WindowResize = () => {
const [width, setWidth] = useState(window.innerWidth);
const [height, setHeight] = useState(window.innerHeight);
const listener = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
};
useEffect(() => {
window.addEventListener("resize", listener);
return () => {
window.removeEventListener("resize", listener);
};
}, []);
return (
<div>
{width} x {height}
</div>
);
};
export default WindowResize;
useEffect() is another of the basic functions that supports three life cycles: mounting, updating and unmounting in one simple function.
useEffect() is the function that first adopts the function that will be performed as part of mounting and refreshing the component. If this function returns a method, it will be executed when the component unmounts. An additional advantage resulting from the application of this approach is the ability to directly determine under which factors the side effect code should be executed again and to what extent the second parameter of useEffect() function is responsible. If we enter an empty array as the parameter, the effect code will be executed only at the stage of component assembly. If the table contains the observed values, the effect will be executed if the set value has changed.
After transposition, the functional component contains about 55% less code.
Conclusion:
- Component code has been simplified.
- It eliminated the problem of distributed code.
- We generated less result code.
Summary
React Hooks is a feature that has revolutionized the development of applications in React. I think that saying that the component code can be up to 90% cleaner is not unfounded. We can easily eliminate many of the current problems and lastly, both the amount of written code and the resulting code is significantly smaller when all is said and done.