Managing state
For simple applications where data is not required to flow between many components, state management can be very straightforward. Data can be encapsulated within individual widgets that need it as the most basic form of state management within a Dojo application.
As applications grow in complexity and start requiring data to be shared and transferred between multiple widgets, a more robust form of state management is required. Here, Dojo begins to prove its value as a reactive framework, allowing applications to define how data should flow between components, then letting the framework manage change detection and re-rendering. This is done by wiring widgets and properties together when declaring VDOM output in a widget's render function.
For large applications, state management can be one of the most challenging aspects to deal with, requiring developers to balance between data consistency, availability and fault tolerance. While a lot of this complexity remains outside the scope of the web application layer, Dojo provides further solutions that help ensure data consistency. The Dojo Stores component provides a centralized state store with a consistent API for accessing and managing data from multiple locations within the application.
Basic: self-encapsulated widget state
Widgets can maintain their own internal state in a variety of ways. Function-based widgets can use the cache
or icache
middleware to store widget-local state, and class-based widgets can use internal class fields.
Internal state data may directly affect the widget's render output, or may be passed as properties to any child widgets where they in turn directly affect the children's render output. Widgets may also allow their internal state to be changed, for example in response to a user interaction event.
The following example illustrates these patterns:
src/widgets/MyEncapsulatedStateWidget.tsx
Function-based variant:
import { create, tsx } from '@dojo/framework/core/vdom';
import cache from '@dojo/framework/core/middleware/cache';
const factory = create({ cache });
export default factory(function MyEncapsulatedStateWidget({ middleware: { cache } }) {
return (
<div>
Current widget state: {cache.get<string>('myState') || 'Hello from a stateful widget!'}
<br />
<button
onclick={() => {
let counter = cache.get<number>('counter') || 0;
let myState = 'State change iteration #' + ++counter;
cache.set('myState', myState);
cache.set('counter', counter);
}}
>
Change State
</button>
</div>
);
});
Class-based variant:
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';
export default class MyEncapsulatedStateWidget extends WidgetBase {
private myState = 'Hello from a stateful widget!';
private counter = 0;
protected render() {
return (
<div>
Current widget state: {this.myState}
<br />
<button
onclick={() => {
this.myState = 'State change iteration #' + ++this.counter;
}}
>
Change State
</button>
</div>
);
}
}
Note that this example is not complete - clicking on the 'Change State' button in the running application will not have any effect on the widget's render output. This is because the state is fully encapsulated within MyEncapsulatedStateWidget
, and Dojo is not aware of any changes made to it. Only the widget's initial render will be processed by the framework.
In order to notify Dojo that a re-render is needed, widgets that encapsulate render state need to invalidate themselves.
Invalidating a widget
Function-based widgets can use the icache
middleware to deal with local state management that automatically invalidates the widget when state is updated. icache
composes cache
and invalidator
middleware, with cache
handling widget state management and invalidator
handling widget invalidation on state change. Function-based widgets can also use invalidator
directly, if desired.
For class-based widgets, there are two ways to invalidate:
- Explicitly calling
this.invalidate()
in an appropriate location where state is being changed.- In the
MyEncapsulatedStateWidget
example, this could be done in the 'Change State' button'sonclick
handler.
- In the
- Annotating any relevant fields with the
@watch()
decorator (from the@dojo/framework/core/vdomecorators/watch
module). When@watch
ed fields are modified,this.invalidate()
will implicitly be called - this can be useful for state fields that always need to trigger a re-render when updated.
Note: marking a widget as invalid won't immediately re-render the widget - instead it acts as a notification to Dojo that the widget is in a dirty state and should be updated and re-rendered in the next render cycle. This means invalidating a widget multiple times within the same render frame won't have a negative impact on application performance, although excessive invalidation should be avoided to ensure optimal performance.
The following is an updated MyEncapsulatedStateWidget
example that will correctly update its output when its state is changed.
Function-based variant:
import { create, tsx } from '@dojo/framework/core/vdom';
import icache from '@dojo/framework/core/middleware/icache';
const factory = create({ icache });
export default factory(function MyEncapsulatedStateWidget({ middleware: { icache } }) {
return (
<div>
Current widget state: {icache.getOrSet<string>('myState', 'Hello from a stateful widget!')}
<br />
<button
onclick={() => {
let counter = icache.get<number>('counter') || 0;
let myState = 'State change iteration #' + ++counter;
icache.set('myState', myState);
icache.set('counter', counter);
}}
>
Change State
</button>
</div>
);
});
Class-based variant:
Here, both myState
and counter
are updated as part of the same application logic operation, so @watch()
could be added to either or both of the fields, with the same net effect and performance profile in all cases:
src/widgets/MyEncapsulatedStateWidget.tsx
import WidgetBase from '@dojo/framework/core/WidgetBase';
import watch from '@dojo/framework/core/decorators/watch';
import { tsx } from '@dojo/framework/core/vdom';
export default class MyEncapsulatedStateWidget extends WidgetBase {
private myState: string = 'Hello from a stateful widget!';
@watch() private counter: number = 0;
protected render() {
return (
<div>
Current widget state: {this.myState}
<br />
<button
onclick={() => {
this.myState = 'State change iteration #' + ++this.counter;
}}
>
Change State
</button>
</div>
);
}
}
Intermediate: passing widget properties
Passing state into a widget via virtual node properties
is the most effective way of wiring up reactive data flows within a Dojo application.
Widgets specify their own properties interface which can include any fields the widget wants to publicly advertise to consumers, including configuration options, fields representing injectable state, as well as any event handler functions.
Function-based widgets pass their properties interface as a generic type argument to the create().properties<MyPropertiesInterface>()
call. The factory returned from this call chain then makes property values available via a properties
function argument in the render function definition.
Class-based widgets can define their properties interface as a generic type argument to WidgetBase
in their class definition, and then access their properties through the this.properties
object.
For example, a widget supporting state and event handler properties:
src/widgets/MyWidget.tsx
Function-based variant:
import { create, tsx } from '@dojo/framework/core/vdom';
import icache from '@dojo/framework/core/middleware/icache';
const factory = create().properties<{
name: string;
onNameChange?(newName: string): void;
}>();
export default factory(function MyWidget({ middleware: { icache }, properties }) {
const { name, onNameChange } = properties();
let newName = icache.get<string>('new-name') || '';
return (
<div>
<span>Hello, {name}! Not you? Set your name:</span>
<input
type="text"
value={newName}
oninput={(e: Event) => {
icache.set('new-name', (e.target as HTMLInputElement).value);
}}
/>
<button
onclick={() => {
icache.set('new-name', undefined);
onNameChange && onNameChange(newName);
}}
>
Set new name
</button>
</div>
);
});
Class-based variant:
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';
export interface MyWidgetProperties {
name: string;
onNameChange?(newName: string): void;
}
export default class MyWidget extends WidgetBase<MyWidgetProperties> {
private newName = '';
protected render() {
const { name, onNameChange } = this.properties;
return (
<div>
<span>Hello, {name}! Not you? Set your name:</span>
<input
type="text"
value={this.newName}
oninput={(e: Event) => {
this.newName = (e.target as HTMLInputElement).value;
this.invalidate();
}}
/>
<button
onclick={() => {
this.newName = '';
onNameChange && onNameChange(newName);
}}
>
Set new name
</button>
</div>
);
}
}
A consumer of this example widget can interact with it by passing in appropriate properties:
src/widgets/NameHandler.tsx
Function-based variant:
import { create, tsx } from '@dojo/framework/core/vdom';
import icache from '@dojo/framework/core/middleware/icache';
import MyWidget from './MyWidget';
const factory = create({ icache });
export default factory(function NameHandler({ middleware: { icache } }) {
let currentName = icache.get<string>('current-name') || 'Alice';
return (
<MyWidget
name={currentName}
onNameChange={(newName) => {
icache.set('current-name', newName);
}}
/>
);
});
Class-based variant:
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';
import watch from '@dojo/framework/core/decorators/watch';
import MyWidget from './MyWidget';
export default class NameHandler extends WidgetBase {
@watch() private currentName: string = 'Alice';
protected render() {
return (
<MyWidget
name={this.currentName}
onNameChange={(newName) => {
this.currentName = newName;
}}
/>
);
}
}
Advanced: abstracting and injecting state
When implementing complex responsibilities, following a pattern of state encapsulation within widgets can result in bloated, unmanageable components. Another problem can arise in large applications with hundreds of widgets structured across tens of layers of structural hierarchy. State is usually required in the leaf widgets, but not in intermediate containers within the VDOM hierarchy. Passing state through all layers of such a complex widget hierarchy adds brittle, unnecessary code.
Dojo provides the Stores component to solve these issues by abstracting state management into its own dedicated context, then injecting relevant portions of the application's state into specific widgets that require it.