dojo dragon main logo

Store concepts in detail

State object

In modern browsers a state object is passed as part of the CommandRequest. Any modification to this state object gets translated to the appropriate patch operations and applied against the store.

import { createCommandFactory } from '@dojo/framework/stores/process';
import { State } from './interfaces';
import { remove, replace } from '@dojo/framework/stores/state/operations';

const createCommand = createCommandFactory<State>();

const addUser = createCommand<User>(({ payload, state }) => {
	const currentUsers = state.users.list || [];
	state.users.list = [...currentUsers, payload];
});

Note that attempting to access state is not supported in IE11 and will immediately throw an error.

StoreProvider

The StoreProvider accepts three properties

  • renderer: A render function that has the store injected to access state and pass processes to child widgets.
  • stateKey: The key of the state in the registry.
  • paths (optional): A function to connect the provider to sections of the state.

Invalidation

The StoreProvider has two main ways to trigger invalidation and cause a re-render.

  1. The recommended approach is to register paths by passing the paths property to the provider to ensure invalidation only occurs when relevant state changes.
  2. A catch-all when no paths are defined for the container, it will invalidate when any data changes in the store.

Process

Lifecycle

A Process has an execution lifecycle that defines the flow of the behavior being defined.

  1. If a transformer is present it gets executed first to transform the payload
  2. before middleware get executed synchronously in order
  3. commands get executed in the order defined
  4. operations get applied from the commands after each command (or block of commands in the case of multiple commands) gets executed
  5. If an exception is thrown during commands, no more commands get executed and the current set of operations are not applied
  6. after middleware get executed synchronously in order

Transformers

By using a transformer, you can modify a payload before it is used by a processes commands. This is a way to create additional process executors that accept different payloads.

interface PricePayload {
	price: number;
}

const createCommand = createCommandFactory<any, PricePayload>();

// `payload` is typed to `PricePayload`
const setNumericPriceCommand = createCommand(({ get, path, payload }) => {});
const setNumericPrice = createProcess('set-price', [setNumericPriceCommand]);

First, create a transformer to convert another type of input into PricePayload:

interface TransformerPayload {
	price: string;
}

// The transformer return type must match the original `PricePayload`
const transformer = (payload: TransformerPayload): PricePayload => {
	return {
		price: parseInt(payload.price, 10)
	};
};

Now simply create the process with this transformer.

const processExecutor = setNumericPrice(store);
const transformedProcessExecutor = setNumericPrice(store, transformer);

processExecutor({ price: 12.5 });
transformedProcessExecutor({ price: '12.50' });

Process middleware

Middleware gets applied around processes using optional before and after methods. This allows for generic, sharable actions to occur around the behavior defined by processes.

Multiple middlewares may get defined by providing a list. Middlewares get called synchronously in the order listed.

Before

A before middleware block gets passed a payload and a reference to the store.

middleware/beforeLogger.ts

const beforeOnly: ProcessCallback = () => ({
	before(payload, store) {
		console.log('before only called');
	}
});

After

An after middleware block gets passed an error (if one occurred) and the result of a process.

middleware/afterLogger.ts

const afterOnly: ProcessCallback = () => ({
	after(error, result) {
		console.log('after only called');
	}
});

The result implements the ProcessResult interface to provide information about the changes applied to the store and provide access to that store.

  • executor - allows additional processes to run against the store
  • store - a reference to the store
  • operations - a list of applied operations
  • undoOperations - a list of operations that can be used to reverse the applied operations
  • apply - the apply method from the store
  • payload - the provided payload
  • id - the id used to name the process

Subscribing to store changes

The Store has an onChange(path, callback) method that takes a path or an array of paths and invokes a callback function whenever that state changes.

main.ts

const store = new Store<State>();
const { path } = store;

store.onChange(path('auth', 'token'), () => {
	console.log('new login');
});

store.onChange([path('users', 'current'), path('users', 'list')], () => {
	// Make sure the current user is in the user list
});

The Store also has an invalidate event that fires any time the store changes.

main.ts

store.on('invalidate', () => {
	// do something when the store's state has been updated.
});