Foreword/Introduction

There are many side effects in our use of traditional Redux or MOBx state management. In the actual project process, we often need a concise and lightweight state management tool. Using a native Context or SATae is too simple. For event handling or interface calls, RXJS can choreographed these so that some of the code looks very concise. I personally am a fan of RXJS. I wanted to build a single source state management library based on RXJS. But instead of reinventing it, I found a great tool called Observabily-Store. However, this library cannot meet all my needs. We need to further enrich it. So first, let’s take a quick look at the observabily-store library.

Note: This document requires some basic knowledge of RXJS. I will not cover RXJS here.

Observabily-store function description

The principle of ObservableStore is simple, as illustrated by one of its official diagrams.

It has a single State source and branches out several services that operate on the same State source. It is then called and retrieved by multiple components.

Feature:

  1. It works with the three most popular frameworks (Angular, Vue,react) and supports TypeScript
  2. State data is immutable
  3. Single data stream
  4. A traceable history of state changes
  5. A lightweight application that is easy to use
  6. Any state changes can be subscribed to, and some subscription rules can be customized

How does observabily-Store implement a single data source

The BehaviorSubject of Rx BehaviorSubject is a BehaviorSubject of Rx BehaviorSubject to perform data responsiveness and instantiation in the global loading phase. ObservableStoreBase: ObservableStoreBase: ObservableStoreBase: ObservableStoreBase: ObservableStoreBase: ObservableStoreBase: ObservableStoreBase: ObservableStoreBase: ObservableStoreBase And we want to do our own custom State management class, all need to inherit from ObservableStore. So he could easily do a single data source. So it looks like this:

How to use observabily-store in React

I use React more, take react as an example, other frameworks will not elaborate, you see the official documentation.

1. First we need to write a Class that inherits from ObservableStore, and then we need to write the constructor.

class CustomersStore extends ObservableStore {
    constructor() {
        super({ trackStateHistory: true });
        //trackStateHistory: True represents the history of states that can be tracked
    }
    
    fetchCustomers() {
        // Call the interface
        // Of course you can use a third party (Axios, Ky, etc.)
        return fetch('/customers')
            .then(response= > response.json())
            .then(customers= > {
                this.setState({ customers }, 'GET_CUSTOMERS');
                return customers;
            });
    }

    getCustomers() {
        let state = this.getState();
        // pull from store cache
        if (state && state.customers) {
            return this.createPromise(null, state.customers);
        }
        // doesn't exist in store so fetch from server
        else {
            return this.fetchCustomers(); }}getCustomer(id) {
        return this.getCustomers()
            .then(custs= > {
                let filteredCusts = custs.filter(cust= > cust.id === id);
                const customer = (filteredCusts && filteredCusts.length) ? filteredCusts[0] : null;                
                this.setState({ customer }, 'GET_CUSTOMER');
                return customer;
            });
    }

    createPromise(err, result) {
        return new Promise((resolve, reject) = > {
            returnerr ? reject(err) : resolve(result); }); }}export default new CustomersStore()
Copy the code
  1. We then subscribe in useEffect in the React component, and stateChanged is an Observable of RXJS. For any data changes, simply subscribe to stateChanged. The following code is about using React.
export const MyComponent = () = >{
     const [customers,setCustomers] = useState([])
     useEffect(() = >{
         CustomersStore.getCustomers();// Get full data
         // Subscription data changes
         CustomersStore.stateChanged.subscribe(state= >setCustomers(state.customers))
     },[])
     return customers.map(customer= ><div>{customer.name}</div>)}Copy the code

There is also a globalStateChanged inside ObservableStore, which is an Observable from ObservableStoreBase that can subscribe to global state changes. StateChanged can only subscribe to changes in state that the current class operates on.

The ObservableStore requirement is that each functional component needs to be supported by a corresponding Store class(or Service). You can put part of your business logic in there that works with your data code. Redux and Mobx are similar.

