Last September, CYcle. js creator Andre Staltz posted a video on why React isn’t a responsive framework and introduced Cycle.js. I thought it was a great idea at the time, but I never understood it. Recently, I used @ngrx/ Effects in Angular for my work. This is a cycle.js idea, which is applied to Angular. This article requires an understanding of RXJS and @Ngrx/Store (Redux in Angular). This pre-knowledge can be gleaned from my previous articles: Angular from 0 to 1: Rx — the hidden sword in Angular and Redux your Angular app
Background and terminology
The root of this approach is to think of all application (or component) logic as a purely data-processing function (and external read and write operations — these are called Effects — that are not the responsibility of this function) and a set of external read and write drivers.
Here’s a quick example:
function main(){
// The logical part
var a = 2;
var b = 10;
var result = a * b;
// Write Effect to console
console.log('result is: ' + result);
// Handle DOM Effect
var resultElement = document.getElementById('result');
resultElement.textContent = result;
}Copy the code
The first three lines of this simple code are the main logic of the code, and the next few lines of code all have an Effect on the external world, so they are all Effects. So let’s rewrite this part of the code according to the principle mentioned above: the logical part does not involve any impact on the outside world.
// The main logic of the program is completely stripped of Effects and only the data is processed
function main(){
var a = 2;
var b = 10;
var result = a * b;
return {
DOM: result,
log: result
};
}
// The effect on Console is written here
function logEffect(result){
console.log('result is: ' + result);
}
// The impact on DOM is written here
function domEffect(result){
var resultElement = document.getElementById('result');
resultElement.textContent = result;
}
// How to connect data to effects, which is a glue
function run(mainFn){
var sink = mainFn();
logEffect(sink.log);
domEffect(sink.DOM);
}
run(main);Copy the code
That’s about it. It doesn’t seem like much, you might think. Don’t worry. We’ll see what it does later. So what does this thinking have to do with Angular?
State, Action flow, and Effect
Reducer in Redux has been a pure function, and it is a completely pure function that only deals with state data. Therefore, Reducer has satisfied the principles we mentioned above. After an Action is issued, the Reducer processes the status data and returns it. However, in general, there are still some actions that can be called effects after an Action, such as making HTTP requests, navigating, writing files, etc. These are things that Redux itself cannot solve, hence middleware such as Redux-Thunk. Let’s take a look at how to solve this problem using @ngrx/ Effects.
To take a small example, for example, the authentication process that is often used, such as login and registration, we usually have the following Action: LOGIN, LOGIN_SUCCESS, LOGIN_FAIL, REGISTER, REGISTER_SUCCESS, REGISTER_FAIL, and LOGOUT.
For LOGIN, we want the process to look like this: Send the LOGIN Action –> Use the LOGIN Service to authenticate the LOGIN. If the LOGIN succeeds, send the LOGIN_SUCCESS Action; if the LOGIN fails, send the LOGIN_FAIL Action. Traditionally, we need to call a Service somewhere in the component to make an HTTP request, and the component or service decides to send either LOGIN_SUCCESS or LOGIN_FAIL when the response is returned.
If we apply the Effect concept we mentioned above, Reducer has acted as a pure data processing function, and Action is a signal flow in @ngrx/effects, which acts as the glue between the state and the Effect to be done. Just like function Run (mainFn) in the code above.
@Injectable()
export class AuthEffects{
// Inject the required service and action signal flows through constructs
constructor(private actions$: Actions, private authService: AuthService) { }
Use the @effect () modifier to indicate that this is an Effect
@Effect()
login$: Observable<Action> = this.actions$ // action Signal flow
.ofType(authActions.ActionTypes.LOGIN) // If it is a LOGIN Action
.map(toPayload) // The payload of the action
.switchMap((val:{username:string, password: string}) = > {
// Invoke the service
return this.authService.login(val.username, val.password);
})
// If the LOGIN_SUCCESS Action is successfully sent to another Effect or Reducer
.map(user= > new authActions.LoginSuccessAction({user: user}))
// If this fails, send a LOGIN_FAIL Action to another Effect or Reducer
.catch(err= > of(new authActions.LoginFailAction(err.json())));
}Copy the code
You might ask, what if we need to navigate to /home after logging in successfully? Navigation is also effect, and actions$is a signal flow, so you can define an effect to listen for LOGIN_SUCCESS and then navigate
@Effect()
navigateHome$: Observable<Action> = this.actions$
.ofType(actions.ActionTypes.LOGIN_SUCCESS)
.map((a)= > go(['/home']));Copy the code
In this case, there is no need for the component to call the Service, just signal it.
onSubmit({value, valid}){
if(! valid)return;
this.store$.dispatch(
new authActions.LoginAction({
username: value.username,
password: value.password
}));
}Copy the code
What if it’s more complicated? Such as login after we need to get the login user’s to-do list, then we difficulty but to return this. TodoService. GetTodos auth. User. (id); Access to auth is also required.
@Effect()
loadTodos$: Observable<Action> = this.actions$
.ofType(todoActions.ActionTypes.LOAD_TODOS)
.map(toPayload)
.switchMap((a)= > {
return this.todoService.getTodos(auth.user.id); // How do I get this auth?
})
.map(todos= > new todoActions.LoadTodosSuccessAction(todos))
.catch(err= > of(new todoActions.LoadTodosFailAction(err.json())));Copy the code
Remember, NGRX is based on RXJS and is very good at merging and manipulating streams. Store is also a stream, which is very easy to do. We just need to get the latest value of Auth in Store and merge the two streams:
@Effect()
loadTodos$: Observable<Action> = this.actions$
.ofType(todoActions.ActionTypes.LOAD_TODOS)
.map(toPayload)
.withLatestFrom(this.store$.select('auth'))
.switchMap(([_, auth]) = > {
return this.todoService.getTodos(auth.user.id);
})
.map(todos= > new todoActions.LoadTodosSuccessAction(todos))
.catch(err= > of(new todoActions.LoadTodosFailAction(err.json())));Copy the code
I feel like this approach to programming really works: if you think logically, your code is almost done.
In addition, my book Angular From Zero to One has been published. Here is a brief introduction:
This book introduces basic Angular knowledge and development skills to help front-end developers get started quickly. There are nine chapters: Chapter 1 introduces basic Angular concepts, chapters 2 to 7 build a To-do app from scratch, and then gradually add features such as login authentication, modularizing the app, multi-user version implementation, using third-party style libraries, and dynamic effects creation. Chapter 8 introduces the concept of responsive programming and the use of Rx in Angular. Chapter 9 introduces the Redux state management mechanism popular in React, which allows for better separation of code and logic and is strongly recommended for team work. This book not only explains basic Angular concepts and best practices, but also shares the author’s problem-solving process and logic. It is a subtle and humorous book for those with a background in object-oriented programming.
Welcome everyone to watch, order, put forward valuable opinions.
Jingdong links: item.m.jd.com/product/120…