helux,一個鼓勵服務注入的響應式react狀態庫

鍾正楷發表於2023-04-15

關於helux

helux是一個鼓勵服務注入,並支援響應式變更react的全新資料流方案,它的前身是concent(一個類vue開發體驗的高效能狀態管理框架),但concent自身因為需要相容class和function保持一致的語法,且為了對其setup功能,導致內部程式碼量實在太大,壓縮後有70多kb,api暴露得也非常多,導致學習難度急劇上升,為了更符合現在流行的DDD圍繞業務構建領域模型的編碼趨勢,helux一開始就設計為鼓勵服務注入支援響應式變更支援依賴收集的輕量級react資料流方案。

它擁有以下優勢:

  • 輕量,壓縮後2kb
  • 簡單,僅暴露7個api,高頻使用的僅createShareduseObjectuseSharedObjectuseService4個介面
  • 高效能,自帶依賴收集
  • 響應式,支援建立響應式物件,在檢視之外變更物件將同步更新檢視
  • 服務注入,配合useService介面輕鬆控制複雜業務邏輯,總是返回穩定的引用,可完全避免useCallback依賴煩擾了
  • 狀態提升0改動,所以地方僅需將useObject換為useSharedObject即可提升狀態共享到其他元件
  • 避免forwordRef 地獄,內建的exposeService模式將輕鬆解決父掉子時的ref轉發晦澀理解問題和傳染性(隔代元件需要層層轉發)
  • ts友好,100% ts 編寫,為你提供全方位型別提示

3.gif

該gif圖和以下所有api均對應有線上示例1示例2,歡迎fork並修改體驗。

為什麼起名helux,雖然內心上我是把它作為concent v3版本去開發的,但因為它的變化實在太大,除了依賴收集不繼承任何concent的特性,同時它也是伴隨我開發的hel-micro誕生一款作品,我期望它成為 hel-micro 生態的 luxury 級別的貢獻,就將 hel-micro 和 luxury 兩個詞拼一起成為了 helux

歡迎點星關注helux,它雖然較新,但已在我自己的使用場景中發揮功不可沒的作用,現已加入hel-micro生態大倉,期待能成為你願意挑選的一款可心資料流方案。

快速上手

極致的簡單是helux最大的優勢,瞭解以下6個api後,你可以輕鬆應付任何複雜場景,最大的魅力在於useSharedObjectuseService兩個介面,且看如下api介紹,或訪問線上示例1示例2fork並修改來體驗。

useObject

使用 useObject 有兩個好處

  • 1 方便定義多個狀態值時,少寫很多 useState
  • 2 內部做了 unmount 判斷,讓非同步函式也可以安全的呼叫 setState,避免 react 出現警告 :
    "Called SetState() on an Unmounted Component" Errors
// 基於物件初始化一個檢視狀態
const [state, setState] = useObject({a:1});
// 基於函式初始化一個檢視狀態
const [state, setState] = useObject(()=>({a:1}));

useForceUpdate

強制更新當前元件檢視,某些特殊的場景可以使用它來做檢視重重新整理

const forUpdate = useForceUpdate();

createSharedObject

建立一個共享物件,可透傳給 useSharedObject,具體使用見 useSharedObject

// 初始化一個共享物件
const sharedObj = createSharedObject({a:1, b:2});
// 基於函式初始化一個共享物件
const sharedObj = createSharedObject(()=>({a:1, b:2}));

createReactiveSharedObject

建立一個響應式的共享物件,可透傳給 useSharedObject

// 初始化一個共享物件
const [reactiveObj, setState] = createReactiveSharedObject({a:1, b:2});

sharedObj.a = 111; // 任意地方修改 a 屬性,觸發檢視渲染
setSharedObj({a: 111}); // 使用此方法修改 a 屬性,同樣也能觸發檢視渲染,深層次的資料修改可使用此方法

createShared

函式簽名

function createShared<T extends Dict = Dict>(
  rawState: T | (() => T),
  enableReactive?: boolean,
): {
  state: SharedObject<T>;
  call: <A extends any[] = any[]>(
    srvFn: (ctx: { args: A; state: T; setState: (partialState: Partial<T>) => void }) => Promise<Partial<T>> | Partial<T> | void,
    ...args: A
  ) => void;
  setState: (partialState: Partial<T>) => void;
};

建立一個響應式的共享物件,可透傳給 useSharedObject,它是createReactiveSharedObjectcreateSharedObject的結合體,當需要呼叫脫離函式上下文的服務函式時(即不需要感知元件props時),可使用該介面,第二位引數為是否建立響應式狀態,為 true 時效果同 createReactiveSharedObject 返回的 sharedObj

 const ret = createShared({ a: 100, b: 2 });
 const ret2 = createShared({ a: 100, b: 2 }, true); // 建立響應式狀態
 // ret.state 可透傳給 useSharedObject
 // ret.setState 可以直接修改狀態
 // ret.call 可以呼叫服務函式,並透傳上下文

以下將舉例兩種具體的定義服務函式的方式,之後使用者便可在其他其他地方任意呼叫這些服務函式修改共享狀態了,如需感知元件上下文(例如props),則需要用到下面介紹的useService介面去定義服務函式。

