How would you model a simple menu

reasonreact

#1

Suppose I have a simple menu (let’s make it non-nested). So, there are several items, one and only one of which is active at any given time.

How do I model that so that impossible states are impossible and yet the code is not awkward?

  • currentItemIdx: int allows to point out of bounds
  • { pre: list(Item), current: Item, post: list(Item) } is awkward and allows items duplications
  • items being of { title: string, active: bool } allow for multiple items to be active

What would you do? And am I overengineering? )


#2

Hm, I usually do a combination of a router component and variants. As a quick example:

Router.re

type route =
  | Dashboard
  | Licenses
  | Updates(option(string));
let getRouteByUrl = (url: ReasonReact.Router.url) => {
  let path = url.hash |> Js.String.split("/");
  switch (path) {
  | [|""|]
  | [|"", ""|]
  | [|"", "dashboard"|] => Dashboard
  | [|"", "licenses"|] => Licenses
  | [|"", "updates"|] => Updates(None)
  | [|"", "updates", updateId|] => Updates(Some(updateId))
  };
};
let component = ReasonReact.reducerComponent("Router");

let make = children => {
  ...component,
  initialState: () =>
    ReasonReact.Router.dangerouslyGetInitialUrl()->getRouteByUrl,
  reducer: (action: route, _state: route) => ReasonReact.Update(action),
  didMount: ({send, onUnmount}) => {
    let interpretUrl = (url: ReasonReact.Router.url) =>
      send(url->getRouteByUrl);
    interpretUrl(ReasonReact.Router.dangerouslyGetInitialUrl());
    let watcherId = ReasonReact.Router.watchUrl(interpretUrl);

    onUnmount(() => watcherId->ReasonReact.Router.unwatchUrl);
  },
  render: ({state}) => children(state),
};

Layout.re

/* ... */
<div className=classes.subContent>
  (
    switch (route) {
    | Dashboard => <Dashboard_Dashboard />
    | Licenses => <Dashboard_Licenses />
    | MailSettings => <Dashboard_MailSettings />
    | Updates(updateId) =>
      <Dashboard_Updates updateId />
    | _ =>
      "Route not implemented"->ReasonReact.string
    }
  )
</div>
/* ... */

Just as an example. The variants / reducer pattern makes it so that you always know which item is active. Or did I get you wrong in that you wish to make this menu more dynamic, like from a database?


#3

No, I don’t take it from a database or config (not yet, at least), but I’d like to be able to navigate with keyboard, and variants mean changing active route would look like:

let nextRoute = currentRoute => {
  switch (currentRoute) {
  | Dashboard => Licenses
  | Licenses => MailSettings
  | MailSettings => Dashboard
  }
}

#4

True, that’s not ideal then. You could add the variants to a list / array and switch them by number instead of the variant itself on keystroke.


#5

Yeah, I think I’m going with something like:

type menuState = {
  items: array(menuItem),
  current: int,
};

let next = (array, current) =>
  if (current < Array.length(array) - 1) {
    current + 1;
  } else {
    0;
  };

I was afraid that int specifies too many values, but I guess it’s not that hard to make it pretty safe after all. Or at least it’ll have to do.

Thanks a lot!


#6

I think you decided on good building blocks for this state, here—the trick to getting the safety that you want may be to then take these implementation details, hide them behind a module, and only expose well-behaved functions (that respect your invariants, and keep the menuState type abstract) to the rest of your code. If random parts of your UI are prevented from doing this index math on their own in the first place, that’ll certainly stop them from doing it wrong!


#7

Yeah, I’m thinking of hiding it in a module, maybe even generalizing it a little. But then premature abstraction is evil, so we’ll see.


#8

I recently had to write a single-level-nested tree which allowed selecting any of the nodes. I went for the pre, current, post approach. Here’s the types in case it is useful:

module SingleLevelTree = {
  type node('a) = {
    parent: 'a,
    prevSiblings: array('a),
    current: 'a,
    children: array('a),
    nextSiblings: array('a),
  };

  type root('a) = {
    elem: 'a,
    children: array('a),
  };

  type t('a) =
    | Root(root('a))
    | Node(node('a));
};

A flat list as you finally ended up with might be a more pragmatic solution here, but I’m going all in with “making invalid states impossible” as much as I can. Thanks to types I can always refactor and pare it back later.


#9

I’ve thought about that. BTW, in Elm they even have a special package for that. My only beef with that is that makes render more complicated: instead of mapping over a single array, you’d map over two arrays and pass a selected item as well.


#10

I use the router since my application uses hash routes and the router is wired up as the menu is always visible in the UI.

Click handler lives at the very top in the container component. My routes accept a variant Dashboard(<route>) where <route> is another variant. Not sure if it’s the best way to do it, but it also lets me use that variant to set a nice title to the tab, etc.