React is view only library. I really like that it doesn’t try to be a full stack solution. Thanks to that it’s easy to fill the gaps with the library you know and prefer. And routing is one of those gap. In my first react’s app I simply took a look at complementary tools page and chose directorjsĀ for a start. It worked well although very soon I stumbled across few annoying bugs (like not calling route action on init). Workarounds exist but looks like author has no time recently to provide proper fixes. But bugs are not a problem actually. Directorjs simply doesn’t fit conceptually to react. They are two different worlds. So I looked further. The next promising library was react router component. Composable routes in a way one compose react’s comonents was exactly what I was looking for. Unfortunately I have to give up on it because it was a nodejs component and I needed simple and pure javascript solution. And it was too big for my needs. Finally I decided to create my own routing component. Of course it doesn’t have so many features – for example it lacks server side routing (I use scala and spray for that) or server side component rendering but it solves client side routing (for now without supporting html5 history). Sources can be find here: https://github.com/unodgs/react-path-router. Screenshot from github demo:
And the demo source file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
/** @jsx React.DOM */ var About = React.createClass({ render: function() { return ( <h1>This is a routing demo!</h1> ); } }); var UserList = React.createClass({ render: function() { var _this = this; var users = [ {id: 1001, username: 'unodgs', password: 'topsecret', firstName: 'Daniel', lastName: 'Kos'}, {id: 1002, username: 'admin', password: 'admin', firstName: 'Jack', lastName: 'Strong'}, {id: 1003, username: 'mike', password: 'mike123', firstName: 'Mike', lastName: 'Weak'}, {id: 1004, username: 'frodo', password: 'ppp222', firstName: 'Michael', lastName: 'Watson'} ]; var usersHtml = _.map(users, function(u) { return ( <tr> <td>{u.username}</td> <td>{u.password}</td> <td>{u.firstName}</td> <td>{u.lastName}</td> <td><A url={_this.props.componentUrl} path={"edit/" + u.id}>Edit</A></td> </tr> ); }); return ( <table className="table"> <thead> <th>Username</th> <th>Password</th> <th>First name</th> <th>Last name</th> <th></th> </thead> <tbody> {usersHtml} </tbody> </table> ); } }); var UserEdit = React.createClass({ render: function() { return ( <Paths> <Path url={this.props.url} name="/"> <form role="form"> <div className="form-group"> <label>Username</label> <input className="form-control"/> </div> <div className="form-group"> <label>Password</label> <input type="password" className="form-control"/> </div> <div className="form-group"> <label>First name</label> <input className="form-control"/> </div> <div className="form-group"> <label>Last name</label> <input className="form-control"/> </div> <A className="btn btn-default" url={this.props.componentUrl} path="addfriend">Add friend</A> </form> </Path> <Path url={this.props.url} name="/addfriend"> <h1>Adding friend form :)</h1> </Path> </Paths> ); } }); var UsersModule = React.createClass({ render: function() { return ( <Paths> <Path url={this.props.url} name="/" render={UserList}/> <Path url={this.props.url} name="edit/:userId" render={UserEdit}/> </Paths> ); } }); var ClientsModule = React.createClass({ render: function() { return ( <h1>This is a client list!</h1> ); } }); var App = React.createClass({ render: function() { var menu = [ { url: 'users', name: 'Users' }, { url: 'clients', name: 'Clients' }, { url: 'about', name: 'About' } ]; var _this = this; var drawMenu = function(menu) { var mm = _.map(menu, function(m) { var currentUrl = PathUtils.current(); var menuUrl = PathUtils.combine(_this.props.parentUrl, m.url); return ( <li className={menuUrl === currentUrl ? "active": ""}> <A url={_this.props.componentUrl} path={m.url}>{m.name}</A> </li> ); }); return ( <nav className="navbar navbar-default navbar-top" role="navigation"> <ul className="nav navbar-nav">{mm}</ul> </nav> ); }; var menuHtml = drawMenu(menu); return ( <div> {menuHtml} <div className="container"> <Path url={this.props.url} name="users" render={UsersModule}/> <Path url={this.props.url} name="clients" render={ClientsModule}/> <Path url={this.props.url} name="about" render={About}/> </div> </div> ); } }); PathInit(<App/>, document.body); |
To initialize router PathInit function must be used. First argument is the main react component instance and the second is a dom node to which rendered component will be attached to. Having done that in App component we can now use <Path/>.
1 2 3 |
<Path url={this.props.url} name="users" render={UsersModule}/> <Path url={this.props.url} name="/clients" render={ClientsModule}/> <Path url={this.props.url} name="about/" render={About}/> |
url is a constant attribute that must be passed to the Path to allow it properly analyze the url. It contains previous part of an url that was matched in the parent component and the “next url” that will be parsed in the context of current component (component that Path belongs to). I hope React authors allow in the future to define attributes that are automatically transferred from parent component. That would allow to avoid this boilerplate making Path declaration clearer. Anyway for now url is required to be provided by the user. Second attributeĀ nameĀ defines part of the url that must be matched to render component pointed by render attribute. Name don’t distinguish between /clients, clients/ and clients. They are all equivalent. Name can also contains many parts: /clients/new, /clients/under/32 and so on. What’s more interesting it can contains parameters like that:
- /client/:id
- /group/:groupName/user/:userId
If such an url is matched, every parameter that was found is passed to the rendered component in it’s props. In the case above id wouldĀ be accessed by this.props.idĀ and the gorupName byĀ this.props.groupName.
Path also passes one more parameter in the props of rendered component. It’s a componentUrlĀ and it contains current component’s url. In the combination with an AĀ component it can be used to build links in a safe way (that means you won’t need to change urls everywhere if for example one of the parent component’s name will change). In the example it’s used like this:
1 2 3 |
<li className={menuUrl === currentUrl ? "active": ""}> <A url={_this.props.componentUrl} path={m.url}>{m.name}</A> </li><span style="background-color: #ffffff; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.625;">Ā </span> |
Sometimes instead of building separate components and passing them as render attribute in many Paths it’s easier to choose what to render directly in one component.
1 2 3 4 5 6 7 8 |
<Paths> <Path url={this.props.url} name="/tab0"> Content of the first tab </Path> <Path url={this.props.url} name="/tab1"> Content of the second tab </Path> </Paths> |
Paths can be replaced with anything (<div> for example). It’s there only because in react you cannot return tags without common parent.
And pretty much that’s all :). I certainly will keep on working on Path component but I think it’s a good start for everyone that would like to develop something similar adjusted to his requirements. The current code is only 196 lines and should be easy to follow. See you.