Dojo Store Concept Explanation

Self translation https://github.com/dojo/framework/blob/master/docs/en/stores/supplemental.md

State object

In modern browsers, state objects are passed in as part of CommandRequest. Any modification to the state object will be translated into the corresponding operation and applied to 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 IE 11 does not support access to state and will throw an error immediately if you try to access it.

StoreProvider

StoreProvider receives three properties

  • renderer: A rendering function that has injected store into it to access the state and pass in the process to the child parts.
  • stateKey: The key value used to register the status.
  • paths (optional): Connect this provider to a part of the state.

Invalid

StoreProvider has two ways to trigger failure and cause re-rendering.

  1. The recommended way is to register the path by passing in the paths attribute to the provider to ensure that only the relevant state changes will fail.
  2. The other is a more general way, when no path is defined for the provider, any data change in the store will cause failure.

Process

life cycle

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

  1. If a converter exists, the converter is first executed to convert the payload object
  2. Executing before middleware synchronously in sequence
  3. Execute defined command s sequentially
  4. After executing each command (if multiple commands are one command), the operation returned by the application command
  5. If an exception is thrown during command execution, subsequent commands will not be executed and the current operation will not be applied.
  6. Executing after middleware synchronously in sequence

Process Middleware

Use the optional before and after methods to apply middleware before and after the process. This allows common, shareable operations to be added before and after the actions defined by process.

Multiple middleware can also be defined in the list. It is called synchronously according to the order in which the middleware is listed.

Before

The before middleware block can get references to the incoming payload and store.

middleware/beforeLogger.ts

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

After

The after middleware block retrieves the incoming error (if an error occurs) and the result of the process.

middleware/afterLogger.ts

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

result implements the ProcessResult interface to provide information about changes applied to stores and access to stores.

  • executor - Allows other process es to run on the store
  • Stor - store reference
  • operations - operations for a set of applications
  • undoOperations - A set of operations used to undo the applied operations
  • Apply method on apply - store
  • Payload - payload provided
  • id - id for naming process

Changes in subscription store s

Store has an onChange(path, callback) method that receives one or a set of paths and calls back functions when the 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
});

There is also an invalidate event in the Store, which is triggered when the store changes.

main.ts

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

Shared state management model

Initial state

When the store was first created, it was empty. Then, you can use a process to populate the initial application state for the store.

main.ts

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

const createCommand = createCommandFactory<State>();

const initialStateCommand = createCommand(({ path }) => {
	return [add(path('auth'), { token: undefined }), add(path('users'), { list: [] })];
});

const initialStateProcess = createProcess('initial', [initialStateCommand]);

initialStateProcess(store)({});

Undo

Dojo store uses patch operation to track changes in the underlying store. In this way, Dojo can easily create a set of operations and then undo them to recover any data modified by a set of command s. undoOperations is part of Process Result and can be used in after middleware.

Undo operation is very useful when a process contains multiple command s that modify the store state and one of them fails to execute and needs to be rolled back.

undo middleware

const undoOnFailure = () => {
	return {
		after: () => (error, result) {
			if (error) {
				result.store.apply(result.undoOperations);
			}
		}
	};
};

const process = createProcess('do-something', [
	command1, command2, command3
], [ undoOnFailure ])

When any command fails during execution, the undoOnFailure middleware is responsible for applying undoOperations.

It should be noted that undoOperations are only applicable to command s that are fully executed in the process. When the rollback state is rolled back, it will not contain any of the following operation s, which may be caused by other processes executed asynchronously, or by state changes executed in the middleware, or directly on the store. These use cases are not within the scope of the undo system.

Optimistic update

Optimistic updates can be used to build responsive UI s, although interactions may take some time to respond, such as saving resources remotely.

For example, if you are adding a todo item, by optimistic updates, you can add a todo item to the store before sending a request for a persistent object to the server, thus avoiding awkward waiting periods or loading indicators. When the server responds, the todo items in the store can be coordinated based on the success or failure of the server operation.

In a successful scenario, the added Todo item is updated with the id provided in the server response, and the color of the Todo item is changed to green to indicate that it has been saved successfully.

In an error scenario, you can display a notification that the request failed, change the color of the Todo item to red, and display a "retry" button. You can even restore or undo the added Todo item, as well as any other operations that occur in the process.

const handleAddTodoErrorProcess = createProcess('error', [ () => [ add(path('failed'), true) ]; ]);

const addTodoErrorMiddleware = () => {
	return {
		after: () => (error, result) {
			if (error) {
				result.store.apply(result.undoOperations);
				result.executor(handleAddTodoErrorProcess);
			}
		}
	};
};

const addTodoProcess = createProcess('add-todo', [
		addTodoCommand,
		calculateCountsCommand,
		postTodoCommand,
		calculateCountsCommand
	],
	[ addTodoCallback ]);
  • addTodoCommand - Add a todo item to the application state
  • calculateCountsCommand - recalculates the number of completed to-do items and the number of to-do items for activities
  • postTodoCommand - Submit todo entries to remote services and use process's after middleware to perform further changes in case of errors
    • Change is restored when failure occurs and the failed status field is set to true
    • Update the id field of the todo item with the value returned from the remote service when successful
  • After calculateCountsCommand - postTodoCommand succeeds, run it again

Synchronous update

