標籤:

TypeScript 如何完美地書寫 React 中的 HOC?


這裡的HOC是指react的Higher-Order Components

假設我要給每個組件注入一個顏色值。

我們有了這麼一個高階組件(HOC)

const { Consumer: GetColor } = React.createContext("#000000");
const InjectColor = (Component) =&> class extends React.Component {
public render() {
return &{color =&> &}&;
}
};

現在就可以用這個HOC給任意組件注入color了

class NeedColor extends React.Component&<{ color: string }, {}&> {
public render() {
return &;
}
}
const Block = InjectColor(NeedColor);

但是上面這些代碼活生生把ts寫成了js,接下來我要開始寫ts了。

首先看不慣InjectColor的參數和返回值類型居然是any。所以我給InjectColor加上類型約束

const InjectColor = (Component: React.ComponentType&<{ color: string }&>): React.ComponentClass&<{}&> =&>
class extends React.Component& {
public render() {
return &{color =&> &}&;
}
};

但是很明顯這麼寫局限性太大了,如果我的NeedColor不只是需要color

class NeedColor extends React.Component&<{ color: string, width: number }, {}&> {
public render() {
return &;
}
}

所以還需要給InjectColor加上一個泛型(如果是用go寫的就不需要這個泛型了)

type Exclude& = T extends U ? never : T;
type Omit& = Pick&&>;
const InjectColor =
&(Component: React.ComponentType&): React.ComponentClass&&> =&>
class extends React.Component&, any&> {
public render() {
return &{color =&> &}&;
}
};

這裡的Omit是把color屬性從Props中去掉的意思。

現在看起來一切都是那麼完美。。。。。

但是我們可不止想注入color,我們還有store,router等等亂七八糟的要注入。

難道每個都這樣寫類型嗎?太不程序員了。

我們發現這些HOC有一個共同的特徵,

他們接受一個組件,返回一個被注入了一些props的組件。

所以可以這麼定義HOC的類型

type HOC& = &(Component: React.ComponentType&) =&> React.ComponentType&&>;

然後改寫我們的InjectColor

//這是用class
const InjectColor: HOC&<{ color: string }&> = Component =&> class extends React.Component& {
public render() {
return &{color =&> &}&;
}
} as React.ComponentClass&;
//這裡as一下是因為class的類型不會根據返回值類型來做推斷而是直接固定為一個匿名類型,也可以直接as any
//下面是function的寫法,會根據返回值類型做推斷,因為我們的InjectColor沒用到有狀態組件的功能所以也能這麼寫。
const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> &}&;

然後現在整個文件變成了這樣

type Exclude& = T extends U ? never : T;
type Omit& = Pick&&>;
type HOC& = &(Component: React.ComponentType&) =&> React.ComponentType&&>;

const { Consumer: GetColor } = React.createContext("#000000");

const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> &}&;

class NeedColor extends React.Component&<{ color: string, width: number }, {}&> {
public render() {
return &;
}
}

const Block = InjectColor(NeedColor);

render(&, document.getElementById("root"));

這還不夠,在實際項目中我們不止一個HOC

const InjectHeight: HOC&<{ height: number }&> = "細節不實現了" as any;

const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> &}&;

class NeedColor extends React.Component&<{ color: string, width: number, height: number,//新增height屬性 }, {}&> {
public render() {
return &

;
}
}

const Block = InjectHeight(InjectColor(NeedColor));

甚至還有connect這種

const connect = &(mapState: (state: any) =&> State): HOC& =&> "細節不實現了" as any;

class NeedColor extends React.Component&<{ color: string, width: number, height: number, text: string,//新增text屬性 }, {}&> {
public render() {
const { text, color, width, height } = this.props;
return &

{text}&;
}
}

const Block = connect(state =&> ({ text: state.text }))(InjectHeight(InjectColor(NeedColor)));

這樣組合函數,丑就一個字。。。。特別是參數更多,組合層數更多的時候。更丑

畢竟學過一丁點函數式編程,這時候自然而然就得想起compose函數啊

