Render props and HOC

Render props

What is render props ?

  • 透過呼叫 function, 來產生 React element(s)
  • 與 Function Component 不同之處是
    • FC 是透過 JSX 的方式去產生 React elements
    • FC 的名稱必須是以大寫字母開頭

可以用在哪 ?

  • 當 Component 有共用邏輯時
  • Inversion control

    Inversion of control which is basically a mechanism for the author of the API to allow the user of the API to control how things work internally

    • Component 要 render 的東西由使用這個 Component 的 developer 決定時
      • react-router 的 Route Component 中的 ‘render’
      • react-window 的 row component
        • 知道要 render 多個 row, 但是 row 的形式交給 API user 決定

覆用共用邏輯

覆用邏輯: 抓取資料

1
2
3
4
5
6
7
8
9
10
11
12
13
const LeaderBoard = ({ children }) => {
// 處理一些抓資料的邏輯, 整理後得到: data

return children(data);
};

const App = () => {
return (
<LeaderBoard>
{data => data.map(item => <Row> ... </Row>)}
</LeaderBoard>
);
};

Inversion control

我要吃晚餐,但我不知道要吃什麼,交給你決定

1
2
3
4
5
6
7
8
const Me = ({ dinner }) => {
return (
<div>
My mouse:
{dinner(...)}
</div>
);
};
1
2
3
4
5
6
7
const App = () => {
return (
<div>
<Me dinner={(...) => <div>Steak</div>}/>
</div>
)
}

HOC (High order Component)

What is HOC ?

  • Input Component, Output Component
    • Component 的加工廠

可以用在哪 ?

  • 當 Component 有共用邏輯時

覆用共用邏輯

覆用邏輯: 抓取資料

1
2
3
4
5
6
7
8
9
function withData(Component) {
function Wrapper(props) {
// 處理一些抓資料的邏輯, 整理後得到: data

return <Component data={data} {...props} />
}

return Wrapper;
}
1
2
3
4
5
6
7
8
9
10
// In LeaderBoard.js
function LeaderBoard({ data }){
return (
<div>
{data.map(item => <Row>{item}</Row>)}
</div>
)
}

export default withData(LeaderBoard);

一些可能需要注意的地方

  • ref forwarding

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function withData(Component) {
    function Wrapper(props, ref) {
    // 處理一些抓資料的邏輯, 整理後得到: data

    return <Component data={data} ref={ref} {...props} />
    }

    return React.forwardRef(Wrapper);
    }
  • Display name in Dev tools

HOC 在 React dev-tools 時會呈現 Unkown 這個 name, 不利於我們 debug

1
2
3
4
5
6
7
8
9
10
11
function withData(Component) {
function Wrapper(props, ref) {
// 處理一些抓資料的邏輯, 整理後得到: data

return <Component data={data} ref={ref} {...props} />
}

Wrapper.displayName = `withToggle(${Component.displayName || Component.name})`;

return React.forwardRef(Wrapper);
}

Render props v.s. HOC

共同點

  • 覆用程式邏輯

HOC 的缺點

  • 容易遇到 Naming collision problem
    • 當包了多層的 HOC 時很難避免與察覺
  • 當包了多個 HOC 後, 無法明確知道 props 是從何而來
1
const FinalComponent = withA(withB(withC(withD(Component))))

只知道 FinalComponent 相較於 Component 會多出了許多 props, 但很難知道分別是從哪一層 HOC 加工而得

  • Static composition - Component 的形式在 Compile time 就定好且無法更動
    • they have no effect on what is rendered, only on what data is used

    • 當我們 call 了 withA(Component) 時就已決定

Render props 的優點

  • 可以用在 Inversion control 這個情境下
  • 任何 HOC 都可以改寫成 render props 方式
  • Dynamic composition - Component 的形式可以在 run time 時動態改變
    • the component itself is injected and we can decide what is rendered in the render time

[補個] Composition

  • 直白來說就是把 Component 們當做各個獨立的零件, 組合成我們的 App
  • 手段包含
    • 透過 props.children 來組成父子 Compoennt
    • 透過 props 來灌 Component 進去, 決定要 render 什麼
    • render props
    • HOC
    • …. etc.
  • 相對於 Composition, 另一種方式是 Inheritance
    • 相較於 Composition 各個單元獨立, Inheritance 偏向階層式關係

通通拿去做 Hooks

Hooks 的出現, 讓 覆用邏輯 這件事情可以更輕易地達成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// render props
class Toggle extends Component {
state = {
on: false,
toggle: () => (),
};

toggle = () => {
this.setState(({ on }) => ({ on: !on }));
};

render() {
return this.props.children({ on: this.state.on, toggle: this.toggle });
}
}

function App() {
return (
<Toggle>
{({on, toggle}) => <button onClick={toggle}>{on ? 'on' : 'offf'}</button>}
</Toggle>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
// hooks
function useToggle() {
const [on, setOn] = useState(false);
const toggle = () => setOn(on => !on);

return { on, toggle };
}

function App() {
const { on, toggle } = useToggle();
return <button onClick={toggle}>{on ? 'on' : 'offf'}</button>
}
  • 但是仍然有需要 render props 的時候 => 在建立一個提供 Inversion control 的 API Component 時

參考文獻