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

Cherrytree and Clientside JavaScript Routing

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Cherrytree and Clientside JavaScript Routing

An internal talk I gave at Qubit about clientside routing and my router library Cherrytree.

Avatar for Karolis Narkevicius

Karolis Narkevicius

January 21, 2016
Tweet

Other Decks in Programming

Transcript

  1. 2 I've worked on cherrytree on and off for over

    2 years in the background of us developing UIs at Qubit.
  2. 3 1. What is clientside routing? 2. Building a simple

    router 3. Cherrytree API 4. Architecting UIs
  3. 4 5 What is routing? (1) In context of computing

    or more like in context of UIs
  4. 6 What is routing? (2) The bit of logic that

    translates a URL into the right content I'd argue that the URL is actually optional. If you consider web applications vs mobile applications. The latter don't have URLs, but there's still a bunch of screens, you click an item in the list, you see a new page with item details. And that to me is still related to routing, practically speaking.
  5. 7 What is routing? (3) A way of managing all

    of the screens in your application Or another way to put it .. showing the right content at the right time
  6. 8 Why should we care about routing? URLs are a

    super nice feature of the web facilitates sharing content facilitates collaboration convenient (e.g. reloading this preso shows the right slide) tabs provide a forking workflow Browser UI patterns back/forward buttons URL autocomplete cmd/middle clicking links
  7. 9 Web evolution These benefits emerged from the design of

    the web In the pre-JavaScript era all this worked out of the box Single Page Applications you need to take special care of the URLs For example, if you just handle a click and call renderSomeScreen() and you only ever render that screen on that click. The click event won't be fired when you refresh the page. Furtermore, you broke back/forward buttons. Middle click won't work. Nothing will work.
  8. 10 Server side routing Lots of server side web frameworks

    use the following pattern router matches URL against patterns and calls the right controller#action pair with parameters extracted from the URL controller then uses those params responds with content # r o u t e s . r b g e t ' / p a t i e n t s / : i d ' , t o : ' p a t i e n t s # s h o w ' # p a t i e n t s _ c o n t r o l l e r . r b d e f s h o w r e s p o n d _ w i t h P a t i e n t s . f i n d ( p a r a m s [ : i d ] ) e n d
  9. 11 Client side routing Allows navigating around the webapp without

    reloading the page Keeps the application state in sync with the URL At first, the APIs mimicked the server side routing B a c k b o n e . R o u t e r . e x t e n d ( { r o u t e s : { ' h e l p ' : ' h e l p ' , / / # h e l p ' s e a r c h / : q u e r y ' : ' s e a r c h ' , / / # s e a r c h / k i w i s ' s e a r c h / : q u e r y / p : p a g e ' : ' s e a r c h ' / / # s e a r c h / k i w i s / p 7 } , h e l p : f u n c t i o n ( ) { } , s e a r c h : f u n c t i o n ( q u e r y , p a g e ) { } } )
  10. 12 Browser History API w i n d o w

    . o n p o p s t a t e = f u n c t i o n ( ) { } w i n d o w . h i s t o r y . p u s h S t a t e ( { } , ' p a g e t i t l e 1 ' , ' / s o m e / u r l ? q = s e a r c h ' ) w i n d o w . h i s t o r y . r e p l a c e S t a t e ( { } , ' p a g e t i t l e 2 ' , ' / s o m e / u r l ? q = s e a r c h ' )
  11. 13 location-bar (1) History API wrapped up in a small

    library http://github.com/KidkArolis/location-bar
  12. 14 location-bar (2) l e t l o c a

    t i o n B a r = n e w L o c a t i o n B a r ( ) / / l i s t e n t o U R L c h a n g e s l o c a t i o n B a r . o n C h a n g e ( f u n c t i o n ( p a t h ) { } ) / / l i s t e n f o r t h i s p a t t e r n l o c a t i o n B a r . r o u t e ( / s o m e \ - r e g e x / , f u n c t i o n ( p a t h ) { } ) ; / / s t a r t l i s t e n i n g l o c a t i o n B a r . s t a r t ( { p u s h S t a t e : t r u e } ) / / u p d a t e t h e U R L l o c a t i o n B a r . u p d a t e ( ' / s o m e / u r l ? p a r a m = 1 2 3 ' )
  13. 15 location-bar (3) Very similar to the native History API

    we saw earlier, but also: Falls back to an older #hashchange API for older browsers Wraps hashchange and pushstate APIs into an identical API Regex matching Supports custom root prefix Nice abstraction if you ever need to manipulate browser's URL.
  14. 17 18 path-to-regexp https://github.com/pillarjs/path-to-regexp l e t { r e

    , p a r a m N a m e s } = p a t h T o R e g e x p ( ' / : f o o / : b a r ' ) r e . e x e c ( ' / t e s t / r o u t e ' ) / / = > [ ' / t e s t / r o u t e ' , ' t e s t ' , ' r o u t e ' ] p a r a m N a m e s / / = > [ { n a m e : ' f o o ' , . . . } , { n a m e : ' b a r ' , . . . } ] * note, I've modified API in examples for readability
  15. f u n c t i o n e x

    t r a c t P a r a m s ( p a t t e r n , p a t h ) { l e t p = p a t h T o R e g e x p ( p a t t e r n ) l e t m a t c h = p a t h . m a t c h ( p . r e ) i f ( ! m a t c h ) { r e t u r n n u l l } r e t u r n p . p a r a m N a m e s . r e d u c e ( f u n c t i o n ( p a r a m s , p a r a m N a m e , i n d e x ) { p a r a m s [ p a r a m N a m e ] = m a t c h [ i n d e x + 1 ] r e t u r n p a r a m s } , { } ) }
  16. 19 l e t r o u t e s

    = { ' / h e l p ' : h e l p , ' / s e a r c h / : q u e r y ' : s e a r c h , ' / s e a r c h / : q u e r y / p : p a g e ' : s e a r c h } r o u t e ( r o u t e s )
  17. 20 f u n c t i o n r

    o u t e ( r o u t e s ) { l e t l o c a t i o n = n e w L o c a t i o n B a r ( ) l o c a t i o n . o n C h a n g e ( f u n c t i o n ( p a t h ) { r o u t e s . f o r E a c h ( f u n c t i o n ( c a l l b a c k , p a t t e r n ) { l e t p a r a m s = e x t r a c t P a r a m s ( p a t t e r n , p a t h ) i f ( p a r a m s ) { c a l l b a c k ( p a r a m s ) r e t u r n f a l s e } } ) } ) l o c a t i o n . s t a r t ( { p u s h S t a t e : t r u e } ) r e t u r n f u n c t i o n d i s p o s e ( ) { l o c a t i o n . d e s t r o y ( ) } }
  18. 21 l e t r o u t e s

    = { ' / h e l p ' : h e l p , ' / s e a r c h / : q u e r y ' : s e a r c h , ' / s e a r c h / : q u e r y / p : p a g e ' : s e a r c h } r o u t e ( r o u t e s )
  19. 22 Might be enough for simple apps r o u

    t e ( { ' / : s l i d e N u m b e r ' : f u n c t i o n r e n d e r S l i d e ( s l i d e N u m b e r ) { . . . } } ) For example, this slideshow is utilising browser APIs to update the URL every time I change the slide.
  20. 23 24 Is that enough? r o u t e

    ( { ' / : s l i d e N u m b e r ' : f u n c t i o n r e n d e r S l i d e ( s l i d e N u m b e r ) { ? ? ? / / < - - t h e i n t e r e s t i n g b i t } } )
  21. 26 A lot of interesting questions here. 1. How do

    we cleanup the List view from the DOM? Granted, it's less of an issue with something like React, because React has a great in built view lifecycle management: componentDidMount, componentWillUnmount. 2. At what point do you render the sidebar?
  22. 27

  23. 28

  24. 31 Beyond pattern matching Make it easier to deal with

    nested routable views Help with the view rendering lifecycle Help with fetching the right data at the right time Describe the UI for URL declaratively instead of imperatively Handle initial page load as well as transitions Generate URLs Turns out, it's not that useful to call different functions per pattern. Cherrytree flips this upside down by calling the same callback on very route change.
  25. 33 I've changed the URL in this example, because I

    think URLs should be human readable. Think of GitHub URLs. URL readability is a thing, you know seeing a URL with some more info than just a bunch of numbers is helpful with identifying what the link is for.
  26. 34

  27. 35

  28. 37 myamazingapp.com/workspace/123/project/456/conversations [ { n a m e : '

    a p p ' , c o m p o n e n t } , { n a m e : ' w o r k s p a c e ' , p a r a m s : { w o r k s p a c e I d : 1 2 3 } , c o m p o n e n t } , { n a m e : ' p r o j e c t ' , p a r a m s : { p r o j e c t I d : 4 5 6 } , c o m p o n e n t } , { n a m e : ' p r o j e c t . c o n v e r s a t i o n s ' , c o m p o n e n t } ]
  29. 38 myamazingapp.com/workspace/123/project/888/list [ { n a m e : '

    a p p ' , c o m p o n e n t } , { n a m e : ' w o r k s p a c e ' , p a r a m s : { w o r k s p a c e I d : 1 2 3 } , c o m p o n e n t } , { n a m e : ' p r o j e c t ' , p a r a m s : { p r o j e c t I d : 8 8 8 } , c o m p o n e n t } , { n a m e : ' p r o j e c t . l i s t ' , c o m p o n e n t } ]
  30. 39 40 API (1) i m p o r t

    c h e r r y t r e e f r o m ' c h e r r y t r e e ' l e t r o u t e r = c h e r r y t r e e ( ) r o u t e r . m a p ( / * r o u t e s * / ) r o u t e r . u s e ( / * m i d d l e w a r e * / ) r o u t e r . l i s t e n ( )
  31. 41 API (2) r o u t e r .

    m a p ( f u n c t i o n r o u t e s ( r o u t e ) { r o u t e ( ' a p p ' , { p a t h : ' / ' , c o m p o n e n t : A p p } , ( ) = > { r o u t e ( ' w o r k s p a c e ' , { p a t h : ' w / : w o r k s p a c e I d ' , c o m p o n e n t : W o r k s p a c e } , ( ) = > { r o u t e ( ' p r o j e c t ' , { p a t h : ' p r o j e c t / : p r o j e c t I d ' , c o m p o n e n t : P r o j e c t } , ( ) = > { r o u t e ( ' p r o j e c t . l i s t ' , { c o m p o n e n t : L i s t } ) r o u t e ( ' p r o j e c t . c o n v e r s a t i o n s ' , { c o m p o n e n t : C o n v e r s a t i o n s } ) } ) } ) r o u t e ( ' s e t t i n g s ' , { c o m p o n e n t : S e t t i n g s } ) } ) } ) i m p o r t c h e r r y t r e e f r o m ' c h e r r y t r e e ' l e t r o u t e r = c h e r r y t r e e ( ) r o u t e r . u s e ( / * m i d d l e w a r e * / ) r o u t e r . l i s t e n ( )
  32. 42 API (3) r o u t e r .

    u s e ( f u n c t i o n r e n d e r ( t r a n s i t i o n ) { l e t { r o u t e s , p a r a m s , q u e r y } = t r a n s i t i o n l e t A p p = r o u t e s . r e d u c e R i g h t ( ( c h i l d r e n , r o u t e ) = > { l e t C o m p o n e n t = r o u t e . o p t i o n s . c o m p o n e n t r e t u r n c r e a t e E l e m e n t ( C o m p o n e n t , { p a r a m s , q u e r y , c h i l d r e n } ) } , n u l l ) R e a c t . r e n d e r ( i m p o r t c h e r r y t r e e f r o m ' c h e r r y t r e e ' i m p o r t r o u t e s f r o m ' . / r o u t e s ' l e t r o u t e r = c h e r r y t r e e ( ) r o u t e r . m a p ( r o u t e s ) < A p p / > , d o c u m e n t . q u e r y S e l e c t o r ( ' . a p p ' ) ) } ) r o u t e r . l i s t e n ( ) By default - nothing happens. No views get rendered, no data gets fetched. You implement these behaviours yourself as you see fit. But the API forces you into structuring this around transforming a URL into views on the page.
  33. 43 API (4) r o u t e r .

    l i s t e n ( ) i m p o r t c h e r r y t r e e f r o m ' c h e r r y t r e e ' i m p o r t r o u t e s f r o m ' . / r o u t e s ' i m p o r t r e n d e r f r o m ' . / r o u t i n g / r e n d e r ' l e t r o u t e r = c h e r r y t r e e ( ) r o u t e r . m a p ( r o u t e s ) r o u t e r . u s e ( r e n d e r )
  34. 44 API (5) i m p o r t c

    h e r r y t r e e f r o m ' c h e r r y t r e e ' i m p o r t r o u t e s f r o m ' . / r o u t e s ' i m p o r t r e n d e r f r o m ' . / r o u t i n g / r e n d e r ' l e t r o u t e r = c h e r r y t r e e ( ) r o u t e r . m a p ( r o u t e s ) r o u t e r . u s e ( r e n d e r ) r o u t e r . l i s t e n ( ) The same middleware functions gets called on every transition. That's the key difference from other common routers. Which approach is better in practise, hard to say, cherrytree has worked quite well for us over the years. It's simiilar to how Ember router and React router works.
  35. 45 API (6) r o u t e r .

    t r a n s i t i o n T o ( ' p r o j e c t . l i s t ' , { w o r k s p a c e I d : 1 , p r o j e c t I d : 2 } ) r o u t e r . t r a n s i t i o n T o ( ' / s e t t i n g s ' ) r o u t e r . r e p l a c e W i t h ( ' p r o j e c t . c o n v e r s a t i o n s ' ) r o u t e r . g e n e r a t e ( ' p r o j e c t . l i s t ' , { w o r k s p a c e I d : 1 , p r o j e c t I d : 2 } ) i m p o r t c h e r r y t r e e f r o m ' c h e r r y t r e e ' i m p o r t r o u t e s f r o m ' . / r o u t e s ' i m p o r t r e n d e r f r o m ' . / r o u t i n g / r e n d e r ' l e t r o u t e r = c h e r r y t r e e ( ) r o u t e r . m a p ( r o u t e s ) r o u t e r . u s e ( r e n d e r ) r o u t e r . l i s t e n ( ) Some more APIs
  36. 46 API (7) i m p o r t c

    h e r r y t r e e f r o m ' c h e r r y t r e e ' i m p o r t r o u t e s f r o m ' . / r o u t e s ' i m p o r t r e n d e r f r o m ' . / r o u t i n g / r e n d e r ' l e t r o u t e r = c h e r r y t r e e ( ) r o u t e r . m a p ( r o u t e s ) r o u t e r . u s e ( r e n d e r ) r o u t e r . u s e ( ? ? ? ) r o u t e r . l i s t e n ( ) You can have multiple middleware functions, express style!
  37. 47 API (8) r o u t e r .

    u s e ( l o a d i n g A n i m a t i o n ) r o u t e r . u s e ( r e d i r e c t ) r o u t e r . u s e ( f e t c h C o m p o n e n t s ) r o u t e r . u s e ( f e t c h D a t a ) r o u t e r . u s e ( r e n d e r ) r o u t e r . u s e ( t r a c k ) r o u t e r . u s e ( e r r o r s ) A lot of this power comes from async handlers. If you initiate a new transition while another one is still underway, cherrytree will handle that and you'll zoom straight into the new transition.
  38. 48 Cherrytree benefits framework agnostic and not very prescriptive handles

    asynchronous transitioning can dynamically load parts of your application intercepts all clicks and calls w i n d o w . p u s h S t a t e transition is a first class citizen - pause, resume transitions using URL is optional (could work for native apps) works on the server side (serverside routing or isomorphic apps)
  39. 49 UI Architecture How do you practically manage data and

    views using cherrytree? How do you implement middleware in real life. The old versions of cherrytree enforced a specific way of managing data with model hooks and route handlers, but all that went way and in my opinion both simplified the core concepts and enabled alternative data fetching strategies.
  40. 50 myamazingapp.com/workspace/123/project/456/conversations / / r o u t e r

    . u s e ( t r a n s i t i o n = > c o n s o l e . l o g ( t r a n s i t i o n . r o u t e s ) ) [ { n a m e : ' a p p ' , c o m p o n e n t } , { n a m e : ' w o r k s p a c e ' , p a r a m s : { w o r k s p a c e I d : 1 2 3 } , c o m p o n e n t } , { n a m e : ' p r o j e c t ' , p a r a m s : { p r o j e c t I d : 4 5 6 } , c o m p o n e n t } , { n a m e : ' p r o j e c t . c o n v e r s a t i o n s ' , c o m p o n e n t } ]
  41. 51 52 Naive implementation loop over every route, call component.fetchData

    pass that data as props to the component render The problem is you refetch data on every transition for everything
  42. 53 Real world implementation compare p r e v R

    o u t e s and r o u t e s to check what's already active fetchData middleware only fetches data for non active routes give components a getter function that can see all levels of data e.g. g e t ( ' u s e r ' ) , g e t ( ' w o r k s p a c e ' ) , g e t ( ' t a s k s ' ) use a higher order component wrapper to project data to props
  43. A lot of other concerns here, such as .. do

    you load this data in parallel. In one/multiple requests? GraphQL? One real issue we faced was, ok, I've loaded this, now I want to show a modal with some more data. How do I load that extra data?
  44. 54 Put data in stores in the router middleware by

    dispatching actions. Later load more data into stores also by dispatching actions, this time from views. But both load data the same way and into the same store.
  45. 55 56 Future work continue simplifying e.g. returning the transition

    object in the middleware deadlocks currently transition is both data and a promise and an instance with methods improve error handling make it more functional/pure externalise all state away from the router continue learning from other projects
  46. 57 Routing is a hot topic rackt/redux-simple-router move router state

    into a global redux store acdlite/router functional router that turns a path into a state object and transforms it via middleware raisemarketplace/ground-control combines react-router with redux and manages a hierarchy of redux stores These might seem to do with application state management and less with routing (e.g. all three projects I mentioned involve
  47. redux)! It seems to me that routing and state management

    are closely linked. 58 Lessons URLs are an important part of the web Consider URLs of your application upfront That will help you structure your application well