Advanced React
Lifecycles
Each component in React has a lifecycle that you can monitor and manipulate during its three main phases.
The three phases are Mounting, Updating, and Unmounting.
Mounting
Mounting means putting the elements into the DOM
In React functional component, to let a function called in this phase, you can put them in useEffect hook with empty arrayDependencies
. The effect will perform only once when the component is rendered and since the dependencies array is empty, it will not perform when there are any other changes in the component.
Some typical things you want to do in this phase are adding an event listener to a global element like window
, or fetching data only one time from the server to be used in the component. Check the example below:
function Count() {const [selectedUser, setSelectedUser] = useState(null)const [users, setUsers] = useState([])const fetchUsers = useCallback(async() => {const result = await fetch('https://randomuser.me/api/?results=5').then(rs => rs.json())const { results } = result || {};setUsers(results)}, [])// fetch users list when component mounteduseEffect(() => {fetchUsers()}, [])return (<><selectdefaultValue=""onChange={(e) => setSelectedUser(e.target.value)}><option disabled value="">Select a user</option>{users.map(u => (<option value={u.email} key={u.email}>{u.name.first} {u.name.last}</option>))}</select><div>Selected user: {selectedUser}</div></>)}
Updating
The next phase in the lifecycle is when a component is updated.
A component is updated whenever there is a change in the component's states
or props
.
In Basic React section, we already talked about useEffect, which will perform a side effect when there is a change in the arrayDependencies
. This makes useEffect
perfect for hooking events in this phase. Check the example below:
function Count() {const [count, setCount] = useState(0)useEffect(() => {document.title = `Current count is ${count}`}, [count])return (<><div>Current count is {count}</div><Button onClick={() => setCount(c => c + 1)}>+</Button><div>Look at the title of the current tab to see the side effect</div></>)}
For performance optimization, if you want to control whether the components should re-render or not when states change, you can use useReducer hook instead of useState. If you want the component to re-render, return a copy of the current state
object, and if you just want to update the variables without re-rendering, return the current state
object. Check the example below:
function Count() {function reducer(state, action) {switch(action.type) {case 'increase':state.count = state.count + 1breakcase 'decrease':state.count = state.count - 1breakdefault:throw new Error()}// returning copy of state if rerender flag existsif (action.rerender) {return {...state}}return state}const [state, dispatch] = useReducer(reducer, { count: 0 })return (<><div>Current count is {state.count}</div><div><Button onClick={() => dispatch({ type: "increase" })}>+</Button><ButtononClick={() => dispatch({ type: "increase", rerender: true })}>+ (with rerender)</Button></div><div><Button onClick={() => dispatch({ type: "decrease" })}>-</Button><ButtononClick={() => dispatch({ type: "decrease", rerender: true })}>- (with rerender)</Button></div></>)}
Unmounting
The next phase in the lifecycle is when a component is removed from the DOM, or unmounting as React calls.
Usually, this phase is when we should clean up things to prevent memory leaks. Check the example below
function Count() {const [count, setCount] = useState(0)useEffect(() => {const handleClickWindow = () => {setCount(c => c + 1)}window.addEventListener("click", handleClickWindow)/*Cleaning the listener since React doesn't control the global element, so the listener is still there even when this component is unmounted*/return (() => {window.removeEventListener("click", handleClickWindow)})}, [])return (<><div>Current count is {count}</div><div>Click everywhere in the current window to increase count</div></>)}