History & Navigation
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, and the Jakarta Servlet 6 server of the Bookshelf scenario, checked on 2026-10-09.
History is client code: History, Hyperlink and Window.Location are compiled to JavaScript and run in the browser.
Nothing in the server or in the build has to change for the default hash-based history, because the History module is part of com.google.gwt.user.User; the pushState variant in the last part of the page needs a server rule, which is explained there.
Every behaviour quoted here was measured: a small Bookshelf lab module was compiled with the 2.13.1 compiler (once with the defaults and once with -checkAssertions and history.noDoubleEncoding set to true) and driven in headless Google Chrome 155 through the DevTools protocol.
The pushState module was compiled and driven the same way, with a static server that answers every unknown path with the host page.
The outputs and messages are quoted as they were printed.
A single-page application has one real URL and many logical screens.
The browser’s address bar, bookmarks, the Back button and "open link in new tab" all work on URLs, so an application that never touches the URL breaks all of them: Back leaves the application, a bookmark always opens the start screen, and a link cannot be sent to a colleague.
GWT’s answer is a small static class, com.google.gwt.user.client.History, that keeps the application’s state in the part of the URL after the #.
This page shows how that works, how to design the string that goes there, what happens at start-up, on Back and Forward, and when a link is clicked, and where the alternatives (pushState and Places) fit.
How GWT history works
The URL fragment is the only part of a URL that a browser changes without loading a new page.
Setting location.hash adds an entry to the browser’s session history, updates the address bar, and fires a hashchange event on the window, and none of that contacts the server.
GWT’s History class is a thin layer over exactly that:
-
a token is the string after the
#; the whole state of the screen should be derivable from it -
History.newItem(token)stores the token and assignslocation.hash, which creates the history entry -
a
hashchangelistener on the window notices Back, Forward, a typed URL, a followed link or a script that setslocation.hash, and -
in every case the registered
ValueChangeHandler<String>objects receive aValueChangeEvent<String>that carries the new token
Two details of the implementation are worth knowing because they explain the behaviour measured later on this page.
History keeps the current token in a static field, so getToken() is answered from memory and never reads the URL.
And the hashchange listener compares the decoded hash with that field before it fires an event, so a change that History itself caused (through newItem or replaceItem) is announced exactly once.
The class needs no host-page markup in 2.13.1: there is no history frame, no history.html and no hidden iframe.
The compiled output of the lab contains no reference to __gwt_historyFrame, and the running page has no element with that id; the only iframe on the page is the bootstrap frame of the default linker, which is unrelated to history.
The Developer Guide page still tells you to embed such a frame, which is why the outdated-material section below spells this out.
The History API
History is a class of static methods in com.google.gwt.user.client.
There is nothing to instantiate and no handler to subclass: you register a ValueChangeHandler<String> and call static methods.
| Method | What it does | Events |
|---|---|---|
|
Returns the current token from the static field (the empty string when there is none). At start-up it is the decoded hash of the page URL. |
none |
|
Adds a history entry. |
one |
|
The same, but the event is issued only when |
one when |
|
Replaces the top entry instead of adding one (it uses |
one |
|
The same, with the event optional. |
one when |
|
Fires a |
one |
|
Registers a handler for every change the history reports. Returns the registration that removes it. |
n/a |
|
Programmatic equivalents of the browser’s Back and Forward buttons ( |
one per resulting token change |
|
Returns the form of the token that goes into the URL (see the encoding section). |
none |
|
Deprecated. The listener API predates |
n/a |
Source: GWT Javadoc — History
replaceItem carries a warning in its Javadoc: the URL is updated with window.location.replace, which has side effects with the deprecated iframe linker (std), so use it only with the cross-site iframe linker (xsiframe), which the Core module adds by default.
The Linkers & Bootstrap page explains the difference.
A first example: tabs that survive Back
The Developer Guide’s example selects a tab and pushes a token for it. The version below is the same idea written for the Bookshelf catalogue: every state of the screen is a token, a single handler turns a token into the screen, and user actions only ever push tokens.
public class HistoryLab implements EntryPoint, ValueChangeHandler<String> {
private final Label view = new Label();
@Override
public void onModuleLoad() {
RootPanel.get().add(view);
// user actions only push tokens
RootPanel.get().add(new Hyperlink("Catalogue", "catalog"));
RootPanel.get().add(new Hyperlink("Book 42", "book/42"));
// register first ...
History.addValueChangeHandler(this);
// ... then ask for the state the page was opened with
History.fireCurrentHistoryState();
}
@Override
public void onValueChange(ValueChangeEvent<String> event) {
Route route = Route.parse(event.getValue());
// setText, never setHTML: the token comes from the address bar
if (route.kind == Route.Kind.BOOK) {
view.setText("Book " + route.bookId);
} else {
view.setText("Catalogue page " + route.page + " query [" + route.query + "]");
}
}
}
Because every path to a screen goes through onValueChange, the screen is a pure function of the token.
That single rule is what makes Back, Forward, bookmarks and shared links work with no extra code.
Start-up: the initial token
A history handler is not called for the token the page was opened with.
getToken() already returns it (its Javadoc says so on purpose, to let you run different initialisation code with and without a token), but no event is fired until the history changes.
The two start-up rules are therefore:
-
register the handler first
-
then call
History.fireCurrentHistoryState()to run it once for the initial state
| Address opened | Token read before the handler was registered | Events after fireCurrentHistoryState() |
|---|---|---|
|
the empty string |
one, with the empty string |
|
|
one, with |
|
|
one, with |
Two start-up mistakes are common.
Calling fireCurrentHistoryState() before addValueChangeHandler runs the event with nobody listening, so the initial screen is never built.
And not calling it at all leaves the page empty until the user clicks something, because no event is fired for the first token.
The Javadoc example does one more thing worth copying: when the page is opened without a token it redirects to a default state with History.newItem("baz") before the handler exists, so that a defined state always exists.
For an application that uses Places, the PlaceHistoryHandler does this for you (see the last part of the page).
Tokens: designing the string
A token is any string. GWT does not parse it, and does not care about its shape, so the design is yours. The Developer Guide gives the rule of thumb: put enough information in the token to restore the state, and nothing that is not needed for it.
-
catalogis the first page of the catalogue -
catalog;page=2;q=javais page 2 of a search for "java" -
book/42is the detail of book 42
The three forms are the Bookshelf’s whole route table. They are parsed by one small class that is shared by the handler and by whatever builds tokens, so the grammar lives in one place.
final class Route {
enum Kind { CATALOG, BOOK }
final Kind kind;
final int page;
final String query;
final long bookId;
// constructors omitted
static Route parse(String token) {
if (token == null || token.isEmpty()) {
return catalog(0, "");
}
if (token.startsWith("book/")) {
try {
return book(Long.parseLong(token.substring("book/".length())));
} catch (NumberFormatException e) {
return catalog(0, ""); // robust: never throw on a bad token
}
}
int page = 0;
String query = "";
for (String part : token.split(";")) {
if (part.startsWith("page=")) {
try {
page = Math.max(0, Integer.parseInt(part.substring(5)));
} catch (NumberFormatException e) {
page = 0;
}
} else if (part.startsWith("q=")) {
query = part.substring(2);
}
}
return catalog(page, query);
}
String toToken() {
if (kind == Kind.BOOK) {
return "book/" + bookId;
}
StringBuilder sb = new StringBuilder("catalog");
if (page > 0) {
sb.append(";page=").append(page);
}
if (!query.isEmpty()) {
sb.append(";q=").append(query);
}
return sb.toString();
}
}
The parser never throws.
The Developer Guide is explicit about why: a user can type any URL by hand, and a bookmark can come from an old version of the application, so the handler has to accept every string.
In the lab, opening #book/abc shows the catalogue instead of failing.
The class has no GWT dependency, so it belongs in the shared module and can be unit-tested on the JVM.
String.split, Long.parseLong and StringBuilder are all part of the emulated JRE.
What belongs in a token
-
Navigation state: which screen, which record, which page, which filter. These are cheap to restore and safe to share.
-
Not secrets: a token is visible in the address bar, in bookmarks, in browser history and in anything the user pastes. Session identifiers, access tokens and personal data do not belong in it. The Developer Guide’s questionnaire example makes the same point for large state: put a key in the token (a session name) and keep the data elsewhere.
-
Not authorisation:
#adminopening the admin screen is navigation, not access control. The server must refuse the data when the user is not allowed to see it. -
No server round trip: the fragment is never sent to the server in an HTTP request (RFC 3986 defines the fragment as handled by the client), so the server cannot see the token and cannot render different HTML for different tokens.
Source: RFC 3986 — Fragment
Encoding: what ends up in the address bar
newItem does not put the token into the URL as it is.
It passes the token through History.encodeHistoryToken, which calls encodeURI and then additionally escapes #, so that any string survives the round trip; getToken() and the event return the original, decoded token.
The Javadoc lists the US-ASCII characters that are not encoded by any currently supported browser: a-z, A-Z, 0-9 and ;,/?:@&=+$-_.!~*().
Token passed to newItem |
location.hash afterwards |
Token in the event |
|---|---|---|
|
|
identical to the input |
|
|
identical to the input |
|
|
|
The last row is the reason for the encoding: a token that already contains a percent escape is escaped again on the way in and decoded exactly once on the way out, so the application sees what it stored.
The same decoding applies to a URL typed by hand: opening #catalog;q=a%20b delivers the token catalog;q=a b, and #catalog;q=a%2520b delivers catalog;q=a%20b.
The encoding is controlled by a module property, declared in History.gwt.xml:
<define-property name="history.noDoubleEncoding" values="true,false" />
<set-property name="history.noDoubleEncoding" value="false" />
Setting it to true replaces the encoder with a no-op one, so the token is passed to the browser unchanged.
The comment in the module says the double encoding exists to make history work across browsers, and that an application that turns it off is responsible for only using tokens that survive in every browser.
The lab, compiled with the property set to true, shows what that means in Chrome 155:
Token passed to newItem |
location.hash afterwards |
Events observed |
|---|---|---|
|
|
two: first with |
|
|
one |
With the no-op encoder the browser rewrites the space itself, the hashchange listener decodes the hash, finds it different from the stored token and fires a second event with the browser’s spelling.
Keep the default unless every token you use is already in a form that browsers leave alone.
Registering and removing handlers
addValueChangeHandler returns a com.google.gwt.event.shared.HandlerRegistration.
Handlers are called in registration order, and the registration removes the handler; the Events & Handlers page covers the dispatch rules and the UmbrellaException that collects failures.
private HandlerRegistration registration;
void start() {
registration = History.addValueChangeHandler(this);
History.fireCurrentHistoryState();
}
void stop() {
if (registration != null) {
registration.removeHandler();
registration = null;
}
}
History is static, so its handler list lives as long as the page.
A handler that is registered by a widget or a presenter and never removed keeps that object alive and keeps reacting to navigation after the object has been thrown away; remove it when the owner goes away.
In the lab, after the handler was removed, newItem("catalog;page=5") still changed the address bar and added a history entry, but the screen did not change and the event counter stayed where it was, and a later fireCurrentHistoryState() reached nobody.
See Events & Handlers for HandlerRegistration, handler order and bus-based alternatives.
Back, Forward and the stack
The browser owns the history stack.
newItem pushes an entry, replaceItem overwrites the top one, and Back and Forward move a pointer along the stack; the application learns about the move through a hashchange and a ValueChangeEvent.
This table was measured with the lab, starting from the empty token; "entries" is the change of history.length.
| Call or action | location.hash afterwards |
Entries added | Events | Remark |
|---|---|---|---|---|
|
|
1 |
1 |
the normal case |
|
unchanged |
0 |
0 |
an equal token is ignored |
|
|
1 |
0 |
the screen does not change; the entry exists |
Back |
|
0 |
1 |
the quiet entry is left behind |
Back again |
empty |
0 |
1 |
event with the empty token |
Forward, then Forward again |
|
0 |
1 each |
the quiet entry now fires when it is reached |
|
|
0 |
1 |
the stack does not grow |
|
|
0 |
0 |
|
|
unchanged |
0 |
1 |
reports |
|
empty (the URL ends with |
1 |
1 |
|
typed |
the new hash |
1 |
1 |
|
Three rules follow from the table.
-
issueEventis not "skip the screen".newItem(token, false)creates a real entry. When the user later reaches it with Back or Forward, the event is fired, so the screen must be built from the token anyway. Usefalsewhen the screen has already been updated by the code that callsnewItem, which is exactly whatPlaceHistoryHandlerdoes. -
Use
replaceItemfor redirects and normalisation. If a token is rewritten (an unknown screen turned into the default, a missing page number filled in), replacing the entry keeps the user from needing two clicks on Back. -
Do not trust that a change came from the application. Back, a typed URL, a followed link and a script that sets
location.hashall produce the same event.
History.back() and History.forward() are one-liners over window.history, and they are asynchronous in the same way as the buttons: the event arrives after the browser has moved, not during the call.
A unit test that calls back() and asserts immediately will fail.
Hyperlink, InlineHyperlink and Anchor
com.google.gwt.user.client.ui.Hyperlink is the widget that calls History.newItem for you.
It renders as a real a element whose href is "#" plus the encoded token, so the browser’s own link features (hover preview, right-click, open in new tab, bookmark) keep working.
FlowPanel links = new FlowPanel();
links.add(new Hyperlink("Catalogue", "catalog"));
links.add(new Hyperlink("Catalogue, page 2", "catalog;page=2"));
links.add(new Hyperlink("Book 42", "book/42"));
links.add(new Hyperlink("Search C# & Java 100%", "catalog;q=C# & Java 100%"));
links.add(new Anchor("Plain anchor", "#catalog;page=9"));
Source: GWT Javadoc — Hyperlink
The lab rendered them as:
<div class="gwt-Hyperlink" id="odd"><a href="#catalog;q=C%23%20&%20Java%20100%25">Search C# & Java 100%</a></div>
<a class="gwt-Anchor" href="#catalog;page=9" id="plain">Plain anchor</a>
A Hyperlink is a div wrapper (CSS class gwt-Hyperlink) around the a.
InlineHyperlink is the variant that renders as the a element alone, with no div wrapper (CSS class gwt-InlineHyperlink), so that it can sit inside running text.
The constructors are:
-
Hyperlink(String text, String targetHistoryToken) -
Hyperlink(SafeHtml html, String targetHistoryToken), plus variants with aDirectionor aDirectionEstimatorfor bidirectional text -
Hyperlink(String html, boolean asHTML, String targetHistoryToken), whereasHTMLmust only betruefor markup you built yourself
The target can be changed with setTargetHistoryToken(String) and read with getTargetHistoryToken().
No other stock widget carries a history token: besides Hyperlink and InlineHyperlink, history changes come from your own code calling History.newItem (or from a plain Anchor whose href starts with #).
What a click does
Clicking runs onBrowserEvent, which asks a browser-specific helper (HyperlinkImpl) whether the click is an ordinary left click.
If it is, the widget calls History.newItem(getTargetHistoryToken()) and prevents the default of the link, so the page does not navigate.
A right click, a middle click, and a click with a modifier key held are left to the browser, so "open in new tab" opens the application in a new tab at that token.
The default helper treats Alt, Ctrl, Meta and Shift as modifiers; the helper selected for the safari user agent (HyperlinkImplSafari, used by Safari and Chrome) ignores Shift except on Chrome, where it opens a new window.
Measured in the lab with synthetic click events on the "Book 42" link:
-
a plain click:
preventDefaultwas called (the dispatch returnedfalse), and the token was pushed byHyperlink -
a click with
ctrlKeyset:preventDefaultwas not called (the dispatch returnedtrue), andHyperlinkdid not callnewItem; the browser followed thehrefitself. In the headless run that changed the hash in the same tab and the history handler still received the event, because the handler listens to the hash and not to the widget; in a desktop browser a real Ctrl+click opens a new tab.
Hyperlink.addClickHandler and addClickListener are deprecated.
The replacement named in the Javadoc is a plain Anchor with a click handler that calls History.newItem itself, for the case where you need to run code before the token is set.
A Hyperlink in UiBinder
The token is a plain attribute in a template, so a navigation bar needs no Java at all:
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<g:FlowPanel>
<g:Hyperlink ui:field="catalog" text="Catalogue (UiBinder)" targetHistoryToken="catalog"/>
<g:Hyperlink ui:field="book" text="Book 42 (UiBinder)" targetHistoryToken="book/42"/>
</g:FlowPanel>
</ui:UiBinder>
Source: GWT Developer Guide — UiBinder
The lab produced <a href="#catalog"> and <a href="#book/42">, and a click on the second changed the hash to #book/42 and fired the event.
See UiBinder for ui:field and templates.
When to use an Anchor instead
An Anchor with an href that starts with # also changes the history, with no Hyperlink involved, because the browser changes the hash and History listens to the hash.
That is an advantage when the link is plain static markup, and a risk when the token needs encoding, which Hyperlink does for you.
The Javadoc is clear about the opposite case: if you want an a element that does not touch the history, use Anchor, not Hyperlink.
Reading the rest of the URL: Window.Location
The fragment is not the only part of the URL an application can read.
com.google.gwt.user.client.Window.Location offers getHash(), getPath(), getQueryString(), getParameter(String), getParameterMap(), getHref(), getHost(), getHostName(), getPort() and getProtocol(), plus assign, replace, reload and createUrlBuilder() to build a new URL.
The query string (?locale=es) and the fragment (#book/42) have different jobs:
-
the query string is sent to the server and changing it reloads the page; use it for things the server must know on the first request, such as the
localeparameter that GWT’s internationalisation reads -
the fragment is client-only and changes without a reload; use it for the state of the running application
Since 2.5.1, Window.Location returns the new parameter values after a call to window.history.pushState, which matters if you mix this API with the pushState modules below.
Source: GWT Javadoc — Window.Location
The Bookshelf worked example
The Bookshelf catalogue has three screens and a search box.
All of its navigation is expressed as tokens, and every screen is rebuilt by the single handler in the first example.
The lab for this page adds two controls so the behaviour can be inspected: a text box for a raw token and buttons for each History call.
// Programmatic navigation, as used by the lab buttons.
buttons.add(button("b-new", "newItem", () -> History.newItem(raw.getText())));
buttons.add(button("b-quiet", "newItem(false)", () -> History.newItem(raw.getText(), false)));
buttons.add(button("b-replace", "replaceItem", () -> History.replaceItem(raw.getText())));
buttons.add(button("b-back", "back", History::back));
buttons.add(button("b-forward", "forward", History::forward));
buttons.add(button("b-fire", "fireCurrentHistoryState", History::fireCurrentHistoryState));
Source: GWT Javadoc — History
Opening http://127.0.0.1:9921/#book/42 printed this log, with the handler registered after the "startup" line was written:
startup token=[book/42] events so far=0
event #1 token=[book/42] getToken=[book/42]
and clicking the "Catalogue, page 2" link and then the "Search C# & Java 100%" link added:
event #2 token=[catalog;page=2] getToken=[catalog;page=2]
event #3 token=[catalog;q=C# & Java 100%] getToken=[catalog;q=C# & Java 100%]
The search link is the interesting one.
The token contains spaces, an ampersand, a hash and a percent sign, and it reached the handler byte for byte, while the address bar showed #catalog;q=C%23%20&%20Java%20100%25.
The screen for a token is always built by onValueChange, never by the link, which is what lets the same code serve a click, a bookmark, a refresh and the Back button.
After a refresh on #catalog;page=2 the browser reloads the module, getToken() returns catalog;page=2, and fireCurrentHistoryState() builds page 2 again.
Bookmarks, refresh and deep links
Because the server never sees the fragment, a "deep link" is just the application’s own URL plus a token, and any static server that serves the host page serves it.
That is the strength of hash history: no server configuration, no rewrite rules, and it works from a file URL, a CDN or any servlet container.
The costs are visible in the URL (# in the middle of an address), and a crawler that does not run JavaScript sees only the host page, whatever the token.
On crawlers: Google announced in October 2015 that it deprecated its 2009 AJAX crawling scheme (the #! and _escaped_fragment_ convention), because its systems can render JavaScript pages, and recommends progressive enhancement, naming the History API pushState() as one of the techniques.
Hash-based GWT history needs no #! prefix for that; if search indexing of deep links matters, see the pushState section.
pushState: URLs without a hash
The History API (history.pushState, history.replaceState and the popstate event) lets a page change the path of its URL without a reload, which gives addresses such as /book/42 instead of /#book/42.
GWT’s own History class does not use it: it listens to hashchange and writes location.hash, as shown at the top of the page.
The 2.5.1 release notes mention pushState only to say that Window.Location reflects its changes.
Support comes from community libraries, which are not part of GWT and are not maintained by the GWT project. Two were verified on 2026-10-09:
| Library | Coordinates and state | Approach |
|---|---|---|
gwt-pushstate (maintained fork) |
|
Replaces the |
gwt-pushstate (original) |
|
The same idea, no longer updated. |
domino-history |
|
A wrapper over the browser history-state API ( |
The first library was compiled and run against GWT 2.13.1.
Its pom declares gwt-commons-lang3 and elemental2-dom with provided scope, so the application must add them itself:
<dependency>
<groupId>de.knightsoft-net</groupId>
<artifactId>gwt-pushstate</artifactId>
<version>2.0.0.27</version>
</dependency>
<dependency>
<groupId>de.knightsoft-net</groupId>
<artifactId>gwt-commons-lang3</artifactId>
<version>3.18.0-0</version>
</dependency>
<dependency>
<groupId>com.google.elemental2</groupId>
<artifactId>elemental2-dom</artifactId>
<version>1.3.2</version>
</dependency>
and its module is inherited next to Place:
<module rename-to="pushlab">
<inherits name="com.google.gwt.user.User" />
<inherits name="com.google.gwt.place.Place" />
<inherits name="com.wallissoftware.pushstate.PushState" />
<entry-point class="com.example.bookshelf.pushlab.PushLab" />
</module>
The module hooks in through deferred binding, which is the extension point PlaceHistoryHandler itself documents: the Historian is created with GWT.create, "so an alternative default implementation can be provided through replace-with rules".
The module’s PushState.gwt.xml replaces PlaceHistoryHandler.Historian with its PushStateHistorian when the property history.pushStateSupported is yes, which a property provider sets to yes when window.history.pushState is a function.
It also replaces Hyperlink and InlineHyperlink with HyperlinkPushState and InlineHyperlinkPushState.
What was measured
The lab was a Places application with a catalogue place and a book place (tokens catalog and book/N), served by a small static server that answers every path that is not a file with the host page.
-
a deep link to
http://127.0.0.1:9921/book/42opened the book 42 screen, andPlaceHistoryHandlerread the token from the path -
controller.goTo(new BookPlace(7))changed the address bar to/book/7with no hash and no reload -
Back and Forward moved between
/book/5,/book/7and/book/42and rebuilt the right screen each time (thepopstatelistener fires the place change) -
a path that maps to no place,
/nothing/here, ended on the default place and the address bar was rewritten to/
The Hyperlink catch
The Hyperlink replacement only applies to widgets created through GWT.create(Hyperlink.class); deferred binding does not see a plain new Hyperlink(...).
The lab compared both:
| Creation | Rendered href |
After a click |
|---|---|---|
|
|
the old hash behaviour: the URL became |
|
no |
the URL became |
So in a pushState application, create links through GWT.create(Hyperlink.class), or avoid Hyperlink and navigate with PlaceController.goTo from click handlers on an Anchor or a Button.
With UiBinder, which creates its widgets with new, a template Hyperlink keeps the hash behaviour, so the hash and pushState behaviours must not be mixed in one application.
Server rule and caveats
Path-based URLs are real URLs: a browser refresh on /book/42 asks the server for /book/42.
The server therefore has to answer every application path with the host page (a "history API fallback" or "SPA fallback" rule).
The lab used a short static server for this; in a servlet container the equivalent is a filter or a web.xml mapping that forwards unknown paths to the host page, written so it does not swallow the module’s own files (pushlab.nocache.js, *.cache.js) or the RPC and REST endpoints.
Other caveats observed or stated in the library’s own README:
-
use the library’s
HistorianandPushStateHistorian.replaceIteminstead ofHistory, and callPushStateHistorian.setRelativePathbefore the first history call when the application is served from a sub-path -
the lab’s host page loaded the module with an absolute script URL (
/pushlab/pushlab.nocache.js), because a relative one would resolve against whatever path the user opened -
the tokens become paths, so token design should become URL design: lower case, hyphens, no spaces
-
the latest release of the maintained fork is from July 2025 and is built against GWT 2.12.2; read its source before relying on it, and pin the version
Source: gwt-pushstate on GitLab
Source: MDN — History API
How Places build on History
The Activities and Places framework does not replace History; it sits on top of it.
The class that connects the two is PlaceHistoryHandler, and its source shows exactly how.
-
A place is a Java object for a screen. A
PlaceTokenizerturns a place into a token and back, and aPlaceHistoryMapperknows all the tokenizers. -
PlaceHistoryHandler.register(placeController, eventBus, defaultPlace)adds two listeners. One is a handler forPlaceChangeEventon the event bus; it callshistorian.newItem(token, false), without an event, because the place has already changed. The other is aValueChangeHandleron the historian; it maps the token back to a place and callsPlaceController.goTo. -
handleCurrentHistory()is thefireCurrentHistoryState()of Places: it handles the token the page was opened with, and it is called once at the end of start-up. -
An empty token means the default place. A token no tokenizer recognises is logged as a warning and also sends the user to the default place.
-
The historian is a small interface (
getToken,newItem(token, issueEvent)andaddValueChangeHandler) whose default implementation,DefaultHistorian, simply delegates to the staticHistory. It exists for unit tests, and it is the hook that the pushState modules replace.
Because PlaceController.goTo also asks the running activity whether it may stop (a PlaceChangeRequestEvent), and shows a confirm dialog if it answers with a warning, navigation through Places can be vetoed, which plain History.newItem cannot do.
The tokenizers, @Prefix, @WithTokenizers and the activity lifecycle belong to the MVP, Activities & Places page; they are not repeated here.
For an application with more than a handful of screens, use that page and register the Bookshelf places there; keep the plain History calls of this page for small applications, for tabs inside a single screen, and for state that is not a screen, such as the active filter of the catalogue.
Testing history code
Three layers can be tested:
-
The token grammar (
Routeabove) is plain Java in the shared module and runs in a normal JUnit test on the JVM. This is where most of the history logic should live. -
The handler can be tested without a browser when it depends on an interface rather than the static
History. TheHistorianinterface ofPlaceHistoryHandlerexists for this: its constructorPlaceHistoryHandler(PlaceHistoryMapper, Historian)accepts a fake that recordsnewItemcalls and lets the test fire value-change events. -
The real
Historyneeds a browser or HtmlUnit throughGWTTestCase. GWT’s own tests for it areHistoryTestandHyperlinkTestin thegwtproject/gwtrepository; they cover encoding,replaceItem,newItemwithout events and the empty token, and are a good source of ideas.
For browser-level checks, a headless Chrome run can read location.hash and history.length directly, which is how the tables on this page were produced.
Hash changes are asynchronous, so a test must wait for the event instead of asserting right after back() or setting location.hash.
Security notes
-
A token is untrusted input. Anyone can craft a link with any token and send it to a user. Render token-derived values with
setText, as the handler above does, and never build HTML from a token withsetHTMLorInlineHTML. When markup is unavoidable, build aSafeHtmlwithSafeHtmlBuilder, which escapes the pieces. -
Never navigate to a token. Passing a token to
Window.Location.assignorreplace, or using it as anhref, turns a link into an open redirect or ajavascript:URL. Treat a token as a key into a fixed set of screens and records. -
Validate what the token names.
book/42selects a record; the server must still check that the user may read book 42, and the client must handle a "not found" or "forbidden" answer. -
No secrets in tokens. Fragments appear in the address bar, bookmarks, history, screenshots and copy-and-paste. They are not sent to the server in the request, but every script on the page (analytics, extensions) can read them.
-
pushState paths are requests. With path-based URLs the path is visible to the server, to proxies and to access logs, so the same rules as for any URL apply.
Pitfalls
The messages and results below were printed or measured by the lab described at the top of the page (Chrome 155, GWT 2.13.1).
| Symptom (messages quoted as printed) | Cause | Fix |
|---|---|---|
The first screen is never built; the log shows |
No |
Register the handler, then call |
|
It ran before |
Register first, fire second. |
Clicking the same link twice does nothing the second time |
|
That is correct for navigation. To force a refresh of the same screen, call the refresh code directly or add a changing part to the token. |
After |
|
Use |
|
|
Pass a token (the empty string is allowed), or use |
|
The same call in a production build without assertions: the token is encoded to the text |
Run with assertions in test builds, and never pass an unchecked value as a token. |
With |
The browser rewrites the space in the hash; the |
Keep the default encoding, or use only tokens that browsers leave unchanged. |
Back works in some entries and does nothing in others in a pushState application |
Links created with |
Create links with |
A refresh on |
Path-based URLs reach the server, which has no such resource. |
Add the fallback rule that answers application paths with the host page. |
|
A |
Use an explicit default token (such as |
The Developer Guide asks for an |
That text is stale: 2.13.1 has no history frame and does not look for one. |
Do not add the frame. It is harmless if present but unused. |
Where older material is outdated
-
The history frame. The book’s host-page chapters tell you to include an
iframewith the id__gwt_historyFrameand ship ahistory.htmlfile, and its history chapter warns that history fails without them. GWT 2.13.1 has neither: the implementation listens tohashchangeand writeslocation.hash. The 2.13.1 Developer Guide page for History still includes the frame in its example, but nothing in the sources reads it. Remove the frame andhistory.htmlfrom old host pages. -
HistoryListener. The book registers aHistoryListenerwithHistory.addHistoryListenerand implementsonHistoryChanged. All three are deprecated; useValueChangeHandler<String>withaddValueChangeHandler.History.onHistoryChanged(String)is also deprecated and, as its Javadoc says, does not change the history state. -
Hosted mode errors. The book shows an error raised by the history subsystem in hosted mode when the frame is missing. Hosted mode is gone; Super Dev Mode runs the history code in a normal browser.
-
Hyperlink click handling.
addClickListenerandaddClickHandleronHyperlinkare deprecated; the Javadoc points to anAnchorwith a click handler that callsHistory.newItemitself.
What changed recently
-
1.5.2.
History.fireCurrentHistoryState()was added; application start-up should use it, andonHistoryChangedwas restored only as a deprecated method. -
2.5.1.
Window.Locationreturns the new parameter values after a call towindow.history.pushState. -
2.7.0.
History.replaceItem()was added. -
2.13.x. No change to the history API in the release notes; the lab in this page ran the unchanged
hashchangeimplementation. -
Ecosystem. GWT still has no built-in pushState history; the options are the community modules above, or a routing library from Ecosystem & Libraries.
Source: GWT — Release Notes