There are a few points I would like to improve on ObservableStore.

  1. Now that you have a history trace state record, there are no state rollback and recovery operations (which is handy in the editor application).
  2. In React, we need to wrap the useState operation with higher-order functions.
  3. Sate cache, we want to be able to control separately and have a cache time
  4. React-hooks are convenient. We can develop some powerful custom hooks that use state instead of sate
  5. RXJS is so powerful that we can package our own EventEmit tool.
  6. With RXJS, can we do some richer encapsulation of the call interface, providing anti-shake or throttling, interface error, automatic retry and other functions

Here’s how I did it.

Build your own state management tool based on ObservableStore

First put my Felix – Obs project address: https://github.com/tanghui315/felix-obs

1. State Rollback and recovery operations

If trackStateHistory is true, I can undo and restore data using my library. How do I do that? According to the above example, the CustomersStore inherits my library’s FelixObservableStore, see the following code demonstration

  class CustomersStore extends FelixObservableStore{
       / /... The code is omitted
  }
  const customerStore = new CustomersStore()
  customerStore.prevState() // Return the data to the previous state. If it reaches the top, do nothing
  customerStore.nextState() // Restore data to the next state, if at the end, do nothing
Copy the code

PrevState, like nextState, triggers the subscribe operation.

2. Encapsulate useState with higher-order functions

After inheriting FelixObservableStore, we have a connect higher-order function that we can use to inject state data into the component Props. The setSate operation is not presented to the user. The code looks like this.

 // Higher-order processing functions
 public connect(CMP: ComponentType<any>): FunctionComponent {
        return (props: unknown): JSX.Element => {
            const [state, setState] = useState(this.getState())
            useEffect(() = > {
                const subject = this.stateChanged.subscribe(s= > setState(s))
                return function () {
                    subject.unsubscribe()
                }
            }, [])
            return useMemo(() = > <CMP {. props} state={{ . state}} / >, [state])
        }
    }


Copy the code

For example, the component in the first example above could be used like this