// 呼叫服務函式第一種方式,直接呼叫定義的函式,配合 ret.setState 修改狀態
function changeAv2(a: number, b: number) {
   ret.setState({ a, b });
}
*
// 第二種方式,使用 ret.call(srvFn, ...args) 呼叫定義在call函式引數第一位的服務函式
function changeA(a: number, b: number) {
   ret.call(async function (ctx) { // ctx 即是透傳的呼叫上下文,
     // args:使用 call 呼叫函式時透傳的引數列表,state:狀態,setState:更新狀態控制程式碼
     // 此處可全部感知到具體的型別
     // const { args, state, setState } = ctx;
     return { a, b };
   }, a, b);
 }

useSharedObject

函式簽名

function useSharedObject<T extends Dict = Dict>(sharedObject: T, enableReactive?: boolean): [
  SharedObject<T>,
  (partialState: Partial<T>) => void,
]

接收一個共享物件,多個檢視裡將共享此物件,內部有依賴收集機制,不依賴到的資料變更將不會影響當前元件更新

const [ obj, setObj ] = useSharedObject(sharedObj);

useSharedObject預設返回非響應式狀態,如需要使用響應式狀態,透傳第二位引數為true即可

const [ obj, setObj ] = useSharedObject(sharedObj);
// now obj is reactive
 setInterval(()=>{
  state.a = Date.now(); // 觸發檢視更新
 }, 2000);

useService

函式簽名

/**
 * 使用用服務模式開發 react 元件:
 * @param compCtx
 * @param serviceImpl
 */
function useService<P extends Dict = Dict, S extends Dict = Dict, T extends Dict = Dict>(
  compCtx: {
    props: P;
    state: S;
    setState: (partialState: Partial<S>) => void;
  },
  serviceImpl: T,
): T & {
  ctx: {
    setState: (partialState: Partial<S>) => void;
    getState: () => S;
    getProps: () => P;
  };
}

它可搭配useObjectuseSharedObject一起使用,會建立服務物件並返回,該服務物件是一個穩定的引用,且它包含的所有方法也是穩定的引用,可安全方法交給其它元件且不會破會元件的pros比較規則,避免煩惱的useMemouseCallback遺漏相關依賴

搭配useObject

function DemoUseService(props: any) {
  const [state, setState] = useObject({ a: 100, b: 2 );
  // srv本身和它包含的方法是一個穩定的引用,
  // 可安全的將 srv.change 方法交給其它元件且不會破會元件的pros比較規則
  const srv = useService({ props, state, setState }, {
    change(a: number) {
      srv.ctx.setState({ a });
    },
  });
  
  return <div>
    DemoUseService:
    <button onClick={() => srv.change(Date.now())}>change a</button>
  </div>;
}

搭配useSharedObject時,只需替換useObject即可,其他程式碼不用做任何改變

+ const sharedObj = createSharedObject({a:100, b:2})

function DemoUseService(props: any) {
-  const [state, setState] = useObject({ a: 100, b: 2 );
+  const [state, setState] = useSharedObject(sharedObj);

getState 和 getProps

stateprops 是不穩定的,所以服務內部函式取的時候需從srv.ctx.getStatesrv.ctx.getProps

// 抽象服務函式
export function useChildService(compCtx: {
  props: IProps;
  state: S;
  setState: (partialState: Partial<S>) => void;
}) {
  const srv = useService<IProps, S>(compCtx, {
    change(label: string) {
      // !!! do not use compCtx.state or compCtx.state due to closure trap
      // console.log("expired state:", compCtx.state.label);

      // get latest state
      const state = srv.ctx.getState();
      console.log("the latest label in state:", state.label);
      // get latest props
      const props = srv.ctx.getProps();
      console.log("the latest props when calling change", props);

      // your logic
      compCtx.setState({ label });
    }
  });
  return srv;
}

export function ChildComp(props: IProps) {
  const [state, setState] = useObject(initFn);
  const srv = useChildService({ props, state, setState });
}

 return (
    <div>
      i am child <br />
      <button onClick={() => srv.change(`self:${Date.now()}`)}>
        change by myself
      </button>
      <h1>{state.label}</h1>;
    </div>
  );

exposeService

當孩子元件props上透傳了exposeService函式時,useService 將自動透傳服務物件給父親元件,是一種比較方便的逃離forwardRef完成父調子的模式

import { ChildSrv, Child } from "./Child";

function App() {
  // 儲存孩子的服務
  const childSrv = React.useRef<{ srv?: ChildSrv }>({});
  const seeState = () => {
    console.log("seeState", childSrv.current.srv?.ctx.getState());
  };

  return (
    <div>
      <button onClick={() => childSrv.current.srv?.change(`${Date.now()}`)}>
        call child logic
      </button>
      <Child
        unstableProp={`${Date.now()}`}
        exposeService={(srv) => (childSrv.current.srv = srv)}
      />
    </div>
  );
}

結語

helux是把concent內部精華全部萃取提煉再加工後的全新作品,期望能得到你的喜歡。❤️

相關文章