React render props 与 children function

74 阅读3分钟

在日常的开发中,常常有一些封装与抽象的需求,本篇文章记录两种较常用的组件设计方法, render porps与children function, 可以用于实现逻辑与UI的解耦,让子组件封装通用的逻辑,父组件实现UI的渲染

1. render props

基本的render props, 父组件在使用子组件的时候传递render函数,自定义渲染逻辑,子组件中进行逻辑操作,执行render函数

const Render = () => {
    const listItem = ["item-1", "item-2", "item-3"];
        return (
            <div>
                <Child
                    items={listItem}
                     render={(item: string) => {
                         return <div key={item}>{item}</div>;
                   }}
                />

        </div>
    );
};

子组件中

const Child: FC<{
    render: ({ item }: { item: string }) => React.ReactNode;
    items: string[];
}> = ({ items, render }) => {
    return (
        <div>
            {items.map((item) => {
            // 执行渲染函数
                return render({ item });
             })}
        </div>
    );
};

Render组件中传递给子组件中的render其实可以看成一个组件

const RenderItem: FC<{ item: string }> = ({ item }) => {
    return <div>{item}</div>;
};

父组件可以修改为

const Render = () => {=
    const listItem = ["item-1", "item-2", "item-3"];
        return (
            <div>
                <Child
                    items={listItem}
                     render={RenderItem}
                />

        </div>
    );
};

利用render props封装DataFetcher组件,分离网络请求与页面渲染


const DataFetcher: FC<{
    url: string;
    render: ({
        data,
        loading,
        error,
    }: {
        data: any;
        loading: boolean;
        error: any;
     }) => React.ReactNode;
}> = ({ url, render }) => {

    const [data, setData] = useState<DATA[] | null>(null);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState("");

    useEffect(() => {
        setLoading(true);
        fetch(url, {
        method: "GET",
    })
    .catch((err) => {
        setError(err);
    })
    .finally(() => {
        setLoading(false);
        // dmeo中mock一个假数据
        setData([
        {
            id: 1,
            name: "wangwu",
        },
        {
            id: 2,
            name: "zhangliu",
        },
        ]);
        });
        }, []);
    
      // render props 渲染UI
      return render({ data, loading, error });
};

使用DataFetcher组件,获取数据,渲染UI

const DataFetchDemo = () => {
    return (
        <DataFetcher
            url="xxxx"
            render={({ data, loading, error }) => {
                if (loading) return <div>Loading</div>;
                if (error) return <div>someThing error</div>;
                return (
                   <>
                    {data &&
                        data.map((d: DATA) => {
                           return <div key={d.id}>{d.name}</div>;
                        })}
                    </>
                  );}}
        />
        );
};

2. children function

  1. 在React中, chilren的类型常是一个 ReactNode, 可以用作类似vue中过的插槽使用,方便父组件与子组件的组合
  2. 在children function中也可以被看成一个函数,返回一个ReactNode, 即(params) => ReactNode, 也可以将children function看成render props的一种形式

利用children function实现一个简单的搜索功能


export const UserFilterDemo = () => {
    const userData = [
        {
            id: 1,
            name: "wang",
        },
        {
            id: 2,
            name: "zhang",
        },
        {
            id: 3,
            name: "li",
        },
    ];

    return (
        <UserFilter>
        // children作为一个函数,接受一个参数,实现自定义的渲染,类似于render props
        // 其可以拿到子组件作用域中传递的数据 filter, 也可以拿到父组件作用域下的数据 userData
            {(filter) => {
                return userData
                    .filter((user) => user.name.includes(filter))
                    .map((item) => {
                return <div key={item.id}>{item.name}</div>;
            });
        }}
        </UserFilter>
    );
};

UserFilter子组件

const UserFilter = ({
    children,
    }: {
    children: (filter: string) => React.ReactNode;
}) => {
    const [filter, setFilter] = useState("");

    return (
        <div>
            <input value={filter} onChange={(e) => setFilter(e.target.value)} />
            <div>{children(filter)}</div>
        </div>
    );
};

搜索组件实现效果 image.png

image.png

利用children function实现分页组件


type Data = {
    id: number;
    text: string;
};

const Pagination: FC<{
    children: (data: Data[]) => React.ReactNode;
    pageSize?: number;
}> = ({ children, pageSize = 10 }) => {

    // 页码索引
    const [pageIndex, setPageIndex] = useState(1);
    const [data, setData] = useState(() => faker.slice(0, pageSize));

    const maxPageIndex = Math.ceil(faker.length / pageSize);

    // 模拟分页请求数据
    useEffect(() => {
        setData(faker.slice((pageIndex - 1) * pageSize, pageIndex * pageSize));
    }, [pageIndex]);

    const handleRight = () => {
        setPageIndex((p) => {
            if (p === maxPageIndex) {
            return p;
            } else {
            return p + 1;
            }
         });
    };
 
    const handleLeft = () => {
        setPageIndex((p) => {
            if (p === 1) {
                return p;
            } else {
                return p - 1;
            }
        });
    };
  
return (
    <div className="paginator-container">
        <div className="pagintion-content">{children(data)}</div>
    <div>
    <div className="pagination-controls">
        <button onClick={handleLeft} disabled={pageIndex === 1}>
            &lt;
        </button>
        <div className="pagiination-input">
            <input
            value={pageIndex}
                onChange={(e) => {
                    const target = Number(e.target.value);
                    if (target >= 1 && target <= maxPageIndex) {
                        setPageIndex(target);
                    }
                }}
            />
            / {maxPageIndex}
        </div>
        <button onClick={handleRight} disabled={pageIndex === maxPageIndex}>
            &gt;
        </button>
        </div>
    </div>
    </div>
);
};

可以假定数据输入如下

const faker: Data[] = (() => {
    const res = [];
    for (let i = 0; i < 100; i++) {
        res.push({
                id: i,
                text: `demo-${i}`,
            });
        }
    return res;
})();

分页组件使用

function App() {
    return (
        <>
            <Pagination>
              // 拿到子组件传递的数据,进行自定义渲染
                {(data) => {
                    return data.map((item) => {
                        return <div key={item.id}>{item.t}</div>;
                });
            }}
            </Pagination>
        </>
    );
}

分页组件样式如下

.paginator-container {
    display: flex;
    flex-direction: column;
    width: 600px;
}

.pagintion-content {
    border: 1px solid #999;
    height: 300px;
    padding: 3px;
    border-radius: 3px;
}

.pagination-controls {
    float: right;
    display: flex;
    margin-top: 5px;
}

.pagination-controls button {
    cursor: pointer;
}

 
.pagiination-input {
    margin-left: 3px;
    margin-right: 3px;
}

.pagiination-input input {
    width: 20px;
    margin-right: 3px;
}

分页组件实现效果

image.png

image.png

3. 总结

  1. 本文对render props与children function的使用与封装做了一些简单的例子,这两种模式其实可以看成是一种,因为 children function其实也是通过 props传递的,通过这两种模式,可以实现渲染时,父子组件之间的数据联动,并且实现逻辑的抽离,可以更好的组合UI
  2. 从render props中可以看出,react的 props 其实就是函数的参数,可以传递任意类型的数据,如jsx等, 不仅仅是某一些状态数据
  3. 如有不对的地方,请各位大佬评论区多多指教