假設我們有如下結構的程序,App組件由Main組件和Login組件組成;
class App extends React.Component {
state = {
user: User
}
loginHandler = user => {
this.setState({ user: user })
}
logoutHandler = () => {
this.setState({ user: null })
}
render() {
return this.state.user
?
(<Main user={this.state.user} onLogout={this.logoutHandler}></Main>)
:
(<Login onLogin={this.loginHandler}></Login>)
}
}
Main組件由Header組件和MessageList組件組成;
const Main = ({ user, onLogout }) => (
<main>
<Header user={user} onLogout={onLogout} />
<MessageList user={user} />
</main>
)
Header組件由Menu組件組成;
const Header = ({ user, onLogout }) => (
<header>
<h2>我的郵件</h2>
<Menu user={user} onLogout={onLogout}></Menu>
</header>
)
Menu組件;
class Menu extends React.Component {
state = {
visible: false
}
avatarRef = React.createRef()
componentDidMount() {
document.addEventListener('click', this.hideMenu)
}
componentWillUnmount() {
document.removeEventListener('click', this.hideMenu)
}
hideMenu = e => {
if (e.target !== this.avatarRef.current) {
this.setState({ visible: false })
}
}
toggleMenu = () => {
this.setState(state => ({ visible: !state.visible }))
}
render() {
return (
<div>
<img
src={this.props.user.avatar}
onClick={this.toggleMenu}
ref={this.avatarRef}
/>
{this.state.visible && (
<ul>
<li onClick={this.props.onLogout}>退出登錄</li>
</ul>
)}
</div>
)
}
}
Login組件;
class Login extends React.Component {
state = {
username: '',
password: '',
error: null,
loading: false
};
inputChangeHandler = e => {
this.setState({
[e.target.name]: e.target.value
});
};
submitHandler = e => {
e.preventDefault();
this.setState({ loading: true, error: null });
login(this.state.username, this.state.password)
.then(user => {
this.setState({ loading: false });
this.props.onLogin(user);
})
.catch(error => this.setState({ error, loading: false }));
};
render() {
const { username, password, error, loading } = this.state;
return (
<div>
<form onSubmit={this.submitHandler}>
<label>
用戶名
<input
name="username"
value={username}
onChange={this.inputChangeHandler}
/>
</label>
<label>
密碼
<input
name="password"
type="password"
value={password}
onChange={this.inputChangeHandler}
/>
</label>
{error && <div>{error.message}</div>}
<button type="submit" disabled={loading}>登錄</button>
</form>
</div>
);
}
}
我們看到,App組件中持有user對象來判斷顯示Main組件還是Login組件;遵循Main組件這條線,我們發現最后一個組件是Menu組件,發現Menu組件也要使用user對象,因此我們不得不使用props來從App傳遞user。線路是這樣地:Main -> Header -> Menu,但Main組件和Header組件其實是不需要user對象的;和Header組件同級的MessageList組件也需要user對象,因此也要給MessageList設置一個屬性用來傳遞user對象。假如Menu組件是在第100層呢,中間那些無辜的組件們都得硬生生地加上一個屬性來傳遞user對象。丑陋啊,忍不了!終于,React帶來了解決方案,Context。廢話少說,看看咋用這個玩意。
新建個文件名為UserContext.js。內容如下:
import React from 'react';
//創建一個Context
const Context = React.createContext();
export default Context;
簡單吧,人如其名,Context,一個環境,只要大家同處在這個環境中,這個環境中的一切都可以為大家共享,為大家所用。當然,現在這個環境中還空無一物,我們進去了也沒啥可用的。
好吧,讓我們往這個環境中扔點什么,譬如user對象,不是Menu小朋友要用嗎?好了,萬事俱備,只欠插入,讓誰插入到這個環境中?誰要用user誰插入。Menu插入嗎?不行,Menu組件中沒有user對象啊,必須在有user對象的環境中進行插入,也就是App組件。我們又知道Menu組件是跟Main組件混的,Main有的吃,Menu肯定有的吃。所以,我們把Main組件插入到環境中,Main和Main的子子孫孫都可以使用user對象了,眼見為實:
class App extends React.Component {
state = {
user: User
}
loginHandler = user => {
this.setState({ user: user })
}
logoutHandler = () => {
this.setState({ user: null })
}
render() {
return this.state.user
?
(
<UserContext.Provider value={this.state.user}>
<Main onLogout={this.logoutHandler}></Main>
</UserContext.Provider>
)
:
(<Login onLogin={this.loginHandler}></Login>)
}
}
別忘了導入UserContext.js啊,我們在扔對象進環境的地方使用Provider,并為value屬性賦值為該對象。然后,我們把Main組件插入到環境中,也就是JSX組件嵌套。
下面看看Menu組件怎么使用user:
class Menu extends React.Component {
state = {
visible: false
}
avatarRef = React.createRef()
componentDidMount() {
document.addEventListener('click', this.hideMenu)
}
componentWillUnmount() {
document.removeEventListener('click', this.hideMenu)
}
hideMenu = e => {
if (e.target !== this.avatarRef.current) {
this.setState({ visible: false })
}
}
toggleMenu = () => {
this.setState(state => ({ visible: !state.visible }))
}
render() {
return (
<UserContext.Consumer>
{
user => (
<div>
<img
src={user.avatar}
onClick={this.toggleMenu}
ref={this.avatarRef}
/>
{this.state.visible && (
<ul>
<li onClick={this.props.onLogout}>退出登錄</li>
</ul>
)}
</div>
)
}
</UserContext.Consumer>
)
}
}
我們看到在返回組件的時候,我們用Consumer把整個JSX包了起來。怎么使用user,代碼說明一切,把user當做參數傳遞到返回JSX的函數中,自然JSX中就可以使用user了。然后Main、Header、Menu組件的user屬性都可以刪掉了。干的漂亮!
使用Context的姿勢是,創建Context對象,在JSX中,扔進共享變量,并插入含有準備使用共享變量的組件的地方用Provider,使用共享變量的地方用Consumer,Consumer里用大括號,大括號里用函數,共享變量當入參,返回值為render函數返回的組件,在組件中自然可以使用共享變量。就這么簡單,趕緊用起來!
漏了個helper.js。App組件和Menu組件要用到。
export const User = {
name: 'du1dume',
username: 'du1dume',
avatar: 'https://www.gravatar.com/avatar/'
}
//模擬后臺登錄
export function login(username, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (username === 'du1dume' && password === 'du1dume') {
resolve(User)
} else {
reject({ message: '無效的用戶名或密碼' })
}
}, 2000)
})
}