Why React hooks
Class component and functional component are two kinds of component type in React, whose difference is mainly class component has its instance while functional component does not. This brings a lot of differences and new implementation style.
- Class component has corresponding lifecycles for mounting, updating and unmounting, but functional component act the same for these phases. Anytime its parent component re-render, it execute itself to get the element tree and run reconciliation. Developers can not “control” functional component whether to re-render as he expected as in class component(via
shouldComponentUpdate
). But on the other hand, functional component make itself easy to understand and explain how it works, as the programming principle, “simple is better”. - Since functional component is simple, it’s lightweight, thus fast, compared to class components. The app with functional components work in high-performance.
- Traditionally we can not do too much in functional components except rendering data(props) to view. Class component is more powerful, and we could have its own state, make side effects, add logic to lifecycle hooks.
We need neat and performant components, we also need powerful components. That’s why and what React hooks fulfill. useState
allows functional components has its own state, useEffect
allows to make side effects, useContext
allows to use context value directly, useRef
allows to apply ref
, useMemo
and useCallback
allow to create memorizable value and function, useReducer
allows to create reusable stores.
Hooks
useState()
useState
allows to apply states in functional components. It return the initial state passed in for the first render (it also support lazy initialization by passing in a function), the stateful value keeps for the subsequent re-render, and it’s different from class component setState
in the way that React will compare the previous state and the new state with Object.is
to decide whether to re-render when invoking the updating function. React will not re-render the component if the two values unchanged.
const View = props => {
const [name, setName] = useState('name');
const handleClick = () => {
// here it will not cause re-render as we set the same value.
setName('name');
}
return <div onClick={handleClick}>{name}</div>
}
If we update state multiple times inside a callback function, React will batch them to one-time re-render for optimization, works in the way of class component.
const View = props => {
const [name, setName] = useState('');
const handleClick = () => {
console.log('call setName first time');
setName('a');
console.log('call setName second time');
setName('b');
}
console.log('View render');
return <div onClick={handleClick}>{name}</div>
}
// console log
View render
call setName first time
call setName second time
View render
useState
also support passing in a function param for updating based on previous state, similar to class component. But for object state we have to merge it with object spread operator by ourselves as React will not merge object state with keys automatically. There’s a trap here as we always return a new object for the new state, equal comparison break down such that it always cause a re-render. Therefore we must check and decide to whether return new state by our self. And there’s no second argument for after-updating callback in class component, because each time updating state will cause a re-render and we could achieve it with useEffect
;
const View = props => {
const [count, setCount] = useState(0);
const [obj, setObj] = useState({ a: 'a' });
const handleClick = () => {
// update state based on previous state
setCount(preCount => preCount + 1);
// update object with object spread operator
setObj(preState => ({ ...preState, b: 'b' }));
// decide whether to update state by developers
setObj(preState => {
const { b } = preState;
if (b == null) {
// return new state if not set
return { ...preState, b: 'b' };
}
// keep unchanged and will not cause a re-render
return preState;
});
}
return <div onClick={handleClick}>{count}</div>;
}
For conditional rendering, useState
will re-initialize as class component.
const Child = () => {
const [name, setName] = useState(() => {
console.log('initialize useState');
return 'name';
});
return <p>{name}</p>
}
const Parent = () => {
const [show, toggleShow] = useState(false);
return show ? <Content /> : null;
}
In the above example, useState
initialization in Child
component will be executed every time show
changed to true
.
useState()
works well for ui state, especially for the case child component change its state and notify parent component without awareness of props from parent. But we still have to uplift states for state sharing and communication between components, as it still meets the React patterns: component composition and one-way data flow (props down
).
useEffect
useEffect
allows us to make side effects inside functional components. By default useEffect
runs after every render (the time after the render is committed to screen, layout and painting), works like componentDidMount
, componentDidUpdate
in class components. There are two useful tips for using useEffect
: 1. return a function inside useEffect
for cleanup. 2. pass in an array of source props as second arg such that useEffect
only fires when any source props change (passing in empty array []
works like componentDidMount
and componentWillUnmount
in class component).
const View props => {
const { show } = props;
useEffect(() => {
// making side effect when `show` changes
}, [show]);
return ...
}
Let’s figure out what’s the differences between useEffect
and class components’ componentDidMount
, componentDidUpdate
and componentWillUnmount
, and why it’s useful.
We have to apply lifecycle hooks for making side effects in class components, e.g. componentDidMount
, componentWillUnmount
, componentDidUpdate
componentWillReceiveProps
etc. But there are two disadvantages when writing business logic inside class components:
- We have to break complex logic block into small pieces and separate them into different lifecycle hooks, contrary to the high cohesion principle and making logic scattered and hard to understand.
- Class component’s instances are kept, and side effects based on props could only achieved by
componentDidUpdate
, which is easy to forget and make component buggy.
// class component: logic code block seperated and buggy for props changed
class View extends React.Component {
componentDidMount() {
const { fetchDetail, id } = this.props;
this.timer = window.setTimeout(() => fetchDetail(id), 1000);
}
// buggy when forget to re-make side effect when props changed.
// many developers solve it with conditional rendering in the parent component, but it's not the effective solution
componentDidUpdate(preProps) {
if (preProps.id !== this.props.id) {
const { fetchDetail } = this.props;
window.clearTimeout(this.timer);
this.timer = window.setTimeout(() => fetchDetail(id), 1000);
}
}
// clean up before unmount
componentWillUnmount() {
window.clearTimeout(this.timer);
}
}
// refactor with `useEffect` making it high cohensive and stable
const View = props => {
const { id, fetchDetail } = props;
useEffect(() => {
const timer = window.setTimeout(() => fetchDetail(id), 1000);
return () => {
window.clearTimeout(timer);
};
}, [id]);
}
As useEffect
allows us to implement dependent logic inside one code block, compared to separating them into multiple lifecycle hooks inside class components, we could write codes more high-cohesive by using multiple useEffect
for different side effects and purposes, align to the principle of separation of concerns.
Then let’s dive deep into how useEffect
works. By default each time React re-render the component, it schedule a different effect, with closure for accessed stale value, and defer it to the queue before new re-renders. That’s a side effect for each source prop’s change. If we miss source props used inside useEffect
, we will get a stale reference value for the side effect.
const View = props => {
const { id, fetchDetail } = props;
// it will not make any side effects when id change. And the side effect get the initial value of prop `id`
useEffect(() => {
const timer = window.setTimeout(() => fetchDetail(id), 1000);
return () => {
window.clearTimeout(timer);
};
}, []);
}
React ensure side effects are executed after the current re-render and before any new re-renders.
There are drawbacks when using useEffect
, which are easy to get into trap for developers.
The first trap is that any values from the component’s state or props referenced inside useEffect
should be listed in the dependency array, otherwise values without declared inside dependency array will be stale. we strongly recommend apply eslint plugin eslint-plugin-react-hooks
with error-prone for compiling and production build.
const View = () => {
const [name, setName] = useState('');
// it will get the stale value if missing `name` in dependency list
// since useEffect doesn't run on each re-render
useEffect(() => {
const value = name;
}, [name]);
}
The second trap is that since useEffect
runs after every update, it will easily cause multiple side effects if source props changed in a run, but we only want the last side effect, which previous sequent side effects are useless and cause performance issues.
const View = () => {
const [orderBy, setOrderBy] = useState(0);
const [order, setOrder] = useState(0);
useEffect(() => {
// side effect will run on every change of `orderBy` and `order`
// when `orderBy` and `order` change in a run, it will cause multiple side effects, but we only need the last side effects.
console.log('side effect');
}, [orderBy, order]);
useEffect(() => {
const timer = window.setTimeout(() => {
setOrderBy(1);
setOrder(1);
}, 2000);
return () => {
window.clearTimeout(timer);
};
}, []);
return <div>{orderBy}/{order}</div>
}
As frequent side effects is a performance issue, and we only need the last side effect, debounce function come to rescue in such situation, which we could use to skip sequential side effect and keep the last update side effects. But since each time component re-render, ordinary debounce function will just run again and return a new function, it doesn’t help. We need a new memorizable debounce function, with the help of useMemo
and useCallback
.
const View = () => {
const [orderBy, setOrderBy] = useState(0);
const [order, setOrder] = useState(0);
const update = useCallback(() => {
console.log(orderBy, order);
}, [orderBy, order]);
useEffect(() => {
console.log('execute update');
update();
}, [update]);
useEffect(() => {
const timer = window.setTimeout(() => {
setOrderBy(1);
setOrder(1);
});
return () => {
window.clearTimeout(timer);
};
}, []);
return <div>{orderBy}/{order}</div>
}
useMemo
, useCallback
and React.memo
They are all aim to promote performance. React.memo
could prevent re-render when props unchanged. useMemo
return the previous memorized value if dependent input values don’t change(compared with ===
), such that unnecessary expensive execution or calculation could be avoided. useCallback
works similar except that it return a callback function.
Let’s think about twice why we need useMemo
and useCallback
.
For functional component, a lot of functionalities are implemented by closure, namely inner function could access outer variable. We always create callback functions inside functional components and pass down as props for children components. In such cases, each time the component render, all variables inside the functional component are declared, assigned and destroyed. Therefore each time those variables are different from previous value, as they have been recreated and reassigned. As these props are not equal to their previous values, they causes children components’ re-render.
const Parent = props => {
const [count, setCount] = useState(0);
// `updateCount` changes each time re-render
const updateCount = () => {
// if we access outer variables, their values are stale as closure
console.log(count);
}
return <Child onUpdate={updateCount} />
}
class Child extends React.PureComponent {
componentWillReceiveProps(nextProps) {
console.log(nextProps.onUpdate !== this.props.onUpdate);
}
render() {
const { onUpdate } = this.props;
return <div onClick={onUpdate}>Click me</div>
}
}
// false
With useCallback
, the props passed down will keep unchanged if deps are not changed.
const Parent = props => {
const [count, setCount] = useState(0);
// `updateCount` keep unchanged if count unchange.
const updateCount = useCallback(() => {
// if we access outer variables, their values are stale as closure
console.log(count);
}, [count]);
return <Child onUpdate={updateCount} />
}
class Child extends React.PureComponent {
componentWillReceiveProps(nextProps) {
console.log(nextProps.onUpdate !== this.props.onUpdate);
}
render() {
const { onUpdate } = this.props;
return <div onClick={onUpdate}>Click me</div>
}
}
// true
We have to make it clear here. Each time Parent
render, it create a new updateCount
function(new Function()
) and pass it down to Child
component. As new Function()
always return a new instance, it’s definitely not equal the previous function, compared with ===
. Therefore, anytime Parent
re-render
will cause every children component which receive updateCount
props re-render.
useCallback
could achieve that updateCount
only change when count
changes.
each variable and will re-declared and assigned, no matter it’s an object, a function or a class instance, so any of these value passed down to component will be unequal to its previous value, causing a re-render of descendant components. We need useMemo
and useCallback
to keep them unchanged such that descendant components could prevent re-render with React.memo
(parent component’s re-render will definitely cause child components’ re-render). See the example below.
const View = () => {
const [name, setName] = useState('');
// `value` keep unchanged if `name` unchange
const value = useMemo(() => {
console.log('useMemo execution');
return name;
}, [name]);
// `updateName` will keep unchanged if `name` unchange.
const updateName = useCallback(() => {
}, [name]);
const value = useMemo
// Editor will not re-render with `React.memo` when `updateName` unchange.
return (
<Editor onUpdate={() => updateName()} />
)
}
Be aware that useMemo
and useCallback
could just prevent change caused by parent re-render but can’t help too much for function props accessing state passed down, as each time the state changes and cause a re-render, a new function is created and passed down. See the example below.
const Desc = props => {
// doesn't work because onUpdate is different for each render
const func = useCallback(() => { props.onUpdate() }, props.onUpdate);
}
const View = props => {
const { originalName, onUpdateName } = props;
const [name, setName] = useState(originalName);
const onChange = name => setName(name);
// `updateName` will changed each time View render, as a new function is created and assigned with closure
const updateName = () => {
// name will be the last updated value and kept with closure, each time View updates `updateName` is re-created and access a new `name` value
onUpdateName(name);
};
// onUpdate will always get the stale value if you prevent it from re-render
return (
<>
<Input value={name} onChange={onChange} />
<Desc onUpdate={updateName} />
</>
);
}
React.memo
is not any kind of hook api, but it boost performance in the way that it reduce the re-render caused by parent’s re-render when props unchanged.
useRef
and useContext
useRef
allows developers to apply ref
in functional components and useContent
allows to access context values.
useReducer
Though react hooks empower us do a lot things inside class components, but it doesn’t solve the draw back of React’s props down drawback: we have to uplift state and pass a lot of props down to most middle props such that components at the bottom could access them, but a lot of props make no sense for those middle components. Redux is a solution, but we all know reusing reducer/actions is kind of impossible with redux. In functional component useReducer
comes to rescue, allowing developers to create reusable reducers. With useReducer
we could build our own hooks contains reducers/actions aligned to redux.
Custom hook
Basically custom hook is nothing mysterious but a function which use hook api inside its body. We could apply any hook api mentioned above, and most important point is it could return anything instead of a react component.
Hooks provide a new way for encapsulation compared to react component. In traditional component tree structure, we could only pass props down the children components. We could invoke child component’s method (public interface exposed) only by ref
and it’s a tricky way which React do not suggest to do so, because react components are mounted, updated and unmounted by the framework inside and reference to child component will be unstable (also it couple the two components together). In conclusion, we could only pass props down and descendant components invoke callbacks to update the state in the traditional way. In custom hook we could return interface with anything.
Hook API for anti-patten state
For anti-pattern state(component has its own state and also change based on props), we could achieve it with useState
and useEffect
, works similar to way we use getDerivedStateFromProps
in class component.
const View = props => {
const [show, toggleShow] = useState(() => {
if ('open' in props) {
return props.open;
} else {
return false;
}
});
useEffect(() => {
toggleShow(props.open);
}, [props.open]);
const toggle = () => {
if ('open' in props) {
const { onToggle } = props;
onToggle(!prop.open);
} else {
toggleShow(preState => !preState);
}
}
return (
<div>
<button onClick={() => toggle()}>toggle</button>
{show ? <p>content</p> : <p>empty</p>}
</div>
);
};
View.defaultProps = {
onToggle: () => {}
};
The significant difference is that when props.open
changes, the View
component re-render, and after it’s updated useEffect
triggers another re-render since state changes. Such implementation bring twice re-render for each update, among which is useless. When applying static getDerivedStateFromProps
in class component it only trigger one-time re-render.