PWA + Stack Navigation == Native

I’ve never seen a Progressive Web App (PWA) that actually feels App-like. Native Apps provide an immersive user experience because they give you the feeling that you’re navigating through a stack of pages or scenes. Scenes animate as they arrive and leave and, on leaving, the previous scene is displayed exactly as you left it. PWA’s don’t feel App-like because the Web doesn’t have Stack Navigation. Until now, that is.

I’ve seen examples of Web page transitions that try to imitate Stack Navigation. But they tend not to work because the browser can’t restore your scroll position when you press back. The browser gets confused about the height of the Web page because both scenes are visible during the transition. Even if you find a workaround, there’s more to Stack Navigation than remembering the scroll position. You have to restore the previous scene exactly as it was. If a form field loses its value or a slider forgets where it was then the stack illusion is shattered.

I’m introducing Stack Navigation for the Web. The following CodePen shows what it can do. You can see how it feels more like Native Twitter than Twitter Lite. Finally, we can build PWA’s that really feel App-like.

Advertisements

How they should rebuild Twitter Lite

‘How we built Twitter Lite’ is an interesting article written by Nicolas Gallagher. It covers the design decisions behind the poster child of the progressive web app. But the key topic it doesn’t discuss is navigation. Navigation on Twitter Lite isn’t as smooth as it is on native because the scenes don’t animate as they come and go. Twitter should rebuild Twitter Lite using the new Navigation router for React. The Navigation router brings native-like navigation to the web. Run the CodePen below to see it in action.

Stack navigation

The Navigation router achieves this native-like animation by managing a stack of scenes. If you click two successive tweets in the CodePen example, then the Navigation router builds a stack containing three scenes. The home scene sits at the bottom of the stack. Next comes the first tweet you clicked and, at the top of the stack, is the second tweet.

The router provides a NavigationMotion component that lets you animate the scenes as they move through the stack. The NavigationMotion component accepts three style props. One style prop for each position a scene can be in. A scene that’s not on the stack has the unmountedStyle, the current scene has the mountedStyle and each visited scene gets the crumbStyle. As a scene changes its position, the NavigationMotion component interpolates new style values. You provide a child callback that assigns the style to the scene so that it animates into its new position.

<NavigationMotion
  unmountedStyle={{translate: 100, opacity: 1}}
  mountedStyle={{translate: 0, opacity: 1}}
  crumbStyle={{translate: 5, opacity: 0}}>
  {({translate, opacity}, scene, key) => (
    <div
      key={key}
      style={{
        transform: `translate(${translate}%, 0)`,
        opacity
      }}>
      {scene}
    </div>
  )}
</NavigationMotion>

Stack navigation has advantages besides native-like animation. You can navigate back through the stack of visited scenes. Because the scenes come from the stack, they appear exactly as you left them. In the CodePen example, navigating back through the stack preserves your scroll position. Twitter wrote a dedicated list component to preserve scroll position across scenes. If they rebuild Twitter Lite with the Navigation router they can delete all that code.

The React Fact that will Change your Mindset

THE FACT: ReactDOM.render is not destructive

If you repeatedly call ReactDOM.render on a component, passing in different props each time, then React doesn’t tear down that component and start from scratch. React updates the component’s props while maintaining its internal state. Before showing how this fact will change your mindset, let’s demonstrate its veracity with the aid of an example.

The example shows a Timer component that increments every second. The colour of the number displayed alternates between red and blue every other second. The Timer stores the seconds count in state and the display colour in props. To toggle the colour, we run a setTimeout every two seconds that calls ReactDOM.render passing the new colour prop to the Timer. You can see that ReactDOM.render is not destructive because the count, held in state, doesn’t reset when we change the colour:

You Don’t Need the React Router

