React應用程序從根本上來講是一棵組件樹,能夠相互通訊數據。在組件之間傳遞數據一般是無痛的。可是,隨着應用程序樹的增加,在保持可讀代碼庫的同時傳遞數據變得更加困難。node
假設咱們有如下樹結構: react
這裏有一個簡單的樹,有3個層次。在該樹中,節點D和節點E都操縱一些相似的數據:假設用戶輸入節點D中的一些文本,咱們但願在節點E中顯示該文本。git
咱們如何將數據從節點D傳遞到節點E?github
本文介紹了三種解決此問題的可行方法:redux
本文的目的是比較這些方法,並代表,當解決一個常見問題時,例如咱們剛纔所說的問題,能夠堅持使用React的context API。api
這樣作的方法是天真的經過props將數據從子節點傳遞到父節點,再將數據從父節點傳遞到子節點,如:D->B->A 而後 A->C->E。安全
這裏的想法是使用onUserInput
從子節點到父節點觸發的函數將輸入數據從節點D傳送到節點A的state
狀態,而後咱們將該數據從節點A的state
狀態傳遞到節點E.bash
咱們從節點D開始:架構
class NodeD extends Component {
render() {
return (
<div className="Child element">
<center> D </center>
<textarea
type="text"
value={this.props.inputValue}
onChange={e => this.props.onUserInput(e.target.value)}
/>
</div>
);
}
}
複製代碼
當用戶鍵入內容時,onChange
監聽函數將從prop
的onUserInput
觸發該函數並傳入用戶輸入。節點D prop中的該函數將觸發節點B prop
中onUserInput
另外一個函數,以下:app
class NodeB extends Component {
render() {
return (
<div className="Tree element">
<center> B</center>
<NodeD onUserInput={inputValue => this.props.onUserInput(inputValue)} />
</div>
);
}
}
複製代碼
最後,當到達根節點A時,onUserInput
在節點B prop中觸發將把節點A中的狀態改變爲用戶輸入的值。
class NodeA extends Component {
state = {
inputValue: ""
};
render() {
return (
<div className="Root element">
<center> A </center>
<NodeB
onUserInput={inputValue => this.setState({ inputValue: inputValue })}
/>
<NodeC inputValue={this.state.inputValue} />
</div>
);
}
}
複製代碼
inputValue
值將經過props從節點C傳遞給子節點E:
class NodeE extends Component {
render() {
return (
<div className="Child element">
<center> E </center>
{this.props.inputValue}
</div>
);
}
}
複製代碼
看到它已經爲咱們的代碼添加了一些複雜性,即便它只是一個小例子。您能夠想象一下應用程序增加時的狀況嗎?🤔
這種方法依賴於樹的深度,所以對於更大的深度,咱們須要經歷更大的組件層。這可能太長而沒法實現,過於重複而且會增長代碼複雜性。
另一種方法是使用像Redux同樣的狀態管理庫。
Redux is a predictable state container for JavaScript apps. The state of our whole application is stored in an object tree within a single store, which your app components depend on. Every component is connected directly to the global store, and the global store life cycle is independent of the components' life cycle.
咱們首先定義應用程序的狀態:咱們感興趣的數據是用戶在節點D中輸入的內容。咱們但願將這些數據提供給節點E.爲此,咱們能夠在store中提供這些數據。而後,節點E能夠訂閱它以便訪問數據。
咱們會稍微回到store。
接下里是定義咱們的reducer。咱們的reducer具體說明了應用程序的狀態是如何根據傳遞到store的actions響應更改的。
定義的reducer以下:
const initialState = {
inputValue: ""
};
const reducer = (state = initialState, action) => {
if (action.type === "USER_INPUT") {
return {
inputValue: action.inputValue
};
}
return state;
};
複製代碼
在用戶輸入任何內容以前,咱們知道咱們的狀態數據或inputValue將是一個空字符串。所以,咱們使用空字符串inputValue
爲reducer
定義默認初始狀態。
這裏的邏輯是:一旦用戶在節點D鍵入內容,不管用戶輸入了什麼,咱們「觸發」或者說派發了一個action來更新state狀態。這裏的「更新」不是指「突變」或者改變了當前狀態,而是說返回了一個新狀態。
if語句將派發的action根據其類型映射到要返回的新狀態。因此咱們已經知道派發的action是一個包含類型鍵的對象。咱們如何得到新狀態的用戶輸入值?咱們只是在操做對象中添加另外一個名爲inputValue的鍵,在咱們的reducer塊中,咱們使新狀態的inputValue具備該輸入值action.inputValue
。因此咱們的應用程序的行爲將遵循這種架構:
{ type: "SOME_TYPE", inputValue: "some_value" }
複製代碼
最終,咱們的dispatch聲明將以下所示:
dispatch({ type: "SOME_TYPE", inputValue: "some_value" })
複製代碼
當咱們從任何組件調用dispatch語句時,咱們傳入操做的類型和用戶輸入值。
好的,如今咱們知道應用程序是如何工做的:在咱們的輸入節點D中,咱們dispatch一個USER_INPUT類型的動做並傳入用戶剛輸入的任何值,在咱們的顯示節點E中咱們傳遞當前的值即用戶輸入爲應用程序的狀態。
爲了使咱們的store可用,咱們將它傳遞給從react-redux
import的Provider
組件,接下來把App
包裹在裏面。因爲咱們知道節點D和E將使用該store中的數據,所以咱們但願Provider組件包含這些節點的公共父節點,所以要麼是根節點A,要麼是整個App組件。讓咱們選擇App
組件包含在咱們的Provider
組件中:
import reducer from "./store/reducer";
import { createStore } from "redux";
import { Provider } from "react-redux";
const store = createStore(reducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
複製代碼
目前咱們已經設置了store和reducer,接下里在節點D和節點E中搞事情。
首先咱們看一下節點D,咱們感興趣用戶在textarea元素中輸入了什麼。這意味着兩件事:
但在作任何這些以前,咱們須要設置一些東西:
咱們首先須要將節點D組件鏈接到咱們的store。爲此,咱們使用react-redux中的connect()函數。它爲鏈接的組件提供了store所需的數據,以及可用於將操做分派給store的功能。
This is why we use the two mapStateToProps and mapDispatchToProps which deal with the store's state and dispatch respectively. We want our node D component to be subscribed to our store updates, as in, our app's state updates. This means that any time the app's state is updated, mapStateToProps will be called. The results of mapStateToProps is an object which will be merged into our node D's component props. Our mapDispatchToProps function lets us create functions that dispatch when called, and pass those functions as props to our component. We will make use of this by returning new function that calls dispatch() which passes in an action.
在咱們的例子中,對於mapStateToProps
函數,咱們只對inputValue
感興趣,因此咱們返回一個對象{ inputValue: state.inputValue }
。對於mapDispatchToProps
,咱們返回一個函數onUserInput
,該函數將輸入值做爲參數,並使用USER_INPUT
類型派發action。返回的新狀態對象mapStateToProps
和onUserInput
函數合併到咱們組件的props
中。因此咱們定義咱們的組件:
class NodeD extends Component {
render() {
return (
<div className="Child element">
<center> D </center>
<textarea
type="text"
value={this.props.inputValue}
onChange={e => this.props.onUserInput(e.target.value)}
/>
</div>
);
}
}
const mapStateToProps = state => {
return {
inputValue: state.inputValue
};
};
const mapDispatchToProps = dispatch => {
return {
onUserInput: inputValue =>
dispatch({ type: "USER_INPUT", inputValue: inputValue })
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(NodeD);
複製代碼
咱們完成了節點D!如今讓咱們轉到節點E,在那裏咱們要顯示用戶輸入。
咱們但願在此節點上顯示用戶輸入數據。咱們已經知道這些數據基本上是咱們應用程序的當前狀態,就像咱們的store同樣。因此最終,咱們但願訪問該store並顯示其數據。爲此,咱們首先須要使用connect()咱們以前使用的相同功能具備mapStateToProps的函數將節點E組件訂閱到store的更新。以後,咱們只須要使用this.props.val
從組件的props中訪問store中的數據:
class NodeE extends Component {
render() {
return (
<div className="Child element">
<center> E </center>
{this.props.val}
</div>
);
}
}
const mapStateToProps = state => {
return {
val: state.inputValue
};
};
export default connect(mapStateToProps)(NodeE);
複製代碼
咱們終於完成了Redux!🎉你能夠看看咱們剛剛在這裏作了些什麼。
在更復雜的示例的狀況下,好比使用具備更多共享/操做存儲的組件的樹,咱們將須要在每一個組件處使用這兩個mapStateToProps和mapDispatchToProps函數。在這種狀況下,經過爲每一個組件建立單獨的文件夾,將咱們的操做類型和reducers與組件分開可能更明智。
......誰有時間?
如今讓咱們使用上下文API重作相同的示例。
React Context API已經存在了一段時間,但直到React的16.3.0版才能在生產中使用它變得安全。這裏的邏輯接近Redux的邏輯:咱們有一個上下文對象,它包含咱們但願從其餘組件訪問的一些全局數據。
首先,咱們建立一個上下文對象,其中包含應用程序的初始狀態做爲默認狀態 而後咱們建立一個Provider
和一個Consumer
組件:
const initialState = {
inputValue: ""
};
const Context = React.createContext(initialState);
export const Provider = Context.Provider;
export const Consumer = Context.Consumer;
複製代碼
咱們的
Provider
組件具備做爲子項的全部組件,咱們但願從中訪問上下文數據。就像Provider
上面的Redux
版本同樣。爲了提取或操縱上下文,咱們使用Consumer
至關於組件。
咱們但願咱們的Provider組件包裝整個App,就像上面的Redux版本同樣。然而,這Provider與咱們見過的前一個有點不一樣。在咱們的App組件中,咱們使用一些數據初始化默認狀態,咱們能夠經過值支持咱們的Provider組件來共享。
在咱們的示例中,咱們將共享this.state.inputValue以及操做狀態的函數,如咱們的onUserInput函數。
class App extends React.Component {
state = {
inputValue: ""
};
onUserInput = newVal => {
this.setState({ inputValue: newVal });
};
render() {
return (
<Provider
value={{ val: this.state.inputValue, onUserInput: this.onUserInput }}
>
<div className="App">
<NodeA />
</div>
</Provider>
);
}
}
複製代碼
如今咱們能夠繼續使用Consumer組件訪問Provider組件的數據:)
對於用戶輸入數據的節點D:
const NodeD = () => {
return (
<div className="Child element">
<center> D </center>
<Consumer>
{({ val, onUserInput }) => (
<textarea
type="text"
value={val}
onChange={e => onUserInput(e.target.value)}
/>
)}
</Consumer>
</div>
);
};
複製代碼
對於咱們在其中顯示用戶輸入的節點E:
const NodeE = () => {
return (
<div className="Child element ">
<center> E </center>
<Consumer>{context => <p>{context.val}</p>}</Consumer>
</div>
);
};
複製代碼
咱們完成了示例的context api版本!🎉這不是很難吧?在這裏查看
若是咱們但願可以訪問更多context的組件,該怎麼辦?咱們可使用Provider組件包裝它們,並使用Consumer組件來訪問/操做上下文!簡單 :)
咱們能夠看到咱們的Redux版本的示例比Context版本花了更多的時間。咱們已經能夠看到Redux了:
若是您正在處理一個更復雜的應用程序,並但願查看您的應用程序的全部dispatch操做的歷史記錄,「點擊」其中任何一個並跳轉到該時間點,而後絕對考慮使用Redux的漂亮的dope devTools擴展!
可是,若是你只想讓一些數據全局化以便從一堆組件中訪問它,你能夠從咱們的例子中看到Redux和React的Context API都作了大體相同的事情。因此在某種程度上,你沒必要使用Redux!