寫在前面
同樣的,這篇文章繼續參考學習"Mr_大木"大佬的《React-Redux基礎(二):React裏面如何使用redux,redux修改操作》這篇文章後的一片筆記。
這篇文章在上一節的基礎之上,來給大家介紹下如何在基於React的項目中使用Redux。因爲是實際操作篇,所以就沒有前面的一些概念介紹的東西,我們直接開始操作環節。
react中使用redux
一、新建react項目demo
1、在介紹開始之前我們首先要創建一個react框架的項目demo。在這裏你可以試用react腳手架像上文一樣創建一個新的項目demo,你也可以將上一節中使用的demo代碼裏的,關於redux那些代碼註釋後繼續使用上一節的代碼。
因爲上一節的代碼裏我們只安裝了一個redux,其餘的都是沒有改變的,所以這裏我們直接使用上一節的代碼,將index.js裏面關於redux部分的代碼註釋即可,即恢復到react項目demo最初始的狀態,如下:
2、不管是新創建的項目demo,還是上節使用過的代碼,創建或恢復初始狀態後,在項目根目錄通過以下命令啓動項目,如下:
npm start
二、react項目中使用redux
1、在使用redux之前,首先要在項目中安裝redux,在react項目中使用redux的時候,除了安裝redux之外,還要安裝一個react-redux,它的作用類似於一個橋樑,將我們的react項目和單獨的redux進行連接,使得我們可以在react中去使用redux。所以通過以下命令全裝這倆插件依賴:
npm install redux react-redux --save-dev
2、安裝完成之後可以看到兩個插件當前的版本號:
3、接下來我們先在index.js文件裏引入createStore來創建一個store,因爲redux提供的全局狀態管理是要在整個應用中來使用的,所以我們在index.js文件裏操作,後期我們再來優化。首先是創建store,代碼如下:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
/***********創建store開始*************/
import { createStore } from 'redux';
function default_reducer(state = {
name: 'X北辰北',
url: 'www.xbeichen.cn'
}, action) {
switch(action.type) {
case 'setName':
return {
...state,
name: action.value
};
default:
return state;
};
};
let store = createStore(default_reducer);
/***********創建store結束*************/
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
上述代碼中創建store的過程跟上一節介紹的是沒有差別的。那我們創建的store怎麼和我們的react應用綁定起來呢,這裏就需要使用react-redux插件裏提供的一些方法了。
4、引入react-redux插件中的Provider,使用它來將我們的store和react應用綁定,這裏要注意的是,Provider組價跟我們的Router路由組件類似,是要放在應用代碼的最外層的,如下:
import { Provider } from 'react-redux';
/****省略其餘代碼***/
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
上述代碼裏使用了Provider組件來將我們新建的store和react應用做了綁定,這個組件中傳進去了我們新建的store作爲組件中store屬性的值,這樣就完成了store和react的綁定。
5、綁定完成之後,我們在組件裏應該怎麼來使用store裏的數據呢,這就需要react-redux插件中的另一個方法,叫connect。在組件中使用connect方法,即可將這個組件和我們的store進行連接,進而這個組件可以使用redux裏面store裏存儲的數據,代碼如下:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux'; //引入connect
class App extends React.Component {
render() {
return (
<div className="App">
<span>App組件</span>
</div>
);
}
}
//export default App;
export default connect()(App); //將組件和redux進行連接
因爲是在App這個組件裏我們來使用redux中的數據,所以我們在App組件代碼裏引入了connect方法,從而使這個組件和redux進行了連接。上面代碼可以看到,我們是用connect的時候,這個方法執行了兩次,所以有兩個小括號,這是因爲第一次執行的時候,它需要做合併和類似於dispatch的操作,第二次纔會將我們的組件包裹進去,進行和redux裏面的store連接。
6、connect()方法第一次執行的時候,會進行合併和dispatch操作,所以在它第一次執行的時候需要傳入兩個參數:第一個參數是一個函數,用來合併store裏的數據和你組件自身的props,所以在這裏可以看到它並不會將你組件自身的state和redux中的store合併,而是將組件自身的props和redux中的store合併,這是因爲redux推薦的一種做法:如果某些數據僅僅是你這個組件自身使用的話,那就通過組件內部的state去維護即可,沒有必要將數據往redux裏去放。所以它並不會合併你組件的state,而是合併組件的props,然後將合併後的數據放在你的props裏面,就是說它給你返回了一個props;第二個參數是一個json,說白了就是我們要傳入reducer的action。在這裏我們先來看下connect函數的第一個參數的用法:
export default connect(function() {}, {})(App);
//或者用箭頭函數
export default connect(() => {}, {})(App);
connect函數的第一個參數是一個用來合併store和組件自身props的函數,它需要兩個參數,第一個參數是state,第二個參數是props。這裏的state參數是redux傳過來的store,props參數是組件自身的props,然後函數體裏面用來合併這倆參數的代碼我們是用Object對象的assign方法,代碼如下:
export default connect((state, props) => {
return Object.assign({}, props, state); //定義了一個空對象
}, {})(App);
上述代碼是將store和props進行了合併,並且合併的時候以state裏的數據爲準,所以將state放在了props的後面,但是在它倆之前定義了一個空對象,這是因爲組件自身的props是隻讀的,我們沒法用state裏的數據去覆蓋props,而是通過一個空對象,將props裏的數據先複製到這個空對象,然後用state去覆蓋這個新定義的對象,進而達到合併的目的。那合併了之後怎麼用裏面的數據呢,我們來看看具體的使用:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux';
class App extends React.Component {
constructor(...args) {
super(...args);
this.state = {
name: 'GeoV'
}
}
render() {
console.log(this.props); //輸出合併後的數據
console.log(this.state); //輸出組件自身的state數據
return (
<div className="App">
<span>App組件</span>
<h5>名稱:{this.props.name}</h5>
</div>
);
}
}
//export default App;
export default connect((state, props) => {
return Object.assign({}, props, state);
}, {})(App);
上述代碼裏我們通過this.props.XXX的方式來在組件中使用合併後的數據,同時爲了區分,我們還定義了組件自身的state數據,將合併後的數據和組件自身的state數據在render方法裏進行了打印,可以看到是完全沒有問題的,符合我們的預期。
以上就是connect函數的第一個參數的基礎使用,到現在爲止,我們的組件從redux取值是沒有問題,在這裏只有一個組件,我們或許看不到redux的優勢,那我們新建兩個組件,來繼續看看它的優勢在哪。
7、在src目錄下新建components目錄,然後在裏面新建兩個組件,如下:
ComponentOne組件代碼:
import React from 'react';
import './ComponentOne.css';
class ComponentOne extends React.Component {
constructor(...args) {
super(...args);
this.state = {
name: 'ComponentOne'
}
}
render() {
return (
<div className="ComponentOne">
<span>ComponentOne組件</span>
</div>
);
}
}
export default ComponentOne;
ComponentTwo組件代碼:
import React from 'react';
import './ComponentTwo.css';
class ComponentTwo extends React.Component {
constructor(...args) {
super(...args);
this.state = {
name: 'ComponentTwo'
}
}
render() {
return (
<div className="ComponentTwo">
<span>ComponentTwo組件</span>
</div>
);
}
}
export default ComponentTwo;
然後在這兩個組件中分別引入connect之後,將其和redux進行連接,代碼如下:
//組件一的代碼
import React from 'react';
import './ComponentOne.css';
import { connect } from 'react-redux';
class ComponentOne extends React.Component {
constructor(...args) {
super(...args);
this.state = {
name: 'ComponentOne'
}
}
render() {
return (
<div className="ComponentOne">
<span>ComponentOne組件</span>
<h5>組件一中獲取的name:{this.props.name}</h5>
</div>
);
}
}
//export default ComponentOne;
export default connect((state, props) => {
return Object.assign({}, props, state);
}, {})(ComponentOne);
//組件二的代碼
import React from 'react';
import './ComponentTwo.css';
import { connect } from 'react-redux';
class ComponentTwo extends React.Component {
constructor(...args) {
super(...args);
this.state = {
name: 'ComponentTwo'
}
}
render() {
return (
<div className="ComponentTwo">
<span>ComponentTwo組件</span>
<h5>組件二中獲取的name:{this.props.name}</h5>
<h5>組件二中獲取的url:{this.props.url}</h5>
</div>
);
}
}
//export default ComponentTwo;
export default connect((state, props) => {
return Object.assign({}, props, state);
}, {})(ComponentTwo);
最後在App組件裏面引入這倆組件,如下:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux';
import ComponentOne from './components/componentOne/ComponentOne'; // 引入兩個組件
import ComponentTwo from './components/componentTwo/ComponentTwo';
class App extends React.Component {
constructor(...args) {
super(...args);
this.state = {
name: 'GeoV'
}
}
render() {
console.log(this.props); //輸出合併後的數據
console.log(this.state); //輸出組件自身的state數據
return (
<div className="App">
<span>App組件</span>
<h5>名稱:{this.props.name}</h5>
<ComponentOne /> //使用兩個組件
<ComponentTwo />
</div>
);
}
}
//export default App;
export default connect((state, props) => {
return Object.assign({}, props, state);
}, {})(App);
由上面結果可看到,我們使用了redux之後,組件之間就不存在複雜的父子組件傳值或者兄弟組件傳值這些問題了,主要組件要想獲取全局的某個數據,直接使用connect將其和redux中的store做連接,然後去獲取store裏的數據即可。以上就是關於組件對於redux中的值的獲取操作,也是connect方法中第一個參數的介紹。
8、接下來看看connect中的第二個參數的介紹。第二個參數之前說過是一個json,它主要是用來封裝action的,當我們涉及到需要修改redux中的數據時就需要這個參數。上一節我們知道修改store中的數據是通過store的dispatch方法去做的,但是在這裏我們不必再寫dispatch,因爲react-redux它幫我們封裝了,我們只需要在connect的第二個參數裏寫方法即可,如下:
import React from 'react';
import './ComponentTwo.css';
import { connect } from 'react-redux';
class ComponentTwo extends React.Component {
constructor(...args) {
super(...args);
this.state = {
name: 'ComponentTwo'
}
}
//修改名稱函數
_changeName=()=> {
this.props.setName(this.refs.name.value); //去調用connect函數中第二個參數中定義的函數setName
}
render() {
return (
<div className="ComponentTwo">
<span>ComponentTwo組件</span>
<h5>組件二中獲取的name:{this.props.name}</h5>
<h5>組件二中獲取的url:{this.props.url}</h5>
<input type='text' ref='name' />
<input type='button' value='修改名稱' onClick={this._changeName} />
</div>
);
}
}
//export default componentTwo;
export default connect((state, props) => {
return Object.assign({}, props, state);
}, {
setName(nameValue) {
return {
type: 'setName', //必須要有一個type屬性,跟reducer中的action的type屬性對應
value: nameValue
}
}
})(ComponentTwo);
由上述代碼可見,我們想要修改redux中的值時,只需要在connect方法中定義第二個參數即可,這個參數是一個json格式,裏面我們只需要直接寫函數就可以,它已經替我們封裝了dispatch方法,我們只需要在定義的函數裏返回action就可以了,返回的時候還是跟上文介紹的一樣,必須攜帶一個type屬性,這個屬性值必須跟reducer裏面的action中的type屬性一致。
9、以上就是關於整個redux在react項目中的應用介紹了,包括react組件中使用redux中的數據、在react組件中修改redux中的數據等。
三、一些優化操作
以上的文章給大家簡單介紹了一下如何在react項目中使用redux的問題,但是上述的過程中存在幾個地方需要優化,如果你不優化的話沒有任何問題,功能照樣滿足、代碼照樣執行,但是它不美觀,也不太友好,所以這部分給大家介紹下如何去優化它們,我們先看看要優化那幾部分:
- 首先要優化的就是index.js文件,因爲我們的store創建這些全部放在了index.js裏面,這樣違背了這個文件最初的用意;
- 其次就是要優化reducer裏面action的字符串,因爲後期涉及到可能會更改某一個type屬性,這樣一來更改的地方有點多,不符合編碼規範;
- 然後就是reducer的優化,目前我們只有一個reducer函數,也就是說當前是一個單一數據對象,如果我們將數據全部扔到這一個reducer裏面,那這個reducer會異常龐大,我們要分割這個reducer函數;
- 最後就是關於App這個組件在index.js裏面的使用問題,因爲目前的做法,當我們同時使用Provide和Router去包裹應用的時候會有問題。
瞭解了以上存在的問題之後,我們接下來就一個個的進行逐步優化。
1、首先是index.js文件裏面內容過多的問題。其實在這裏說的內容過多就是我們將store的創建放在了index.js裏面而已,所以我們需要將這個過程從index.js裏面提出來,單獨進行。
1.1、在src目錄下新建一個store目錄,然後在此目錄下新建一個index.js文件,如下:
1.2、新建完成之後,我們在項目根目錄的index.js文件裏面刪除掉關於redux的引用,然後將裏面的reducer定義和store創建代碼全都剪切到新建的store目錄下的index.js文件裏面,如下:
//store目錄下的index.js文件中的代碼
import { createStore } from 'redux';
function default_reducer(state = {
name: 'X北辰北',
url: 'www.xbeichen.cn'
}, action) {
switch(action.type) {
case 'setName':
return {
...state,
name: action.value
};
default:
return state;
};
};
let store = createStore(default_reducer);
export default store; //在最後要添加這一行代碼,將新建的store導出
1.3、然後在根目錄下的index.js裏面引入新建的store目錄下的index.js文件,並將其綁定到react應用上面,這時候根目錄下的index.js文件裏的代碼如下所示:
//項目根目錄下的index.js裏的代碼
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import store from './store/index'; //導入store
ReactDOM.render(
<Provider store={store}> //做綁定
<App />
</Provider>,
document.getElementById('root')
);
1.4、這樣一來我們就將store的創建從index.js文件裏面移動出去了,此時的index.js文件看起來清爽了很多。
2、接下來優化一下reducer裏面action的字符串問題,說白了就是下面這些代碼:
switch(action.type) {
case 'setName': //這行代碼裏的setName字符串
return {
...state,
name: action.value
};
default:
return state;
};
因爲上述代碼裏的setName字符串是寫死在代碼裏的,當我們想要更改的話,就要去這個代碼裏面來進行更改,如果這個文件裏有不同reducer函數裏都有setName這個字符串,那是一件很麻煩的事情,所以我們將它配置成一個變量,在另一份文件裏單獨維護,具體做法如下。
2.1、在store目錄下新建一個action.js文件,裏面將我們所用的action的字符串導出即可,如下:
export const SET_NAME = 'setName';
export const SET_URL = 'setUrl';
2.2、然後修改store目錄下的index.js文件代碼,如下:
import { createStore } from 'redux';
import { SET_NAME, SET_URL } from './action'; //先導入要使用的常量
function default_reducer(state = {
name: 'X北辰北',
url: 'www.xbeichen.cn'
}, action) {
switch(action.type) {
case SET_NAME: //用常量去替換掉代碼裏的字符串
return {
...state,
name: action.value
};
case SET_URL: //用常量去替換掉代碼裏的字符串
return {
...state,
url: action.value
};
default:
return state;
};
};
let store = createStore(default_reducer);
export default store;
同樣的,用到setName字符串的地方都可以這樣做,比如在組件二里面也用到了這個字符串,那我們可以同樣的去優化,關鍵代碼如下:
import { SET_NAME } from '../../store/action'; //先是導入這個常量
/***中間過程省略***/
export default connect((state, props) => {
return Object.assign({}, props, state);
}, {
setName(nameValue) {
return {
type: SET_NAME, //然後在是使用字符串的地方去替換即可
value: nameValue
}
}
})(ComponentTwo);
2.3、以上就是優化action裏的字符串過程,這樣一來後期修改字符串的話直接去維護action.js文件就可以,不用再回到reducer函數裏面去修改了。
3、接下來我們介紹下如何分割reducer,因爲一個真實的項目應用裏面不可能只有reducer函數,肯定是分割後的多個reducer函數,所以這就需要我們將分割後的多個reducer函數在創建store之前進行合併。
3.1、reducer的分割其實是我們開發的時候,自己定義多個reducer函數來形成的,如下:
function default_reducer(state = {
name: 'X北辰北',
url: 'www.xbeichen.cn'
}, action) {
switch(action.type) {
case SET_NAME:
return {
...state,
name: action.value
};
case SET_URL:
return {
...state,
url: action.value
};
default:
return state;
};
};
function second_reducer(state = {
age: 25,
phone: '2250685378'
},action) {
switch(action.type) {
case SET_AGE:
return {
...state,
age: action.value
};
default:
return state;
};
};
像上述的代碼一樣,我們定義了兩個reducer,那這個時候我們如果去直接創建store會有問題的,因爲你不知道要給createStore去傳哪一個reducer,所以在創建store之前我們首先要將這兩個分割的reducer合併。
3.2、合併的時候使用redux提供的combineReducers方法進行合併,如下:
import { createStore, combineReducers } from 'redux'; //引入combineReducers
import { SET_NAME, SET_URL, SET_AGE } from './action';
function default_reducer(state = {
name: 'X北辰北',
url: 'www.xbeichen.cn'
}, action) {
switch(action.type) {
case SET_NAME:
return {
...state,
name: action.value
};
case SET_URL:
return {
...state,
url: action.value
};
default:
return state;
};
};
function second_reducer(state = {
age: 25,
phone: '2250685378'
},action) {
switch(action.type) {
case SET_AGE:
return {
...state,
age: action.value
};
default:
return state;
};
};
let cobineReducer = combineReducers({ //合併reducer
first: default_reducer, //合併的時候指定一個命名空間
second: second_reducer
});
let store = createStore(cobineReducer); //用合併後的reducer去創建store
export default store;
3.3、上述代碼將兩個分割後的reducer進行了合併,並且用合併後的reducer去創建了store,但是在我們組建裏面使用store的時候,不能像以前一樣通過this.props.XXX的形式去訪問了,而是要加上合併時指定的一個命名空間,如下:
3.4、以上就是分割reducer和合並reducer並且使用store裏面的數據的介紹。
4、關於redux和router共同使用時會有衝突的問題,是由於兩個插件都在佔用props這個屬性造成的,一般是由於這種寫法造成的,如下:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import store from './store/index';
ReactDOM.render(
<Provider store={store}> //Provider和Router共同使用
<Router>
<App />
</Router>
</Provider>,
document.getElementById('root')
);
如果是上面這種寫法,當我們在路由跳轉的時候會在App這個組件裏會報錯。這個時候我們這樣改寫代碼即可:
import { Provider } from 'react-redux';
import { BrowserRouter as Router, Route } from 'react-router-dom'; //引入Route
ReactDOM.render(
<Provider store={store}>
<Router>
<Route component={App} /> //使用Route包裹一下App組件即可
</Router>
</Provider>,
document.getElementById('root')
);
其實上述的衝突問題一般不會遇到,只有在index.js文件裏同時使用Provider和Router去包裹App組件時纔有可能會發生,上面已經給瞭解決方法。因爲平時我們的路由配置一般不會寫在index.js這裏面,所以一般是不會遇到衝突問題的。
總結
以上就是這篇文章的全部內容了。文章從開始介紹react應用裏如何使用redux,並且給大家介紹瞭如何去優化的問題,當然,本文只是一個簡單的介紹,大家在項目開發時會遇到很多未知的問題,遇到問題的話記得滴滴博主,我們一起來學習。
附:
demo代碼地址如下:
https://gitee.com/XuQianWen/redux_basic