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:
I used the word "currently" because my thinking on this topic is still evolving. Below, I will elaborate on these rules.
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:
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.
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.
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.
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.
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
.
export const UrlRouter = observer((props: PropsT) => {
return (
<PostsStateProvider>
<Route path="/posts">
<PostsView />
<Route path="/posts/:postSlug">
<HighlightPostEffect />
<PostsView />
</Route>
</Route>
</PostsStateProvider>
);
});