React Hooks in action

Martin Lechner

27.06.2019

About me

Martin Lechner

Fullstack Engineer at Autoscout24

I ❀️ πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦, Functional Programming, Photography, Bouldering & Boardgames

www.lechner.work

@m4nl5r

Why Hooks?

Who has heard of function components?

Who prefers function components over class components?

Lifecycle vs Function Components?

  • componentDidMount
  • componentDidUpdate
  • ...

State in Function Components

Nope 😒

Who likes complex component hierarchies?

Reusing stateful logic: i18n

Code re-use for handling translations

Render Props

              
                        function Example() {
                          return (
                            <Translation>
                              {
                                (translate) => <p>{translate('my-translation-key')}</p>
                              }
                            </Translation>
                          );
                        }
                      

Higher Order Components


                        function MyComponent({ translate }) {
                          return <p>{translate('my-translation-key')}</p>
                        }
                        export default withTranslation()(MyComponent);
                      

Hooks


                        function MyComponent() {
                          const { translate } = useTranslation();
                          return <p>{translate('my-translation-key')}</p>
                        }
                      

Your first hook


              class Example extends React.Component {
                constructor(props) {
                  super(props);
                  this.state = {
                    count: 0
                  };
                }
                
                render() {
                  return (
                  <div>
                    <p>You clicked {this.state.count} times</p>
                    <button onClick={() => this.setState({ count: this.state.count + 1 })}>
                      Click me
                    </button>
                  </div>
                  );
                }
              }
            

Your first hook - useState


    import React, { useState } from 'react';

    function Counter() {
      const [count, setCount] = useState(0);
      return (
        <div>
          <p>You clicked {count} times.</p>
          <button onClick={() => setCount(count + 1)}>
            Inc
          </button>
        </div>  
      )
    }
    
Demo

callbacks in set


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

              <button onClick={() => setCount(c => c + 1)}>
                Inc
              </button>
              

Two rules of the hook club

  • Only use toplevel
  • Only use in React functions

Linting

eslint-plugin-react-hooks

ts-lint-react-hooks

