想象一下,你必须建立一个用户管理仪表板,用于激活和停用用户,其简单的用户界面包括一个表格和一个按钮来切换每个用户的激活状态。然而,不是所有的用户都喜欢表格,那么如果我们允许用户在表格和网格布局之间动态切换呢?虽然这可能会给应用程序增加额外的代码,但它也可以提供用户满意度,这是任何产品的首要目标。
在这篇文章中,我们将通过利用React复合组件模式和一小部分Context API的味道来建立一个简单的LayoutSwitch 组件,以使我们的生活更轻松。
下面是一个GIF图,显示了我们在本文结束时将构建的完成的LayoutSwitch 组件。

开始工作
理想情况下,LayoutSwitch 组件将是现有 React 应用程序的一部分。为了这篇文章和实验的目的,让我们用Create React App模板创建一个新的React应用程序。
# using yarn
yarn create react-app react-layout-switch
# using npx
npx create-react-app react-layout-switch
切换到app目录并启动React应用。
# switch to app directory
cd react-layout-switch
# start using yarn
yarn start
# start using npm
npm run start
这篇文章主要关注构建LayoutSwitch 组件;由于时间关系,我们不会在本教程中编写任何样式,所以用这个Gist文件中的样式替换App.css 和index.css 中的样式。
组件概述
下面是我们将要构建的组件的概述。
基础组件
App是根组件(在这个用例中,这将是AdminDashboard或类似的东西)。UsersTable是用于渲染表格布局的JSX。UsersGrid是用于生成网格布局的JSX。
更多的布局可以根据业务逻辑添加,比如类似Trello的下拉板,用户可以在活动和非活动状态之间移动,或者投诉管理系统的开放/保留/关闭状态控制。表格或网格布局可以提供数据的快速概览,而板块布局可以提供更多的控制。这种用例是没有限制的。
布局控制组件
LayoutSwitch是父组件,持有布局状态并控制子组件的渲染。Options是布局选项按钮的封装组件。Button是每个布局选项的单独按钮。Content是所有布局组件的封装组件,包括网格、表格等。
Options 和Content 将各自的组件组合在一起,对渲染逻辑和样式给予更多的控制。
获取数据和初始布局设置
设置App 组件,显示从JSON Placeholder API获取的用户列表。
添加下面的代码到App.jsx 。
import React from 'react';
import { useFetch } from './hooks/useFetch';
import './App.scss';
function App() {
const {
data: users,
error,
loading,
} = useFetch('/users');
if (error) {
return <h2 className="error">{error}</h2>;
}
return (
<main className="container app">
<h1>Users</h1>
{loading && <h3>Loading Users...</h3>}
{users !== null ? (
<React.Fragment>
{/** Coming Soon... Table, Grid and what not */}
</React.Fragment>
) : (
<h3>No Users Yet</h3>
)}
</main>
);
}
export default App;
useFetch 是一个简单的自定义Hook,它接受一个端点并使用Fetch API来获取和返回一个带有data,error, 和loading 的对象。
在src/hooks 下创建一个名为useFetch.js 的新文件,并从这个GitHub Gist复制代码。另外,你也可以使用React Query、SWR或任何其他偏好来获取数据。
现在,用户的数据已经可用,让我们添加UsersTable 和UsersGrid ,在各自的布局中渲染用户列表。
首先,添加UsersTable.jsx 。
import React from 'react';
function UsersTable({ users }) {
return (
<div className="users-table-container">
<table className="users-table">
<thead className="users-table__head">
<tr>
<th>#</th>
<th>Name</th>
<th>Company</th>
<th></th>
</tr>
</thead>
<tbody className="users-table__body">
{users.map(({ id, name, username, company }) => (
<tr key={username}>
<td>{id}</td>
<td>
<p>{name}</p>
</td>
<td>
<p>{company.name}</p>
</td>
<td>
<button>View Posts</button>
</td>
</tr>
))}
{!users.length && (
<tr>
<td colSpan={4}>No users....</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
export default UsersTable;
然后,添加UsersGrid.jsx 。
import React from 'react';
function UsersGrid({ users }) {
return (
<div className="user-grid-container">
<div className="user-cards__list">
{users.map(({ id, name, username, company }) => (
<div key={username} className="user-card card">
<h3 className="user-name">
<p>{name}</p>
</h3>
<p className="company-name">
Company: <span>{company.name}</span>
</p>
<span className="user-posts-link">
<button>View Posts</button>
</span>
</div>
))}
{!users.length && <h3>No users....</h3>}
</div>
</div>
);
}
export default UsersGrid;
现在,更新App.jsx 来渲染这两个组件。
.....
import UsersTable from './UsersTable';
import UsersGrid from './UsersGrid';
.....
{users !== null ? (
<React.Fragment>
<UsersTable users={users} />
<UsersGrid users={users} />
</React.Fragment>
) : (
......
这两个布局将相继呈现。我们将继续,接下来在LayoutSwitch ,以控制布局。
LayoutSwitch 和它的孩子
通过利用复合组件模式,切换逻辑可以被抽象到专用组件,LayoutSwitch 。
让我们先为activeLayout 添加一个状态,渲染包裹在这个根组件下的子组件。activeLayout 与defaultLayout 一起被初始化,这是与这个组件的子组件并列的唯一道具。
import React, { useState } from 'react';
function LayoutSwitch({ children, defaultLayout }) {
const [activeLayout, setActiveLayout] = useState(defaultLayout);
return (
<React.Fragment>
{children}
</React.Fragment>
);
}
为了与子组件共享这个状态,并允许来自子组件的状态更新,在这种情况下,按钮组件,我们可以使用React的Context API。
创建一个LayoutContext ,并添加activeLayout 和setActiveLayout 作为提供者的值。
import React, { useState, createContext } from 'react';
const LayoutContext = createContext();
function LayoutSwitch({ children, defaultLayout }) {
const [activeLayout, setActiveLayout] = useState(defaultLayout);
const value = {
activeLayout,
setActiveLayout,
};
return (
<LayoutContext.Provider value={value}>
{children}
</LayoutContext.Provider>
);
}
使用React.useContext Hook,我们可以从其他组件的上下文中读取数据。
const context = useContext(LayoutContext);
但是对于LayoutSwitch 以外的组件,这个上下文将不可用,也不应该被允许。因此,让我们添加自定义Hook,useLayoutContext ,以方便阅读,并在根提供者组件之外使用时引发错误。
function useLayoutContext() {
const context = useContext(LayoutContext);
if (!context) {
throw new Error(
`Components that require LayoutContext must be children of LayoutSwitch component`
);
}
return context;
}
比如说。
# using useLayoutContext
const context = useLayoutContext();
现在基础组件已经设置好了,让我们添加Content 组件。这将渲染子组件,如Table 或Grid ,与activeLayout 状态相匹配。
UsersTable 和UsersGrid 将是子组件,每个子组件都有一个道具layout ,以便让Content 组件与状态进行比较,并渲染一个匹配的。
Content 渲染器决定为给定的活动布局状态渲染哪个布局组件,要么是Table ,要么是Grid 。
function Content({ children }) {
const { activeLayout } = useLayoutContext();
return (
<React.Fragment>
{children.map(child => {
if (child.props.activeLayout !== activeLayout) return null;
return child;
})}
</React.Fragment>
);
}
现在,我们有内容和状态来存储activeLayout 。但我们如何在布局之间实际切换呢?
要做到这一点,可以添加Button 组件,它从上下文中获取布局的setActiveLayout ,并相应地更新状态。
function Button({ children, layoutPreference, title }) {
const { activeLayout, setActiveLayout } = useLayoutContext();
return (
<button
className={`layout-btn ${
activeLayout === layoutPreference ? 'active' : ''
}`}
onClick={() => setActiveLayout(layoutPreference)}
title={title}
>
{children}
</button>
);
}
我们还可以添加Options 组件,以达到样式设计的目的,并获得对按钮的更多控制。
function Options({ children }) {
return (
<div className="layout-switch-container">
{children}
</div>
);
}
我们已经添加了所有必要的组件,一切看起来都不错。但它可以做得更好。
LayoutSwitch 应该只负责渲染和控制布局相关的组件。任何不相关的东西都会破坏UI或组件本身。
因此,让我们使用ReactisValidElement和 [child.type.name](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name)来确保不相关的组件和元素不会与LayoutSwitch 及其子女一起呈现。
所以为了做到这一点,我们必须迭代子元素,并验证每个子元素。如果它是一个有效的元素,就渲染它。否则,忽略它或抛出一个错误,说它不允许。
对于LayoutSwitch ,只有Options 和Content 组件可以被允许作为子元素。
function LayoutSwitch({ children, ... }) {
....
return (
<LayoutContext.Provider value={value}>
{children.map(child => {
if (!React.isValidElement(child)) return null;
if (![Options.name, Content.name].includes(child.type.name)) {
throw new Error(
`${
child.type.name || child.type
} cannot be rendered inside LayoutSwitch
Valid Components are [${Options.name}, ${Content.name}]`
);
}
return child;
})}
</LayoutContext.Provider>
);
....
}
让我们也给Options 组件以类似的权力。只有Button 是允许的。
function Options({ children }) {
return (
<div className="layout-switch-container">
{children.map(child => {
if (!React.isValidElement(child)) return null;
if (child.type.name !== Button.name) {
throw new Error(
`${
child.type.name || child.type
} cannot be rendered inside LayoutSwitch.Options
Valid Components are [${Button.name}]`
);
}
return child;
})}
</div>
);
}
Component.name
作为一个简短的说明,不要将组件名称作为字符串与child.type.name 进行平等检查。
# DO NOT DO THIS
child.type.name === "Options"
当代码通过uglify/webpack ,组件名称在生产中不会保持不变。Options 不会显示为 "Options";相反,它将显示为任何单个字符,如本例中的y 或t 。
现在,当我们读取组件名称并比较child.type.name 和Component.name 时,其结果总是各自的子节点和组件的值相同。
child.type.name === Options.name
# y === y or t === t or whatever === whatever
导出LayoutSwitch 和复合组件
我们可以单独导出所有组件,然后导入每个组件。
# export individually
export { LayoutSwitch, Options, Button, Content };
# usage
import { LayoutSwitch, Options, Button, Content } from './LayoutSwitch';
<LayoutSwitch>
<Options>...</Options>
<Content>...</Content>
</LayoutSwitch>
另一个简单的方法是在LayoutSwitch 下给所有组件命名,然后导入一个组件。
# OR, export under single namespace
LayoutSwitch.Button = Button;
LayoutSwitch.Options = Options;
LayoutSwitch.Content = Content;
export default LayoutSwitch;
# usage
import LayoutSwitch from './LayoutSwitch';
<LayoutSwitch>
<LayoutSwitch.Options>...</LayoutSwitch.Options>
<LayoutSwitch.Content>...</LayoutSwitch.Content>
</LayoutSwitch>
完全取决于你用哪种方式导出和导入。
写好了我们需要的所有组件,我们就可以把东西连在一起了。
完成工作
现在是将LayoutSwitch 及其子代复合组件放在一起的时候了。我建议将所有选项作为一个 [Object.freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)常量,这样孩子或任何外部因素就不能改变布局选项对象。更新后的App 组件应该看起来像下面的代码。
...
import { LayoutSwitch, Options, Button, Content } from './LayoutSwitch';
import { BsTable, BsGridFill } from 'react-icons/bs';
const LAYOUT_OPTIONS = Object.freeze({ table: 'table', grid: 'grid' });
function App() {
return (
.....
{users !== null ? (
<LayoutSwitch defaultLayout={LAYOUT_OPTIONS.grid}>
<Options>
<Button
layoutPreference={LAYOUT_OPTIONS.table}
title="Table Layout"
>
<BsTable />
</Button>
<Button
layoutPreference={LAYOUT_OPTIONS.grid}
title="Grid Layout"
>
<BsGridFill />
</Button>
</Options>
<Content>
<UsersTable activeLayout={LAYOUT_OPTIONS.table} users={users} />
<UsersGrid activeLayout={LAYOUT_OPTIONS.grid} users={users} />
</Content>
</LayoutSwitch>
) : (
......
)
}
为了保持用户选择的选项,利用localStorage 。虽然这不在本教程的范围内,但你可以根据自己的喜好进行探索和坚持。
这就是目前的全部内容!你可以在我的GitHub repo中找到已完成的代码,并可以访问这个演示的最终布局,看看用户如何从表格切换到网格布局。
谢谢你的阅读。我希望你觉得这篇文章对你有帮助,并请与那些可能从中受益的人分享它。Ciao!
The postConverting tables to grids with React compound componentsappeared first onLogRocket Blog.