React Hooks

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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值