Responsive Service
Simple and practical state management tools (part 2: Dependency Injection)
Reactive -service/ React: a simple and comprehensive front-end status management tool, and a very comprehensive front-end status management solution.
At present, react version has been used in my company’s project, and vUE and other versions will be released later.
Step 1: Simple state sharing
const appService = {
data: {
loginUser: null.messages: []},setLoginUser(user) {
this.data.loginUser = user;
},
pushMessage(msg) {
this.data.messages.push(msg); }}function App() {
const loginUser = service.data.loginUser;
return (
<>
<div>Login User: {loginUser.name}</div>
<LoginBox />
<MessageBox />
</>
);
}
function LoginBox() {
const loginSuccess = (user) = > {
service.setLoginUser(user);
service.pushMessage("login success!");
};
/ /...
return (
<div>{/ *... * /}</div>
);
}
function MessageBox({ id }) {
const messages = service.data.;
return (
<div>
{messages.map((msg) => (
<p>{msg}</p>
))}
</div>
);
}
Copy the code
This serves the purpose of state sharing, but there is another problem: when we change service.data, we do not trigger component updates.
Don’t worry. Let’s look at step two.
Step 2: Data response
In VUE, because the framework itself has the data response feature, the examples in step 1 can achieve the purpose of state sharing. But with React, we had to resort to frameworks like MOBx.
Mobx also has its drawbacks, which I won’t cover here, but we’ll go straight to our approach: a combination of RXJS + hooks.
In RXJS, there is a class called BehaviorSubject, which has the following properties:
- Keep it recent: Each observer gets its most recent value when it subscribes to it.
- Push new status: Every time it receives a new value, it pushes the new value to all observers that subscribe to it.
Is that similar to state? Yes, it is a natural state helper, which we can use with hooks.
const appService = {
data: {
loginUser: new BehaviorSubject(null),
messages: new BehaviorSubject([]),
},
setLoginUser(user) {
this.data.loginUser.next(user);
},
pushMessage(msg) {
this.data.messages.next([ ...this.data.messages.value, msg ]); }}// Subscribe when components are installed and unsubscribe when components are uninstalled
const useBehavior = (subject) = > {
const [state, setState] = useState(subject.value);
useEffect(() = > {
const subscription = subject.subscribe((v) = > {
setState(v);
});
return () = > {
subscription.unsubscribe();
}
}, [subject]);
}
function App() {
const loginUser = useBehavior(service.data.loginUser);
return (
<>
<div>Login User: {loginUser.name}</div>
<LoginBox />
<MessageBox />
</>
);
}
function LoginBox() {
const loginSuccess = (user) = > {
service.setLoginUser(user);
service.pushMessage("login success!");
};
/ /...
return (
<div>{/ *... * /}</div>
);
}
function MessageBox({ id }) {
const messages = useBehavior(service.data.messages);
return (
<div>
{messages.map((msg) => (
<p>{msg}</p>
))}
</div>
);
}
Copy the code
As a result, mobx features are easy to implement and easy to use (assuming you’re familiar with RXJS, which you might find difficult to learn, but the benefits of RXJS go beyond that, more on that later).
This approach has the following advantages over tools such as Redux and Unstated-Next:
- No complex concept, simple to use, as long as you can use
rxjs
It’s easy to understand. - States are more granular,
A state
Is not triggeredEntire component tree
Update, will only updateSubscribed to it
The component.
Step 3: Asynchronous data processing
The state sharing described above is just one feature of RXJS that can be easily implemented in other ways. The real essence of RXJS is in the processing of asynchronous data flows.
Imagine a complex scenario where a user pulls down a search box and needs to implement the following functions.
- A debounce feature is required so that if input is too frequent, a request will only be submitted to the server if there is no new input for 200ms.
- Loading is displayed during loading.
- Users to be displayed in the drop-down list
Your company information
andFinancial information
But for some reasonYour company information
andFinancial information
You have to call another interface. - When a new request is issued, if the old request does not return a value, the old request needs to be terminated or the value returned by the old request needs to be ignored.
- If an error occurs during the request, try again.
Without tools, we would have wasted a lot of brain cells, defined a lot of variables, and written a lot of convoluted code.
With RXJS, however, it’s very simple:
import { zip } from "rxjs";
import { debounceTime, tap, switchMap, map, retry, catchError } from "rxjs/operators";
function Search() {
const [value, setValue] = useState(' ');
const [loading, setLoading] = useState(false);
const [users, setUsers] = useState([]);
const [serach$] = useState(() = > {
return new Subject();
});
// The API returns an Observable
useEffect(() = > {
const subscription = serach$.pipe(
// debounce 200ms
debounceTime(200),
// set loading
tap(() = > {
setLoading(true)}),// get users
switchMap((keyword) = > {
return api.searchUsers({ keyword });
}),
// 'company information' and 'financial information'
switchMap((users) = > {
const ids = users.map(item= > item.id);
return zip(
api.getUsersCompanyInfos({ ids }),
api.getUsersFinanceinfos({ ids }),
).pipe(
map([companyInfos, financeInfos] => {
// Put 'company information' and 'financial information' into the user list
const newUsers = mergeInfosToUsers(companyInfos, financeInfos);
returnnewUsers; }}))),// If an error occurs, try again
retry(1),
// Error handling
catchError((error, caught) = > {
console.log(error);
setLoading(false);
return caught;
})
).subscribe({
// The request succeeded
next: (users) = > {
setLoading(false); setUsers(users); }});return () = > {
subscription.unsubscribe();
}
}, [serach$]);
return (
<div>
<input value={value} onChange={(e)= >{ setValue(e.target.value); serach$.next(e.target.value); }} / > {/ *... * /}</div>)}Copy the code
This implements the functionality described above, and the code is clear and easy to understand, which is the beauty of RXJS for handling asynchronous data flows. Of course, this knowledge is part of the functionality of RXJS, please read the documentation for more usage.
Step 4: Combine asynchronous data flow operations into the Service
class AppService {
subscriptions = [];
data = {
loginUser: new BehaviorSubject(null),
messages: new BehaviorSubject([])
}
actions = {
login: new Subject(),
pushMessage: new Subject()
}
consturctor() {
const subscription = this.actions.login.pipe(
switchMap((params) = > {
return api.login(params);
})
).subscribe({
next: (user) = > {
this.data.loginUser.next(user);
this.actions.pushMessage.next("login suecess"); }})const subscription2 = this.actions.pushMessage.subscribe({
next: (msg) = > {
this.data.messages.next([
...this.data.messages.value,
msg
])
}
})
this.subscriptions.push(subscription, subscription2);
}
dispose() {
this.subscriptions.forEach((subscription) = >{ subscription.unsubscribe(); }); }}const appService = new AppService();
function App() {
const loginUser = useBehavior(appService.data.loginUser);
// When the app is uninstalled, destroy the service, clean up the subscription, etc
useEffect(() = > {
return () = >{ appService.dispose(); }} []);return (
<>
<div>Login User: {loginUser.name}</div>
<LoginBox />
<MessageBox />
</>
);
}
Copy the code
Step 5: Standardize the Service
Above we combine state response and asynchronous data flow control, and basically have all the data management capabilities we need.
However, it still looks a little messy and needs to be regulated and constrained.
After thinking about the previous project experience, I found that the parts of a service that need to be exposed to the outside only include the following:
- State: A State value that can be subscribed to, keeping the last value and receiving new values, typically a normal State value.
- Event: events (or notifications) that can be subscribed to. We do not need to know the previous value, only the value after we subscribed, such as the processing of message notifications.
- The outside world wants to
service
Passing data, notificationsservice
Change.
Thus, we can decouple the UI layer from the Service layer:
UI
: Only care about where to subscribe to data (State
andEvent
), and where to issue itAction
.Service
Write:service
When we only care about the definition and processing of data structure and data logic, do not care too muchUI
Layer concrete implementation.
Step 6: Tool implementation
Based on the above, we define a Service base class to help manage and use directly. If used in typescript projects, there are excellent type hints.
First, install:
npm i rxjs @reactive-service/react
Copy the code
Use:
// services/app.ts
import { Service } from "@reactive-service/react";
type AppServiceState = {
loginUser: {
id: string;
name: string;
} | null.messages: string[];
};
type AppServiceEvents = {
error: Error;
}
type AppServiceActions = {
login: {
username: string;
password: string; }}export class AppService extends Service<
AppServiceState.AppServiceEvents.AppServiceActions
> {
constructor() {
/ / initialization
super({
state: {
loginUser: null.messages: []},events: ['error'].actions: ['setLoginUser']});// Asynchronous data is processed
this.subscribe(
this.$.setLoginUser.pipe(
/ /...))}}const appService = new AppService();
Copy the code
import { useBehavior, useSubscribe } from '@reactive-service/react';
// App.tsx
function App() {
/ / to subscribe to the state
const loginUser = useBehavior(appService.$$.loginUser);
// Subscribe to notifications
useSubscribe(appService.$e.error, {
next: (error) = >{ alert(error); }});/ / send the action
const login = (params) = > {
appService.$.login.next(params);
}
// When the app is uninstalled, destroy the service, clean up the subscription, etc
useEffect(() = > {
return () = >{ appService.dispose(); }} []);return (
<>
<div>Login User: {loginUser.name}</div>
<LoginBox />
<MessageBox />
</>
);
}
Copy the code
A service instance created in this way exposes the following methods:
service.$$
: State set.service.$e
: Events collection.service.$
: Actions collection.
For more details, see apis;
Step 7: Organize, install, and uninstall the Service
Some very simple components and projects may not need to use the service we introduced, or a service may be enough for the entire application.
However, in a slightly more complicated project, one service is definitely not enough. There may be global service, local service, etc., which involves the installation, uninstallation, sharing and other operations of service.
Where should a service be installed, uninstalled, and shared among components?
See the dependency injection section for this feature.