preface

Another brand new week, with a happy mood came to the company, the department manager saw me very happy to tell me there are two good news. First, we have opened a new project, and I still leave it to you. Shit. We said we’d have fun fishing. The second is that the technical framework of the project is React. The last project was Angular. The last project was Vue. Now it’s React.

With a happy mood in a wild stroke ~

Angular uses NgRx or NGXS,Vue uses Vuex or Pinia, and React uses Redux or MboX. Is there a response data store that can be used by all three frameworks without being different Frame project toggle back and forth.

The problem is that if it needs to work in any framework, it needs to be independent of the running framework itself. I found rXJs-based responsive data storage solutions to be the right choice, because it is based on RXJS-based implementations, so it can work in Vue,React, and Angular(needless to say) ) in the good run.

So no problem, it pays off. After a bit of research, I found that Akita fits my needs perfectly, so let’s take a look at this Akita.

Akita

Akita is a responsive data storage tool based on RxJS, which is mainly divided into Store,Service and Query.

Store

The Store part is used to define the data structure and initial default values, similar to State in Vuex and Redux

import { Store, StoreConfig } from '@datorama/akita';

export interface SessionState {
    token: string;
    name: string;
}

export function createInitialState() :SessionState {
    return {
        token: ' '.name: ' '
    };
}

@StoreConfig({ name: 'session' })
export class SessionStore extends Store<SessionState> {
    constructor() {
        super(createInitialState()); }}Copy the code

Service

The Service part is responsible for modifying and manipulating data. It helps us to modify and manipulate data through corresponding public methods, which is similar to Action and Mutation in Vuex

import { SessionStore } from './session.store';

export class SessionService {

    constructor(private sessionStore: SessionStore) {}

    updateUserName(newName: string) {
        this.sessionStore.update({ name: newName }); }}Copy the code

The update function is used to modify the data in the Service by accessing the Store.

Query

Query is responsible for data acquisition and Query. In Query, we can obtain Observable objects through this.select or obtain real-time data through getValue method.

import { Query } from '@datorama/akita';
import { SessionState } from './session.store';

export class SessionQuery extends Query<SessionState> {
    allState$ = this.select();
    isLoggedIn$ = this.select(state= >!!!!! state.token); selectName$ =this.select('name');

    // Returns { name, age }
    multiProps$ = this.select(['name'.'age']);

    // Returns [name, age]
    multiPropsCallback$ = this.select(
        [state= > state.name, state= > state.age]
    )

    constructor(protected store: SessionStore) {
        super(store); }}Copy the code

Not only can we retrieve store data directly through this.select, we can also use RxJS pipes to combine the data we need, or use TypeScript get properties to retrieve data directly.

import { Query } from '@datorama/akita';
import { SessionState } from './session.store';

export class SessionQuery extends Query<SessionState> {
    constructor(protected store: SessionStore) {
        super(store);
    }
    