In some cases, it is better to wait for the back-end call to complete before proceeding with the process. For example, when process deletes an element from the screen, or outlet changes to display different views, restoring the state that triggers these operations can be weird.

Because process supports asynchronous command, it simply returns Promise to wait for the result.

function byId(id: string) {
	return (item: any) => id === item.id;
}

async function deleteTodoCommand({ get, payload: { id } }: CommandRequest) {
	const { todo, index } = find(get('/todos'), byId(id));
	await fetch(`/todo/${todo.id}`, { method: 'DELETE' });
	return [remove(path('todos', index))];
}

const deleteTodoProcess = createProcess('delete', [deleteTodoCommand, calculateCountsCommand]);

Concurrent command

Process supports concurrent execution of multiple command s, just placing them in an array.

process.ts

createProcess('my-process', [commandLeft, [concurrentCommandOne, concurrentCommandTwo], commandRight]);

In this example, commandLeft executes first, then concurrent CommandOne and concurrent CommandTwo are executed concurrently. When all concurrent commands are executed, the returned results are applied on demand. If any concurrent command fails, no action will be applied. Finally, execute commandRight.

Replaceable state implementation

When the store is instantiated, the implementation of the MutableState interface is used by default. In most cases, the default state interfaces are well optimized to suit common situations. If a particular use case requires another implementation, it can be passed in at initialization time.

const store = new Store({ state: myStateImpl });

MutableState API

Any State implementation must provide four methods to apply operations correctly in state.

  • Get < S > (path: Path < M, S >): S receives a Path object and returns the value that the path points to in the current state
  • At < S extends Path < M, Array < any > (path: S, index: number): Path < M, S ['value'] [0]> returns a Path object that points to the value indexed in the array to which path is located.
  • Path: StatePaths < M > Generates a Path object for a given path in the state in a type-safe manner
  • Apply (operations: PatchOperation < T > []): PatchOperation < T > [] applies the provided operation to the current state

ImmutableState

Dojo Store passes Immutable It provides an implementation for the MutableState interface. This implementation may improve performance if the state of the store is updated frequently and at a deeper level. Before you finally decide to use this implementation, you should test and validate performance.

Using Immutable

import State from './interfaces';
import Store from '@dojo/framework/stores/Store';
import Registry from '@dojo/framework/widget-core/Registry';
import ImmutableState from '@dojo/framework/stores/state/ImmutableState';

const registry = new Registry();
const customStore = new ImmutableState<State>();
const store = new Store<State>({ store: customStore });

Local storage

Dojo Store provides a set of tools to use local storage.

Local storage middleware monitors changes on specified paths and stores them on local disks using the structures defined in IDS and paths provided in collector.

Using local storage middleware:

export const myProcess = createProcess(
	'my-process',
	[command],
	collector('my-process', (path) => {
		return [path('state', 'to', 'save'), path('other', 'state', 'to', 'save')];
	})
);

load functions from LocalStorage are used to combine with store s

Combining with state:

import { load } from '@dojo/framework/stores/middleware/localStorage';
import { Store } from '@dojo/framework/stores/Store';

const store = new Store();
load('my-process', store);

Note that the data should be serialized for storage and overwritten after each call to process. This implementation does not apply to data that cannot be serialized (such as Date and Array Buffer).

Advanced store operation

Dojo Store uses operations to change the underlying state of the application. This design of operations helps simplify common interactions with stores, for example, operations automatically create the underlying structures needed to support add or replace operation s.

Execute a deep add in an uninitialized store:

import Store from '@dojo/framework/stores/Store';
import { add } from '@dojo/framework/stores/state/operations';

const store = new Store<State>();
const { at, path, apply } = store;
const user = { id: '0', name: 'Paul' };

apply([add(at(path('users', 'list'), 10), user)]);

The result is:

{
	"users": {
		"list": [
			{
				"id": "0",
				"name": "Paul"
			}
		]
	}
}

Even if the state is not initialized, Dojo can create the underlying hierarchy based on the path provided. This operation is safe because TypeScript and Dojo provide type safety. This allows users to naturally use the State interface used by the store without having to pay explicit attention to the data stored in the store.

When you need to use data explicitly, you can use the test operation or assert the information by capturing the underlying data and verify it programmatically.

This example uses the test operation to ensure that it is initialized and that user is always added to the end of the list:

import Store from '@dojo/framework/stores/Store';
import { test } from '@dojo/framework/stores/state/operations';

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

apply([test(at(path('users', 'list', 'length'), 0))]);

This example programmatically ensures that user is always added to the end of the list as the last element:

import Store from '@dojo/framework/stores/Store';
import { add, test } from '@dojo/framework/stores/state/operations';

const store = new Store<State>();
const { get, at, path, apply } = store;
const user = { id: '0', name: 'Paul' };
const pos = get(path('users', 'list', 'length')) || 0;
apply([
	add(at(path('users', 'list'), pos), user),
	test(at(path('users', 'list'), pos), user),
	test(path('users', 'list', 'length'), pos + 1)
]);

Disallow access to the root node of the state. If accessed, an error will be raised, such as trying to execute get(path('/'). This restriction also applies to operations; an operation that updates the status root node cannot be created. @ The best practice of dojo/framewok/stores is to encourage access to only the smallest and necessary parts of the store.

Keywords: Front-end github IE Attribute TypeScript

Added by mars_rahul on Wed, 11 Sep 2019 14:07:00 +0300