Side effects

Side effects

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, ...

Side effects

typical use cases for side effects:

  • triggering API queries
    • when a component is rendered for the first time
    • when some data (state / props) have changed (e.g. the user has selected a specific item to view its details)
  • saving some data to the browser storage if it has changed
  • explicitly manipulating the document (DOM)
  • starting timers
  • ...

Side effects

some side effects may need to be "cleaned up":

  • aborting API queries if they are not needed anymore (e.g. if a search term has changed)
  • stopping timers
  • ...

Side effects

implementation:

in function components: via the effect hook

in class components: via lifecycle methods

Side effects in class components

Side effects in class components

three main lifecycle methods that can be implemented in a component class:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

Side effects in class components

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();
    }
  }
}

Side effects in class components

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();
    }
  }
}

Side effects in class components

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();
  }
}

Effect hook

Effect hook

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

Effect hook

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]);

Effect hook

example: loading a set of todos when the component is first mounted:

const [todos, setTodos] = useState([]);
async function loadTodos() {
  // ...
}
useEffect(() => {
  loadTodos();
}, []);

Effect hook

If no second parameter is passed in, the effect will run after every rendering; this can potentially avoid problems with obsolete data

useEffect(() => {
  ensureExchangeRateIsLoaded();
});

Effect hook: cleanup

Cleanup functions

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

Cleanup functions

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]);
  // ...
}

Asynchronous effects and cleanup functions

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

Asynchronous effects and cleanup functions

invalid:

useEffect(loadSearchResultsAsync, [query]);

valid:

useEffect(() => {
  loadSearchResultsAsync();
}, [query]);

Examples and exercises: APIs

Example: loading exchange rate data

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
}

Example: loading exchange rate data

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;
}

Example: loading exchange rate data

complete code (class components and function components, including effect cleanups):

https://codesandbox.io/s/side-effects-exchange-rate-2z42d

Exercises

example APIs:

Exercises

  • Load todos when the user opens the todolist application
  • Show data for a specific SpaceX launch based on the launch number
  • Show data for a pokémon based on its number
  • Show hacker news articles based on a search term

Examples and exercises: others

Examples and exercises

Examples and exercises for use cases that don't interact with APIs

  • clock
  • counter that saves to localStorage
  • component that "renders" text to the document title
  • logout timer

Example: Clock component

function Clock() {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => clearInterval(intervalId);
  }, []);
  return <div>{time.toLocaleTimeString()}</div>;
}

Example: persistent counter

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>
  );
}

Exercises

Exercise: save the state of one of the previous applications (e.g. slideshow) to localStorage

Example: DocumentTitle component

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.

Example: DocumentTitle component

const DocumentTitle = (props) => {
  useEffect(() => {
    document.title = props.value;
  }, [props.value]);
  return null;
};

Example: Logout timer

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>
  );
};