Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Hooks in action: implementing a 1KB React router

Hooks in action: implementing a 1KB React router

Video: https://youtu.be/bFYxkONAmn8

Still confuse useRef and useState? Not sure what useImperativeHandle does? This talk is a nice step-by-step intro to React hooks and some advanced patterns around them. We're going to build a tiny router for React with API similar to React Router's one.

Alexey is a full-stack developer with over 10 years of experience. His primary stack includes React, Node.js and Ruby on Rails. While he mainly works with backend/infrastructure as a head of engineering of resume.io (a project he cofounded in 2016), he's also super passionate about design and user interfaces. A true hackathon addict, Alexey believes that working on an open-source project is no different than working on a product and that good design is the main driving factor of a successful project.

Alexey Taktarov

August 14, 2019
Tweet

More Decks by Alexey Taktarov

Other Decks in Programming

Transcript

  1. Hooks in Action:
    Implementing a 1KB Router in React
    Alexey Taktarov
    @mlfrg · molefrog.com

    View Slide

  2. Alexey Taktarov
    believes that design > engineering
    currently busy working on resume.io
    4M+ resumes worldwide, distributed team !+"
    @mlfrg
    molefrog
    molefrog
    Twitter
    GitHub
    Instagram

    View Slide

  3. image source: http://inrussia.com/the-vyborg-library
    V I I P U R I L I B R A R Y — A L V A R A A L T O , 1 9 3 5

    View Slide

  4. source: http://inrussia.com/the-vyborg-library

    View Slide

  5. “the mission of an architect is to give
    life a more sensitive structure and
    to put the material world into
    harmony with human life”
    Alvar Aalto
    image source: http://inrussia.com/the-vyborg-library

    View Slide

  6. the function of building can be fulfilled by the combination of
    the fundamental elements: air, light, materials

    View Slide

  7. Let’s get back to React…

    View Slide

  8. React Hooks — the building blocks of
    an interface logic

    View Slide

  9. useState → useReducer
    allows to subscribe to primitive
    state value changes

    View Slide

  10. const [value, setValue] = useState(0);
    // custom hooks
    const [value, setValue] = useGlobalState(0);
    const [value, setValue] = useLocalStorageState(0);
    Hooks are composable: one interface, different
    behaviour

    View Slide

  11. learn React Hooks by implementing a real
    open-source project: a tiny router for React
    What are we going to do today?

    View Slide

  12. Routing in React apps
    routing approaches have
    transformed along with
    the React ecosystem

    View Slide

  13. const routes = (





    );
    React Router v1.0.3: a DSL for describing routes

    View Slide

  14. v4: no outlets, no top-level configs in favour of a simple
    component:

    View Slide

  15. fewer constraints → more freedom

    View Slide

  16. Minimal Viable Router:
    implementing router’s core
    hooks useLocation and useRouter

    View Slide

  17. location.pathname
    router

    View Slide

  18. const [location, setLocation] = useLocation();
    useLocation: the tiniest router ever

    View Slide

  19. const useLocation = () => {
    const [location, setLocation] = useState(location.pathname);
    // call `setLocation` whenever the pathname changes
    return [location, to => history.pushState(0, 0, to)];
    };
    if pathname changes → re-render

    View Slide

  20. mount — subscribe, unmount — unsubscribe
    const useLocation = () => {
    const [location, setLocation] = useState(location.pathname);
    // call `setLocation` whenever the pathname changes
    useEffect(() => {
    // subscribe to pushState/replaceState
    return () => { /* unsub */ }
    }, []);
    return [location, to => history.pushState(0, 0, to)];
    };

    View Slide

  21. Hooks pro-tip #1: always specify useEffect dependencies!
    const useLocation = () => {
    const [location, setLocation] = useState(location.pathname);
    // call `setLocation` whenever the pathname changes
    useEffect(() => {
    // subscribe to pushState/replaceState
    return () => { /* unsub */ }
    }, []);
    return [location, to => history.pushState(0, 0, to)];
    };

    View Slide

  22. useEffect
    a gate to subscriptions;
    a replacement for lifecycle-methods

    View Slide

  23. how do we subscribe to pushState/replaceState?

    View Slide

  24. how to subscribe to pushState/replaceState?
    monkey-patch these methods intercept link clicks
    → github.com/storeon/router
    → github.com/molefrog/wouter

    View Slide

  25. how do I test hooks?
    hooks are «special» functions, they can’t run
    outside of React reconciler

    View Slide

  26. import { renderHook, act } from "react-hooks-testing-library";
    it("reacts to pushState / replaceState", () => {
    const { result, unmount } = renderHook(() => useLocation());
    act(() => history.pushState(0, 0, "/foo"));
    expect(result.current[0]).toBe("/foo");
    unmount();
    });
    testing hooks = testing components

    View Slide

  27. pattern-based routing?

    View Slide

  28. /users/:id/info
    /users/22/info => true, id: 22
    pattern-based routing?

    View Slide

  29. useRoute("/users/:id") => [true, { id: 13 }]

    View Slide

  30. https://github.com/pillarjs/path-to-regexp
    path-to-regexp powers libraries like express and
    React Router

    View Slide

  31. Adding Components
    Route, Link and Switch

    View Slide

  32. useRoute is quite handy, but results in boilerplate
    const UserPage = () => {
    const [match, params] = useRoute("/users/:id")
    if (!match) return null;
    return `User id: ${params.id}`;
    }

    View Slide

  33. let’s design Route component

    View Slide

  34. import { createElement as h } from "react";
    const Route = ({ path, component }) => {
    const [match, params] = useRoute(path);
    if (!match) return null;
    return h(component, { params: params });
    }

    View Slide

  35. Adding a navigation
    About Us

    View Slide

  36. const Link = props => {
    const [_, navigate] = useLocation();
    const handleClick = useCallback(
    event => {
    event.preventDefault();
    navigate(href);
    },
    [href, navigate]
    );
    // render link
    };
    hooks pro-tip #2: use useCallback for event handlers;
    this makes your code optimizitation-friendly

    View Slide

  37. useMemo → useCallback
    use it to describe computed values with
    proper caching

    View Slide

  38. How useEffect and useMemo process dependencies
    A B C D
    A A
    A A
    A B
    A B
    always stale, recomputed
    on every render
    undefined
    always fresh,
    computed once
    []
    recomputed only when
    either x or y changes
    [x, y]

    View Slide

  39. // all of these should produce the same html
    // Contact
    Contact
    Contact
    Contact

    View Slide

  40. how do we add a prop to an element?
    if (isValidElement(children)) {
    return cloneElement(children, { href, onClick: handleClick });
    }

    View Slide

  41. Switch Component
    Exclusive routing and default routes

    View Slide






  42. /users/all →

    View Slide






  43. /users/22 →

    View Slide






  44. /foo/bar →

    View Slide

  45. Children.forEach(children, element => {
    if (isValidElement(element)) {
    const [match] = matcher(element.props.path, location);
    /* render element in case of a match */
    });
    isValidElement filters out these things: “foo”, 122 etc.
    see Switch implementation from React Router

    View Slide

  46. Towards a 1KB router!
    Our bundle is 2.2KB already,
    let’s drop some things out
    path-to-regexp — 65%

    View Slide

  47. pathToRegexp what we need?
    case-sensitive, partial matching
    case insensitive, match the
    entire string
    nameless params
    /:foo/(.*)
    simple modifiers
    /:foo*, /:foo? и /:foo+
    typed params
    /icon-:foo(\d+).png

    View Slide

  48. https://github.com/molefrog/wouter/issues/1
    2.2KB → 1.1KB

    View Slide

  49. Size Limit helps to keep your library in good shape
    https://github.com/ai/size-limit

    View Slide

  50. making the matcher backwards-compatible?

    View Slide




  51. you can always specify an original matcher through the
    optional top-level Router

    View Slide

  52. const Router = props => {
    const ref = useRef(null);
    // this little trick allows to avoid having unnecessary
    // calls to potentially expensive `buildRouter` method.
    const router = ref.current || (ref.current = buildRouter(props));
    // provide router using context:
    };
    should only create router once!

    View Slide

  53. useRef
    saves the value between re-renders;
    replaces this.*

    View Slide

  54. accessing router from any component?

    View Slide

  55. useContext
    a «portal» from the context

    View Slide

  56. const useRouter = () => {
    const providedRouter = useContext(RouterCtx);
    // either obtain the router from the outer context
    // (provided by the ` component) or create
    // a default one on demand.
    return useMemo(
    () => (providedRouter ? providedRouter : buildRouter()),
    [providedRouter]
    );
    }
    useMemo allows to make the top-level Router optional

    View Slide

  57. const useRouter = () => {
    const providedRouter = useContext(RouterCtx);
    // either obtain the router from the outer context
    // (provided by the ` component) or create
    // a default one on demand.
    return useMemo(
    () => (providedRouter ? providedRouter : buildRouter()),
    [providedRouter]
    );
    }
    we need a way to save a router somewhere «globally»

    View Slide

  58. // one of the coolest features of React Context:
    // when no value is provided, default object is used.
    const GlobalRefCtx = createContext({ current: null })
    const useGlobalRef = () => useContext(GlobalRefCtx)
    Hooks pro-tip #3: a global Ref trick — one value shared between
    multiple components. No needed!

    View Slide

  59. we’ve got a nice extension point now!
    // useStaticLocation, useGlobalState, useLocalStorage, ...

    by customising a useLocation hook we can achieve some
    interesting effects: like hash-based routing, or persisted routing

    View Slide

  60. Supporting Preact
    conquering new markets

    View Slide

  61. import { useRoute } from "wouter/preact";

    View Slide

  62. copy sources into preact/ folder 

    on prepublishOnly NPM event
    ├── index.js
    ├── react-deps.js
    └── preact
    ├── index.js
    └── react-deps.js (* preact)

    View Slide

  63. // make sure the children prop is always an array
    // note: returns [[]] in case of an empty array!
    children = children && children.length ? children : [children];
    children in Preact is always an array

    View Slide

  64. Conclusion
    what did we get?

    View Slide

  65. useState useEffect useMemo
    useRef useContext ?

    View Slide

  66. useImperativeHandle
    use it when you want to control your
    components in imperative way

    View Slide

  67. const ConfettiLauncher = forwardRef((props, ref) => {
    // attach an imperative func to a ref
    useImperativeHandle(ref, () => ({
    fire: () => {
    Confetti.create({ ... });
    }
    }));
    // render canvas
    });
    https://github.com/catdad/canvas-confetti

    View Slide

  68. followed the lifecycle of an open-source library:
    from design to the launch

    View Slide

  69. Meet Wouter: github.com/molefrog/wouter

    View Slide

  70. ! For all Wouters in the audience: no offence!

    View Slide

  71. «form follows function» principle
    a top-down design

    View Slide

  72. Alexey Taktarov
    @mlfrg · molefrog.com
    Illustrations
    Font used
    Simacheva Katya
    instagram.com/simacheva.katya
    Zangezi Sans, Daria Petrova
    futurefonts.xyz/daria-petrova/zangezi-sans
    Project repo github.com/molefrog/wouter

    View Slide