React组件Props读取错误:无法读取null的'CustomerJoin'属性
React Props渲染报错:
Cannot read properties of null (reading 'CustomerJoin') 解决 问题说明
报错信息:Cannot read properties of null (reading 'CustomerJoin')
场景:项目包含Home父组件及两个子组件Customers、CustomerProfilePage。用户在Customers组件的列表中选择客户后,选中项会被设置到Home组件的state中,再以Props形式传递给CustomerProfilePage,但该组件触发上述报错。
状态管理逻辑:Home组件通过传递函数统一维护state,再将state作为Props分发给子组件使用。
相关组件代码
Home组件代码
class Home extends React.Component { constructor(props) { super(props); this.state = { activeComponent: window.location.pathname.replace('/',''), activeCustomer: null, activeFlight: null, activeContact: null, activeEmail: null} } setActiveCustomer = (selectedCustomer) => { this.setState({activeCustomer: selectedCustomer}) console.log('active customer - Home Component: ', selectedCustomer) } setComponent = (component) => { this.setState({activeComponent: component}) } renderComponent() { const path = this.state.activeComponent; if(path === 'CreateContacts') { return <CreateCustomer activeCustomer={this.state.activeCustomer}/> } else if (path === 'Customers') { return <Customers setActiveCustomer={this.setActiveCustomer}/> } else if (path.substring(0, path.indexOf('/')) === 'Customer') { return <CustomerProfilePage activeCustomer={this.state.activeCustomer} setComponent={this.setComponent} setActiveCustomer={this.setActiveCustomer} setActiveContact={this.setActiveContact}/> } else if (path === 'CreateContact'){ return <CreateContact activeCustomer={this.state.activeCustomer} setComponent={this.setComponent} activeContact={this.state.activeContact}/> } else if (path === 'Flights'){ return <Flights setFlight={this.setFlight} setComponent={this.setComponent}/> } else if (path === 'Flights/FlightConfiguration') { return <FlightPage activeFlight={this.state.activeFlight} setComponent={this.setComponent} setActiveEmail={this.setActiveEmail}/> } else if (path === 'EmailConfiguration'){ return <EmailConfiguration activeFlight={this.state.activeFlight} activeEmail={this.state.activeEmail}/> } return <HomeComponent/> } render() { return ( <BrowserRouter> <div className='container'> <div className='row'> <div className='col'> <Sidebar/> </div> </div> </div> <div className='component-container grey rounded-corners padding'> <div className='row'> <div className='col'> {this.renderComponent()} </div> </div> </div> </BrowserRouter> ) } }
CustomerProfilePage组件代码(报错组件)
import React from 'react'; import HeaderComponent from '../HelperComponents/ComponentHeaders' import { removeContact, getListOfContacts } from '../../Library/API_CALLS' export default class CustomerProfilePage extends React.Component { constructor(props){ super(props); this.state = { component: 'Customer Profile Page', activeCustomer: this.props.activeCustomer, activeContactList:[] } } componentDidMount(){ this.getContactList() } getContactList = () => { getListOfContacts(this.props.activeCustomer.CustomerId) .then((this.getContactListSuccess)) .catch((this.getContactListError)) } getContactListSuccess = (response) => { this.setState({activeContactList: response}) } getContactListError = (returnedError) => { console.log('Error Message: ', returnedError) } deleteContact = (deleteContact) => { removeContact(deleteContact.ContactId); this.setState(prevState => { const indexOfContact = prevState.activeContactList.findIndex( contact => contact.Email === deleteContact.Email); const newContactList = [...prevState.activeContactList]; if(indexOfContact >= 0) newContactList.splite(indexOfContact, 1); return {activeContactList: newContactList} }); } editContact = (contact) => { this.props.setActiveContact(contact) this.setComponent() } editContactButton = () => { this.props.setComponent('CreateContacts') } setComponent = () => { this.props.setComponent('CreateContacts') this.props.setActiveCustomer(this.props.activeCustomer) } convertDate = (entryDate) => { let returnDate = new Date(entryDate).getFullYear() return returnDate } buildContactTable(customerContactList, editContact){ const contactRow = customerContactList.map(contact => { return <tr><td><span>{contact.FirstName + ' ' + contact.LastName}</span></td><td>{contact.Email}</td><td>{contact.PhoneNumber}</td><td><button className='rounded-corners contact-button' onClick={function(){editContact(contact)}}>Edit</button><button className='red rounded-corners contact-button' onClick={() => this.deleteContact(contact)}> Delete</button></td></tr> }) return( <div className='row'> <div className='col'> <table> <thead> <tr className='green'> <th>Name</th> <th>Email</th> <th>Phone</th> </tr> </thead> <tbody> {contactRow} </tbody> </table> </div> </div> ) } render(){ const renderedDate = this.convertDate(this.state.activeCustomer.CustomerJoin) const contactList = this.state.activeContactList; return( <div className={this.state.component}> <HeaderComponent component={this.state.component} activeCustomer={this.props.activeCustomer.CustomerName}/> <div className='Component-Body'> <div className='Component-Element-Container'> <div className='row'> <div className='col'> <div className='center Component-Header-Text green'> <h3>{this.props.activeCustomer.CustomerName}</h3> </div> </div> </div> <div className='row'> <div className='col'> This Organization's profile is below </div> </div> <div className='row'> <div className='col'> <b>Organization Detail:</b> <div className='center'> <button className='rounded-corners customer-profile-button' onClick={this.setComponent}>Edit Customer</button> <button onClick={this.setComponent} className='rounded-corners customer-profile-button'>Add Contact</button> <button className='red rounded-corners customer-profile-button'> Delete Customer</button> </div> </div> </div> <div className='row'> <div className='col'> <table> <thead> <tr> <th>Hello</th> <th>Contact</th> <th>Country</th> </tr> </thead> <tbody> <tr> <td><b>Organization Name:</b> {this.props.activeCustomer.CustomerName}</td> <td><b>Phone: </b></td> <td><b>Fax: </b></td> </tr> <tr> <td><b>Primary Contact: </b></td> <td><b></b></td> <td><b></b></td> </tr> <tr> <td><b>Default Language: </b></td> <td><b>Fiscal Year Starts: </b></td> <td><b>Newsletter?: </b></td> </tr> <tr> <td><b>Customer Street:</b>{this.props.activeCustomer.CustomerStreet1}</td> <td><b>Customer Street 2: </b></td> <td><b>City: </b>{this.props.activeCustomer.CustomerCity}</td> </tr> <tr> <td><b>Customer State: </b>{this.props.activeCustomer.CustomerCity}</td> <td><b>Customer Acquisition Date: </b>{renderedDate}</td> <td></td> </tr> <tr> <td><b>Created By: </b></td> <td><b>Modified By: </b></td> <td></td> </tr> </tbody> </table> </div> </div> </div> <div className='Component-Element-Container'> {contactList.length > 0 ? this.buildContactTable(contactList, this.editContact) : null} </div> </div> </div> ) } }
Customers组件代码
import React from 'react'; import HeaderComponent from '../HelperComponents/ComponentHeaders' import {getCustomers} from '../../Library/API_CALLS' export default class Customers extends React.Component { constructor(props){ super(props); this.state = { component: 'Customers', customerList: [], firstPage: 0, lastPage: 5} this.handleClick = this.handleClick.bind(this) //There is presently no way to set the state for active customer in this component. //This is because the entire customer set of information is not required on this page. } componentDidMount(){ this.getListOfCustomers(); } getListOfCustomers = () => { getCustomers() .then((this.getCustomersListSuccess)) .catch((this.getContactListError)) } getCustomersListSuccess = (returnedCustomerList) => { this.setState({customerList: returnedCustomerList}) } getContactListError = (error) => { console.log(error) } setCustomer = (customer) => { console.log('Customer List Component: ', customer) this.props.setActiveCustomer(customer) } renderCustomerRow = (customer) => { let customerCard = customer return( <div className='Component-Element-Container' onClick={() => this.setCustomer(customerCard)} key={customer.CustomerId}> <a href={`/Customer/${customer.CustomerName.replace(/\s/g, '')}`}><h3>{customer.CustomerName}</h3></a> <p>{customer.CustomerStreet1}</p> </div> ) } handleClick() { this.manageList() } manageList(){ let lowIndex = Math.floor(Math.random() * this.state.customerList.length - 5); let highIndex = lowIndex + 5; if(lowIndex <= 0){ lowIndex = 0 highIndex = 5 } this.setState({firstPage: lowIndex, lastPage: highIndex}) //Since we're working on this just as a prototype, we have configured it in such a way that the pages will randomly flip and return //a positive value that is within the list of pages. //in a full blown application, this would be managed systematically. } render(){ const CustomerList = this.state.customerList.slice(this.state.firstPage,this.state.lastPage) return( <div className='Customers'> <HeaderComponent component={this.state.component}/> <div className='Component-Body'> {CustomerList.length > 0 ? CustomerList.map(this.renderCustomerRow) : <div>No number</div>} <div className='component-element-container'> <div className='center'> <button className='green forward-button' id='forward-button'onClick={this.handleClick.bind(this)}>Click</button> </div> </div> </div> </div> ) }}
原因分析
- 初始空值未处理:Home组件初始state中
activeCustomer为null,如果直接访问CustomerProfilePage(比如刷新页面、手动输入URL),此时activeCustomer还未被设置,组件渲染时直接访问this.state.activeCustomer.CustomerJoin会触发空值报错。 - Props到State的同步问题:
CustomerProfilePage在constructor中把props.activeCustomer存到自身state,但Props更新时State不会自动同步,即使Home的activeCustomer后续被设置,组件内部State还是旧的null。 - 生命周期中的空值调用:
componentDidMount中直接调用getListOfContacts(this.props.activeCustomer.CustomerId),如果此时activeCustomer为null,会触发同样的空值报错。
解决方案
1. 全局添加空值检查(核心修复)
(1)修改CustomerProfilePage的render方法,优先判断空值
render(){ // 先判断activeCustomer是否存在,不存在则显示加载占位 if (!this.props.activeCustomer) { return <div>加载中...</div>; } // 直接使用props中的activeCustomer,不再依赖组件内部state const renderedDate = this.convertDate(this.props.activeCustomer.CustomerJoin) const contactList = this.state.activeContactList; return( <div className={this.state.component}> <HeaderComponent component={this.state.component} activeCustomer={this.props.activeCustomer.CustomerName}/> <div className='Component-Body'> {/* 原渲染内容保持不变,所有访问activeCustomer的地方统一用props */} </div> </div> ) }
(2)移除组件内部对activeCustomer的State存储
constructor(props){ super(props); // 去掉activeCustomer,直接使用props this.state = { component: 'Customer Profile Page', activeContactList:[] } }
(3)给getContactList添加空值判断
getContactList = () => { if (!this.props.activeCustomer) return; getListOfContacts(this.props.activeCustomer.CustomerId) .then(this.getContactListSuccess) .catch(this.getContactListError) }
2. 监听Props变化,同步更新数据
添加componentDidUpdate生命周期,当activeCustomer变化时重新获取联系人列表:
componentDidUpdate(prevProps) { if (this.props.activeCustomer !== prevProps.activeCustomer) { this.getContactList(); } }
3. 优化路由跳转逻辑(避免跳转后State未更新)
将Customers组件中的a标签跳转改为先设置State再跳转,确保activeCustomer在页面渲染前已更新:
// 先导入withRouter import { withRouter } from 'react-router-dom'; // 修改renderCustomerRow方法 renderCustomerRow = (customer) => { const handleNavigate = () => { this.setCustomer(customer); // 使用history跳转代替a标签 this.props.history.push(`/Customer/${customer.CustomerName.replace(/\s/g, '')}`); } return( <div className='Component-Element-Container' onClick={handleNavigate} key={customer.CustomerId}> <h3>{customer.CustomerName}</h3> <p>{customer.CustomerStreet1}</p> </div> ) } // 导出时用withRouter包裹组件 export default withRouter(Customers);
4. 添加路由守卫(可选)
在Home组件的renderComponent方法中,判断如果访问CustomerProfilePage但activeCustomer为空,直接重定向到Customers页面:
// 先导入Redirect import { Redirect } from 'react-router-dom'; // 修改对应分支逻辑 else if (path.substring(0, path.indexOf('/')) === 'Customer') { if (!this.state.activeCustomer) { return <Redirect to="/Customers" />; } return <CustomerProfilePage activeCustomer={this.state.activeCustomer} setComponent={this.setComponent} setActiveCustomer={this.setActiveCustomer} setActiveContact={this.setActiveContact}/>; }
内容的提问来源于stack exchange,提问作者Stephen Smith
相关产品推荐
相关产品推荐

