数据传递
React 的特性单向数据流,限制了正常情况下,数据只能自上而下传递(父组件 > 子组件)。
可以通过其他方式打破这种限制:
- 组件组合(props.children上绑定数据):省略了嵌套层级太多的组件层层传递数据。类似vue插槽(slot)
- 事件绑定:通过props将触发事件执行的函数绑定到子组件,实现子组件向上通讯。
- Context:创建context,使整个组件树共享数据。
- Redux:使用React的Context原理的工具。
静态页面配置
本次学习Context,创建一个简单的可以切换主题样式的列表查询功能。
// index.css
*{margin:0;padding:0;box-sizing: border-box;}
html,body{font-size:14px;}
#root{
width:500px;
margin: 50px auto;
}
.theme{
margin-bottom:10px;
}
.theme label{
margin-right:20px;
}
.dark .theme{
color:#fff;
}
#searchBox{
background-color: #eee;
padding: 10px;
}
#searchBox.dark{
background-color: #333;
}
.query{
margin-bottom: 10px;
}
.query input[type=text]{
height: 36px;
line-height: 36px;
font-size: 14px;
padding:0 5px;
border: 1px solid #999;
}
.query select{
margin-left: 10px;
height: 36px;
line-height: 36px;
padding:0 5px;
border: 1px solid #999;
}
.query.dark input[type=text],.query.dark select{
border-color:#eee;
background-color: #777;
color:#ccc;
}
.table{
background-color: #fff;
border: solid #999;
border-width: 1px 1px 0 0;
width: 100%;
border-spacing: 0;
}
.table td,.table th{
border: solid #999;
border-width: 0 0 1px 1px;
padding: 4px;
}
.table.dark,.table.dark td,.table.dark th{
border-color:#eee;
background-color: #777;
color:#ccc;
}
// SearchApp.js
import React from 'react';
// table行组件
class TableRow extends React.Component {
render() {
return <tr>
<td>category</td>
<td>name</td>
<td>price</td>
</tr>;
}
}
// table组件
class Table extends React.Component {
render() {
let rows = [];
return (
<table className={'table '}>
<thead>
<tr>
<th>Category</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
);
}
}
// 查询项组件
class SearchBar extends React.Component {
render() {
return (
<div className={'query '}>
<input type="text" />
<select>
<option value="">All</option>
<option value="Sporting Goods">Sporting Goods</option>
<option value="Electronics">Electronics</option>
</select>
</div>
);
}
}
// 查询功能组件
class SearchBox extends React.Component {
render() {
return (
<div id="searchBox">
<SearchBar />
<Table />
</div>
);
}
}
// 功能容器组件
class SearchApp extends React.Component {
render() {
return (
<SearchBox />
);
}
}
export default SearchApp;
Context 简单使用
创建context对象
const ContextName = React.createContext(defaultValue);
每个Context对象都返回两个React组件:Provider和Consumer
Provider:允许消费组件(被包裹的组件树)订阅Context的变更。
Consumer:在子孙级组件使用,允许消费组件订阅Context变更。子元素是一个函数。用于一个组件消费多个Context 或 传递函数变更Context的默认值(实际上是变更Provider组件的value属性,不是变更Context对象的defaultValue)
读取context
指定组件的contextType属性(只能指向一个Context对象),就可以在组件内甚至render中使用this.context读取Context的值。
import React from 'react';
// 创建一个context对象,存储主题相关数据,默认值light
const ThemeContext = React.createContext('light');
// 使用组件<ThemeContext.Provider> <ThemeContext.Consumer>
// 或如此声明:const {Provider,Consumer} = ThemeContext;
// 使用组件<Provider> <Consumer>
// table行组件
class TableRow extends React.Component {
render() {
return <tr>
<td>category</td>
<td>name</td>
<td>price</td>
</tr>;
}
}
// table组件
class Table extends React.Component {
render() {
const theme = this.context;
let rows = [];
return (
<table className={'table ' + theme}>
<thead>
<tr>
<th>Category</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
);
}
}
// 指定组件的contextType
Table.contextType = ThemeContext;
// 查询项组件
class SearchBar extends React.Component {
// 指定contextType的新写法
static contextType = ThemeContext;
render() {
const theme = this.context;
return (
<div className={'query ' + theme}>
<input type="text" />
<select>
<option value="">All</option>
<option value="Sporting Goods">Sporting Goods</option>
<option value="Electronics">Electronics</option>
</select>
</div>
);
}
}
// 查询功能组件
class SearchBox extends React.Component {
render() {
return (
<div id="searchBox" className={this.context}>
<SearchBar />
<Table />
</div>
);
}
}
SearchBox.contextType = ThemeContext;
// 功能容器组件
class SearchApp extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<div>SearchApp组件获取到的ThemeContext值是:{this.context}</div>
<SearchBox />
</ThemeContext.Provider>
);
}
}
SearchApp.contextType = ThemeContext;
export default SearchApp;
当 React 渲染一个订阅了 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
所以上面SearchApp组件读取的context值是 light,而不是它下级组件配置的默认值 dark。
动态修改context
将context的默认值指向组件的state中的数据,并将一个可以变更这个数据的方法传递给context。可以使组件树中的组件动态的修改context。
Provider传递函数
import React from 'react';
// 默认值将被组件的state替换,可以不设置
const ThemeContext = React.createContext();
// 或设置一个默认值,避免意外
// const ThemeContext = React.createContext({
// theme: 'light',
// toggleTheme: () => {}
// });
// table行组件
class TableRow extends React.Component {
render() {
return <tr>
<td>category</td>
<td>name</td>
<td>price</td>
</tr>;
}
}
// table组件
class Table extends React.Component {
render() {
const {theme} = this.context;
let rows = [];
return (
<table className={'table ' + theme}>
<thead>
<tr>
<th>Category</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
);
}
}
// 指定组件的contextType
Table.contextType = ThemeContext;
// 查询项组件
class SearchBar extends React.Component {
// 指定contextType的新写法
static contextType = ThemeContext;
render() {
const {theme} = this.context;
return (
<div className={'query ' + theme}>
<input type="text" />
<select>
<option value="">All</option>
<option value="Sporting Goods">Sporting Goods</option>
<option value="Electronics">Electronics</option>
</select>
</div>
);
}
}
// 查询功能组件
class SearchBox extends React.Component {
render() {
const {theme,toggleTheme} = this.context;
return (
<div id="searchBox" className={theme}>
<div className="theme">
<label><input type="radio" name="theme" value="light" checked={theme==='light'} onChange={(e) => {toggleTheme(e.target.value)}} /> light</label>
<label><input type="radio" name="theme" value="dark" checked={theme==='dark'} onChange={(e) => {toggleTheme(e.target.value)}} /> dark</label>
</div>
<SearchBar />
<Table />
</div>
);
}
}
SearchBox.contextType = ThemeContext;
// 功能容器组件
class SearchApp extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = this.toggleTheme.bind(this);
this.state = {
theme: 'dark',
toggleTheme: this.toggleTheme
}
}
// 变更主题
toggleTheme(val) {
this.setState({
theme: val
})
}
render() {
const {theme,toggleTheme} = this.state;
return (
// 或直接指向this.state
<ThemeContext.Provider value={{theme,toggleTheme}}>
<SearchBox />
</ThemeContext.Provider>
);
}
}
// SearchApp.contextType = ThemeContext; // 使用自己的state设置默认值,不需要读取Context
export default SearchApp;
Consumer传递函数
在子孙级组件使用Consumer,可以通过函数传递context的值,而不需要指定组件的contextType。
修改SearchBox组件,使功能与上例一样:
// 查询功能组件
class SearchBox extends React.Component {
render() {
// const {theme,toggleTheme} = this.context; // 未指定contextType,this.context此时为{}
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => {
return (
<div id="searchBox" className={theme}>
<div className="theme">
<label><input type="radio" name="theme" value="light" checked={theme==='light'} onChange={(e) => {toggleTheme(e.target.value)}} /> light</label>
<label><input type="radio" name="theme" value="dark" checked={theme==='dark'} onChange={(e) => {toggleTheme(e.target.value)}} /> dark</label>
</div>
<SearchBar />
<Table />
</div>
);
}}
</ThemeContext.Consumer>
);
}
}
// SearchBox.contextType = ThemeContext; // 不用指定contextType
使用多个Context
嵌套Provider
创建一个Context,用于共享产品列表数据和筛选方法。
通过嵌套Provider组件使用多个Context。
import React from 'react';
const ThemeContext = React.createContext();
const ProductsContext = React.createContext();
// table行组件
class TableRow extends React.Component {
render() {
return <tr>
<td>{this.props.category}</td>
<td>{this.props.name}</td>
<td>{this.props.price}</td>
</tr>;
}
}
// table组件
class Table extends React.Component {
render() {
// const {theme} = this.context;
const theme = 'dark'; // contextType只能指定一个Context对象
const {products} = this.context;
let rows = products.map((v,i)=><TableRow {...v} key={i}></TableRow>);
return (
<table className={'table ' + theme}>
<thead>
<tr>
<th>Category</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
);
}
}
// Table.contextType = ThemeContext;
Table.contextType = ProductsContext; // 改为指向ProductsContext
// 查询项组件
class SearchBar extends React.Component {
// static contextType = ThemeContext;
static contextType = ProductsContext; // 改为指向ProductsContext
render() {
// const {theme} = this.context;
const theme = 'dark'; // contextType只能指定一个Context对象
const {searchList,keyword,category} = this.context;
return (
<div className={'query ' + theme}>
<input type="text" name="keyword" value={keyword} onChange={searchList} />
<select name="category" value={category} onChange={searchList} >
<option value="">All</option>
<option value="Sporting Goods">Sporting Goods</option>
<option value="Electronics">Electronics</option>
</select>
</div>
);
}
}
// 查询功能组件
class SearchBox extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<div id="searchBox" className={theme}>
<div className="theme">
<label><input type="radio" name="theme" value="light" checked={theme==='light'} onChange={(e) => {toggleTheme(e.target.value)}} /> light</label>
<label><input type="radio" name="theme" value="dark" checked={theme==='dark'} onChange={(e) => {toggleTheme(e.target.value)}} /> dark</label>
</div>
<SearchBar />
<Table />
</div>
)}
</ThemeContext.Consumer>
);
}
}
// 功能容器组件
class SearchApp extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = this.toggleTheme.bind(this);
this.searchList = this.searchList.bind(this);
this.products = [
{ category: "Sporting Goods", price: "$49.99", name: "Football" },
{ category: "Sporting Goods", price: "$9.99", name: "Baseball" },
{ category: "Sporting Goods", price: "$29.99", name: "Basketball" },
{ category: "Electronics", price: "$99.99", name: "iPod Touch" },
{ category: "Electronics", price: "$399.99", name: "iPhone 5" },
{ category: "Electronics", price: "$199.99", name: "Nexus 7" }
];
this.state = {
theme: 'dark',
toggleTheme: this.toggleTheme,
products: this.products,
searchList: this.searchList,
keyword: '',
category: ''
}
}
// 变更主题
toggleTheme(val) {
this.setState({
theme: val
})
}
// 筛选产品列表
searchList(event) {
let {keyword,category} = this.state;
let products = this.products;
let value = event.target.value;
if (event.target.name === 'category') {
category = value;
}
if (event.target.name === 'keyword') {
keyword = value;
}
if (category) {
products = products.filter(v => {
return v.category === category;
});
}
if (keyword) {
products = products.filter(v => {
return v.name.indexOf(keyword) !== -1;
});
}
this.setState({
products,
[event.target.name]: value
})
}
render() {
const {theme,toggleTheme,products,searchList,keyword,category} = this.state;
return (
<ProductsContext.Provider value={{products,searchList,keyword,category}}>
<ThemeContext.Provider value={{theme,toggleTheme}}>
<SearchBox />
</ThemeContext.Provider>
</ProductsContext.Provider>
);
}
}
export default SearchApp;
嵌套Consumer
虽然实现了传递多个Context,但是使用contextType只能指向一个Context对象,所以这种方式不能读取所有的Context。
这时就是使用Consumer的时候了,同样也是嵌套,修改代码:
import React from 'react';
const ThemeContext = React.createContext();
const ProductsContext = React.createContext();
// table行组件
class TableRow extends React.Component {
render() {
return <tr>
<td>{this.props.category}</td>
<td>{this.props.name}</td>
<td>{this.props.price}</td>
</tr>;
}
}
// table组件
class Table extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{({theme}) => (
<ProductsContext.Consumer>
{({products}) => (
<table className={'table ' + theme}>
<thead>
<tr>
<th>Category</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{
products.map((v,i)=><TableRow {...v} key={i}></TableRow>)
}
</tbody>
</table>
)}
</ProductsContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
}
// Table.contextType = ThemeContext;
Table.contextType = ProductsContext; // 改为指向ProductsContext
// 查询项组件
class SearchBar extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{({theme}) => (
<ProductsContext.Consumer>
{({searchList,keyword,category}) => (
<div className={'query ' + theme}>
<input type="text" name="keyword" value={keyword} onChange={searchList} />
<select name="category" value={category} onChange={searchList} >
<option value="">All</option>
<option value="Sporting Goods">Sporting Goods</option>
<option value="Electronics">Electronics</option>
</select>
</div>
)}
</ProductsContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
}
// 查询功能组件
class SearchBox extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<div id="searchBox" className={theme}>
<div className="theme">
<label><input type="radio" name="theme" value="light" checked={theme==='light'} onChange={(e) => {toggleTheme(e.target.value)}} /> light</label>
<label><input type="radio" name="theme" value="dark" checked={theme==='dark'} onChange={(e) => {toggleTheme(e.target.value)}} /> dark</label>
</div>
<SearchBar />
<Table />
</div>
)}
</ThemeContext.Consumer>
);
}
}
// 功能容器组件
class SearchApp extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = this.toggleTheme.bind(this);
this.searchList = this.searchList.bind(this);
this.products = [
{ category: "Sporting Goods", price: "$49.99", name: "Football" },
{ category: "Sporting Goods", price: "$9.99", name: "Baseball" },
{ category: "Sporting Goods", price: "$29.99", name: "Basketball" },
{ category: "Electronics", price: "$99.99", name: "iPod Touch" },
{ category: "Electronics", price: "$399.99", name: "iPhone 5" },
{ category: "Electronics", price: "$199.99", name: "Nexus 7" }
];
this.state = {
theme: 'dark',
toggleTheme: this.toggleTheme,
products: this.products,
searchList: this.searchList,
keyword: '',
category: ''
}
}
// 变更主题
toggleTheme(val) {
this.setState({
theme: val
})
}
// 筛选产品列表
searchList(event) {
let {keyword,category} = this.state;
let products = this.products;
let value = event.target.value;
if (event.target.name === 'category') {
category = value;
}
if (event.target.name === 'keyword') {
keyword = value;
}
if (category) {
products = products.filter(v => {
return v.category === category;
});
}
if (keyword) {
products = products.filter(v => {
return v.name.indexOf(keyword) !== -1;
});
}
this.setState({
products,
[event.target.name]: value
})
}
render() {
const {theme,toggleTheme,products,searchList,keyword,category} = this.state;
return (
<ProductsContext.Provider value={{products,searchList,keyword,category}}>
<ThemeContext.Provider value={{theme,toggleTheme}}>
<SearchBox />
</ThemeContext.Provider>
</ProductsContext.Provider>
);
}
}
export default SearchApp;
contextType和Consumer的使用
- contextType只对一个Context有效,如果一个组件只使用一个context,请使用contextType,如果一个组件需要读取多个context,那就需要使用Consumer。
- contextType简化了Consumer的使用,直接赋值即可。
- contextType可以在组件任意地方读取context,Consumber只能在<Consumber>的子元素函数中使用。
Context应用场景
Context依赖了类似“全局”的变量,来共享那些对一个组件树而言是“全局”的数据。
会使组件的复用性变差,应谨慎使用。
主要应用场景在于很多不同层级的组件需要访问一些同样的数据。
但是Context会让组件变得不纯粹,因为依赖了全局变量。所以这决定了Context一般不会大规模的使用。所以一般在一个组件中使用一个Context就好。
例如当前的locale,theme,一些缓存数据,或一些全局的公共数据。
如果只是像避免方便的进行数据传递,React更推荐使用组件组合来解决。
Redux:依据context原理和Flux架构,在最顶层通过一个集中的Data Store管理公共数据和状态变更,类似vuex。