State management

Overview

Client-side state management within the Tui framework refers to data that should persist, be updated or otherwise managed within a page that has completed its initial load. Many of Totara's new pages built on the Tui framework manage their state within a component tree, saving their data back to the server or refetching data as required. Major core Tui components that manage their own state (currently) include Uniform for aggregating Form components and Weka Editor.

Beyond component-level management of internal state, including the provision of props, to date the Tui framework has limited client-side state management in effect.

Implementation

Component-level management of internal state

The flow of data into and component, and the difference between that provided data and the component's own internal data is illustrated by the following diagram:

There are exceptions to this, but this is almost always the preferred approach.

Vue components operate with a lifecycle, and it is useful to keep this in mind when thinking about how data enters a components. See the official Vue component lifecycle diagram for more information.

Persistent state

The Tui framework supports a basic mechanism for storing and retrieving user-interaction decisions, currently implemented with LocalStorage. Currently there are not many examples requiring detailed state persistence, and the Totara Development and Design teams have prioritised a robust solution to this post Totara 13's release to accommodate more complex UIs.

Retrieving state data

Typically, state will be requested from the server for synchronously rendered first render components, therefore when those components become visible to end users, they will already have the data they need to display appropriately. This data is requested using GraphQL and the client-side library we use is Apollo Client, with the Vue Apollo integration.

Apollo provides a number of features to simplify communication of state and changes between the server and client.

One key feature is that Apollo has a non-persistent cache for query results. This means that, generally speaking, if a component is unmounted and remounted (e.g. when switching tabs) it does not need to fetch data again from the server, unless you have configured it to behave in that way.

The Apollo cache also acts as a source-of-truth of sorts - if multiple components use the same query, they will share an entry in the Apollo cache. If that entry is updated (e.g. after a mutation), either by refetching the query or using readQuery/writeQuery to update the query result, all components using that query will also update.

Totara's back-end architecture implements the Context System, which presents a particular challenge with retrieving data in a predictable way on the client and holding a single source of truth so that all data on the page is consistent. In essence, a given data query may retrieve information whose result has made its way through a variety of capability checks that may differ each time. Ideally we would prefer to cache data and assume it is accurate the next time a query-dependent component mounts - without the need to refetch the query after a potential context change, and in multiple places on a given page. To facilitate predictable and user-friendly state persistence and web page performance, this challenge will be solved as a priority post Totara 13's release. For an overview of the Context System see Contexts and the in product conceptual architecture.  

Presently, we have a partial solution to this that is available on an opt-in basis per query, which is used in some places in Engage and Perform such as workspaces. If we are certain that a given type does not vary in the values of its fields based on how it is queried, we can add the "__typename" field to that type's fields in the query. Apollo will then normalise all entities that have the same typename and ID to a single entry in the cache - meaning if any query happens to fetch that entity, even if it is not the same query, it will update that entity in all queries in the cache. It also means that if you return an entity's updated values from a mutation and it will automatically update the values in queries that have fetched that entity. If the entity does not include a "__typename", Apollo will not cache the queried data as independent of the query - it will not propagate to components that have a potentially different context

Tips and known limitations

  • After triggering a mutation, you may be faced with the issue of the data in the UI not reflecting the data you have just submitted. At the moment, the recommended way of solving this is to use Apollo's refetchQueries option:
    refetchQueries: [{ query: getWorkspace, variables: { id: this.workspaceId } }]
    You can also use readQuery/writeQuery to update the value of the query directly in the cache.
  • If you use v-if="$apollo.loading" to wait until a query is loaded to show your component, the content of the v-if will be briefly unmounted while the query is refetching. In order to avoid this, we recommend doing a v-if on the query result instead, as that will stay as it is until the new value is available. See the JavaScript page for more info.