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 userxjsIt’s easy to understand.
  • States are more granular,A stateIs not triggeredEntire component treeUpdate, will only updateSubscribed to itThe 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.

  1. 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.
  2. Loading is displayed during loading.
  3. Users to be displayed in the drop-down listYour company informationandFinancial informationBut for some reasonYour company informationandFinancial informationYou have to call another interface.
  4. 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.
  5. 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 toservicePassing data, notificationsserviceChange.

Thus, we can decouple the UI layer from the Service layer:

  • UI: Only care about where to subscribe to data (StateandEvent), and where to issue itAction.
  • ServiceWrite:serviceWhen we only care about the definition and processing of data structure and data logic, do not care too muchUILayer 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.