export const MyComponent = CustomersStore.connect(({ state }) = >{
     useEffect(() = >{
         CustomersStore.getCustomers();// Get full data}, [])return state.customers.map(customer= ><div>{state.customer.name}</div>)})Copy the code

This code is a lot cleaner. This can be improved, of course, by selectively passing in the key of the object.

3. Sate cache, we want to be able to control separately, and have a cache time

FelixObservableStore independently encapsulates a dispatch and dispatchWithTimerClean method for modifying data. DispatchWithTimerClean will cache the data at a certain time. Let’s say we want to clear the cache after 1 minute. So here’s how it works:

 //dispatchWithTimerClean(key: string, state: T, cleanTime: number)
 //key Indicates the object key
 //state indicates the data to be modified
 //cleanTime indicates the cache expiration time in seconds
 CustomersStore.dispatchWithTimerClean("customer",state,60)
Copy the code

4. Customize useObservableStore hooks function

For lightweight components, we want our Observable to be as easy to use as useState. It also maintains a unique data source that can be retrieved and modified from anywhere. So what do I do? I’ll post up the code and then I’ll talk about it.

/ * * *@descriptionA custom hook function based on Rx stream data processing and state management@param {T} InitState Indicates the initialized state data value *@param {obsFunc} Additional Observalbe observalbe is an observalbe object method that allows for fine-grained control over data using RXJS manipulation methods@param {customKey} This parameter is optional. If this parameter is not worn, use the key * randomly generated by the system@return {*} Returns an array that is consistent with the useState operation */
function useObservableStore<T> (initState: T, additional? : obsFunc<T> |null, customKey? :string) :T, (state: T) = >void.string] {
    const KEY = useConstant(() = > customKey ? customKey : Math.random().toString(36).slice(-8))
    const [state, setState] = useState(initState)
    const store = useConstant(() = > new FelixObservableStore(KEY, initState))
    const $input = new BehaviorSubject<T>(initState)
    useEffect(() = > {
        let customSub: Subscription
        if (additional) {
            customSub = additional($input).subscribe(state= > {
                state && store.dispatch(KEY, state)
            })
        }
        const subscription = store.stateChanged.subscribe(state= > {
            state && setState(state[KEY])

        })
        return function () {
            subscription.unsubscribe()
            customSub && customSub.unsubscribe()
            $input.complete()
        }
    }, [])

    return [state, (state) = > store.dispatch(KEY, state), KEY]
}

Copy the code

Why do we have the concept of key? I may be confused when I first see it, so I want to explain some things. ObservableStore essentially builds a large tree of objects. It is like a map object, and the corresponding state can be found through the key. However, the data that is modified and saved each time is maintained immutability. But you don’t have to. The data is reactive. Any adjustment to Dispatch triggers the subscribe operation of the data. No diff operations between objects are required. Take a look at the following example:

import { from } from 'rxjs'
import { map, switchMap, delay } from 'rxjs/operators'

export const MyComponent = () = >{
     // Change useState to useObservableStore
     // Use the second argument to pass itself rx processing
     const [customers,setCustomers,key] = useObservableStore([],($obs) = > $obs.pipe(
        delay(2000),// Delay output by 2 seconds to highlight the loading effect
        switchMap(() = > from(fech("get/customers")).pipe( // Handle interface operations directly
          map(result= > result.data)  // Fetch interface data and pass it to store))))return customers.map(customer= ><div>{customer.name}</div>)}Copy the code

For lightweight components, we don’t need to build a separate Store. The data problem can be solved directly using useObservableStore. And the second parameter, the RXJS Observable object function, is very handy. It can call the interface directly, automatically bind data to the key, or handle DOM events in this way. Having this entry makes the hooks function very flexible.

If I have another component, I can call Dispatch by key or use setCustomers to modify the data. If we use a custom Key. So you don’t have to pass setCustomers. Call Dispatch directly, enabling cross-component communication across scenarios.

5. Attach an EventEmiter tool

Publish and subscribe is an essential feature in large applications. This can easily be done with RXJS. FelixObservableStore incidentally provides one. The following is an example:

FelixObservableStore.getEmitter.listen("event name".(data) = >{})  // Listen for an event
FelixObservableStore.getEmitter.emit("event name",data) // Send an event
FelixObservableStore.getEmitter.removeListen("event name") // Delete a single listener event
FelixObservableStore.getEmitter.dispose() // Delete all listening events
Copy the code

6. Fine-grained interface control

Interface operations are often the most frequently used state management scenarios. We need an independent package to define some commonly used functions of the interface, and only need to transmit the configuration of response, without bringing complexity to users. Lightweight solution of problems, while giving users maximum freedom.

// Define the configuration type
interfaceAjaxSetting { initData? :any.// Give the interface data an initial valuedebounceTimes? :number.// Anti-shake configuration in millisecondsthrollteTimes? :number.// Throttling is configured in millisecondsretryCount? :number.// Interface error, retry timesinitialDelayTimes? :number.// Retry interval delay in millisecondsfetchCacheTimes? :number // Interface data cache time, in seconds
}
// Call the interface
//key indicates the key of the data
//handler a function that returns promise
//isAuto is true to handle the subscribe to FelixObservableStore, false to handle the subscribe manually, and returns an Observable
//setting indicates the transfer request configuration
public fetchData(key: string.handler : ajaxFunc<any>, isAuto: boolean = true, setting? : AjaxSetting)Copy the code

This is the basic configuration and function definition required to define fetchData. Let’s use the above example to try it out:

// Modify the above example
export const MyComponent =  CustomersStore.connect(({ state }) = >{
      useEffect(() = >{
          CustomersStore.fetchData("customer",fech("get/customers"),true, {initData: [].debounceTimes:3000.retryCount:3.initialDelayTimes:1000.fetchCacheTimes:60
          })
          FetchData ("customer",fech("get/customers"),false,{initData:[], debounceTimes:3000, retryCount:3, initialDelayTimes:1000, }).subscribe(data=>{... }) * /}, [])return state.customers.map(customer= ><div>{state.customer.name}</div>)}Copy the code

This request interface function is not much richer. Of course, if the third argument is false, you can control the subscribe yourself, you can be very flexible.

conclusion

Although I have used the tool in real projects, I am too busy to maintain it on a regular basis. Interested partners, you can maintain and improve together, welcome everyone to actively submit PR, I am very willing to pass, haha. Finally once again give Felix – Obs project address: https://github.com/tanghui315/felix-obs