噹噹噹噹~~~~~

function compose&(hoc1: HOC&, hoc2: HOC&): HOC&;
function compose&(hoc1: HOC&, hoc2: HOC&, hoc3: HOC&): HOC&;
function compose&(hoc1: HOC&, hoc2: HOC&, hoc3: HOC&, hoc4: HOC&): HOC&;
function compose(...hocs: Array&) {
return (c: React.ComponentType) =&> hocs.reduce((acc, hoc) =&> hoc(acc), c);
}

別笑,雖然傻,但是能用,用上去看看效果

const Block = compose(
connect(state =&> ({ text: state.text })),
InjectHeight,
InjectColor)(NeedColor);

妙不可言。但是compose的實現好蠢啊。

放著oop的鏈式調用不使,非得用函數,吃飽了撐的。

改造一下

class Lifter& {
public static lift = &(hoc: HOC&): Lifter& =&> new Lifter([hoc]);
private constructor(private hocs: Array&&>) { }
public lift = &(hoc: HOC&): Lifter& =&> new Lifter([...this.hocs, hoc]);
public use: HOC& = c =&> this.hocs.reduce((acc: any, hoc) =&> hoc(acc), c);
}
const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.use(NeedColor);

看起來還不錯。再看一下整體效果

type Exclude& = T extends U ? never : T;
type Omit& = Pick&&>;
type HOC& = &(Component: React.ComponentType&) =&> React.ComponentType&&>;

class Lifter& {
public static lift = &(hoc: HOC&): Lifter& =&> new Lifter([hoc]);
private constructor(private hocs: Array&&>) { }
public lift = &(hoc: HOC&): Lifter& =&> new Lifter([...this.hocs, hoc]);
public use: HOC& = c =&> this.hocs.reduce((acc: any, hoc) =&> hoc(acc), c);
}

const { Consumer: GetColor } = React.createContext("#000000");

const InjectHeight: HOC&<{ height: number }&> = "細節不實現了" as any;

const connect = &(mapState: (state: any) =&> State): HOC& =&> "細節不實現了" as any;

const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> &}&;

class NeedColor extends React.Component&<{ color: string, width: number, height: number, text: string, }, {}&> {
public render() {
const { text, color, width, height } = this.props;
return &

{text}&;
}
}

const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.use(NeedColor);

render(&, document.getElementById("root"));

我們發現,NeedColor根本不需要用class寫組件,因為NeedColor是無狀態的。

再改一下

const NeedColor: React.SFC&<{ color: string, width: number, height: number, text: string, }&> = ({ text, color, height, width }) =&> &

{text}&;

還是感覺有點啰嗦,這個組件既然只有一個地方用,幹嘛不直接寫到use里去,還省了寫類型。

但是width並不是注入的,所以我們這裡要對HOC類型做一些改變

type OldHOC& = &(Component: React.ComponentType&) =&> React.ComponentType&&>;

type HOC& = &(Component: React.ComponentType&) =&> React.ComponentType&;

之前那個是官方給的HOC類型,下面那個是我喜歡用的HOC類型。

官方給的使用了Omit的類型缺陷在於你沒辦法單獨指定非注入的Props類型。

因為Props是繼承於InjectProps的,你指定的Props必須包含所有InjectProps的屬性。

也就是說使用OldHOC我需要添加一個非注入的width需要這麼寫:

const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.use&<{ width: number text: string,//一大堆不必要的 color: string,//一大堆不必要的 height: number,//一大堆不必要的 }&>(({ text, color, height, width }) =&> &

{text}&);

而使用第二個HOC只需要這麼寫:

const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.use&<{ width: number }&>(({ text, color, height, width }) =&> &

{text}&);

但是很明顯,官方不用我寫的這個是有原因的。

我用的這個的缺陷在於,ts的類型推導沒辦法在知道InjectProps和InjectPropsProps這兩個泛型的具體類型的情況下從PropsInjectProps中分離Props的類型。。。。。。

這個缺陷表現出來的現象是這樣的:

