interface Action { type: string; payload?: any; }
Payload
(payload) - this is an optional property, because sometimes we can send to the store actions that do not take the payload, although in most cases this property is involved. Here we have in mind that when creating an action, we describe it, for example, as follows: const action: Action = { type: 'ADD_TODO', payload: { label: 'Eat pizza,', complete: false }, };
function reducer(state, action) { //... }
type
property of the action (we just saw this property). This is usually done using the switch
construction: function reducer(state, action) { switch (action.type) { case 'ADD_TODO': { // , - ... } } }
case
branch inside the switch
allows you to respond to different types of actions that are involved in shaping the state of the application. For example, suppose we need to add a property with some value to the state tree. To do this, we perform some actions and return the changed state: function reducer(state = {}, action) { switch (action.type) { case 'ADD_TODO': { return { ...state, // , , todos // todos: [...state.todos, { label: 'Eat pizza,', complete: false }], }; } } return state; }
state
return command at the bottom of the code. This is done in order to return the initial state if there is no case
branch in the reducer corresponding to some action. Here you can add that the construction state = {}
is added as the first argument. This is the default value of the parameter. The original state objects are usually formed outside the limits of the reducer, we will talk about this later.case
. It is a combination of the previous state and the changes made to it. As a result, the output is a slightly modified version of the original state tree. Here, first, apply the command …state
, the extension operator, after which new properties are added to the current state.action.payload
come into action.payload
? Ideally, you should not rigidly set certain values ​​in the reducer, unless these are some simple things like transferring a logical value from false
to true
. Now, in order to complete the theme of handling actions, we use the action.payload
property, accessible due to the action passed to the editor when it is called, and we get the necessary data: function reducer(state = {}, action) { switch (action.type) { case 'ADD_TODO': { // const todo = action.payload; // const todos = [...state.todos, todo]; // return { ...state, todos, }; } } return state; }
const store = new Store(reducers, initialState);
dispatch
allow us to give instructions to the repository, informing it that we intend to change the state tree. This operation is performed by means of a reducer, as we have already discussed above.subscribe
method allows organizing the transfer to the repository of subscriber functions interested in changing the state tree. The corresponding information is transferred to these functions when the state tree changes.value
property will be configured as a getter , it returns the internal state tree (as a result, we will be able to access the properties).Store
class: export class Store { constructor() {} dispatch() {} subscribe() {} }
state
. Add it: export class Store { private state: { [key: string]: any }; constructor() { this.state = {}; } get value() { return this.state; } dispatch() {} subscribe() {} }
state
object will consist of string keys, which can correspond to values ​​of any type. This is exactly what you need to work with our data structures.get value() {}
added here, which returns a state
object when it is accessed as a property: console.log(store.value);
const store = new Store();
dispatch
method: store.dispatch({ type: 'ADD_TODO', payload: { label: 'Eat pizza', complete: false }, });
dispatch
method, we will bring it to this form so that it can transfer the action to it: export class Store { // ... dispatch(action) { // ! } // ... }
dispatch
method, you need to update the state tree. But first, let us ask ourselves - what does it look like - is this a state tree? { todos: { data: [], loaded: false, loading: false, } }
todo
property in the state tree, or the todo
property layer, will be controlled by the reducer. At the moment, our reducer will work with the data
, loaded
, and loading
properties. Here, the loaded
(loaded) and loading
(loaded) properties are used, because when an asynchronous operation is performed, like loading JSON over HTTP, we would like to control various steps that are performed as part of this operation - from initiating a request to its successful completion.dispatch
method. export class Store { // ... dispatch(action) { this.state = { todos: { data: [...this.state.todos.data, action.payload], loaded: true, loading: false, }, }; } // ... }
'ADD_TODO'
to the dispatch
method, the status will look like this: { todos: { data: [{ label: 'Eat pizza', complete: false }], loaded: false, loading: false, } }
export const initialState = { data: [], loaded: false, loading: false, };
state
argument, the default value of which is the above-described initialState
object. This allows preparing the reducer for the first boot when we call the reducer in the storage in order to connect all the reducers with the initial state: export function todosReducer( state = initialState, action: { type: string, payload: any } ) { // return state; }
export function todosReducer( state = initialState, action: { type: string, payload: any } ) { switch (action.type) { case 'ADD_TODO': { const todo = action.payload; const data = [...state.data, todo]; return { ...state, data, }; } } return state; }
Store
object: export class Store { private state: { [key: string]: any }; constructor() { this.state = {}; } get value() { return this.state; } dispatch(action) { this.state = { todos: { data: [...this.state.todos.data, action.payload], loaded: true, loading: false, }, }; } }
export class Store { private state: { [key: string]: any }; private reducers: { [key: string]: Function }; constructor(reducers = {}, initialState = {}) { this.reducers = reducers; this.state = {}; } }
initialState
, so we can, if desired, transfer it when we create the repository.todos
property is in the state tree, and we must attach a reducer function to it. Let me remind you, we are going to work with the state layer called todos
: const reducers = { todos: todosReducer, }; const store = new Store(reducers);
todos
property becomes the result of a call to the todosReducer reducer todosReducer
, which, as we know, returns a new state based on some action.Array.prototype.reduce
, which leads the array processed to it to a certain single value. Reducers work in a similar way, taking the old state, performing some actions on it, and returning a new state.reduce
here: export class Store { // ... dispatch(action) { this.state = this.reduce(this.state, action); } private reduce(state, action) { // return {}; } }
reduce
method of the Store
class that we just created, and pass it the state and the action. This design is called the root reduction gear. You can see that it takes state
and action
- just as it does todosReducer
.reduce
method, since this is the most important step in building a state tree and bringing together everything we are talking about here. export class Store { private state: { [key: string]: any }; private reducers: { [key: string]: Function }; constructor(reducers = {}, initialState = {}) { this.reducers = reducers; this.state = {}; } dispatch(action) { this.state = this.reduce(this.state, action); } private reduce(state, action) { const newState = {}; for (const prop in this.reducers) { newState[prop] = this.reducers[prop](state[prop], action); } return newState; } }
newState
object that will contain the new state tree.this.reducers
through this.reducers
object registered in the repository.todos
, to newState
.state[prop]
) and the actionprop
in this case is just todos
, so all this can be viewed as: newState.todos = this.reducers.todos(state.todos, action);
initialState
object. If we are going to use a record of the type Store(reducers, initialState)
to prepare the initial state of the entire storage, we need to process it with the reducer during the creation of the storage: export class Store { private state: { [key: string]: any }; private reducers: { [key: string]: Function }; constructor(reducers = {}, initialState = {}) { this.reducers = reducers; this.state = this.reduce(initialState, {}); } // ... }
return state
? Now you know why. We have this so that we can pass an empty object as an action, {}
, meaning that the branches of the switch
will be skipped, and as a result we will have a state tree obtained through the constructor
. const store = new Store(reducers); store.subscribe(state => { // - `state` });
export class Store { private subscribers: Function[]; constructor(reducers = {}, initialState = {}) { this.subscribers = []; // ... } subscribe(fn) {} // ... }
subscribe
method, which now takes the function ( fn
) as an argument. Now we need to pass each such function to the subscribers
array: export class Store { // ... subscribe(fn) { this.subscribers = [...this.subscribers, fn]; } // ... }
dispatch
method! export class Store { // ... get value() { return this.state; } dispatch(action) { this.state = this.reduce(this.state, action); this.subscribers.forEach(fn => fn(this.value)); } // ... }
dispatch
, we pass a state to the reduce
method and bypass the subscribers, passing it this.value
(remember that the getter value
triggered here)..subscribe()
, we do not want (at this particular moment) to get the value of state
. We want to get it after executing the dispatch
method. Therefore, we will decide to inform new subscribers about the current status as soon as they subscribe: export class Store { // ... subscribe(fn) { this.subscribers = [...this.subscribers, fn]; fn(this.value); } // ... }
subscribe
method and, after making the subscription, we call it with the transfer of the state tree to it. export class Store { // ... subscribe(fn) { this.subscribers = [...this.subscribers, fn]; fn(this.value); return () => { this.subscribers = this.subscribers.filter(sub => sub !== fn); }; } // ... }
fn
. Next, using Array.prototype.filter
, what is no longer needed is removed from the subscriber array. You can use it like this: const store = new Store(reducers); const unsubscribe = store.subscribe(state => {}); destroyButton.on('click', unsubscribe, false);
export class Store { private subscribers: Function[]; private reducers: { [key: string]: Function }; private state: { [key: string]: any }; constructor(reducers = {}, initialState = {}) { this.subscribers = []; this.reducers = reducers; this.state = this.reduce(initialState, {}); } get value() { return this.state; } subscribe(fn) { this.subscribers = [...this.subscribers, fn]; fn(this.value); return () => { this.subscribers = this.subscribers.filter(sub => sub !== fn); }; } dispatch(action) { this.state = this.reduce(this.state, action); this.subscribers.forEach(fn => fn(this.value)); } private reduce(state, action) { const newState = {}; for (const prop in this.reducers) { newState[prop] = this.reducers[prop](state[prop], action); } return newState; } }
dispatch
method informs the repository about the need to perform the process of defining a new state by calling each reducer and attempting to match action.typ
e with one of the branches of the switch
. And the state tree is the final view of what comes after calling all the reducers.Source: https://habr.com/ru/post/345340/
All Articles