Don't Forget Browser Button UX In Your Vue.js App
A Vue.js whiz explains how to create different kinds of browser navigation using this powerful JavaScript framework. Read on for more!
Join the DZone community and get the full member experience.
Join For Freewhen building single-page applications many vue developers forget about ux for browser button navigation. they mistakenly assume that this kind of navigation is the same as hyperlink navigation when in fact it can be quite different.
unlike hyperlink navigation, if a user goes forward and back between pages they expect the page to still look like it did when they return or they'll consider the ux "weird" or "annoying."
for example, if i were browsing a thread on hacker news and i scroll down to a comment and collapse it, then i clicked through to another page, then i clicked "back," i'd expect to still be scrolled down to the comment and for it to still be collapsed!
in a vue.js app, though, this is not the default behavior; scroll position and app data are not persisted by default. we need to consciously set up our app to ensure we have a smooth and predictable ux for the browser navigation buttons.
configuring vue router
vue router's role in the optimal back and forward ux is in controlling scroll behavior . a user's expectations with this would be:
- when moving back and forward, return to the previous scroll position.
- when navigating by links, scroll to the top.
we can achieve this by adding a
scrollbehavior
callback to our router configuration. note that
savedposition
is made available when using the browser back and forward buttons and not when using hyperlinks.
const scrollbehavior = (to, from, savedposition) => {
if (savedposition) {
return savedposition
} else {
position.x = 0
position.y = 0
}
return position
}
}
const router = new vuerouter({
mode: 'history',
scrollbehavior,
routes: []
})
more comprehensive scroll behavior settings can be found in this example .
state persistence
even more critical than scroll behavior is persisting the state of the app. for example, if a user makes a selection on page 1, then navigates to page 2, then back to page 1, they expect the selection to be persisted.
in the naive implementation below,
foo
's
checked
state will not persist between route transitions. when the route changes, vue destroys
foo
and replaces it with
home
, or vice versa. as we know with components, the state is created freshly on each mount.
const foo = vue.component('foo', {
template: '<div @click="checked = !checked">{{ message }}</div>',
data () {
return { checked: false };
}
computed: {
message() {
return this.checked ? 'checked' : 'not checked';
}
}
});
const router = new vuerouter({
mode: 'history',
scrollbehavior,
routes: [
{ path: '/', component: home },
{ path: '/bar', component: foo }
]
});
this would be equivalent to uncollapsing all the comments you collapsed in hacker news when you navigate back to an article's comments, i.e. very annoying!
keep-alive
the special
keep-alive
component can be used to alleviate this problem. it tells vue
not
to destroy any child components when they're no longer in the dom, but instead, keep them in memory. this is useful not just for a route transition, but even when
v-if
takes a component in and out of a page.
<div id="app">
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
the advantage of using
keep-alive
is that's it's very easy to setup; it can be simply wrapped around a component and it works as expected.
vuex
there's a scenario where
keep-alive
will not be sufficient: what if the user refreshes the page or clicks back and forward to another website? the data would be wiped and we're back to square one. a more robust solution than
keep-alive
is to use the browser's local storage to persist component state.
since html5, we've been able to use the browser to store a small amount of arbitrary data. the easiest way to do this is to first set up a vuex store. any data that needs to be cached between route transitions or site visits go in the store. later we will persist it to local storage.
let's now modify our example above to use vuex to store
foo
's
checked
state:
const store = new vuex.store({
state: {
checked: false
},
mutations: {
updatechecked(state, payload) {
state.checked = payload;
}
}
});
const foo = vue.component('foo', {
template: '<div @click="checked">{{ message }}</div>',
methods: {
checked() {
this.$store.commit('updatechecked', !this.$store.state.checked);
}
},
computed: {
message() {
return this.$store.state.checked ? 'checked' : 'not checked';
}
}
});
we can now get rid of the
keep-alive
as changing the page will no longer destroy the state information about our component as vuex persists across routes.
local storage
now, every time the vuex store is updated, we want to store a snapshot of it in local storage. then when the app is first loaded we can check if there's any local storage and use it to seed our vuex store. this means that even if we navigate to another url we can persist our state.
fortunately, there's a tool for this already: vuex-localstorage . it's really easy to setup and integrate into vuex. below is everything you need to get it to do what was just described:
import createpersist from 'vuex-localstorage';
const store = new vuex.store({
plugins: [ createpersist({
namespace: 'test-app',
initialstate: {},
expires: 7 * 24 * 60 * 60 * 1000
}) ],
state: {
checked: false
},
mutations: {
updatechecked(state, payload) {
state.checked = payload;
}
}
});
back and forward ux vs. hyperlink ux
you may want to differentiate behavior between the back and forward navigation and hyperlink navigation. we expect data in the back and forward navigation to persist, while in hyperlink navigation it should not.
for example, returning to hacker news, a user would expect comment collapse to be reset if you navigate with hyperlinks back to the front page and then back into a thread. try it for yourself and you'll notice this subtle difference in your expectation.
in a vue app, we can simply add a navigation guard to our home route where we can reset any state variables:
const router = new vuerouter({
mode: 'history',
scrollbehavior,
routes: [
{ path: '/', component: home, beforeenter(to, from, next) {
store.state.checked = false;
next();
} },
{ path: '/bar', component: foo }
]
});
Published at DZone with permission of Anthony Gore, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments