Cached Vaadin Routes (UI-scoped Routes)

If you need to preserve the state of certain route during the duration of user’s session (e.g. to preserve a half-filled form so that the user can return to the form and fill it afterwards, or to create a multi-route wizard with a back+forward navigation), you may use the following code to cache the route components in your session.

WARNING: session-scoping a route is very dangerous since if the user opens two tabs, the route instance will be shared between the two tabs, leading to unpredictable errors. See Session-Scoped Routes for more details. Scope to UI instead. Note that this can not be done properly, see Vaadin UI/Tab Scoping.

The trick here is to create a customized Instantiator which, instead of creating new instances of routes, would store the instances into the session and reuse them afterwards.

For Vaadin 14.5.x and lower, customizing Instantiator is a bit tricky, see+vote Vaadin Flow #8427 for more details. The code looks like this:

@WebServlet(urlPatterns = "/*", asyncSupported = true)
public class MyServlet extends VaadinServlet {

    @Override
    protected VaadinServletService createServletService(DeploymentConfiguration deploymentConfiguration) throws ServiceException {
        VaadinServletService service = new VaadinServletService(this,
                deploymentConfiguration) {
            @Override
            protected Instantiator createInstantiator() throws ServiceException {
                return new MyInstantiator(this);
            }
        };
        service.init();
        return service;
    }

    public static class MyInstantiator extends DefaultInstantiator {
        public MyInstantiator(VaadinService service) {
            super(service);
        }

        @Override
        public <T extends HasElement> T createRouteTarget(Class<T> routeTargetType, NavigationEvent event) {
            if (routeTargetType == MainView.class) {
                final MainView view = RouteUICache.getOrCreate(MainView.class);
                return ((T) view);
            }
            return super.createRouteTarget(routeTargetType, event);
        }
    }

    /**
     * @deprecated don't use - two browser tabs will share the same route component instance, leading to unpredictable errors.
     */
    @Deprecated
    public static class RouteSessionCache implements Serializable {
        private final Map<Class<?>, Serializable> routeCache = new HashMap<>();

        public static <T extends HasElement> T getOrCreate(Class<T> clazz) {
            RouteSessionCache cache = VaadinSession.getCurrent().getAttribute(RouteSessionCache.class);
            if (cache == null) {
                cache = new RouteSessionCache();
                VaadinSession.getCurrent().setAttribute(RouteSessionCache.class, cache);
            }
            final Serializable instance = cache.routeCache.computeIfAbsent(clazz,
                    (c) -> ((Serializable) ReflectTools.createInstance(clazz)));
            final T route = clazz.cast(instance);
            route.getElement().removeFromTree();
            return route;
        }
    }

    /**
     * Note that this can not be done properly, see [Vaadin UI/Tab Scoping](../vaadin-ui-scope/).
     */
    public static class RouteUICache implements Serializable {
        private final Map<Class<?>, Serializable> routeCache = new HashMap<>();

        public static <T extends HasElement> T getOrCreate(Class<T> clazz) {
            RouteUICache cache = ComponentUtil.getData(UI.getCurrent(), RouteUICache.class);
            if (cache == null) {
                cache = new RouteUICache();
                ComponentUtil.setData(UI.getCurrent(), RouteUICache.class, cache);
            }
            final Serializable instance = cache.routeCache.computeIfAbsent(clazz,
                    (c) -> ReflectTools.createInstance(clazz));
            final T route = clazz.cast(instance);
            route.getElement().removeFromTree();
            return route;
        }
    }
}

For Vaadin 14.6+ you can simply provide your own implementation of InstantiatorFactory in order to produce MyInstantiator; you will no longer need to create a custom MyServlet. See Custom Instantiator on how that’s done.

FAQ

Q: I’m getting

ERROR com.vaadin.flow.router.InternalServerError: There was an exception while trying to navigate to ''
java.lang.IllegalStateException: Can't move a node from one state tree to another. If this is intentional, first remove the node from its current state tree by calling removeFromTree

A: Yes, if using RouteSessionCache (WHICH YOU SHOULDN’T), you must call route.getElement().removeFromTree(); to cleanly detach the component from the previous UI. Note that this is a very bad idea (see the top of this article); also see Issue #9376 and Can’t Move Node for more details.

Written on April 8, 2021