Redux入门:将Redux与React连接起来

35 阅读9分钟

这是《Redux入门》系列的第三部分,在本教程中,我们将学习如何将Redux商店与React连接。Redux是一个独立的库,可以与所有流行的前端库和框架一起工作。由于其功能化的方法,它与React的合作是完美无缺的。

你不需要跟随本系列的前几部分,本教程就会有意义。如果你是来学习使用React和Redux的,你可以参加下面的快速回顾,然后查看上一部分的代码,从那里开始。

快速回顾

第一篇文章中,我们了解了Redux的工作流程,并回答了一个问题:为什么是Redux? 我们创建了一个非常基本的演示应用程序,并向你展示了Redux的各个组件--反应、还原器和存储--是如何连接的。

上一篇文章中,我们开始构建一个联系人列表应用程序,让你添加联系人,然后以列表形式显示。我们为我们的联系人列表创建了一个Redux商店,并添加了一些还原器和动作。我们尝试使用store.dispatch()store.getState() 等存储方法来调度动作和检索新状态。

在本文结束时,你将学会:

  1. 容器组件和展示性组件的区别
  2. 关于 react-redux 库和 redux-js-toolkit
  3. 如何使用connect()绑定 react 和 redux
  4. 如何使用mapDispatchToProps来调度动作
  5. 如何使用mapStateToProps检索状态
  6. 如何使用新的Redux钩子调度动作和获取状态:useDispatchuseSelector

本教程的代码可以在GitHub的react-redux-demorepo中找到。从主分支抓取代码,并将其作为本教程的起点。如果你想知道本教程结束时应用程序的样子,可以试试v2分支。让我们开始吧。

设计一个组件层次结构。聪明的组件与愚蠢的组件

这是一个你可能已经听说过的概念,但让我们快速看一下智能组件和哑巴组件的区别。回想一下,我们为组件创建了两个独立的目录,一个名为容器/,另一个名为组件/。这种方法的好处是,行为逻辑与视图分离。

呈现型组件被说成是哑巴,因为它们关注的是事物的外观。它们与应用程序的业务逻辑解耦,并完全通过props从父组件接收数据和回调。如果数据来自于父组件的本地状态,它们并不关心你的应用程序是否连接到Redux商店。

另一方面,容器组件处理的是行为部分,应该包含非常有限的DOM标记和样式。它们将需要渲染的数据作为道具传递给哑巴组件。

继续,让我们看看我们将如何组织我们的组件:

Designing component Hierarchy

呈现型组件

以下是我们在本教程中要使用的展示型组件:

components/AddContactForm.jsx

import React from 'react';

const AddContactForm = ({onInputChange, onFormSubmit}) => 
    (
		<form>
			<div className="form-group">
			    <label htmlFor="emailAddress">Email address</label>
			    <input type="email" class="form-control" name="email" onChange={onInputChange} placeholder="name@example.com" />
			</div>
			
		{/* Some code omitted for brevity */}
			  
			<div className="form-group">
			    <label htmlFor="physicalAddress">Address</label>
			    <textarea className="form-control" name="address" onChange={onInputChange} rows="3"></textarea>
			</div>

			<button type="submit" onClick={onFormSubmit} class="btn btn-primary"> Submit </button>
		</form>
	)

export default AddContactForm;

这是一个用于添加新联系人的HTML表单。该组件接收onInputChangeonFormSubmit 回调作为道具。当输入值发生变化时,onInputChange 事件被触发,当表单被提交时,onFormSubmit

components/ContactList.jsx

const ContactList = (props) => {
    return(	<ul className="list-group" id="contact-list">
           		{props.contactList.map(
                  (contact) => 
                  <li key={contact.email} className="list-group-item"> 
                    <ContactCard contact = {contact}/>
                  </li>
              	)}
            </ul>)
}

export default ContactList;

这个组件接收一个联系人对象的数组作为道具,因此被称为ContactList。我们使用Array.map() 方法来提取单个联系人的详细信息,然后将这些数据传递给<ContactCard />

组件/ContactCard.jsx

const ContactCard = ({contact}) => {
    
	return(
        <div>
	        <div className="col-xs-4 col-sm-3">
	           {contact.photo !== undefined ?  <img src={contact.photo} alt={contact.name} className="img-fluid rounded-circle" /> :
	            						 <img src="img/profile_img.png" alt ={contact.name} className="img-fluid rounded-circle" />}
	        </div>
	        <div className="col-xs-8 col-sm-9">
	            <span className="name">{contact.name + ' ' + contact.surname}</span><br/>
	            
	            {/* Some code omitted for brevity */}
                
	        </div>
	      </div>
        
    )
}

