在日常的开发中,常常有一些封装与抽象的需求,本篇文章记录两种较常用的组件设计方法, 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
- 在React中, chilren的类型常是一个 ReactNode, 可以用作类似vue中过的插槽使用,方便父组件与子组件的组合
- 在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>
);
};
搜索组件实现效果
利用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}>
<
</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}>
>
</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;
}
分页组件实现效果
3. 总结
- 本文对render props与children function的使用与封装做了一些简单的例子,这两种模式其实可以看成是一种,因为 children function其实也是通过 props传递的,通过这两种模式,可以实现渲染时,父子组件之间的数据联动,并且实现逻辑的抽离,可以更好的组合UI
- 从render props中可以看出,react的 props 其实就是函数的参数,可以传递任意类型的数据,如jsx等, 不仅仅是某一些状态数据
- 如有不对的地方,请各位大佬评论区多多指教