Source: GitHub
Introduction to React Hooks
Agenda
About React
- React is a JavaScript library
- Developed in 2011 by Jordan Walke
- Create dynamic user interfaces through reusable components
- Still popular for large scale applications, maintainable and scalable
- Plenty of libraries and tools developed by a thriving community
Timeline
- 2011: React was first deployed to Facebook's News Feed
- 2012: Instagram built its website using React
- May, 2013: Jordan Walke introduced React at the JS Conference. It becomes open-sourced
- Early, 2014: The React JS world tour conferences began to build communities
- March, 2015: React Native was released, which enabled cross-platform development
- 2016: React gains lots of popularity
- April, 2017: React 16.0.0 (Fiber) was released, improving rendering performance and UI responsiveness
- February, 2019: React 16.8.0 was released, officially introduce hooks
What are hooks?
- Traditionally, components were built using classes
- React hooks are functions that can manage state and lifecycle features of functional components
- Hooks should be called at the top level
- Cannot be called in conditions, loops or nested functions
Some commonly used built-in hooks
- useState
- useEffect
- useRef
- useContext
- useReducer
- useMemo
- useCallback
- Custom Hooks?
Why should we use hooks?
- Future development is focused on hooks
- Extract and reuse stateful logic across components
- Functional components are smaller, less verbose syntax
- No repeated logic across lifecycle methods
- More readable, easier to maintain
useState
What is useState?
A hook to manage state in functional components
import { useState } from "react";
function Component() {
const [state, setState] = useState(initialState);
return ...
}
Managing state
-
Parameters:
initialState
-
Returns:
- The current
state
- A
set
function to update the state
- The current
-
state
is set toinitialState
ONLY on the first render -
set
can accept a new state or a function to calculate from previous state
State with Class Components
- Constructor initialises state and bind functions to the component
this.state
is an object- Update state with
this.setState
- Verbose syntax with
this
class Counter extends React.Component {
constructor() {
super();
this.state = {
count: 0
};
this.handleIncrement = this.handleIncrement.bind(this);
}
handleIncrement() {
this.setState(prevState => ({ count: prevState.count + 1 }));
}
render() {
return (
<section>
<h1>{this.state.count}</h1>
<button onClick={this.handleIncrement}>Increment</button>
</section>
);
}
}
State with Functional Components
- Less code, more readable
- State can be primitive data
- Don't need to worry about
this
- No binding in functions
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(prevCount => prevCount + 1);
}
return (
<section>
<h1>{count}</h1>
<button onClick={handleIncrement}>Increment</button>
</section>
)
}
Counter Demo
Exercise
- Describe the behaviour of the code
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1);
setCount(count + 1);
};
return (
<section>
<h1>{count}</h1>
<button onClick={handleIncrement}>Increment</button>
</section>
);
}
Exercise Demo
count
is not incrementing by 2 after invoking setCount
twice?
Snapshot
- Snapshot state after render
- During first render,
count
is fixed to 0 setCount
is set two times to 1- State updates are processed after event handler finishes
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1); // count = 0 + 1
setCount(count + 1); // count = 0 + 1
};
Batching
- Queue state updates until the end
- Prevent triggering too many renders
- Stores the most recent calculated state
- Only the state of the most recent call is processed
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1); // count = 0 + 1
setCount(prevCount => prevCount + 3); // count = 1 + 3
setCount(50); // count = 50
};
Solution
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(prevCount => prevCount + 1); // count = 0 + 1
setCount(prevCount => prevCount + 1); // count = 1 + 1
};
return (
<section>
<h1>{count}</h1>
<button onClick={handleIncrement}>Increment</button>
</section>
);
}
useEffect
What is useEffect?
A hook to manage side effects in functional components
import { useEffect } from "react";
function Component() {
useEffect.(callbackFunction, dependencyArray?);
return ...
}
Side effects
- Generally, functions have side effects if it does anything but take input and return an output
- In React, a component should only take props and state to return a description of the UI
- Include DOM manipulation, API calls, subscriptions, or anything external...
Managing side effects
-
Parameters:
setup
function that optionally returns acleanup
function- optional:
dependency
array containing reactive values withinsetup
-
Returns:
undefined
-
Run
setup
if a value in thedependency
changes -
cleanup
runs after re-render beforesetup
runs
Side Effects with Class Components
- Many types of lifecycle methods
componentDidMount
componentDidUpdate
componentWillUnmount
- Need to check if props/state updated
- Potential for duplicated code
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
this.handleIncrement = this.handleIncrement.bind(this);
}
componentDidMount() {
document.title = this.state.count;
}
componentDidUpdate(prevState) {
if (prevState.count !== this.state.count) {
document.title = this.state.count;
}
}
handleIncrement() {
this.setState(prevState => ({ count: prevState.count + 1 }));
}
render() {
return (
<section>
<h1>{this.state.count}</h1>
<button onClick={this.handleIncrement}>Increment</button>
</section>
);
}
}
Side Effects with Functional Components
- Less code with useEffect
- No more duplicated code
- Runs at least once on initial render,
componentDidMount
- Dependency array mimics behaviour of
componentDidUpdate
- Checks for changes internally
- Optional
cleanup
equivalent tocomponentWillUnmount
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = count;
}, [count]);
const handleIncrement = () => {
setCount(prevCount => prevCount + 1);
};
return (
<section>
<h1>{count}</h1>
<button onClick={handleIncrement}>Increment</button>
</section>
);
}
Counter Demo
- This only works if you open sandbox, then copy the preview URL and open it in a new tab
- Observe the title of the current tab
Exercise
- Describe the behaviour of this code
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = count;
}, []);
const handleIncrement = () => {
setCount(prevCount => prevCount + 1);
};
return (
<section>
<h1>{count}</h1>
<button onClick={handleIncrement}>Increment</button>
</section>
);
}
Exercise Demo
- This only works if you open sandbox, then copy the preview URL and open it in a new tab
- Observe the title of the current tab
Our side effect is not run!
Dependencies
- No
dependency
: Runs after every render - Empty
dependency
: Runs once after the initial render - Non-empty
dependency
: Runs if variable changes value
Interval counter
- Setup event listeners or timed functions once on initial render
- Becareful of loops
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => {
clearInterval(id);
}
}, [count]);
- Now our code doesn't rely on
count
anymore!
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
return () => {
clearInterval(id);
}
}, []);
Fetch country population
- Fetch updates information when a dependent variable changes
const [country, setCountry] = useState("Australia");
const [population, setPopulation] = useState(null);
useEffect(() => {
// Declare fetch function
const fetchPopulation = async () => {
const url = "https://fakeURL/" + country;
const response = await fetch(url);
const countryPopulation = await response.json();
setPopulation(countryPopulation);
};
// Invoke fetch function whenever useEffect runs
fetchPopulation();
}, [country]);
useRef
What is useRef?
A hook that lets you reference a value that's not needed for rendering
import { useRef } from "react";
function Component() {
const ref = useRef(initialValue);
return ...
}
Managing references
- Parameters:
initialValue
- Returns:
- An object with the mutable
current
property
- An object with the mutable
current
is set toinitialValue
ONLY on the first render- Passing object to
ref
attribute of a JSX element will set itscurrent
property
References with Class Components
- Call
createRef
in constructor - Use
this
to access object - Pass as
ref
to JSX for DOM tasks
class Counter extends React.Component {
constructor() {
super();
this.state = {
count: 0,
timerRunning: false,
};
this.intervalRef = createRef(null);
this.timerHandler = this.timerHandler.bind(this);
}
timerHandler() {
if (this.state.timerRunning) {
clearInterval(this.intervalRef.current);
} else {
this.intervalRef.current = setInterval(() => {
this.setState({ count: this.state.count + 1 });
}, 1000);
}
this.setState({ timerRunning: !this.state.timerRunning });
}
render() {
return (
<section>
<h1>{this.state.count}</h1>
<button onClick={this.timerHandler}>
{this.state.timerRunning ? "Stop" : "Start"}
</button>
</section>
);
}
}
References with Functional Components
- Initialisation same as
useState
- No more
this
- Access to DOM same as class components
function Counter() {
const [count, setCount] = useState(0);
const [timerRunning, setTimerRunning] = useState(false);
const intervalRef = useRef(null);
const timerHandler = () => {
if (timerRunning) {
clearInterval(intervalRef.current);
}
else {
intervalRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
}
setTimerRunning(prevTimerRunning => !prevTimerRunning);
}
return (
<section>
<h1>{count}</h1>
<button onClick={timerHandler}>
{timerRunning ? "Stop" : "Start"}
</button>
</section>
);
}
Counter Demo
Exercise
- Describe what is wrong with this code?
function Counter() {
const countRef = useRef(0);
const handleIncrement = () => {
countRef.current = countRef.current + 1;
};
return (
<section>
<h1>{countRef.current}</h1>
<button onClick={handleIncrement}>Increment</button>
</section>
);
}
Exercise Demo
- Under the hood,
countRef.current
increments but the UI doesn't reflect that change
Mutating ref.current
does not trigger render
Manipulate the DOM
- Might need to update DOM node directly
- Call functions from the Browser API
- Includes focusing on a node, scroll node into view, measure dimensions
const inputRef = useRef(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return <input ref={inputRef} />
Other built-in hooks
useContext
pass data within component tree through global contextuseMemo
cache value between re-rendersuseCallback
cache functions between re-rendersuseReducer
accumulate actions over time into state
Custom Hooks
What are custom hooks?
- Not as complicated as it sounds
- Basically just functions which use other hooks
- Main purpose of sharing reusable non-visual logic between components
- Typically written in another file and exported to wherever the application needs it
Rules
1. Custom hook names MUST begin with "use"
2. MUST call at least one built-in hook
Counter from useRef Demo
function Counter() {
const [count, setCount] = useState(0);
const [timerRunning, setTimerRunning] = useState(false);
const intervalRef = useRef(null);
const timerHandler = () => {
if (timerRunning) {
clearInterval(intervalRef.current);
}
else {
intervalRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
}
setTimerRunning(prevTimerRunning => !prevTimerRunning);
}
return (
<section>
<h1>{count}</h1>
<button onClick={timerHandler}>
{timerRunning ? "Stop" : "Start"}
</button>
</section>
);
}
Extracting Counter and Timer
- No longer coupled both functionalities
- Both custom hooks can be used generally across other components
export function useCounter(initialCount) {
const [count, setCount] = useState(initialCount);
const incrementCount = () => {
setCount(prevCount => prevCount + 1);
};
return [count, incrementCount];
}
export function useTimer(callbackFn, delay) {
const [timerRunning, setTimerRunning] = useState(false);
const intervalRef = useRef(null);
const timerHandler = () => {
if (timerRunning) {
clearInterval(intervalRef.current);
} else {
intervalRef.current = setInterval(callbackFn, delay);
}
setTimerRunning(prevTimerRunning => !prevTimerRunning);
};
return [timerRunning, timerHandler];
}
After Refactoring
- Extracted all the logic out, looks more readable
- No UI was changed
import { useCounter, useTimer } from "./file";
function Counter() {
const [count, incrementCount] = useCounter(0);
const [timerRunning, timerHandler] = useTimer(incrementCount, 1000);
return (
<section>
<h1>{count}</h1>
<button onClick={timerHandler}>{timerRunning ? "Stop" : "Start"}</button>
</section>
);
}
Reading Material
- React's In-built hooks: https://react.dev/reference/react/hooks
- Browser API for DOM: https://developer.mozilla.org/en-US/docs/Web/API/Element