const C: React.SFC&<{ height: number, color: string }&> = "細節不實現了" as any;
const X = InjectColor(C);//X的類型為React.ComponentType&<{ height: number, color: string }&>

ts並沒有因為InjectProps有color,PropsInjectProps有color和height推斷出Props有height。

而是推斷出Props有height和color。。。。。

解決方法是永遠手動標非注入的屬性

const X = InjectColor&<{ height: number }&>(({ height, color }) =&> null);

兩種HOC類型寫法都有利有弊,喜歡用哪種就用哪種吧。

因為第一種對我繼續完善HOC的DSL來說弊端更多,所以我選擇第二種。

由於新的HOC類型,寫無狀態組件更加方便了,但是我要是想要有狀態怎麼辦呢?

相信HOC的力量,recompose庫了解一下,withState了解一下:

const withState =
&
(setName: SetName, name: Name, value: Value): HOC&<{ [K in keyof SetName]: (v: Value) =&> void } { [K in keyof Name]: Value }&> =&> "細節不實現了" as any;

使用效果:

const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.lift(withState("setCount", "count", 0))//注入了一個state
.use&<{ width: number }&>(({ text, color, height, width, setCount, count }) =&> &

setCount(count + 1)}&>{text} clicked {count} times&);

光有state怎麼行,有狀態組件還得有生命周期啊。

先說到這,洗澡睡覺下次再說。

整個文件現在是這樣的:

type Exclude& = T extends U ? never : T;
type Omit& = Pick&&>;

type HOC& = &(Component: React.ComponentType&) =&> React.ComponentType&;

class Lifter& {
public static lift = &(hoc: HOC&): Lifter& =&> new Lifter([hoc]);
private constructor(private hocs: Array&&>) { }
public lift = &(hoc: HOC&): Lifter& =&> new Lifter([...this.hocs, hoc]);
public use: HOC& = c =&> this.hocs.reduce((acc: any, hoc) =&> hoc(acc), c);
}

const { Consumer: GetColor } = React.createContext("#000000");

const InjectHeight: HOC&<{ height: number }&> = "細節不實現了" as any;

const withState =
&
(setName: SetName, name: Name, value: Value): HOC&<{ [K in SetName]: (v: Value) =&> void } { [K in Name]: Value }&> =&> "細節不實現了" as any;

const connect = &(mapState: (state: any) =&> State): HOC& =&> "細節不實現了" as any;

const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> &}&;

const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.lift(withState("setCount", "count", 0))
.use&<{ width: number }&>(({ text, color, height, width, setCount, count }) =&> &

setCount(count + 1)}&>{text} clicked {count} times&);

render(&, document.getElementById("root"));

出浴後的故事


請參考這篇文章:

https://levelup.gitconnected.com/ultimate-react-component-patterns-with-typescript-2-8-82990c516935?

levelup.gitconnected.com


不清楚有沒有完美的方法。個人最常用的是PropsOf,快速獲取被包裹組件Props的一個類型函數。用法,這行獲取了div的所有Props。

以及這裡的一些類型函數。基本可以把類型寫得比較簡潔。

還有用好自帶的React.forwardRef(),以及遵守官網上的其他最佳實踐。


自己項目里的一段,僅供參考。有幾個any是因為我不想折騰了,搞半天沒搞好直接any。。

雖然用any不太好,主要是我這還用redux包了一下,官網也沒有很好的解決方案不用any,但是不用的話太折騰浪費時間啊。該用any的時候還是用any的,別愣頭青,就是不用any才算完美?

匿了。


可以參考別人已經寫過的 declaration...

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/recompose/index.d.ts

然後自己也有寫過一點點

https://github.com/xialvjun/create-react-context/blob/v2/src/Context.ts


推薦閱讀:

有趣也有用的現代類型系統
angular 和 typescript 到底是否適合最佳實踐?
求教:TypeScript override 父類的get set 報錯?
你所不知道的 Typescript 與 Redux 類型優化
有哪些公司在使用或者準備使用Angular2?

TAG:TypeScript | React |