本文内容共 2000 字,预计阅读时长 5 分钟
一般来说,组件之间的值传递是通过 pro 属性一层层的传给子组件的,如下图所示:
从上图可以看出,及时中间的几个组件及时没有用到相关的属性,为了传递给最后一个 button 组件,每个组件都需要带上这个属性,如下段代码所示:
import React, { useState, useContext } from 'react'; import { data } from '../../../data'; const ContextAPI = () => { const [people, setPeople] = useState(data); const removePerson = (id) => { setPeople((people) => { return people.filter((person) => person.id !== id); }); }; return ( <> <h3>prop drilling</h3> <List people={people} removePerson={removePerson} /> </> ); }; const List = ({ people, removePerson }) => { return ( <> {people.map((person) => { return ( <SinglePerson key={person.id} {...person} removePerson={removePerson} /> ); })} </> ); }; const SinglePerson = ({ id, name, removePerson }) => { return ( <div className='item'> <h4>{name}</h4> <button onClick={() => removePerson(id)}>remove</button> </div> ); }; export default ContextAPI;
如上述代码所示,你也许发现了 removePerson 这个事件属性,通过 pro 属性一层层的传递给 SinglePerson 组件,如果组件多的话,我们就不知道在哪个组件会用到这个事件,会不会有更好的方式呢?
那就是 Context
介绍
useContext
前一定要先來说Context
因为useContext
只是他之中Context.Consumer 的 语法糖而已。
Context Object 通过 createContext() 进行创建,对应有两个属性:
const testContext = createContext(); const { Provider, Consumer } = testContext;
我们会用 Context.Provider
包含 value
属性进行传递值
我们使用 Context.Provider 包含子组件,并传递 value 属性,这样就能把值传递给其中的自组件,示例代码如下:
const PersonContext = React.createContext(); // two components - Provider, Consumer const ContextAPI = () => { const [people, setPeople] = useState(data); const removePerson = (id) => { setPeople((people) => { return people.filter((person) => person.id !== id); }); }; return ( <PersonContext.Provider value={{ removePerson, people }}> <h3>Context API / useContext</h3> <List /> </PersonContext.Provider> ); }; const List = () => { const mainData = useContext(PersonContext); console.log(mainData); return ( <> {mainData.people.map((person) => { return <SinglePerson key={person.id} {...person} />; })} </> ); }; const SinglePerson = ({ id, name }) => { const { removePerson } = useContext(PersonContext); return ( <div className='item'> <h4>{name}</h4> <button onClick={() => removePerson(id)}>remove</button> </div> ); };
从上述代码,大家可能看出我们并没有在 List
和 SinglePerson
传递 people
、removePerson
属性,而是通过 useContext
获取 Provider
的 value 的值。
从上个示例我们看出,我们使用useContext
获取 Provider
的 value 的值。
接收一个 context object(React.createContext 的回传值)并回传该 context 目前的值。 Context 目前的值是取决于由上层 component 距离最近的 MyContext.Provider 的 value prop。
除了使用 useContext
获取值,我们还可以通过 Context.Consumer
获取值,如下段代码所示:
app.js 文件
// App.js export const ThemeContext = React.createContext() const App = () => { const [dark, setDark] = useState(true); return ( <> <ThemeContext.Provider value={dark}> <FunctionComponent /> <ClassComponent /> </ThemeContext.Provider> </> ); } // FunctionComponent.js const FunctionComponent = () => { return ( <ButtonGroupComponent /> ); }
ButtonGroupComponent.js
// ButtonGroupComponent.js import { ThemeContext } from './App.js'; const ButtonGroupComponent = () => { const themeStyle = (dark) => { return { backgroundColor: dark ? '#2c3e50': '#f1c40f', color: dark ? '#ecf0f1' : '#2c3e50' } }; return ( <ThemeContext.Consumer> { dark => <button style={themeStyle(dark)}>Class Component</button> } </ThemeContext.Consumer> ); }
另外有一点需要说明的,Context 也可以包含 Context,就是互相嵌套,如下段代码所示:
// ThemeProvider.js const ThemeContext = React.createContext(); const ThemeUpdate = React.createContext(); const ThemeProvider = ({children}) => { const [dark, setDark] = useState(true); const changeTheme = () => { setDark(prevDark => !prevDark) } return ( <ThemeContext.Provider value={dark}> <ThemeUpdate.Provider value={changeTheme}> {children} </ThemeUpdate.Provider> </ThemeContext.Provider> ) } // App.js const App = () => { return ( <ThemeProvider> <FunctionComponent /> <ClassComponent /> </ThemeProvider> ) };
学习完了 useContext
, 我们来通过几个实例来深入理解下 useContext。
用户登录成功后的欢迎页,是一个很常见的需求,如下图所示,登录成功后显示用户的头像
接下来我们使用 useContext
完成此需求,新建 UserContext.js 文件,如下图所示:
const UserContext = React.createContext() export default UserContext
在应用中,我们可以通过 UserContext.Provider 初始化用户数据,示例代码如下:
import UserContext from './UserContext' const App = ({ initialUser }) => { const [user, changeUser] = useState(initialUser) const value = { user, changeUser } return ( <UserContext.Provider value={value}> <div> <Header /> <Site /> </div> </UserContext.Provider> ) }
App 组件创建一个初始化用户状态 initialUser。并将值传递给 user 数据状态,并通过 changeUser 更改其值,以通过 UserContext.Provider 将值进行共享。
在 Logo 组件里,我们可以通过 useContext 进行获取值,示例代码如下:
import UserContext from './UserContext' const Logo = () => { const { user } = useContext(UserContext) return ( <div> <img url={ user.imageUrl } /> <span>{ user.username }</span> </div> ) }
同样在欢迎语组件里,我们也做类似的调用,示例代码如下:
import UserContext from './UserContext' const Greeting = () => { const { user } = useContext(UserContext) return <h1>Hello {user.username}</h1> }
更改 context 的值
接下来我们增加一个退出按钮,更改 UserContext 中 user 的值,并根据值动态显示用户的信息或登录提示信息,修改后的 Logo 组件代码如下:
import UserContext from './UserContext' const Logo = () => { const { user, changeUser } = useContext(UserContext) const { imageUrl, username } = user const authenticated = username != undefined const onLogout = () => { changeUser({}) } const onLogin = () => { // redirect to LoginForm } return ( <div> {authenticated ? ( <> <img url={ imageUrl } /> <span>{ username }</span> <button onClick={onLogout}>Logout</button> </> ) : ( <button onClick={onLogin}>Login</button> )} </div> ) }
在上述代码中,changeUser 函数是通过 UserContext 提供的。它根据用户信息是否有值,显示用户信息、“注销” 或 “登录”按钮。
如果用户未通过身份验证,则会显示“登录”按钮。点击后,会触发 onLogin 事件处理程序,我们可以将用户重定向到 LoginForm(此处省略重定向代码):
const LoginForm = () => { const { changeUser } = useContext(UserContext) const onSubmit = (_user) => { changeUser(_user) // redirect to home page } return ... }
LoginForm 还使用 UserContext 并读取 changeUser 属性。成功提交表单后,我们可以再次使用 changeUser 将空用户更改为经过身份验证的用户。同样,新更新的值会传播给所有消费者,包括Logo 和 Greeting 组件。
用户登录和注销通常是站点的常见需求,因为它直接影响站点的身份验证和授权。特别适合局部刷新界面,context 非常适合读取和写入用户对象,而无需担心各种组件之间的数据不同步。
涉及 context 的一个常见的用法是主题化,它允许用户根据自己的喜好在浅色和深色主题之间切换。
要实现此功能,让我们先从 ThemeContext 开始:
const ThemeContext = React.createContext({ mode: 'light', // or 'dark' })
我们可以使用 mode 对象设置默认主题,该属性是一个字符串,用于保存“light”或“dark”。
任何需要主题的组件都可以从 ThemeContext 中读取设置。让我们构建一个主题按钮:
const Button = () => { const theme = useContext(ThemeContext) return ( <ButtonStyle mode={theme.mode}> Ok </ButtonStyle> }
我们可以根据 ButtonStyle 组件的属性,为按钮的样式设置主题:
const color = props => props.mode === 'light' : 'black' ? 'white' const ButtonStyle = styled.button` color: ${color}; `
我们可以将需要用到样式切换的组件放置在 ThemeContext.Provider 内部,这样就能做到相关组件的样式的切换。
const App = ({ theme }) => { return ( <ThemeContext.Provider value={theme}> ... </ThemeContext.Provider> }
除了可以切换浅色和设色主题,还可以切换站点的样式主题,比如蓝色主题,你可以自定义更多的主题样式,示例代码如下:
const ThemeContext = React.createContext({ primaryColor: 'blue', secondaryColor: 'red' })
接下来我们,我们可以将 ThemeContext 的值通过属性的方式传递给子组件,示例代码如下:
const color = props => props.primaryColor const ButtonStyle = styled.button` color: ${color}; `
表格的需求也很常见,这次我们使用 context 的方式创建一个表格,如下图所示,同时可以删除表格对应的行数据。
首先我们先定义单元格组件,就是表格每行对应的单元格,默认的 DefaultCell,示例代码如下:
const DefaultCell = ({ value }) => { return <div>{value}</div> }
接下来我们来定义表格的水果数据,数据形式如下所示:
const fruits = [ { title: 'Apple', status: true }, { title: 'Orange', }, { title: 'Strawberry' }, { title: 'Pineapple', status: true }, { title: 'Watermelon' } ]
接下来我们定义显示水果名称的单元格,同时依据水果的状态,显示 Nice 或 Ok,StatusCell 单元格的代码如下:
const StatusCell = ({ value }) => { const s = value ? 'Nice' : 'Ok' return <div>{s}</div> }
我们继续需要定一个单元格组件 TableCell ,根据单元格的属性不同,我们来显示不同的单元格组件,比如显示水果名称的单元格或操作单元格的按钮。
const TableCell = ({ col }) => { const Component = col.Cell || DefaultCell return <Component /> }
接下来我们来定义列数据(表头)的形式,示例代码如下:
const cols = [ { name: 'title' }, { name: 'status', Cell: StatusCell } ]
接下来我们继续设计本案例的核心部分,设计 TableContext 数据部分,如下段代码声明 TableContext ,示例代码如下:
const TableContext = React.createContext({ rows: [], cols: [], row: {} }) export default TableContext
接下来我们定义 TableRow 组件,读取共享的TableContext 数据,用来加载每行数据,示例代码如下:
import TableContext from './TableContext' const TableRow = ({ row }) => { const table = useContext(TableContext) const value = { ...table, row } const cols = table.cols return ( <TableContext.Provider value={value}> <TableRowStyle> {cols.map(col => { <TableCell row={row} col={col} /> })} </TableRowStyle> </TableContext.Provider> ) }
接下来我们调整单元格的数据源形式,方便我们更加灵活的显示自定义单元格,cols 数据源如下,根据不同 name 显示不同的单元格组件:
const cols = [ { name: 'title' }, { name: 'status', Cell: StatusCell }, { name: 'combo', Cell: ComboCell } ]
这里新增了 ComboCell 组件,我们需要进行定义,示例代码如下:
const ComboCell = () => { const { row } = useContext(TableContext) const s = row.status ? 'Nice' : 'Ok' return <div>{row.title} – {s}</div> }
接下来我们将 TableRow 嵌入 Table 组件里,根据数据源渲染表格的行,代码如下:
const Table = ({ cols, rows, setRows }) => { const value = { cols, rows, setRows } return ( <TableContext.Provider value={value}> <TableStyle> {rows.map(row => { <TableRow row={row} /> })} </TableStyle> </TableContext.Provider> ) }
根据需求,我们还需在最后一列,自定义删除操作,示例代码如下:
const DeleteCell = () => { const { row, rows, setRows } = useContext(TableContext) const onClick = () => { const newRows = rows.filter(r => r.title !== row.title) setRows(newRows) } return ( <div> <button onClick={onClick}>Remove</button> </div> ) }
最后我们来定义 APP 组件,将相关数据传递 Table 组件,示例代码如下:
const cols = [ { name: 'combo', Cell: ComboCell }, { name: 'action', Cell: DeleteCell } ] const App = () => { const [rows, setRows] = useState(fruits) return <Table rows={rows} cols={cols} setRows={setRows} /> }
到这里本示例就介绍完了,完整版的源码和示例预览,大家可以到以下链接地址进行体验:
https://codepen.io/windmaomao/pen/VwzWeMa
到这里关于 useContext 的使用总结就到这里介绍完了,我们学习了什么是 useContext,以及如何使用 useContext 。通过此特性,我们可以在组件之间共享值,解决了 props 属性在组件之间层层传递的问题。但是并不是说一定要使用 useContext 去替代原本的 props 方法,而是根据实际的情况选择是否合适,毕竟现在 context 有一个很大的问题,每当数据状态改变,与其相关的所有组件都会更新。因此不会常变的值才适合使用 useContext,比如主题样式。
注:本文属于原创文章,版权属于「前端达人」公众号及 qianduandaren.com 所有,未经授权,谢绝一切形式的转载