ReactNative 开发之 HOC (高阶组件)

220 阅读4分钟

多人协作开发项目时最容易出现的问题如下

  1. 主页面和详情页非同一人所写、内外页面数据不同步。例如发现页视频评论数为28,进入详情页后发布评论、详情页内评论数为29。返回后主页面评论数依然为28。
  2. 同一个Query接口、不同主页次页所传参数数量不一致、导致返回数据不同。本身需求是应该取出完全相同的数据、所传参数数量应当相同。
  3. 重复编写 Query 查询接口代码、严重的情况是使用同一接口不同按钮因为样式不同都各自写了一遍Query代码。
  4. 例如粉丝、关注两个页面,同为列表,都有下拉刷新、上啦加载更多。仅仅查询的GQL不一样。两个页面都各自写一遍查询逻辑、刷新、加载逻辑。
  5. 还有更多场景这里不一一介绍
HOC (高阶组件)

本质: 接收一个组件、返回一个组件。其本身是一个function (函数)

可以返回class组件、也可以返回function组件、并无严格规定。

基本结构如下:

function withMovieList(WrapperedComponent){
    return function MovieListComponent(props){
        // ... logic code here
        // 🅿️1️⃣
        return <WrapperedComponent {...props}/>
    }
}

基本用法如下:

const MovieListComponentView = (props) => {
    //... own logic code here
    return (
        <YourUIComponents></YourUIComponents >
    )
}

const MovieListComponent = withMovieList(MovieListComponentView);

const HomePage = (props) => {
    return (
        <View>
            <MovieListComponent enableRefresh={true}/>
        </View>
    )
}

注意, 在 🅿️1️⃣ 处、就可以把公共逻辑写在那里、并将相应的回调、数据通过props传递给 WrapperedComponent

在 HomePage 传递给 MovieListComponent 的 enableRefresh 属性,会直接出现在

withMovieList返回的函数组件props中。

下面将通过一个中低等复杂度例子来说明高阶组件的实际用法,类似的会在新视频工厂App模板项目中大量出现。

一 . 粉丝、关注列表

粉丝列表Query GQL :UserFollowers

关注列表Query GQL :UserFollowings

接收参数: { user_id: number; count: number; page: number }

高阶组件业务说明、需要为其返回的组件提供用户列表数据data ,提供 refetch 和 fetchMore 方法,提供能够获取到当前查询的页码的方法或直接属性。

高阶组件代码如下:

interface UserRelationListComponentProps {
    count?: number;
    initialPage?: number;
    user_id?: number;
}

interface UserRelationListWrapperedComponentProps {
    loadRelationList:any;
    relationListQueryPage:number;
    updateRelationListQueryPage:(page:number) => void;
    isRelationListQueryCalled: boolean;
    isRelationListQueryLoading: boolean;
    relationListQueryError: any;
    relationListQueryData: any;
    relationListQueryRefetch: () => void;
    relationListQueryFetchMore: () => void;
}

const withUserRelationsList = (
    WrapperedComponent,
    GQLDocument: any,
    options?: { fetchImmediately: boolean; }
) => {

    const fetchImmediately = options.fetchImmediately ?? false;
    return function RelationListComponent(props) {
        const count = props.count ?? 15;
        const page = useRef(props.initialPage ?? 1);
        const user_id = props.user_id ?? -1;
        const injectedProps = useMemo(() => {
            let _propsWithoutHocProps = { ...(props) };
            for (let del of ['count', 'initialPage', 'user_id']) {
                delete _propsWithoutHocProps[del];
            }
            return _propsWithoutHocProps;
        }, [props])
        const [loadRelationList, { called, loading, error, data, refetch, fetchMore }] = useLazyQuery(
            GQLDocument,
            { variables: { page, count, user_id }, fetchPolicy: 'network-only' }
        );

        const relationListQueryFetchMore = () => {
            page.current += 1;
            return fetchMore({ variables: { page: page.current } })
        }

        const relationListQueryRefetch = () => {
            page.current = 1;
            return refetch({ variables: page.current });
        }

        useEffect(() => {
            if (fetchImmediately && user_id !== -1) loadRelationList();
        }, [])

        return <WrapperedComponent
            {...injectedProps}
            loadRelationList={loadRelationList}
            relationListQueryPage={page.current}
            updateRelationListQueryPage={(p: number) => { page.current = p; }}
            isRelationListQueryCalled={called}
            isRelationListQueryLoading={loading}
            relationListQueryError={error}
            relationListQueryData={data}
            relationListQueryRefetch={relationListQueryRefetch}
            relationListQueryFetchMore={relationListQueryFetchMore}
        />
    }
}