Let’s modify the Timer example so that, instead of changing the display colour inside of a setTimeout, we pass the colour in the URL. We’ll add ‘red’ and ‘blue’ Hyperlinks that set the respective colour in the URL. Whenever the hash changes, we’ll call ReactDOM.render on the Timer component passing in the colour prop:

window.addEventListener('hashchange', function(){
  ReactDOM.render(
    <Timer colour={location.hash} />,
    document.getElementById('container')
  );
};

Without even trying to, we’ve created a basic router. We’ve been led to believe that updating components when the URL changes is so difficult that only the React Router can do it. Think of the complexity the React Router would introduce to the Timer example. By owning the Timer component, the React Router would prevent us from passing in the colour prop. Realising that ReactDOM.render isn’t destructive is like casting a magic spell that makes the React Router disappear. Taking back ownership of your components will change your mindset.

Giving up the React Router doesn’t mean giving up the ability to share UI across pages because React supports templates. A React template is any component that hosts a placeholder DOM element. We render this template when our app loads and target all subsequent ReactDOM.render calls at the template’s placeholder. Below is an example of an App template with a ‘content’ placeholder:

var App = () => (
  <div>
    <h1>Example<h1>
    <div id="content"></div>
  <div>
);

ReactDOM.render(
  <App />,
  document.getElementById('container')
);

If we add this App template to the Timer example then, whenever the URL changes, we ReactDOM.render the Timer component into the ‘content’ placeholder of the template instead of the ‘container’ element in the HTML:

ReactDOM.render(
  <Timer colour={location.hash} />,
  document.getElementById('content')
);

You’ve nothing to fear on the performance front because rendering your own templates and components is at least as fast the React Router doing it for us:

Try the Navigation Router

I’m not suggesting you don’t need a router. Just that you use one that embraces, rather than competes with, React. One that doesn’t render your components and that focuses instead on what React doesn’t do. I recommend my Navigation router. Its data-first approach to routing complements React’s top-down render as you can see in the Timer example below:

Streamlined React Components

A React component’s render should be a pure function of its props. Pass in the same props and you should always get back the same results. You can then streamline the component with a shouldComponentUpdate function that bypasses rendering when the props haven’t changed. This optimisation technique works smoothly, except when it comes to Hyperlinks. Hyperlinks have a nasty habit of forcing props into components, making them update when they shouldn’t have to. In this post, I’ll show how to tame your Hyperlinks using a new declarative approach to routing.

Let’s start with an example of a list of items that can be sorted and filtered. It’s made up of three components: Sort, Filter and List. If we use Buttons, rather than Hyperlinks, to implement the sorting and filtering functionality, then here’s the props each component needs:

  • Sort component needs the sort prop to determine the new sort order.
  • Filter component needs the filter prop to style the currently active filter.
  • List component needs the sort, filter and items props to display the list

Components coloured by props when Buttons used

If we imagine that the filter prop is blue and the sort prop yellow, we can draw a picture of the example and colour in the components to match their props. The Sort component is yellow because it needs the sort prop, the Filter’s blue to match the filter prop and the List is green because it expects both yellow and blue props.

Any component that isn’t green can be streamlined with a shouldComponentUpdate function. The green List component can’t be streamlined because it has to update when either the filter or the sort changes. The yellow Sort component, on the other hand, only has to update when the sort changes:

shouldComponentUpdate(oldProps, props) {
  return oldProps.sort !== props.sort;
}

However, all the components turn green if we implement the sorting and filtering using Hyperlinks instead of Buttons. The key difference between a Hyperlink and a Button is that a Hyperlink has an href attribute. To populate an href we need both the filter and the sort. For example, the Sort component needs the filter prop as well as the sort prop so it can build a URL that holds the current filter along with the new sort order.
Components coloured by props when Hyperlinks used
You can see how Hyperlinks force props into your components. They force the filter prop into the Sort component and the sort prop into the Filter component. These extra props turn the Sort and Filter components green. Green components can’t be streamlined with shouldComponentUpdate functions because they always have to update. But, we can turn the Sort back to yellow and the Filter back to blue by using a declarative approach to routing.

Declarative Routing

The Navigation router introduces a declarative way to build Hyperlinks. We’ll use it to remove the filter prop from the Sort component and the sort prop from the Filter component, turning them back to yellow and blue respectively. First, let’s see what the sorting Hyperlink looks like built using a traditional imperative router like the React Router:

var data = {
  sort: this.props.sort === 'up' ? 'down' : 'up',
  filter: this.props.filter
};
<Link to={`${data.filter}/${data.sort}`} />

You can see that the React Router expects us to pass the URL to the Link component. With this imperative approach, there’s no way to turn the Sort component yellow because we can’t build the URL without the filter prop. Compare this to the equivalent sorting Hyperlink built using the RefreshLink component from the Navigation router:

<RefreshLink
  navigationData={{
    sort: this.props.sort === 'up' ? 'down' : 'up',
    filter: this.props.filter
  }} />

This code is much more declarative. Instead of building the URL ourselves, we’re telling the RefreshLink what data we want in the href and letting it build the URL for us. But, the Sort component is still green. To remove the filter prop and turn the Sort component yellow, we can get the RefreshLink component to keep track of the current data rather than pass the filter in:

<RefreshLink
  navigationData={{sort: this.props.sort === 'up' ? 'down' : 'up'}}
  includeCurrentData={true} />

This code is even more declarative. We specify that the href should include the current filter value without deciding how and when to get it. Without the filter prop, the yellow Sort component can be streamlined with a shouldComponentUpdate function that prevents it from updating unless the sort prop changes. This doesn’t stop the latest filter making it into the href because the RefreshLink auto updates whenever the current data changes.

We can apply these declarative techniques to the filter Hyperlinks to turn the Filter component back to blue. If we also declaratively style the active filter Hyperlink, then we can turn the Filter component white so that it never has to update. You can see the yellow Sort and white Filter components in action in this JsFiddle of the complete example.

A Route by any Other Name

If your router doesn’t support named routes then you’re missing out. Without named routes, changes to URLs ripple throughout your code base. Named routes insulate your code from URL changes, allowing you to safely delay decisions about how your URLs should look.

When the React Router brought in async configuration it dropped support for named routes. That so few people complained about their removal I attribute to that router’s flawed implementation of them. To see how good named routes can be I’ll show you how they work in my Navigation router.

Imagine we’re tasked with displaying a paged list of items. Our Navigation router configuration will start out looking something like:

var stateNavigator = new StateNavigator([
  {key: 'list', route: 'list'}
]);

We can then use the NavigationLink component to build a Hyperlink that navigates to the second page of items as shown below. You can see that, because we’re using named routes, we don’t hard-code the URL. Instead we pass the key from the configuration together with the page number and leave it up to the router to generate the URL for us:

<NavigationLink stateKey="list" navigationData={{page: 2}} />

This generates a URL of ‘/list?page=2’. We’re happy to live with this scruffy URL throughout development but it needs smartening up before we go live.

We’ll make the URL read ‘/items/2’ instead of ‘/list?page=2’. With the React Router’s (now defunct) named routes support, we couldn’t change page number from a query string to a route parameter without corresponding code changes. But with the Navigation router we can, because our application code isn’t aware of where in the URL the page number lives:

var stateNavigator = new StateNavigator([
  {key: 'list', route: 'items/{page}'}
]);

The icing on the cake is for the URL back to the first page to be ‘/items’ instead of ‘/items/1’. With any other router this would require a code change. With the Navigation router this is only a configuration change, because we can specify a default value for the page number:

var stateNavigator = new StateNavigator([
  {key: 'list', route: 'items/{page?}', defaults: {page: 1}}
]);

The Real React Router Use Cases?

I’m a big fan of Dan Abramov so I tweeted him my blog about how you’re better off using my Navigation router than the React Router. He wasn’t convinced and tweeted back that I was ignoring the real React Router use cases like server rendering and code splitting. This surprised me because I think of server rendering as a React use case and code splitting as a use case for bundlers like webpack. In fact, the Navigation router doesn’t have to do anything special to get them to work, as you can see from these server rendering and code splitting examples. I’ll explain how the React Router, on the other hand, actually breaks server rendering and code splitting.

The Real React Router Use Cases

Server Rendering

With server rendering, users no longer have to wait until the JavaScript downloads and executes before they see any content. React lets you render your app on the server so you can return meaningful HTML in the first response. Along with the HTML, you also send back the JSON props you used to render the component. Once the JavaScript loads, you use these props to render that same component to the DOM, allowing the client to catch up with the server rendered content.

You can see from my server rendering example that this approach plays nicely with the Navigation router. However, the same can’t be said for the React Router. That’s because the React Router is responsible for rendering your components, preventing you from passing props into them. If you can’t pass props into your components then server rendering breaks. Although you can get it working again by bringing in Redux, you shouldn’t let your router dictate your architecture.

Code Splitting

Code splitting allows you to divide up your JavaScript into separate bundles and load them on demand. This keeps your application feeling snappy, avoiding slow page load times when users first visit your site. You can individually bundle up your components and load each one just before the corresponding route becomes active.

You can see this approach in action in my code splitting example, where I put the bundle loading logic inside the Navigation router’s transition hooks. However, because the React Router owns your components, you don’t have this luxury. You have to place the code splitting logic inside the configuration.

<Route path="courses/:courseId" getComponent={(location, cb) => {
  // Load the component bundle
  cb(null, Course)
}} />

To see why this difference is important let’s consider a typical example where you must fetch some data before rendering the new component. There are now two asynchronous calls: the AJAX data request and the component bundle. With the Navigation router, you can execute these requests in parallel from inside the transition hook. But with the React Router, you have to suffer the performance cost of running them in serial because you can’t put the AJAX request inside the getComponent function.

TL;DR

I’m surprised that Dan Abramov wasn’t convinced by my previous blog that shows you’re better off using my Navigation router than the React Router. I’m sure my server rendering and code splitting examples will persuade him this time.

React Routing Debunked

THE MYTH: You can’t build a React web app without the React Router

The Reality

The React Router maintainers argue that your code will be a mess of branching logic (‘if’ conditions and ‘switch’ statements) if you don’t use their router. They construct an email application example with nested views to support their claim. But, it isn’t true. You can use any router with React without introducing branching logic. To show this, I’ve built the email example using my Navigation router:

The React Router is a templating library that associates components with routes. The top level route holds the template component which hosts the child route components. But the React Router is trying to solve a problem that doesn’t exist, because React supports templates natively. Here’s an example of a React template, written as a functional stateless component:

var App = () => <div id="content"></div>;

You don’t need the React Router to ensure the correct component is loaded when a route is hit. All routers notify you when the route changes. To load the right component without introducing branching logic, you add separate listeners to the individual route change events. In the email example, when the ‘inbox’ route is hit, you load the Inbox component into the ‘content’ placeholder in the App template:

ReactDOM.render(<App/>, document.body);
ReactDOM.render(<Inbox/>, document.getElementById('content'));

If you use the React Router, then you can’t pass props into your components. With any other router you can, because it’s up to you, and not the router, to create your components. The example’s Inbox component needs to display a list of messages. The React Router has no choice but to hold the list in the component’s internal state. But, by using another router, you can follow the React way and pass it in as props instead:

<Inbox messages={messages} />

I’ve debunked the myth that you can’t build a React app without the React Router. You don’t have to worry that your code will turn into a branching mess without it. You’ve seen that any other router gives you back control over your components. So, you can ditch state in favour of props. The components in my email application example are all functional and stateless. The ones from the React Router documentation aren’t.