export default ContactCard;

这个组件接收一个联系人对象并显示联系人的姓名和图像。对于实际应用来说, 在云中托管 JavaScript图像可能是有意义的。

容器组件

我们还将构建裸体的容器组件。

容器/Contacts.jsx

function Contacts(props) {

  const returnContactList = () => {
    // Retrieve contactlist from the store
  }
   
    return (
        <div>   
         	<AddContact/>
         	<br />
          <ContactList contactList={returnContactList()} />
        </div>
    );
  
}


export default Contacts;

returnContactList() 函数检索联系人对象的数组并将其传递给ContactList组件。由于returnContactList() ,从商店检索数据,我们将暂时把这个逻辑留空。

containers/AddContact.jsx

function AddContact() {
    const shouldAddContactBox = () => {
        /* Logic for toggling ContactForm */
    }

    const handleInputChange = (event) => {
        const target = event.target;
        const value = target.value;
    	const name = target.name;

        /* Logic for handling Input Change */
    }

    const handleSubmit = (e) => {
        e.preventDefault()

        /* Logic for hiding the form and update the state */
    }

    const renderForm = () => {
        return(
			<div className="col-sm-8 offset-sm-2">
				<AddContactForm onFormSubmit={handleSubmit} onInputChange={handleInputChange} />
 			</div>
 		)
    }

    return(
        <div>            
            { /* A conditional statement goes here that checks whether the form 
                should be displayed or not */}
        </div>
    )
}

export default AddContact;

我们已经创建了三个赤裸裸的处理方法,与这三个动作相对应。它们都会派发动作来更新状态。因为我们需要获取状态,所以我们省去了显示/隐藏表单的逻辑。

现在让我们看看如何将 react 和 redux 绑定在一起。

react-redux 库

Redux中默认没有React的绑定。你需要先安装一个额外的库,叫做 react-redux。

npm install --save react-redux

该库导出了许多重要的 API,包括一个<Provider /> 组件、一个被称为connect() 的高阶函数以及useSelector()useDispatch() 等实用钩子。

提供者组件

像Redux这样的库需要让整个React组件树访问存储数据,从根组件开始。Provider模式允许库从上到下传递数据。下面的代码演示了Provider如何神奇地将状态添加到组件树中的所有组件。

演示代码

import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

整个应用程序都需要访问商店。因此,我们将提供者包裹在应用程序组件周围,然后将我们需要的数据添加到树的上下文中。组件的后代就可以访问这些数据了。

connect() 方法

现在,我们已经为我们的应用程序提供了商店,我们需要将React连接到商店。你可以与存储的唯一方式是通过调度行动和检索状态来进行通信。我们之前已经用store.dispatch() 来调度动作,用store.getState() 来检索状态的最新快照。connect() 可以让你做到这一点,但需要借助两个方法,即mapDispatchToPropsmapStateToProps 。我在下面的例子中演示了这个概念。

演示代码

import {connect} from 'react-redux'

const AddContact = ({newContact, addContact}) => {
  return (
    <div>
      {newContact.name} <br />
      {newContact.email} <br />
      {newContact.phone} <br />
      
      Are you sure you want to add this contact?
      <span onClick={addContact}> Yes </span>
    </div>
  )
}

const mapStateToProps = state => {
  return {
    newContact : state.contacts.newContact
  }
}

