In the previous section, we will discuss react functional components and some hook concepts like useState, useEffect and useContext. Now we discuss remaining hook concepts.
React useRef Hook
Use the useRef Hook to persist values between renders.
You can use it to store mutable values that don’t require re-rendering when they are updated.
You can use it to access DOM elements directly.
Does Not Cause Re-renders
If we tried to count the number of times the useState Hook renders our application, we would be caught in an infinite loop as this Hook itself causes a re-render.
To avoid this, we can use the useRef Hook.
Example: Use useRef to track application renders. import { useState, useEffect, useRef } from "react"; import ReactDOM from "react-dom"; function App() { const [inputValue, setInputValue] = useState(""); const count = useRef(0); useEffect(() => { count.current = count.current + 1; }); return ( <> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <h1>Render Count: {count.current}</h1> </> ); } ReactDOM.render(<App />, document.getElementById('root'));Output:
The useRef() method only returns one item. It returns an Object called current.
Initializing useRef, we set its initial value.(useRef(0)).
It's like doing this: const count = {current: 0}.
Use count.current to get the current count.
Try typing in the input on your computer to see the render count increase.
Accessing DOM Elements
Generally, we want React to handle all DOM manipulation.
However, in some cases useRef can be used without causing issues.
We can add a ref attribute to an element in React to directly access it in the DOM.
Example: Use useRef to focus the input: import { useRef } from "react"; import ReactDOM from "react-dom"; function App() { const inputElement = useRef(); const focusInput = () => { inputElement.current.focus(); }; return ( <> <input type="text" ref={inputElement} /> <button onClick={focusInput}>Focus Input</button> </> ); } ReactDOM.render(<App />, document.getElementById('root'));Output:
Tracking State Changes
In addition, the useRef Hook can be used to keep track of previous state values.
This is because we are able to persist useRef values across renders.
Example: Use useRef to keep track of previous state values: import { useState, useEffect, useRef } from "react"; import ReactDOM from "react-dom"; function App() { const [inputValue, setInputValue] = useState(""); const previousInputValue = useRef(""); useEffect(() => { previousInputValue.current = inputValue; }, [inputValue]); return ( <> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <h2>Current Value: {inputValue}</h2> <h2>Previous Value: {previousInputValue.current}</h2> </> ); } ReactDOM.render(<App />, document.getElementById('root'));Output:
To keep track of the previous state, we use a combination of useState, useEffect, and useRef.
We update the useRef current value each time the inputValue is updated by entering text into the input field within the useEffect.
React useMemo Hook
The React useMemo Hook returns a memoized value.
Memories cache values so that they do not have to be recalculated.
The useMemo Hook runs only when a dependency updates.
This can improve performance.
The useMemo and useCallback Hooks are similar. UseMemo returns a memoized value, while useCallback returns a memoized function.
Performance
By using the useMemo Hook, you can prevent expensive, resource-intensive functions from running needlessly.
We have an expensive function that runs on every render in this example.
If you modify the count or add to everything, you will notice a delay in execution.
Example: It performs poorly. The expensiveCalculation function runs on every render: import { useState } from "react"; import ReactDOM from "react-dom"; const App = () => { const [count, setCount] = useState(0); const [todos, setTodos] = useState([]); const calculation = expensiveCalculation(count); const increment = () => { setCount((c) => c + 1); }; const addTodo = () => { setTodos((t) => [...t, "New Todo"]); }; return ( <div> <div> <h2>My Todos</h2> {todos.map((todo, index) => { return <p key={index}>{todo}</p>; })} <button onClick={addTodo}>Add Todo</button> </div> <hr /> <div> Count: {count} <button onClick={increment}>+</button> <h2>Expensive Calculation</h2> {calculation} </div> </div> ); }; const expensiveCalculation = (num) => { console.log("Calculating..."); for (let i = 0; i < 1000000000; i++) { num += 1; } return num; }; ReactDOM.render(<App />, document.getElementById('root'));Output:
Use useMemo
We can fix this performance issue by using the useMemo Hook to memoize the expensiveCalculation function. In this manner, the expensiveCalculation function only runs when necessary.
With useMemo, we can wrap the expensive function call.
A second parameter to useMemoHook declares dependencies. The expensive function will only run when its dependencies have changed.
As seen in the following example, the expensive function is only executed when the count is changed and not when tasks are added.
Example: Performance example using the useMemo Hook: import { useState, useMemo } from "react"; import ReactDOM from "react-dom"; const App = () => { const [count, setCount] = useState(0); const [todos, setTodos] = useState([]); const calculation = useMemo(() => expensiveCalculation(count), [count]); const increment = () => { setCount((c) => c + 1); }; const addTodo = () => { setTodos((t) => [...t, "New Todo"]); }; return ( <div> <div> <h2>My Todos</h2> {todos.map((todo, index) => { return <p key={index}>{todo}</p>; })} <button onClick={addTodo}>Add Todo</button> </div> <hr /> <div> Count: {count} <button onClick={increment}>+</button> <h2>Expensive Calculation</h2> {calculation} </div> </div> ); }; const expensiveCalculation = (num) => { console.log("Calculating..."); for (let i = 0; i < 1000000000; i++) { num += 1; } return num; }; ReactDOM.render(<App />, document.getElementById('root'));Output:
React useCallback Hook
The useCallback Hook for React returns a memoized callback function.
By doing this, we can isolate resource-intensive functions so that they don’t run automatically on every render.
Whenever one of its dependencies updates, the useCallback Hook runs.
It can improve performance.
Problem
The purpose of useCallback is to prevent re-rendering of a component unless its properties have changed.
As an example, you might think that the Todos component will not re-render unless the todos change:
This is a similar example to the one in the userMmeo section.
Example: index.js import { useState } from "react"; import ReactDOM from "react-dom"; import Todos from "./Todos"; const App = () => { const [count, setCount] = useState(0); const [todos, setTodos] = useState([]); const increment = () => { setCount((c) => c + 1); }; const addTodo = () => { setTodos((t) => [...t, "New Todo"]); }; return ( <> <Todos todos={todos} addTodo={addTodo} /> <hr /> <div> Count: {count} <button onClick={increment}>+</button> </div> </> ); }; ReactDOM.render(<App />, document.getElementById('root'));
Todos.js import { memo } from "react"; const Todos = ({ todos, addTodo }) => { console.log("child render"); return ( <> <h2>My Todos</h2> {todos.map((todo, index) => { return <p key={index}>{todo}</p>; })} <button onClick={addTodo}>Add Todo</button> </> ); }; export default memo(Todos);Output:
Run this and click the count increment button.
It is noticeable that the Todos component re-renders even when the tasks don’t change.
When we increase the todo count, neither the todos state nor the addTodo function should re-render since we are using memo.
The reason is a concept known as “referential equality”.
Re-rendering a component recreates its functions, so adding todo actually changed.
Solution
Use the useCallback hook to prevent the function from being recreated unless absolutely necessary.
By using the useCallback Hook, the Todos component will not be re-rendered excessively:
Example: index.js import { useState, useCallback } from "react"; import ReactDOM from "react-dom"; import Todos from "./Todos"; const App = () => { const [count, setCount] = useState(0); const [todos, setTodos] = useState([]); const increment = () => { setCount((c) => c + 1); }; const addTodo = useCallback(() => { setTodos((t) => [...t, "New Todo"]); }, [todos]); return ( <> <Todos todos={todos} addTodo={addTodo} /> <hr /> <div> Count: {count} <button onClick={increment}>+</button> </div> </> ); }; ReactDOM.render(<App />, document.getElementById('root'));
Todos.js import { memo } from "react"; const Todos = ({ todos, addTodo }) => { console.log("child render"); return ( <> <h2>My Todos</h2> {todos.map((todo, index) => { return <p key={index}>{todo}</p>; })} <button onClick={addTodo}>Add Todo</button> </> ); }; export default memo(Todos);Output:
The Todos component will now only re-render when the Todos prop changes.