Navigating & Observing

Navigation and observation are closely linked. Navigation is used to change locations, while observation is used to detect navigation changes and react (e.g. re-render the application).

Detecting Navigation#

The Curi router uses an observer pattern to call registered functions (called response handlers) when there is a new response. The main function for response handlers is to use the new response to render the application, but any other functionality (like logging) can also be performed.

Response Handlers#

When response handlers are called, they are passed an object with three properties: router, response, and navigation. Which objects/properties you use depends on what the response handler is doing.

function responseHandler({
  router,
  response,
  navigation
}) {
  // ...
}

Registering Response Handlers#

There are two ways to attach response handlers to the router: router.once() and router.observe(). Response handlers registered with router.once() will only be called one time, while those registered with router.observe() will be called for every new response.

When you register a response handler using router.observer(), it will return a function that you can use to stop calling the response handler for new responses. You should rarely need to do this, but it can be useful for memory management if you are adding and removing lots of observers.

// fn will only be called one time
router.once(fn);

// obs will be called for every new response
const stop = router.observer(fn);

Use Cases#

What should you use response handlers for?

Setup#

If any of the routes in an application have resolve functions, when they match their responses are created asynchronously. When the application first renders, if the router matches an async route, the response isn't immediately ready to use. To deal with this, you can use an observer to render once the initial response is ready.

A setup function only needs to be called one time, so you can register it with router.once().

Note: In most applications, waiting for the initial response is the only time you may need to write response handlers yourself.
const Router = curiProvider(router);
              
function setup() {
  ReactDOM.render((
    <Router>
      {({ response }) => <response.body />}
    </Router>
  ), document.getElementById('root'));
}

router.once(setup);

Rendering#

Rendering libraries need to know when there is a new response so that they can re-render the application.

The Curi rendering packages (@curi/react-dom, @curi/react-native, @curi/vue, and @curi/svelte) setup an observer internally so that they can automatically re-render.

If you are using vanilla JavaScript to render your application or you are writing your own framework implementation, you would use router.observer() to re-render new responses.

function observer({ response }) {
  // let the app know there is a new response
}

router.observer(observer);

Side Effects#

Side effects are observers that are provided to the router at creation instead of by calling router.observe(). These can be useful for tasks that are not rendering related as well as for tasks that need to be performed after a render has completed.

The @curi/side-effect-title package provides a side effect that will use response.title to set the page's document.title.

With single-page applications, clicking on links wish hashes won't always scroll to the matching element in the page. The @curi/side-effect-scroll package adds this behavior by scrolling the page to the element that matches the new response's hash (response.location.hash) after the new response has rendered.

If you need to add logging to your application, you could write your own observer to do this. Your observer can either be added as a side effect when the router is constructed or later using router.observe().

function logger({ response }) {
  loggingAPI.add(response.location);
}

// as a side-effect
const router = curi(history, routes, {
  sideEffects: [{ fn: logger }]
});

// as an observer
router.observe(logger);