# Refactor Tasks
## Task 1 - convert `VBaseComponent` into a custom hook
Before any refactoring to a functional component takes place, we have to first move all the functionality in `VBaseComponent` into a custom hook to import it and use the methods defined in `VBaseComponent`.
**NOTE:** do not delete `VBaseComponent`
VBaseComponent: `apps/icm-web/src/components/common/baseComponent.tsx`
This file contains helper functions for translations and localization, ie:
```jsx
protected formatMessage(...args) {
return LocaleHelper.formatMessage(this.store.getState(), ...args);
}
```
Create a new file here: `apps/icm-web/src/customHooks`
Name it `useTranslations`
Create a function and export it like this:
> implementation: apps/icm-web/src/customHooks/useTranslations.tsx
```jsx
import { useState, useEffect } from 'react';
import LocaleHelper from 'helpers/localeHelper';
import * as SystemTranslations from 'localizations/systemTables/systemTranslations';
import { ICustomStore } from 'store';
// this is just an example to get started
// the details are subject to change
export const useTranslations = () => {
const [store, setStore] = useState<ICustomStore | null>(null);
useEffect(() => {
setStore(window.store);
}, []);
// could be a one-liner arrow function
const formatMessage = (...args) => {
return LocaleHelper.formatMessage(store.getState(), ...args);
};
// ... etc
return {
formatMessage,
// ... other functions
};
};
```
> usage in some functional component
```jsx
import { useTranslations } from 'customHooks/useTranslations';
const SomeFunctionalComponent = ({ someProp }: SomeShape) => {
// initialize hook and destruct the methods you needs
// in this case {formatMessage}
const { formatMessage } = useTranslations();
return (
<>
<a href="/">{formatMessage('OK')}</a>
</>
);
};
export default SomeFunctionalComponent;
```
---
## Task 2 - refactor class-based components into functional components
Once we have the custom hook (`useTranslations`) we can start converting classical components to functional components.
Here are some of the steps to refactor at a high-level:
1. rename component to a typescript file (`.tsx`)
2. all references to `this` should be removed
3. all functions can and should be converted to an arrow function
4. remove constructor function
5. all `setState` calls can be converted to implement the `useState` (if a string, number, boolean) or `useReducer` (if the state is an object)
6. remove `componentDidMount` and `componentDidUnmount`, convert it to `useEffect`
7. remove `componentWillReceiveProps` to convert it to `useEffect`
8. generally all React lifecycle functions can be converted to a `useEffect` function with specific dependencies array
9. `render` function is removed, the component will now render whatever the function returns
10. Redux provides custom hooks for connect to the global store. Therefore we can replace `@connect` or `mapStateToProps`/`mapDispatchToProps` with the custom hook `useSelector` and `useDispatch` (https://react-redux.js.org/api/hooks)
### Examples
> Class-based component before refactor
```jsx
import React from 'react';
import _ from 'lodash';
import VBaseComponent from 'components/common/baseComponent';
@connect((store) => ({
fetchMessageData: store.fetchMessageData,
cancelFetchMessageData: store.cancelFetchMessageData,
globalExpensiveFunction: store.globalExpensiveFunction,
}))
class Message extends VBaseComponent {
constructor(props) {
super(props);
this.state = {
message: '',
count: 0,
arrayOfNames: this.props.globalExpensiveFunction(this.props.language),
};
this.onMessageButtonClick = this.onMessageButtonClick.bind(this);
}
componentDidMount() {
this.props.fetchMessageData(this.props.language);
}
componentWillUnmount() {
this.props.cancelFetchMessageData();
}
componentWillReceiveProps(nextProps) {
// when {language} prop changes
// we fetch new message data providing the language as a param
// #ifblock1
if (!_.isEqual(this.props.language, nextProps.language)) {
this.props.fetchMessageData(nextProps.language);
}
// when both {language} and {inFocus} props change and {inFocus} equals TRUE
// then we call the expensive global function passing in the new `language` value
// and set the state variable {arrayOfNames} to equal the result of the {globalExpensiveFunction}
// #ifblock2
if (
!_.isEqual(this.props.language, nextProps.language) &&
!_.isEqual(this.props.inFocus, nextProps.inFocus) &&
nextProps.inFocus
) {
this.setState({
arrayOfNames: this.props.globalExpensiveFunction(nextProps.language),
});
}
}
onButtonClicked() {
// chained set states isn't great
// this will be refactored
this.setState(
(previousState) => ({ count: previousState + 1 }),
(newState) => {
this.setState({ message: `clicked ${newState.count} times` });
}
);
}
render() {
const { isLoggedIn } = this.props;
const { message } = this.state;
return (
<main>
{isLoggedIn ? (
<>
{formatMessage('MESSAGE_TITLE')}
<ComponentWithClickListenerInside
customClickEvent={this.onButtonClicked}
/>
<p>{message}</p>
<ul>
{this.arrayOfNames.map((name) => (
<li>{name}</li>
))}
</ul>
</>
) : (
<p>Please log in to see message</p>
)}
</main>
);
}
}
```
> Functional component **after refactor**
```jsx
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useTranslations } from 'customHooks/useTranslations';
const Message = (props) => {
// first destructure all props
const { isLoggedIn, language, inFocus } = props;
// accessing redux store
const fetchMessageData = useSelector((store) => store.fetchMessageData);
const cancelFetchMessageData = useSelector(
(store) => store.cancelFetchMessageData
);
const globalExpensiveFunction = useSelector(
(store) => store.globalExpensiveFunction
);
// new custom hook
const { formatMessage } = useTranslations();
// replaces setState
const [message, setMessage] = useState('');
const [count, setCount] = useState(0);
const [arrayOfNames, setArrayOfNames] = useState([]);
useEffect(() => {
// since this useEffect only fire once when the component loads
// this pattern replaces the {componentDidMount} lifecycle hook
fetchMessageData(language);
// note that {globalExpensiveFunction} returns an array of strings for simplicity
setArrayOfNames(globalExpensiveFunction(language));
return () => {
// returning a function within the `useEffect` acts the same as {componentWillUnmount}
cancelFetchMessageData();
};
}, []); // an empty dependencies array ensure that this useEffect only fires ONCE
// each logical IF code block in {componentWillReceiveProps} can be split into their own seperate useEffect hooks
// the same as #ifblock1 in the pre-refactor example
useEffect(() => {
fetchMessageData(language);
}, [language]);
// the same as #ifblock2 in the pre-refactor example
useEffect(() => {
if (inFocus) {
setArrayOfNames(globalExpensiveFunction(language));
}
}, [language, inFocus]);
// PREVIOUSLY we were using chained setState calls to first update the {count} value
// then update the {message} value
// NOW we only update the {count} value in the {onButtonClicked} method, but can achieve the same effect by
// defining a `useEffect` that only runs when {count} changes
// which sets the state of {message} with the new value of {count}
useEffect(() => {
setMessage(`clicked ${count} times`);
}, [count]);
const onButtonClicked = () => {
setCount(count + 1);
};
return (
<main>
{isLoggedIn ? (
<>
{formatMessage('MESSAGE_TITLE')}
<ComponentWithClickListenerInside
customClickEvent={onButtonClicked}
/>
<p>{message}</p>
<ul>
{arrayOfNames.map((name) => (
<li>{name}</li>
))}
</ul>
</>
) : (
<p>Please log in to see message</p>
)}
</main>
);
};
export default Message;
```
> Converted to typescript
```jsx
import React, { FC, ReactElement, useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
// you can import {FC} or {FunctionComponent}, they're exactly the same
import { useTranslations } from 'customHooks/useTranslations';
type ChildProps = {
isLoggedIn: boolean;
inFocus: boolean;
language: string;
};
const Message: FC<ChildProps> = (props: ChildProps): ReactElement => {
const { isLoggedIn, language, inFocus } = props;
const fetchMessageData = useSelector(
(store: IGlobalStateShape) => store.fetchMessageData
);
const cancelFetchMessageData = useSelector(
(store: IGlobalStateShape) => store.cancelFetchMessageData
);
const globalExpensiveFunction = useSelector(
(store: IGlobalStateShape) => store.globalExpensiveFunction
);
const { formatMessage } = useTranslations();
const [message, setMessage] = useState<string>('');
const [count, setCount] = useState<number>(0);
const [arrayOfNames, setArrayOfNames] = useState<string[]>([]);
useEffect(() => {
fetchMessageData(language);
setArrayOfNames(globalExpensiveFunction(language));
return () => {
cancelFetchMessageData();
};
}, []);
useEffect(() => {
fetchMessageData(language);
}, [language]);
useEffect(() => {
if (inFocus) {
setArrayOfNames(globalExpensiveFunction(language));
}
}, [language, inFocus]);
useEffect(() => {
setMessage(`clicked ${count} times`);
}, [count]);
const onButtonClicked = () => {
setCount(count + 1)
};
return (
<main>
{isLoggedIn ? (
<>
{formatMessage('MESSAGE_TITLE')}
<ComponentWithClickListenerInside
customClickEvent={onButtonClicked}
/>
<p>{message}</p>
<ul>
{arrayOfNames.map((name) => (
<li>{name}</li>
))}
</ul>
</>
) : (
<p>Please log in to see message</p>
)}
</main>
);
};
export default Message;
```
A lot is going on here. But a few things to highlight are:
- there are no more references to `this`
- props are destructed at the top of the function and referenced directly, no more `this.props.stateVariable`
- the lifecycle hook functions (`componentDidMount`, `componentWillUnmount`, `componentWillReceiveProps`) have been moved into `useEffect` code blocks
- any function that is not part of the lifecycle hooks has been converted to an arrow function ie: `onButtonClicked`
- the logic in the constructor which defines the "initial state" has been moved to `useState` (more on this in a bit)
- there are no more references to `setState`, all state logic is now handled through `useState`
- the render function now is simply a return statement which returns JSX
- we were previously extending `VBaseComponent` to get access to certain translation methods, now we access this through our new custom hook `useTranslations`
- the `@connect` for redux is removed in favour of the `useSelector` hook
----
## Task 3 - optimize components
Now that we have refactored the component into a functional component, we can now add built-in React hooks to help optimize them. By "optimize," I mean to limit the number of redundant/needless re-renders. In other words, if none of the props change the value, then the component shouldn't re-render again. Here I will outline two hooks and one wrapper component (HoC), which helps with this common issue.
----
### `React.useMemo`
> useMemo can help the performance of an application by "remembering" expensive functions and preventing a re-render every time there is a change in the application.
[Source](https://www.digitalocean.com/community/tutorials/react-usememo)
In the previous example, we referenced a method called `globalExpensiveFunction`.
Notice that we define a `useState` and call the setter function in two separate `useEffect` code blocks. We're going to change that.
Let's say this `globalExpensiveFunction` method was an expensive global function with multiple implications throughout the app. We probably want to call this function only when specific criteria are met. This would be a good candidate for `useMemo`.
Let's refactor our code from the previous example.
> We define this `useState` and update it in multiple places in the code.
```jsx
const [arrayOfNames, setArrayOfNames] = useState<string[]>([]);
```
This works, but it's not ideal because the conditions for when we update the {`arrayOfNames`} state variable is essentially the same, which means the code is duplicated. But perhaps more importantly, the "expensive function" isn't being memoized. So even though none of the conditions have changed, it's still executing the expensive function and returning the value every time. We would ideally want to return the "cached" results from the previous function call **only if the conditions haven't changed**. If the conditions have changed, then we would call the function again.
```jsx
// we can remove the `useState` for {arrayOfNames}
// and also all calls to {setArrayOfNames}
// and use the return memoized value from `useMemo` call as a state variable
// now this function is only called when either {language} or {inFocus} props change value
const arrayOfNames = useMemo(() => {
if (inFocus) {
globalExpensiveFunction(language);
}
}, [language, inFocus]);
```
Now the function is memoized and will only actually execute `globalExpensiveFunction()` when necessary.
----
### `React.memo`
> Memo derives from memoization. It means that the result of the function wrapped in React.memo is saved in memory and returns the cached result if it's being called with the same input again.
[Source](https://felixgerschau.com/react-performance-react-memo/)
The HoC `memo` is a wrapper component. It can be used to improve performance by limiting re-renders. It's similar to React's class-based component variation `PureComponent`.
> Basic example
```jsx
import React, { memo } from 'react';
const PersonComponent = ({ name }) => <p>My name is {name}!</p>;
const MemoizedPersonComponent = memo(PersonComponent);
export default MemoizedPersonComponent
```
Now the `PersonComponent` component will only re-render when the value of `name` changes. This works only for [primitives](https://www.vojtechruzicka.com/javascript-primitives/). Which means this will not work on functions and objects.
That's when `useCallback` comes in.
----
### `React.useCallback`
> The main purpose of React useCallback hook is to memoize functions. The main reason for this is increasing performance of your React applications. How is this related? Every time your component re-renders it also re-creates functions that are defined inside it. Memoizing functions helps you prevent this.
[Source](https://blog.alexdevero.com/react-usecallback-hook/)
In JavaScript, functions can never equal one another, even if both functions are the same in signature. Like objects, functions can only ever equal themselves. Therefore in React, when you pass a function as a prop to another component, that component will **always** interpret that prop as "_changed_" and thus trigger a re-render.
The most common function to pass down to other components are event handlers, such as `onClick`, `onMouseDown` etc.
The `useCallback` hook solves this specific issue and goes hand-in-hand with `memo` wrapper component. But note that **the `useCallback` hook is not a band-aid solution for all situations**. For example, the cost of `useCallback` could be more expensive than the actual callback function itself, and that would defeat the purpose of optimizing.
Like the `useEffect` and `useMemo` pattern, the `useCallback` takes a function and a dependency array as the parameters.
```jsx
const onButtonClicked = useCallback(() => {
toggleExampleVariable(!exampleVariable);
}, [someOtherVariable]);
```
In general, you would want to `useCallback` in conjunction with `memo`.
Here is a working example of a parent and child component. The parent component renders two child components. Both the same but with different props.
> In this example, the `CountButton` and `DualCounter` components are not optimized. So every time we click the button to increment the count, both CountButton components re-render multiple times
```jsx
import React, { useState, useRef } from 'react';
const CountButton = ({ onClick, count, name }) => {
return (
<div>
<button onClick={onClick}>{`${name} clicked ${count} times`}</button>
</div>
);
};
const DualCounter = () => {
const [count1, setCount1] = useState(0);
const onIncrement1 = () => setCount1((c) => c + 1);
const [count2, setCount2] = useState(0);
const onIncrement2 = () => setCount2((c) => c + 1);
return (
<>
<CountButton name="Bert" count={count1} onClick={onIncrement1} />
<CountButton name="Ernie" count={count2} onClick={onIncrement2} />
</>
);
};
```
[See Codebox Example](https://codesandbox.io/s/xenodochial-browser-rtz7j?file=/src/App.js)
However, if we wrap the `CountButton` component in the memo and change the onClick callback functions to use `useCallback`, then the `CountButton` will only re-render itself when neccessary. Meaning less or no re-renders.
> Here we imported `memo` and `useCallback`
```jsx
import React, { memo, useState, useRef, useCallback } from 'react';
const CountButton = memo(({ onClick, count, name }) => {
return (
<div>
<button onClick={onClick}>{`${name} clicked ${count} times`}</button>
</div>
);
});
const DualCounter = () => {
const [count1, setCount1] = useState(0);
const onIncrement1 = useCallback(() => setCount1((c) => c + 1), []);
const [count2, setCount2] = useState(0);
const onIncrement2 = useCallback(() => setCount2((c) => c + 1), []);
return (
<>
<CountButton name="Bert" count={count1} onClick={onIncrement1} />
<CountButton name="Ernie" count={count2} onClick={onIncrement2} />
</>
);
};
```
[See Codebox Example](https://codesandbox.io/s/relaxed-chatelet-v28yb?file=/src/App.js)
----
## Potentially Helpful Links
See here for some articles outlining the steps to converting a component to a functional component.
- [Offical React Doc - Custom hooks](https://reactjs.org/docs/hooks-custom.html)
- [Offical React Doc - Lifecycle](https://reactjs.org/docs/state-and-lifecycle.html)
- [React lifecycle hooks](https://medium.com/swlh/react-lifecycle-hooks-71547ef4e7a8)
- [How To Convert React Class Components to Functional Components with React Hooks](https://www.digitalocean.com/community/tutorials/five-ways-to-convert-react-class-components-to-functional-components-with-react-hooks)
- [10 Steps to Convert React Class Component to React Functional Component with Hooks](https://olinations.medium.com/10-steps-to-convert-a-react-class-component-to-a-functional-component-with-hooks-ab198e0fa139)
- [useCallback official docs](https://reactjs.org/docs/hooks-reference.html#usecallback)
- [Your Guide to React.useCallback](https://dmitripavlutin.com/dont-overuse-react-usecallback)