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. Alexey Taktarov believes that design > engineering currently busy working

    on resume.io 4M+ resumes worldwide, distributed team !+" @mlfrg molefrog molefrog Twitter GitHub Instagram
  2. 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
  3. “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
  4. the function of building can be fulfilled by the combination

    of the fundamental elements: air, light, materials
  5. const [value, setValue] = useState(0); // custom hooks const [value,

    setValue] = useGlobalState(0); const [value, setValue] = useLocalStorageState(0); Hooks are composable: one interface, different behaviour
  6. learn React Hooks by implementing a real open-source project: a

    tiny router for React What are we going to do today?
  7. 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
  8. v4: no outlets, no top-level configs in favour of a

    simple component: <Route path="/news" component={NewsFeed} />
  9. 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
  10. 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)]; };
  11. 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)]; };
  12. how to subscribe to pushState/replaceState? monkey-patch these methods intercept link

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

    can’t run outside of React reconciler
  14. 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
  15. 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}`; }
  16. import { createElement as h } from "react"; const Route

    = ({ path, component }) => { const [match, params] = useRoute(path); if (!match) return null; return h(component, { params: params }); }
  17. 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
  18. 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]
  19. // 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>
  20. how do we add a prop to an element? if

    (isValidElement(children)) { return cloneElement(children, { href, onClick: handleClick }); }
  21. 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
  22. Towards a 1KB router! Our bundle is 2.2KB already, let’s

    drop some things out path-to-regexp — 65%
  23. 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 —
  24. Size Limit helps to keep your library in good shape

    https://github.com/ai/size-limit
  25. <Router matcher={pathToRegexpMatcher}> <Route ... /> </Router> you can always specify

    an original matcher through the optional top-level Router
  26. 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!
  27. 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
  28. 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»
  29. // 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!
  30. 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
  31. copy sources into preact/ folder 
 on prepublishOnly NPM event

    ├── index.js ├── react-deps.js └── preact ├── index.js └── react-deps.js (* preact)
  32. // 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
  33. 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
  34. 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