Hooks Primer

7 Aug 2019

- Introduction

Intro

- Introduction

Resources

- Introduction

Documentation

- Introduction

useState

Example and comparison

- useState

class Counter extends Component {
    state = {
        counter: 0
    }

    increment = this.setState(
        ({ counter }) => ({ counter: counter + 1 }
    )
    decrement = this.setState(
        ({ counter }) => ({ counter: counter - 1 }
    )

    render() {
        return <div>
            <button onClick={increment}>Increment</button>
            <p>Count: {counter}<p> 
            <button onClick={decrement}>Increment</button>
        </div>
    }
}

- useState

Class Example

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

    const increment = () => setCount(count + 1);
    const decrement = () => setCount(count - 1);

    return <div>
        <button onClick={increment}>Increment</button>
        <p>Count: {counter}<p> 
        <button onClick={decrement}>Increment</button>
    </div>;
}

- useState

Hooks Example

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

    const increment = () => setCount(count => count + 1);
    const decrement = () => setCount(count => count - 1);

    return <div>
        <button onClick={increment}>Increment</button>
        <p>Count: {counter}<p> 
        <button onClick={decrement}>Increment</button>
    </div>;
}

- useState

Hooks Example

- useState

Takeaways

  • Granular Control
  • Memoized Functions
  • Takes in an update object or update function ( T | T => T )
  • Changes value and setter on each value change

- useState

Best Practices

  • Keep the amount of state data to a minimum
  • Prefer derived data (see: later)
  • Make sure that, if you need your state to be a function, you prevent useState from treating your inbound state as an update function ( T => T ). 

useEffect

Example and comparison

- useEffect

class Counter extends Component {
    state = {
        counter: 0
    }

    componentDidMount() {
        document.title = `Ready to count!`;
    }

    componentDidUpdate(props, state) {
        if (this.state.counter !== state.counter) {
            document.title = `Counted: ${state.counter}`;
        }
    }

    render() {
        return <div>
            <button onClick={increment}>Increment</button>
            <p>Count: {counter}<p> 
            <button onClick={decrement}>Increment</button>
        </div>
    }
}

- useEffect

Class Example

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

    useEffect(
        () => document.title = `Counted ${count}`,
        [count],
    );
    useEffect(
        () => document.title = `Ready to count`,
        [],
    );

    return <div>
        <button onClick={increment}>Increment</button>
        <p>Count: {counter}<p> 
        <button onClick={decrement}>Increment</button>
    </div>;
}

- useEffect

Hook Example

const NotificationsBadge = ({ id }) => {
    const [notifications, setNotifications] = useState(0);

    useEffect(
        () => {
            const subscription = API.subscribeToNotifications(
                id,
                notification => setNotifications(
                    notifications => [...notifications, notification]
                )
            );
            return subscription.unsubscribe();
        },
        [id],
    );

    return (...);
}

- useEffect

Hook Subscription Example

- useEffect

Takeaways

  • Scoped to memoized input array
  • Takes in an effect function
  • Effect function acts as an effect constructor, that returns the effect destructor
    • Creates a subscription
    • Handles functionality
    • Returns the unsubscribe handler

- useEffect

Best Practices

  • Don't duplicate effects on input array
  • Use block functions as a default, to prevent useEffect interpreting any return values as functions (when they are by-products of an update process, for example)
  • If dealing with effects that need destructuring on change, make sure to return a destructure function

- useEffect

Resize Example

const NotificationsBadge = () => {
    const [size, setSize] = useState({ width: 0, height: 0 });

    useEffect(
        () => {
            const handler = () => setSize({ 
                width: window.innerWidth,
                height: window.innerHeight,
            });
            window.addEventListener('resize', handler);
            return () => window.removeEventListener('resize', handler);
        },
        [],
    );

    return (...);
}

useMemo

Example

- useMemo

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

    const [hasCounted, setHasCounted] = useState(false);
    useEffect(
        () => { 
            if (count !== 0) { 
                setHasCounted(true); 
            } 
        },
        [count],
    );

    const title = useMemo(
        () => hasCounted ? `Counted ${count}` : `Ready to count`,
        [hasCounted, count],
    );
    useEffect(
        () => document.title = title,
        [title],
    );

    return <div>
        <h1>{title}</h1>
        <button onClick={increment}>Increment</button>
        <p>Count: {counter}<p> 
        <button onClick={decrement}>Increment</button>
    </div>;
}

