Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

“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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Let’s get back to React…

Slide 8

Slide 8 text

React Hooks — the building blocks of an interface logic

Slide 9

Slide 9 text

useState → useReducer allows to subscribe to primitive state value changes

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

fewer constraints → more freedom

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

location.pathname router

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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)]; };

Slide 21

Slide 21 text

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)]; };

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

how do we subscribe to pushState/replaceState?

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

pattern-based routing?

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Adding Components Route, Link and Switch

Slide 32

Slide 32 text

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}`; }

Slide 33

Slide 33 text

let’s design Route component

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Adding a navigation About Us

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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]

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Switch Component Exclusive routing and default routes

Slide 42

Slide 42 text

/users/all →

Slide 43

Slide 43 text

/users/22 →

Slide 44

Slide 44 text

/foo/bar →

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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 —

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

making the matcher backwards-compatible?

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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!

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

accessing router from any component?

Slide 55

Slide 55 text

useContext a «portal» from the context

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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»

Slide 58

Slide 58 text

// 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!

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Supporting Preact conquering new markets

Slide 61

Slide 61 text

import { useRoute } from "wouter/preact";

Slide 62

Slide 62 text

copy sources into preact/ folder 
 on prepublishOnly NPM event ├── index.js ├── react-deps.js └── preact ├── index.js └── react-deps.js (* preact)

Slide 63

Slide 63 text

// 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

Slide 64

Slide 64 text

Conclusion what did we get?

Slide 65

Slide 65 text

useState useEffect useMemo useRef useContext ?

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Meet Wouter: github.com/molefrog/wouter

Slide 70

Slide 70 text

! For all Wouters in the audience: no offence!

Slide 71

Slide 71 text

«form follows function» principle a top-down design

Slide 72

Slide 72 text

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