React: Component props

最近負責的專案遇到一點寫法上的瓶頸,codebase搞得亂七八糟的(太菜😅),所以開始看一些跟模式(pattern)有點相關的文章。

這篇筆記主要內容出自於 React component as prop: the right way™️,在講述如何傳遞元件(component)當作props。

會想傳遞components當作props,主要是為了方便分享state或props、並且讓程式碼更靈活更彈性,而文章作者將傳遞方式大致上分為三種模式:

  1. Pass components as Elements;
  2. Pass components as Components;
  3. Pass components as Functions

因為有在用React-Router-DOM,最熟悉的應該是第一種pattern,不過這三種傳遞方式各有優缺點,需要好好認識一下不同情境下、不同傳遞模式的使用方式。



1. Pass Component as an Element

第一種就是像下面React-Router-DOM傳遞 <Root /><ErrorPage /> 的方式:

這種傳遞元件方式的好處在於,可避免在不同元件之間傳遞過多props,使得程式碼變得更複雜。

以上圖React-Router-DOM的 <ErrorPage /> 為例,假設每個路徑有各自的錯誤頁,如果是把元件包在路徑的父元件 <Root />

// Root.tsx
export function Root({hasError, errorStatus, errorMessage, errorPageStyles}){

    if(hasError){
        return <ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} {...errorPageStyles}/>
    }
    ...
}

// App.tsx
export function App(){
    const [hasError, setHasError] = useState(false);

    return <Root hasError={hasError} errorStatus={errorStatus} errorMessage={errorMessage} errorPageStyles={errorPageStyles}/>
}

若是改成直接將 <ErrorPage /> 當作props傳遞,可省去透過 <Root /> 多傳遞一層的麻煩:

// Root.tsx
export function Root({errorPage}: {errorPage: ReactElement<ErrorPropss>}){

    if(hasError){
        return <>{errorPage}</>
    }
    ...
}

// App.tsx
export function App(){
    const [hasError, setHasError] = useState(false);

    return <Root hasError={hasError} errorPage={<ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} {...errorPageStyles}/>}
}

不過這種方式的缺點在於如果要加入預設值會比較麻煩,需要用到 React.cloneElement()

// Root.tsx
const defaultErrorMsg = 'Oops, There is something wrong!';

export function Root({errorPage}: {errorPage: ReactElemeny<ErrorPropss>}){
    const clonedErrorPage = React.cloneElement(errorPage, {
        errorMessage: errorPage.props.errorMessage || defaultErrorMsg
    })

    if(hasError){
        return <>{errorPage}</>
    }
    ...
}



2. Pass Component as a Component

第二種傳遞方式的名稱看起來有點繞口,直接來看基本的程式碼:

// Root.tsx
export function Root({ErrorPage}: {errorPage: ComponentType<ErrorPropss>}){

    if(hasError){
        return <ErrorPage />
    }
    ...
}

// App.tsx
const PassedErrorPage = () => <ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} errorPageStyles={errorPageStyles} />;

export function App(){
    const [hasError, setHasError] = useState(false);

    return <Root hasError={hasError} ErrorPage={PassedErrorPage} />
}

上面的 PassedErrorPage 並沒有傳遞任何props,如果想要加入props讓這段程式碼更靈活可以寫成:

const PassedErrorPage =
    (props) => <ErrorPage errorStatus={props.errorStatus} errorMessage={props.errorMessage} errorPageStyles={props.errorPageStyles} />;

或者簡單寫成:

const PassedErrorPage = (props) => <ErrorPage {...props} />;

第二種傳遞方式雖然第一眼看起來跟名稱一樣難懂,但在加入預設值方面變得方便許多:

// Root.tsx
const defaultErrorMsg = 'Oops, There is something wrong!';

export function Root({ErrorPage}: {errorPage: ComponentType<ErrorPropss>}){

    if(hasError){
        return <ErrorPage errorMessage={defaultErrorMsg}/>
    }
    ...
}

// App.tsx
const PassedErrorPage = (props) => <ErrorPage errorStatus={props.errorStatus} errorPageStyles={props.errorPageStyles} />;

export function App(){
    const [hasError, setHasError] = useState(false);

    return <Root hasError={hasError} ErrorPage={PassedErrorPage} />
}

如果像上面範例一樣沒加入 errorMessage props就會以預設值呈現,反之有加入就會覆蓋掉預設值。


3. Pass Component as a Function

第三種方式光看名稱也是讓人困惑😅,一樣直接看程式碼:

// Root.tsx
export function Root({renderErrorPage}: {renderErrorPage: () => ReactElement<ErrorPropss>}){
    const errorPage = renderErrorPage();

    if(hasError){
        return <>{errorPage}</>
    }
    ...
}

// App.tsx
export function App(){
    const [hasError, setHasError] = useState(false);

    return <Root hasError={hasError} renderErrorPage={() => <ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} errorPageStyles={errorPageStyles} />} />
}

如果要加入預設值:

// Root.tsx
const defaultErrorMsg = 'Oops, There is something wrong!';

export function Root({renderErrorPage}: {renderErrorPage: () => ReactElement<ErrorPropss>}){
    const errorPage = renderErrorPage({
        errorMessage: defaultErrorMsg,
    });

    if(hasError){
        return <>{errorPage}</>
    }
    ...
}

// App.tsx
const passedErrorPage = () => <ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} errorPageStyles={errorPageStyles} />;

export function App(){
    const [hasError, setHasError] = useState(false);

    return <Root hasError={hasError} renderErrorPage={passedErrorPage} />
}



小結

雖然作者最後有簡單分享她認為的三種傳遞方式使用時機,但看完還是不太了解第二、三種傳遞方式使用時機的差異😅。

不過個人見解覺得從以上程式碼可以發現,第一種傳遞方式比較能簡潔有力地表達出要傳遞的是元件;但如果要讓元件更有彈性地變化,例如客製化元件,很顯然地第二、三種傳遞方式能讓程式碼更靈活。

而相較於第一、二種傳遞方式,語法上第三種以函式傳遞元件的方式若是命名不好,會比較難一眼看出這是在傳遞元件當作props,不過在設定預設值方面的程式碼可閱讀性會比其他兩種高一點。