hyperapp-router
hyperapp-router copied to clipboard
RFC: Hyperapp Router 2.0
It is time to think to the new router for HAV2. (or I am a little bit too hasty ?).
This router of choice will be state first, as requested by many of us. It will stick to the new shiny Subscription API.
And is mostly inspired by the already in HAV2 concepts, plus some good tastes from Router5 and Riot/router.
I am not so good at writing long text, so I will go for a code walkthrough.
Init the router
The router module will exports both the default location state and the Route subscription.
import { location, Route } from "@hyperapp/router"
app({
init: () => ({ location }),
subscribe: state => [<Route />]
})
I am still not sure about the location state, as the Subscription can perform an init action at the first call.
Shoud we go for explicit or implicit state wiring ?
Defining the routes
A route definition have a pretty close API than subscriptions. And pretty close to the old properties, too. You are able to define many route subscriptions for the same route path. Unless you use the non greedy property. That will make the first route to match stop the subscriptions propagation in the routes array.
import { location, Route } from "@hyperapp/router"
const routes = ({ props: state }) => [{
effect: <Profile {...state} />,
props: {
route: "/profile/me"
}
}, {
effect: <Profile {...state} />,
props: {
route: "/profile/:user",
}
}, {
effect: <Route404 {...state} />,
props: {
route: "/profile",
parent: true
}
}]
app({
init: () => ({ location }),
subscribe: state => [<Route routes={routes} greedy={false} props={state} />]
})
Route (effect)
A route have an API pretty close to the Hyperapp effect API.
A route is a funtion to handle something similar to an onRouteEnter event. And like an effect susbscription, it will return a function to handle the onRouteLeave event.
Plus, a route can return falsy in some context, to make the non greedy router to continue the route subscriptions propagation.
A route, like subscriptions, have the dispatch function given as second parameter to perform actions.
const Profile = (state) => ({ params }, dispatch) => {
if (params.user === "me") {
dispatch(http(`https://reqres.in/api/users/${state.loggedUser.id}`, setUserProfile))
} else if (Number(params.user) == params.user) {
dispatch(http(`https://reqres.in/api/users/${params.user}`, setUserProfile))
} else {
return false // 'continue' instruction for non-greedy router
}
return (state) => {
dispatch(removeUserProfile)
}
}
Route views
Unlike HAV1 router, the consuming of a route into the view, will be only performed regarding to the state like any other state properties. Nothing fancy here.
import { location, Route } from "@hyperapp/router"
app({
init: () => ({ location }), // implicit or explicit ?
view: state => {
<main>
{state.location.path.startsWith("/profile") && <ProfileView {...state.userProfile} />}
</main>
},
subscribe: state => [<Route routes={routes} greedy={false} props={state} />]
})
Thats it.
As you can see, this new router is more about the actions you will perform on route changes a lot more than how you will present the view for a given route.
You can read through the whole example source here
import { app, h as jsx } from "hyperapp"
import { http } from "@hyperapp/fx"
import { location, Route } from "@hyperapp/router"
const setUserProfile = (state, { data }) => ({ userProfile: data })
const removeUserProfile = (state) => ({ userProfile: undefined })
const setDummyProfile = (state) => ({ userProfile: { id: 0, name: "John Doe", age: "404" } })
const Profile = (state) => ({ params }, dispatch) => {
// onRouteEnter
if (params.user === "me") {
dispatch(http(`https://reqres.in/api/users/${state.loggedUser.id}`, setUserProfile))
} else if (Number(params.user) == params.user) {
dispatch(http(`https://reqres.in/api/users/${params.user}`, setUserProfile))
} else {
return false // 'continue' instruction for non-greedy router
}
return (state) => {
// onRouteLeave
dispatch(removeUserProfile)
}
}
const Route404 = () => ({ path }, dispatch) => {
dispatch(setDummyProfile)
return (state) => {
dispatch(removeUserProfile)
}
}
const routes = ({ props: state }) => [{
effect: (state.loggedUser !== undefined) && <Profile {...state} />,
props: {
route: "/profile/me"
}
}, {
effect: <Profile {...state} />,
props: {
route: "/profile/:user",
}
}, {
effect: <Route404 {...state} />,
props: {
route: "/profile",
parent: true
}
}]
app({
init: () => ({ location }), // implicit or explicit ?
view: state => {
<main>
{state.location.path.startsWith("/profile") && <ProfileView {...state.userProfile} />}
</main>
},
subscribe: state => [<Route routes={routes} greedy={false} props={state} />]
})
How will rendering different routes look like? If I understood it correctly, we will have to look into state.location.path and render a view accordingly, right?
I can imagine one reason to have it this way is to give complete freedom as of how to render routes, and it can be useful if you want to render components depending on path fragments (like all routes that start with /profile should have the ProfileView at the top.
I'm thinking, though, that maybe we could get a helper renderer for the basic case (a different view for each route), something like that comes to mind:
import { location, Route, renderRoute } from "@hyperapp/router"
const routes = ({ props: state }) => [{
effect: <Profile {...state} />,
props: {
route: "/profile/:user",
view: state => (<div>{state.text}</div>)
}
}]
app({
init: () => ({ location }),
view: state => {
<main>
{ renderRoute(routes, state) }
</main>
})
So that we could specify a view property to render the route in each route, and then the helper renderRoute would take care of rendering the current location.
I'm aware that this could be easily implemented in any project that needs it, but it would be nice to get it out of the box. What do you think?
Like in HAV1, I want to keep the ability to render multiple different pieces of the view accordingly to the route.
This is how the greedy property works.
I am open to a component as helper for state.location.path test.
But it would look like the following :
import { location, Router, Route} from "@hyperapp/router"
const routes = ({ props: state }) => [{
effect: <Profile {...state} />,
props: {
route: "/profile/:user",
name: "profile"
}
}]
app({
init: () => ({ location }),
view: state => {
<main>
<Route name="profile" routes={routes} location={state.location}>
<ProfileView {...state.userProfile} />
</Route>
</main>
}),
subscribe: state => [<Router routes={routes} greedy={false} props={state} />]
})
@Swizz that looks better than what I came up with.
Reanimating this issue, since we have [email protected] now. (Couldn't find a more relevant issue than this one.)
I have started experimenting with hav2, and I think next point for me and most of people would be having more solid examples that rely on critical complementary libraries especially like @hyperapp/router.
What are your thoughts? I want to help to accelerate this transition in any way I can, but I think best implementation path is not clear to me yet, so maybe it needs more discussion for everyone as well.
Some days ago I was thinking about making a PR. But I was out of the loop from months now about Hav2. I will take the time to read the changes on Hav2 since I left the loop.
@gungorkocak Maybe you can reach me on slack to take the time to talk about it.
@Swizz please take a look at https://github.com/jorgebucaran/hyperapp-router/pull/100