    getTargetUser(){
        return this.select(['age'.} get isLoggedIn() {return!! this.getValue().token; }}Copy the code

Store,Service, and Query are the main components of Akita. We use these to implement the data storage and access functions we need. Let’s see how we integrate React and Vue.

Integrated Akita

Akita has good support. In fact, the official document provides the React and Sevlte integration method, which I also refer to for implementation.

Akita provides responsive data based on Observables. All we need to do is convert an Observable to the type we want.

The essence of the implementation is that we create a data source, and then listen to RxJS to dynamically modify our data source can be implemented, as for the rest of the operation is for our implementation more elegant.

React

React works just as well as we understood it.

function UseStoreSelect<T> (steam: Observable
       
        , defaultValue? : T
       ) {
  const [value, setValue] = useState<T | undefined>(
    defaultValue
  )

  useEffect(() = > {
    const subscription = steam.subscribe(newValue= > {
      setValue(newValue)
    })
    return () = > subscription.unsubscribe()
  }, [])

  return value
}
Copy the code

We need to pass in the Observable object and the default value, useEffect to monitor the Observable to modify the data, and directly use the returned data.

const user = UseStoreSelect(userQuery.getUser())
Copy the code

But this is not very friendly when you need to get the default value, and you need to add some synchronous processing in Query, so let’s optimize the notation a little bit.

function UseStoreQuery<T.R> (query: Query
       
        , project: (store: T) => R
       ) :R
function UseStoreQuery<
  T.R extends [(state: R) = >any] | Array< (state: R) = >any> > (query: Query<T>, selectorFns: R) :ReturnTypes<R>
function UseStoreQuery<T.R> (query: Query<T>) :R
function UseStoreQuery<T.R> (query: Query<T>, project? : ((store: T) => R) | ((state: T) =>any) []) {
  let steam: Observable<R | T | any[] >let state: R | T | any[]

  if (isFunction(project)) {
    steam = query.select(project)
    state = project(query.getValue())}else if (Array.isArray(project)) {
    steam = query.select(project)
    state = project.map(p => p(query.getValue()))}else {
    steam = query.select()
    state = query.getValue()
  }

  const [value, setValue] = useState<R | T | any[]>(state)

  useEffect(() = > {
    const subscription = steam.subscribe(newValue= > {
      setValue(newValue)
    })
    return () = > subscription.unsubscribe()
  }, [])

  return value
}
Copy the code

Now we can use the Query operations that are not created in Query directly by passing expressions

const user = UseStoreQuery(userQuery,store= >store.user)
Copy the code

This also automatically fetches the corresponding default value from Query, saving us from having to Query it ourselves.

Vue

Vue – Refs in Vue3 are less restricted than react-hooks, so use a similar scheme.

function UseStoreQuery<T.R> (query: Query
       
        , project: (store: T) => R
       ) :R
function UseStoreQuery<
  T.R extends [(state: R) = >any] | Array< (state: R) = >any> > (query: Query<T>, selectorFns: R) :ReturnTypes<R>
function UseStoreQuery<T.R> (query: Query<T>) :R
function UseStoreQuery<T.R> (query: Query<T>, project? : ((store: T) => R) | ((state: T) =>any) []) {
  let steam: Observable<R | T | any[] >let state: R | T | any[]

  if (isFunction(project)) {
    steam = query.select(project)
    state = project(query.getValue())}else if (Array.isArray(project)) {
    steam = query.select(project)
    state = project.map(p => p(query.getValue()))}else {
    steam = query.select()
    state = query.getValue()
  }

  const target = ref<R | T | any[]>(state)

  const subscription = steam.subscribe(newValue= > {
     target.value = newValue
  })

  return target
}
Copy the code

You can also use the Vue-rx library to make it easier to use an Observable.

However, Vue and React do not have dependency injection modules. They need to be manually instantiated in Store,Service, and Query.

// user.store.ts.export const userStore = new UserStore()

// user.query.ts.export const userQuery = new UserQuery(userStore)

// user.service.ts.export const userService = new UserService(userStore)

Copy the code

Data persistence

The persistState function is provided in @datorama/ Akita to help us persist the plug-in.

persistState({ include: ['auth'.'todos']});Copy the code

To control the granularity of the storage, PersistStateSelectFn is used to select the data to persist

export const userStore = new UserStore()

// Persistent storage
export const UserPersistState: PersistStateSelectFn<UserState> =
  state= > ({
    access_token: state.access_token,
    refresh_token: state.refresh_token
  })

UserPersistState.storeName = 'user'
Copy the code
import { AppPersistState } from './app'
import { UserPersistState } from './user'

// Persistent storage
persistState({
  select: [AppPersistState, UserPersistState]
})

Copy the code

And we’re done.

conclusion

So we can use the Akita basic framework to support the original corresponding response type data is stored in the implementation of the, if the company have more than one need not framework project, we can basically maintain consistent data access, at least in the framework of different use a consistent idea and apis to store data operation, need not to switch to cut the document, as for the rest of time Oh, well, of course it’s good fishing.

If you think this article is helpful to you, you might as well ๐Ÿ‰ follow + like + favorite + comment + forward ๐Ÿ‰ support hey ~~๐Ÿ˜› github