从上面代码可以看出几乎和GQL相关的数据都通过props传给了高阶函数的第一个参数的这个组件。

下面是 关注用户列表 页面的相关代码

  1. 关注用户列表组件

下面UI部分组件并没有进一步进行解耦、实际上应该将FlatList 和 renderItem 部分再抽象出来,并编写相关数据转换代码,将这里拿到的数据转换成UI组件可展示的数据相关属性。

//此为UI部分组件

const FollowListComponent = (props:UserRelationListWrapperedComponentProps) => {
    const { relationListQueryData,relationListQueryFetchMore } = props;
    const onEndReached = () => {
        relationListQueryFetchMore();
    }

    return (
        <FlatList
            data={relationListQueryData}
            renderItem={({ item, index }) => <View />}
            onEndReached={onEndReached}
        />
    )
}

var UserFollowers: any; //这里替换为引入的 GQL
interface FollowersListProps extends UserRelationListComponentProps {}

//该FollowersList是包含了UI、Data的关注列表组件

const FollowersList: (props:FollowersListProps) => any = withUserRelationsList(
    FollowersListComponent,
    UserFollowers,
    { fetchImmediately: true }
);

下面是关注列表页面

const FollowersPage = (props) => {

    const user_id = props.router.params.user_id;

    return (
        <View style={{ flex: 1 }}>
            <FollowersList user_id={user_id}/>
        </View>
    )
}
  1. 粉丝列表组件
//此为UI部分组件

var UserFollowings: any; //这里替换为引入的 GQL
interface FollowingsListProps extends UserRelationListComponentProps {}

//该FollowersList是包含了UI、Data的关注列表组件
const FollowingsList: (props:FollowingsListProps) => any = withUserRelationsList(
    FollowingsListComponent,
    UserFollowings,
    { fetchImmediately: false }
);

这里注意到, FollowListComponent 为 粉丝和关注列表公用组件、因为两个列表在实际业务中几乎仅 renderItem 渲染的组件有细微区别、因此这里可以完全公用,实际项目中将renderitem 渲染的 item 通过 props传给FollowListComponent即可。

粉丝页面

const FollowingsPage = (props) => {

    const user_id = props.router.params.user_id;

    return (
        <View style={{ flex: 1 }}>
            <FollowingsList user_id={user_id}/>
        </View>
    )
}

从上面HOC的用法可以看出、项目中通过将GQL 和 HOC 层紧密结合、UI组件就可以完全解放,无需关心后端的GQL结构是什么。在使用HOC包裹的组件时加一层 转换 函数即可,如下

// GQL返回结构

{
    data: {
        id: 1,
        username: 'lily',
        age: 12
    }
}

纯UI组件如下

const PlainListItemOne = (props:{
    title?:string;
    subtitle?:string;
}) => {

    const title = props.title ?? '';
    const subtitle = props.subtitle ?? '';
    return (
        <View>
            <Text>{title}<Text/>
            <Text>{subtitle}</Text>
        </View>
    )
}

在编写使用HOC包裹的组件时用到该 PlainListItemOne 时需要的转换函数

function convert(value){
    return ({
        title: value.data.username,
        subtitle: value.data.age
    })

}