React: switching from class-based components to functional

Following is a guide written originally by Bartek Maziarka for our internal needs at SMOK (which was tutoring our interns and the CTO). It has been only translated by me. Permission was obtained to publish it.

E-mail address of the original author

You will need basic knowledge of React and JavaScript in order to proceed.

Functional components

Up until not very long ago functional components were used to present data, displaying it basing it on received props. However, tasks which required to store state or execute actions at particular points in the component lifecycle were done with class components. Basic example of this app is DeDietrich Home Control OT (there’s almost no functional components there to be found).

Since version 16.8 React has added hooks. They allow you to use state and other functions of React without using classes. Thanks to it you can finally let go of the frequently complicated this or HoCs (Higher-Order Components) and drastically limit the amount of props passed up and down using your own hooks and contexts.

Introduced changes

State of the component in class-based components have been defined using an object containing default state, whereas it was changed using the function this.setState. In functional components you would rather utilize a hook called useState, accepting as an argument the default state and returning an array of form [value, setValue].

ClassFunctional
this.state = {
is_payroll: false
};

this.setState({is_payroll: true});
this.setState(prevState => ({is_payroll: prevState.is_payroll}));
const [is_payroll, setIsPayroll] = useState(false);

setIsPayroll(true);
setIsPayroll(prevPayroll => !prevPayroll)
Difference when using state in class-based and functional components

Most frequently the component state consisted of multiple variables, in this case while using the useState hook it is required to update only a single one, passing the rest of variables unchanged. We can avoid this defining our own hook allowing us to support multiple variables using one state-changing state: useObjectState

useObjectState

This hook will allow to update the current state with changing parameters. Created hooks frequently start their name with use and return an array – the value and a function allowing you to update it.

const useObjectState = (initialState) => {
const [state, setState] = useState(initialState);
const setObjectState = newState =>
setState(prevState => Object.assign({}, prevState, newState)
);
return [state, setObjectState];
}

An example of use:

ClassFunctional
this.state = {
devices: [],
loaded: false
}
const [{devices, loaded}, setState] = useObjectState({
devices: [],
loaded: false
});

Update of state is done as previously with the setState function. Knowing the keys of values stored inside state, we can use the aforementioned notation to use them later calling them by their names – devices or loaded without prefixing them with this.state each time.

Higher-Order Components

While using class-based components functions from external libraries were provided through props using higher-order components, which wrap our components. Functional components allow you to use hooks, wrapping entire application with a context that allows to use this hook inside the component with arbitrarily nested inside the component tree.

ClassFunctional
this.history = props.history;
this.history.push("/");

this.t = props.t;
this.t('login.action');

export default withTranslation()(withRouter(Login));
const history = useHistory();
history.push("/");

const {t} = useTranslation();
t('login.action')

export default Login;

Extending components with new functionality

Class-based components can be extended via inheritance from other classes. Assuming that we have a defined FormBasedComponent providing us with a changeValue method, we can create components having forms utilizing this function. State management should be carefully considered (either we store the state inside FormBasedComponent, or we define an object inside the component’s state with given name). We can alternatively define our own hook, that will store the state and provide a function enabling us to later set this field’s value:

const useForm = (defaultState) => {
const [state, setState] = useState(defaultState);
const changeValue = (valueName) => {
return (event) => {
setState({
...state,
[valueName]: event.target.value
}
);
};
};

return [state, setState, changeValue];
};

Assuming we do not use nested field names, this hook is very easy to construct. Inside application we will find however a hook supporting nested names like user.name, however in order to present it’s working a simpler version was posted here. The changeValuefunction returns another function, thanks to which it can be called inside child component’s onChange prop. This time our hook will return 3 values. We can additionally manually change the form value, which can be useful for implementing additional logic. Below is an example of this hook’s use:

const [{login, password}, , changeValue] = useForm({
login: '', password: ''
});

const loginUser = () => {
alert(`login: ${login}, password: ${password}`);
};
return (
<FormControl>
<TextField onChange={changeValue('login')}/>
<TextField onChange={changeValue('password')}/>
<Button onClick={loginUser}>{t('login.action')}</Button>
</FormControl>
);

Life cycle methods

Inside class-based components we have different life cycle functions, such as componentDidMount, componentDidUpdate, componentWillUnmount. Inside functional components the functions of all these methods is implemented using the useEffect hook.

componentDidMount, componentWillUnmountcomponentDidUpdate
componentDidMount() {
this.bootstrapData();
}

componentWillUnmount() {
this.cleanUp();
}
constructor(props) {
super(props);
this.state = {
login: '';
};
}
componentDidUpdate(prevProps, prevState) {
if (prevState.login !== this.state.login) {
console.log(login);
}
}
useEffect(() => {
bootstrapData();
return cleanUp;
}, []);
const [login, setLogin] = useState('');
useEffect(() => {
console.log(login);
}, [login]);

useEffect accepts as arguments functions and an array of variables, which will need to change in order for the function to execute. A function returned from the passed function will be executed before the next execution of this function’s body. An exception to this is passing an empty array of dependencies. The function in this case will be called after mounting, and the function returned from it – after unmounting the component.

Access to props

Functional components receive their props as their first argument, so it’s natural to write:

const User = (props) => {
return (
    <div className="user-combobox">{props.userName}</div>);
}

However, if we know what props we’ll receive, we can (as in the example above) refer to them directly by their variable name, and assing them default values if they’re not given. Their value will be overwritten if we give the component an attribute with the name which will not be a false-y value.

const User = ({
userName = 'Bartek'
}) => {
return (
<div className="user-combobox">{userName}</div>);
}

Extra changes – jshint

Jshint sees the closing tags of elements as regexes. I’ve removed it and replaced it with ESLint, just drop the following into your package.json:

“eslintConfig”: { “extends”: [ “react-app”, “react-app/jest” ] },

Extra changes – absolute imports

Imports styled as following:

import api from "../../../axios";

can be replaced with absolute imports, it’s enough to pass the main directory into jsconfig.json file:

{
"compilerOptions": {
"baseUrl": "src"
}
}

and we can enjoy imports without backtracking to the main directory:

import api from "axios";

Leave a comment

Your email address will not be published. Required fields are marked *