const mapDispatchToProps = dispatch => {
  return {
    addContact : () => dispatch(addContact())
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(AddContact)

mapStateToProps 和 都返回一个对象,而这个对象的键成为连接组件的一个道具。例如, 被映射到 。动作创建者 被映射到 。mapDispatchToProps state.contacts.newContact props.newContact addContact() props.addContact

但是,要使这一方法奏效,你需要上面代码片断中的最后一行:

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(AddContact)

我们不是直接导出AddContact 组件,而是导出一个连接组件。该连接提供addContactnewContact 作为<AddContact/> 组件的道具。

用Redux Hooks简化代码

我们在上一节中学习了如何将我们的React组件连接到状态。上面使用的技术的问题是我们不得不写大量的代码。我们不得不重复函数,将状态映射到动作调度器,将组件映射到存储。对于大型代码库来说,这可能成为一个更大的问题。

幸运的是,一些工具被添加到React Redux库中,唯一的目的是减少模板的数量,其中一个工具是useSelector 钩子。有了这个钩子,你不需要映射任何东西,也不需要connect(),只要导入这个钩子并使用它来访问你的应用程序中的任何地方的应用程序状态。

演示代码

import {useSelector, useDispatch} from 'react-redux'

const AddContact = ({newContact, addContact}) => {
  const dispatch = useDispatch()
  const newContact = useSelector(state => state.contact.newContact)
  return (
    <div>
      {newContact.name} <br />
      {newContact.email} <br />
      {newContact.phone} <br />
      
      Are you sure you want to add this contact?
      <span onClick={dispatch(addContact)}> Yes </span>
    </div>
  )
}

另一个钩子3useDispatch()-被用来在点击span元素时分配一个动作。与上一节的代码相比,你会同意这个代码更干净,更容易理解。也没有代码重复,这使得它在处理大型代码库时非常有用。

你应该注意,这些钩子是从React Redux v7.1开始引入的,所以你必须安装该版本或更高版本才能使用它们。

如何连接React和Redux

接下来,我们将介绍连接React和Redux需要遵循的步骤。

安装 react-redux 库

如果你还没有安装 react-redux 库。你可以使用NPM或Yarn来安装它。

npm install react-redux --save 

为你的应用程序组件提供商店

首先创建商店。然后,通过将其作为道具传递给<Provider /> ,使商店对象能够被你的组件树访问。

index.js

import React from 'react';
import {render}from 'react-dom';
import { Provider } from 'react-redux'
import App from './App';

import makeStore from './store'

const store = makeStore();

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

将React容器连接到Redux以使用状态

connect 函数被用来将React容器与Redux绑定。这意味着,你可以使用连接功能来:

  1. 订阅商店并将其状态映射到你的props上
  2. 派遣行动并将派遣回调映射到你的道具中

然而,我们将不再使用connect 函数来连接我们的商店。相反,我们将使用钩子从我们的商店获取信息,并在需要时调度行动。

首先,在 AddContact.jsx中导入useSelectoruseDispatch 和你想调度的动作:

import { useSelector, useDispatch } from 'react-redux';
import { addContact, handleInputChange, toggleContactForm } from '../actions/';

其次,在AddContact() 函数的第一行,导入组件所需的状态,并获得分配器:

const isHidden = useSelector(state => state.ui.isAddContactFormHidden)
const newContact = useSelector(state => state.contacts.newContact)

const dispatch = useDispatch()

现在,该组件已经具备了从商店读取状态和调度动作的能力。接下来,handeInputChange,handleSubmitshowAddContactBox 的逻辑应该被更新,如下:

showAddContactBox() {
    dispatch(toggleContactForm())		
}

handleInputChange(event) {
	const target = event.target;
	const value = target.value;
	const name = target.name;
   	        
    dispatch(handleInputChange(name, value))    	
}

handleSubmit(e) {
	e.preventDefault();
	dispatch(toggleContactForm())    	
    dispatch(addContact())    			
}

我们已经定义了处理方法,但仍然缺少一个部分--render 函数中的条件语句:

return(
	<div>			
		{ isHidden === false ? enderForm(): <button onClick={showAddContactBox} className="btn"> Add Contact </button>}
	</div>
)

如果isHidden 是假的,表单就会被呈现出来。否则,一个按钮就会被呈现出来。

显示联系人

我们已经完成了最具挑战性的部分。现在,剩下的就是将这些联系人显示为一个列表。Contacts 容器是该逻辑的最佳位置:

import React from 'react';
import { useSelector } from 'react-redux'; 
/* Component import omitted for brevity */

function Contacts() {
  const contactList = useSelector(state => state.contacts.contactList)     
  
  const returnContactList = () => {
    return contactList;
  }
    
    return (
        <div>
     		<br />
         	<AddContact/>
         	<br />
          <ContactList contactList= {returnContactList()} />
        </div>
    );  
}

export default Contacts

我们已经经历了上面连接联系人组件和Redux商店的相同程序,我们使用useSelector 来抓取所需的状态分支,也就是contactList 。这就完成了我们的应用程序与Redux商店的状态的整合。