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.

01bf97df3784afa10fc2b561e251a41c?s=128

Alexey Taktarov

August 14, 2019
Tweet

Transcript

  1. Hooks in Action: Implementing a 1KB Router in React Alexey

    Taktarov @mlfrg · molefrog.com
  2. Alexey Taktarov believes that design > engineering currently busy working

    on resume.io 4M+ resumes worldwide, distributed team !+" @mlfrg molefrog molefrog Twitter GitHub Instagram
  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
  4. source: http://inrussia.com/the-vyborg-library

  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
  6. the function of building can be fulfilled by the combination

    of the fundamental elements: air, light, materials
  7. Let’s get back to React…

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

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

    changes
  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
  11. learn React Hooks by implementing a real open-source project: a

    tiny router for React What are we going to do today?
  12. Routing in React apps routing approaches have transformed along with

    the React ecosystem
  13. const routes = ( <Route path="/" component={App}> <Route path="/users" components={{

    main: Main }}> <Route path="/:item" component={Item} /> </Route> </Route> ); React Router v1.0.3: a DSL for describing routes
  14. v4: no outlets, no top-level configs in favour of a

    simple component: <Route path="/news" component={NewsFeed} />
  15. fewer constraints → more freedom

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

  17. location.pathname router

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

  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
  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)]; };
  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)]; };
  22. useEffect a gate to subscriptions; a replacement for lifecycle-methods

  23. how do we subscribe to pushState/replaceState?

  24. how to subscribe to pushState/replaceState? monkey-patch these methods intercept link

    clicks → github.com/storeon/router → github.com/molefrog/wouter
  25. how do I test hooks? hooks are «special» functions, they

    can’t run outside of React reconciler
  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
  27. pattern-based routing?

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

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

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

  31. Adding Components Route, Link and Switch

  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}`; }
  33. let’s design Route component <Route path="/orders/:sid" component={OrderCard} />

  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 }); }
  35. Adding a navigation <Link href=“/about">About Us</Link>

  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
  37. useMemo → useCallback use it to describe computed values with

    proper caching
  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]
  39. // all of these should produce the same html //

    <a href="/contact">Contact</a> <Link href="/contact">Contact</Link> <Link href="/contact"><a>Contact</a></Link> <Link href="/contact"><A>Contact</A></Link>
  40. how do we add a prop to an element? if

    (isValidElement(children)) { return cloneElement(children, { href, onClick: handleClick }); }
  41. Switch Component Exclusive routing and default routes

  42. <Switch> <Route path="/users/all" component={AllUsers} /> <Route path="/users/:id" component={UserCard} /> <Route

    path="/:rest*" component={NotFound} /> </Switch> /users/all →
  43. <Switch> <Route path="/users/all" component={AllUsers} /> <Route path="/users/:id" component={UserCard} /> <Route

    path="/:rest*" component={NotFound} /> </Switch> /users/22 →
  44. <Switch> <Route path="/users/all" component={AllUsers} /> <Route path="/users/:id" component={UserCard} /> <Route

    path="/:rest*" component={NotFound} /> </Switch> /foo/bar →
  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
  46. Towards a 1KB router! Our bundle is 2.2KB already, let’s

    drop some things out path-to-regexp — 65%
  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 —
  48. https://github.com/molefrog/wouter/issues/1 2.2KB → 1.1KB

  49. Size Limit helps to keep your library in good shape

    https://github.com/ai/size-limit
  50. making the matcher backwards-compatible?

  51. <Router matcher={pathToRegexpMatcher}> <Route ... /> </Router> you can always specify

    an original matcher through the optional top-level Router
  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: <RouterCtx.Provider value={…} /> }; should only create router once!
  53. useRef saves the value between re-renders; replaces this.*

  54. accessing router from any component?

  55. useContext a «portal» from the context

  56. const useRouter = () => { const providedRouter = useContext(RouterCtx);

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

    // either obtain the router from the outer context // (provided by the `<Router /> component) or create // a default one on demand. return useMemo( () => (providedRouter ? providedRouter : buildRouter()), [providedRouter] ); } we need a way to save a router somewhere «globally»
  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 <Provider /> needed!
  59. we’ve got a nice extension point now! // useStaticLocation, useGlobalState,

    useLocalStorage, ... <Router hook={useHashLocation} /> by customising a useLocation hook we can achieve some interesting effects: like hash-based routing, or persisted routing
  60. Supporting Preact conquering new markets

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

  62. copy sources into preact/ folder 
 on prepublishOnly NPM event

    ├── index.js ├── react-deps.js └── preact ├── index.js └── react-deps.js (* preact)
  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
  64. Conclusion what did we get?

  65. useState useEffect useMemo useRef useContext ?

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

    in imperative way
  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
  68. followed the lifecycle of an open-source library: from design to

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

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

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

  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