Use Effect

  • Side effects outside of React
  • Executed after every render (unless specified otherwise)
  • Non-blocking
  • Props, State bound to the wrapped function

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={()=> setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
            

Mental model for rendering

  • Counter is bound to 0
  • Component: Creates render result & dispatch effect
  • React renders to DOM, Browser paints
  • React running effects

Click button

  • Counter is bound to 1
  • Component: Creates render result & dispatch effect
  • React renders to DOM, Browser paints
  • React cleaning up old & running the new effects

Cleanup


              useEffect(() => {
                websocket.connect(props.customerId);
              });
            

Cleanup


                        useEffect(() => {
                          websocket.connect(props.customerId);
                          return () => websocket.disconnect(props.customerId);
                        });
                      

Not executing every render

 
                                    useEffect(() => {
                                      websocket.connect(props.customerId);
                                      return () => websocket.disconnect(props.customerId);
                                    }, [props.customerId]);
                                  

Only execute once


                                                useEffect(() => {
                                                 ...
                                                }, []);
                                              

Minimizing dependencies


                  const [count, setCount] = useState(0);
                  useEffect(() => {
                    const id = setInterval(() => {
                      setCount(count + 1);
                    }, 1000);
                    return () => clearInterval(id);
                  }, [count]);
            

Minimizing dependencies


                  const [count, setCount] = useState(0);
                  useEffect(() => {
                    const id = setInterval(() => {
                      setCount(c => c + 1);
                    }, 1000);
                    return () => clearInterval(id);
                  }, []);
            

Can the component lifecycle methods be modeled in a similar way to class components with useEffect?

useEffect vs component lifecycle methods

  • useEffect is non-blocking
  • component lifecycle methods are blocking browser painting
  • Closest to componentDidMount, componentDidUpdate is useLayoutEffect

Custom hooks

Custom hooks by example

  • Data fetching from a json based api
  • Loading handling
  • Error handling

            function useApi(url) {
              const [error, setError] = useState(null);
              const [loading, setLoading] = useState(false);
              const [data, setData] = useState(null)
          
              useEffect(() => ???)
              
              return { error, loading, data };
            }

  function useApi(url) {
    const [error, setError] = useState(null);
    const [loading, setLoading] = useState(false);
    const [data, setData] = useState(null)

    useEffect(() => {
      const fetchData = async () => {
        setLoading(true);
        setError(null);

        ???

        setLoading(false);
      };
      fetchData();
      }, [url]);
    return { error, loading, data };
  }

                      function useApi(url) {
                        const [error, setError] = useState(null);
                        const [loading, setLoading] = useState(false);
                        const [data, setData] = useState(null)
                    
                        useEffect(() => {
                          const fetchData = async () => {
                            setLoading(true);
                            setError(null);
                            try {
                              const response = await axios.get(url);
                              setData(response.data);
                            } catch (e) {
                              setError(e);
                            }
                            setLoading(false);
                          };
                          fetchData();
                          }, [url]);
                        return { error, loading, data };
                      }

Demo

Codesandbox

Testing custom hooks


  import axios from 'axios';
  import { renderHook } from '@testing-library/react-hooks';

  import { useApi } from './useApi';

  jest.mock('axios');

  it('should fetch data', async () => {
    axios.get.mockResolvedValue({ data: '1' });

    const { result, waitForNextUpdate } = renderHook(() => useApi('test'));

    expect(result.current.loading).toEqual(true);

    await waitForNextUpdate();

    expect(result.current.loading).toEqual(false);
    expect(result.current.data).toEqual('1');
  });

Other hooks (react api)

Use Context

const value = useContext(MyContext);

Use Ref


                const refContainer = useRef(initialValue);

                refContainer.current = 123;
                refContainer.current
                // 123
            

Use Ref with DOM elements


              function TextInputWithFocusButton() {
                const inputEl = useRef(null);
                const onButtonClick = () => {
                  inputEl.current.focus();
                };
                return (
                  <>
                    <input ref={inputEl} type="text" />
                    <button onClick={onButtonClick}>Focus input</button>
                  </>
                );
              }
            

Use Callback


              const memoizedCallback = useCallback(
                () => {
                  doSomething(a, b);
                },
                [a, b],
              );
            

Callback ref

 
              function MeasureExample() {
                const [height, setHeight] = useState(0);
              
                const measuredRef = useCallback(node => {
                  if (node !== null) {
                    setHeight(node.getBoundingClientRect().height);
                  } 
                }, []);
              
                return (
                <>
                  <h1 ref={measuredRef}>Hello, world</h1>
                  <h2>The above header is {Math.round(height)}px tall</h2>
                </>
                );
              }
            

Use Reducer


              const [state, dispatch] = useReducer(reducer, initialArg, init);

              const initialState = {count: 0};         
              function reducer(state, action) {
                switch (action.type) {
                  case 'increment':
                    return {count: state.count + 1};
                  case 'decrement':
                    return {count: state.count - 1};
                  default:
                    throw new Error();
                }
              }
              
              function Counter() {
                const [state, dispatch] = useReducer(reducer, initialState);
                return (
                  <>
                    Count: {state.count}
                    <button onClick={() => dispatch({type: 'increment'})}>
                      +</button>
                    <button onClick={() => dispatch({type: 'decrement'})}>
                      -</button>
                 </>);
              }
            

              const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
            

Hooks in the wild (libraries)

  • react-redux
  • react-apollo-hooks
  • react-i18next
  • Hook Search

Thanks!

Sources

  • https://reactjs.org/docs/hooks-intro.html
  • https://overreacted.io/a-complete-guide-to-useeffect/
  • https://www.robinwieruch.de/react-hooks-fetch-data/
  • https://react-hooks-testing-library.netlify.com/
  • https://react.i18next.com/