When component props / state change or the component is included for the first time:
"main effect": component (re-)renders with current data
potential "side effects": triggering API queries, saving data, explicitly manipulating the document, starting timers, ...
typical use cases for side effects:
some side effects may need to be "cleaned up":
implementation:
in function components: via the effect hook
in class components: via lifecycle methods
three main lifecycle methods that can be implemented in a component class:
componentDidMount
componentDidUpdate
componentWillUnmount
componentDidUpdate
will receive the previous props and previous state as parameters:
class MyComponent extends Component {
// ...
componentDidUpdate(prevProps, prevState) {
if (this.state.foo !== prevState.foo) {
doSomething();
}
}
}
example: component that loads the exchange rate between two currencies
class ExchangeRate extends Component {
// ...
componentDidMount() {
this.loadExchangeRate();
}
componentDidUpdate(prevProps, prevState) {
if (
this.state.from !== prevState.from ||
this.state.to !== prevState.to
) {
this.loadExchangeRate();
}
}
}
extended example: cancelling outdated API queries
class ExchangeRate extends Component {
// ...
componentDidMount() {
this.loadExchangeRate();
}
componentDidUpdate(prevProps, prevState) {
if (
this.state.from !== prevState.from ||
this.state.to !== prevState.to
) {
this.cancelPreviousQuery();
this.loadExchangeRate();
}
}
componentWillUnmount() {
this.cancelPreviousQuery();
}
}
The effect hook can be used to perform actions when a component was mounted for the first time or when its props / state have changed.
useEffect(
effect, // what should happen
dependencies // array of values to watch
);
The effect function will run after the component (re-)rendered if one of the dependencies has changed
example: loading exchange rates when the component is first mounted and whenever a currency changes:
const [from, setFrom] = useState('USD');
const [to, setTo] = useState('EUR');
const [rate, setRate] = useState<number | null>(null);
async function loadRate(from: number, to: number) {
// ...
}
useEffect(() => {
loadRate(from, to);
}, [from, to]);
example: loading a set of todos when the component is first mounted:
const [todos, setTodos] = useState([]);
async function loadTodos() {
// ...
}
useEffect(() => {
loadTodos();
}, []);
If no second parameter is passed in, the effect will run after every rendering; this can potentially avoid problems with obsolete data
useEffect(() => {
ensureExchangeRateIsLoaded();
});
An effect function may return a "cleanup function"
This function will be executed before the next run of the effect or before the component is unmounted
example: exchange rate component that cancels any outdated queries:
function ExchangeRate() {
// ...
useEffect(() => {
async function loadExchangeRate() {
// ... (initiate a new query)
}
function cancel() {
// ... (cancel this query)
}
loadExchangeRate();
return cancel;
}, [from, to]);
// ...
}
An effect function must not be an async function
reason:
any return value of an effect function is treated as a cleanup function
async functions always return promises
invalid:
useEffect(loadSearchResultsAsync, [query]);
valid:
useEffect(() => {
loadSearchResultsAsync();
}, [query]);
example: loading exchange rate data from an API when selected currencies change:
function ExchangeRate() {
const [from, setFrom] = useState('USD');
const [to, setTo] = useState('EUR');
const [rate, setRate] = useState(null);
async function loadRate(from: string, to: string) {
setRate(null);
const rate = await fetchExchangeRate(from, to);
setRate(rate);
}
useEffect(() => {
loadRate(from, to);
}, [from, to]);
// render two dropdowns for selecting currencies
// and show the exchange rate
}
function that fetches data:
async function fetchExchangeRate(
from: string,
to: string
): Promise<number> {
const res = await fetch(
`https://api.exchangerate.host/convert?from=${from}&to=${to}`
);
const data = await res.json();
return data.result;
}
complete code (class components and function components, including effect cleanups):
example APIs:
Examples and exercises for use cases that don't interact with APIs
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>{time.toLocaleTimeString()}</div>;
}
Counter that saves its value to localStorage whenever the value changes:
function PersistentCounter() {
const [count, setCount] = useState(null);
function loadCount() {
const lsCount = localStorage.getItem('count');
if (lsCount !== null) {
setCount(Number(lsCount));
} else {
setCount(0);
}
}
function saveCount() {
if (count !== null) {
localStorage.setItem('count', count);
}
}
useEffect(loadCount, []);
useEffect(saveCount, [count]);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Exercise: save the state of one of the previous applications (e.g. slideshow) to localStorage
We will create a component that can set the document title dynamically:
<DocumentTitle value="my custom title" />
This component may appear anywhere in the React application.
const DocumentTitle = (props) => {
useEffect(() => {
document.title = props.value;
}, [props.value]);
return null;
};
user will be logged out after 10 seconds of inactivity
const App = () => {
const [loggedIn, setLoggedIn] = useState(true);
const [lastInteraction, setLastInteraction] = useState(
new Date()
);
// restart the logout timer on user interaction
useEffect(() => {
const logout = () => setLoggedIn(false);
const timeoutId = setTimeout(logout, 10000);
return () => clearTimeout(timeoutId);
}, [lastInteraction]);
return (
<button onClick={() => setLastInteraction(new Date())}>
{loggedIn
? 'click to stay logged in'
: 'logged out automatically'}
</button>
);
};