Graduate Program KB

Source: GitHub


Introduction to React Hooks


Agenda

  1. About React
  2. useState
  3. useEffect
  4. useRef
  5. Custom Hooks
  6. Reading Material

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
  • state is set to initialState 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


import { useState } from "react";
import "./App.css";

function Counter() {
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    setCount(prevCount => prevCount + 1);
  }

  return (
    <section>
      <h1>{count}</h1>
      <button onClick={handleIncrement}>Increment</button>
    </section>
  )
}

export default function App() {
  return <Counter />;
};
Read-only

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?


import { useState } from "react";
import "./App.css";

function Counter() {
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    setCount(count + 1);  // newCount = count + 1
    setCount(count + 1);  // newCount = count + 1
  };

  return (
    <section>
      <h1>{count}</h1>
      <button onClick={handleIncrement}>Increment</button>
    </section>
  );
}

export default function App() {
  return <Counter />;
};
Read-only

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 a cleanup function
    • optional: dependency array containing reactive values within setup
  • Returns:

    • undefined
  • Run setup if a value in the dependency changes

  • cleanup runs after re-render before setup 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 to componentWillUnmount
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

import { useState, useEffect } from "react";
import "./App.css";

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

export default function App() {
  return <Counter />;
};
Read-only

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!


import { useState, useEffect } from "react";
import "./App.css";

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

export default function App() {
  return <Counter />;
};
Read-only

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
  • current is set to initialValue ONLY on the first render
  • Passing object to ref attribute of a JSX element will set its current 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


import { useState, useRef } from "react";
import "./App.css";

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

export default function App() {
  return <Counter />;
};
Read-only

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


import { useRef } from "react";
import "./App.css";

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

export default function App() {
  return <Counter />;
};
Read-only

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 context
  • useMemo cache value between re-renders
  • useCallback cache functions between re-renders
  • useReducer 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