Decoupled navigation request handling
May 2023, by Maarten Nieber
Url effects

The relation between application state and the url

Though the application state and the url state are obviously connected, it's not immediately clear what their relationship should be. In my applications, I currently apply the following rules:

  • the url belongs to the "view" part of the application. The application state (which can exist independently of any views) is agnostic about the url;
  • whenever a part of the state is reflected in the url, then any component that causes a state change must call a navigation function (as discussed in the previous chapter) to update the url;
  • when the url changes, then a so-called url effect must be used to update the state accordingly.

I used the word "currently" because my thinking on this topic is still evolving. Below, I will elaborate on these rules.

Components synchronize the url with the state

The first two rules imply that components are responsible for ensuring that the url remains synchronized with the state. This is quite a big responsibility, that seems to go against the idea of keeping components as simple as possible. Ideally, we'd like components to do just two things:

  • render information and controls, such as buttons;
  • respond to user actions by calling a function that updates the state.

We've just added a third responsibility: call a navigation function to update the url. To keep components simple, we will have to make this step as simple as possible. This means that components should be agnostic about (possibly context dependent) url changes. The previous chapter described how this can be achieved. The component only has to call the navigation function, the rest is taken care of.

In my opinion, this approach actually makes the code easier to understand. Typically, in a click handler, you will first see the state changes that are triggered (e.g. a call to clipsHighlight.set) followed by a url change (e.g. navToClip(clip)). We could try to automatically derive the url change from the state change, but this would be less concrete. I expect such an approach to also be more challenging and complex.

Url effects synchronize the state with the url

The third rules states that a so-called url effect must update the state when the url changes. A url effect is a special type of React component. When this component mounts, or whenever the url changes, it inspects the url and updates the state accordingly.

To illustrate this, let's consider the case where the application is loaded from the /blog-posts/:post-slug url. First of all, the data about blog-posts needs to be fetched from the server. I've discussed this in this post. Second, we must highlight the correct post in the application state, based on the :post-slug parameter. The following url-effect can take care of this.

HighlightPostEffect.tsx
type PropsT = {
  posts: PostT[],
  postsHighlight: Highlight<PostT>
};

export const HighlightPostEffect = observer((props: PropsT) => {
  const location = useLocation();
  const { postSlug } = useParams() as ObjT;
  const refUrl = React.useRef<string>('');

  // Define local variables
  const { postsHighlight, posts } = props;
  const postFromUrl = R.find((x: PostT) => x.slug === postSlug)(posts);
  const isPostInSyncWithUrl = postsHighlight.id === postFromUrl?.id;

  React.useEffect(() => {
    // If there is a new url
    if (refUrl.current !== location.pathname) {
      if (postFromUrl) {
        refUrl.current = location.pathname;
        if (!isPostInSyncWithUrl) {
          postsHighlight.set({ id: postFromUrl?.id });
        }
      }
    }
  });

  return (
    <UrlEffectView resourceName="post" isInSync={isPostInSyncWithUrl}/>
  );
});

Note that the example code depends on the Skandha library. This library provides generic behaviours, such as Highlight. In the example code, we're assuming that a Highlight object is used to track the highlighted post.

The UrlEffectView

The url-effect uses a UrlEffectView to alert the programmer when a component forgot to update the url. UrlEffectView is a utility component that checks the isInSync flag and prints an error to the console if this flag has been false for longer than a certain threshold (the default is one second). If the app is running in development mode, then it shows the same warning as a div, to make it more noticeable.

Inserting the url-effect in the router

The HighlightPostEffect must be inserted in the appropriate place in the component tree. At this location, there should be a postSlug url, and both posts and postsHighlight should be available as default properties. Therefore, we will insert HighlightPostEffect right above the PostsView.

UrlRouter.tsx
export const UrlRouter = observer((props: PropsT) => {
  return (
    <PostsStateProvider>
      <Route path="/posts">
        <PostsView />

        <Route path="/posts/:postSlug">
          <HighlightPostEffect />
          <PostsView />
        </Route>

      </Route>
    </PostsStateProvider>

);
});