- useMemo

- useMemo

Takeaways

  • Granular Control
  • Memoized Returns
  • Mostly used for derived data
  • Takes a function that returns the value ( () => T ) 

- useMemo

Best Practices

  • Use as much as needed instead of useState
  • Be careful with the input array, make sure it's satisfactory to the output
  • Remember that useState setters change on value set
  • If derived data is a function, use useCallback instead

useCallback

Example and comparison

- useCallback

- useCallback

useCallback vs useMemo

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

    const increment = useMemo(
        () => 
            () => setCount(count => count + 1),
        [setCount],
    );
    const decrement = useCallback(
        () => setCount(count => count - 1),
        [setCount],
    );

    return <div>
        <button onClick={increment}>Increment</button>
        <p>Count: {counter}<p> 
        <button onClick={decrement}>Increment</button>
    </div>;
}

- useCallback

useCallback vs useState (don't)

const Counter = () => {
    const [count, setCount] = useState(0);
    const [increment, setIncrement] = useState(null);

    // Won't work as expected
    useEffect(
        () => setIncrement(
             () => setCount(count => count + 1)
        ),
        [setCount],
    );    

    const decrement = useCallback(
        () => setCount(count => count - 1),
        [setCount],
    );

    return <div>
        <button onClick={increment}>Increment</button>
        <p>Count: {counter}<p> 
        <button onClick={decrement}>Increment</button>
    </div>;
}

useContext

Example and comparison

- useContext

const CounterContext = 
    createContext({ counter, increment, decrement });

class Counter extends Component {
    static contextType = CounterContext;

    render() {
        const { counter, increment, decrement } =
            this.context;

        return <div>
            <button onClick={increment}>Increment</button>
            <p>Count: {counter}<p> 
            <button onClick={decrement}>Increment</button>
        </div>
    }
}

- useContext

Class Example

- useContext

Consumer Example

const CounterContext = 
    createContext({ counter, increment, decrement });

const Counter = () => (
    <CounterContext.Consumer>
        {({ counter, increment, decrement }) => (
            <div>
                <button onClick={increment}>Increment</button>
                <p>Count: {counter}<p> 
                <button onClick={decrement}>Increment</button>
            </div>
        )}
    </CounterContext.Consumer>
}

- useContext

Hooks Example

const CounterContext = 
    createContext({ counter, increment, decrement });

const Counter = () => {
    const { counter, increment, decrement } = useContext(CounterContext);
    return (
        <div>
            <button onClick={increment}>Increment</button>
            <p>Count: {counter}<p> 
            <button onClick={decrement}>Increment</button>
        </div>
    );
}

- useContext

Takeaways

  • Can consume N contexts, before rendering
  • Can be used in conjunction with other use hooks

Custom Hooks

Example and comparison

- Custom Hooks

useReducer

Example and comparison

- useReducer

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

    const increment = useCallback(() => setCount(count => count + 1));
    const decrement = useCallback(() => setCount(count => count - 1));

    return <div>
        <button onClick={increment}>Increment</button>
        <p>Count: {counter}<p> 
        <button onClick={decrement}>Increment</button>
    </div>;
}

- useReducer

 

Basic State Example

const Counter = () => {
    const [{ count: counter }, dispatch] = useReducer(
        ({ count: c }, { type, value }) => {
            switch (type) {
                case 'increment':
                    return { count: c + 1 };
                case 'decrement':
                    return { count: c - 1 };
                default: 
                    throw new Error('Unknown action');
            }
        },
        { count: 0 }
    );

    const increment = useCallback(() => dispatch({ type: 'increment' }), [dispatch]);
    const increment = useCallback(() => dispatch({ type: 'decrement' }), [dispatch]);

    return <div>
        <button onClick={increment}>Increment</button>
        <p>Count: {counter}<p> 
        <button onClick={decrement}>Increment</button>
    </div>;
}

- useReducer

 

useReducer Example

- useReducer

Takeaways

  • Reminiscent of Flux / Redux
  • Increases code size
  • Usually not required

- All Done!

Hooks Primer

By Sabin Marcu

Hooks Primer

  • 560