this.setState()不触发重渲染:D3Tree组件重载后右键无响应问题
异常原因解析
咱们先拆解下为什么会出现这个问题:
全局函数的绑定冲突
你把contextmenu定义成了全局函数,然后在组件的constructor里执行contextmenu = contextmenu.bind(this)——这相当于把全局的contextmenu变量替换成了当前组件实例绑定后的版本。当你切换页面,旧的D3Tree组件卸载,再重新挂载新实例时,新实例会再次覆盖全局的contextmenu变量,但之前d3绑定到DOM元素上的右键事件,仍然引用的是旧实例绑定的那个函数。这时候右键点击,调用的是已卸载旧组件实例的setState,新组件的状态根本没变化,自然不会触发重渲染。DOM操作时机错误
你在shouldComponentUpdate里调用renderTree执行d3渲染,这是不合适的。shouldComponentUpdate的作用是判断组件是否需要重渲染,此时组件的DOM还没更新,在这里操作DOM可能会拿到过时的节点,甚至导致渲染逻辑混乱。废弃API的不当使用
ReactDom.findDOMNode已经被React官方废弃了,而且你用它来获取已经通过ref拿到的节点,完全没必要,还可能引发潜在问题。
解决方案
针对这些问题,咱们一步步修复:
1. 将contextmenu改为组件类方法
把全局的contextmenu移到组件内部,作为类的实例方法,这样每个组件实例都有自己独立的方法,不会互相覆盖干扰。
2. 调整DOM操作的时机
把renderTree的调用从shouldComponentUpdate移到componentDidUpdate,这个生命周期钩子是在组件更新完成、DOM已经更新后执行的,适合做DOM相关的操作。
3. 组件卸载时清理d3事件
在componentWillUnmount里移除d3绑定的右键事件,避免旧的事件绑定残留,影响新组件实例。
4. 替换findDOMNode为已有的ref
你已经通过ref={(tree) => { this.tree = tree; }}拿到了DOM节点,直接用this.tree就行,不用再调用findDOMNode。
修复后的完整代码
export default class D3Tree extends BaseWidget { constructor(props) { super(props); this.state = { style_popup : { top : 90, left : 90, position : 'absolute' }, render_on_click : false }; // 绑定组件自身的contextmenu方法到当前实例 this.contextmenu = this.contextmenu.bind(this); } componentDidMount(){ // 直接使用ref获取的DOM节点 if (this.props.treeData) { renderTree(this.props.treeData, this.tree, this.props.nodeName, this.contextmenu); } } componentDidUpdate(prevProps){ // 当treeData变化时,重新渲染树 if (prevProps.treeData !== this.props.treeData) { renderTree(this.props.treeData, this.tree, this.props.nodeName, this.contextmenu); } } componentWillUnmount(){ // 清理d3绑定的右键事件,避免残留 d3.select(this.tree).selectAll('*').on('contextmenu', null); } // 将contextmenu改为组件实例方法 contextmenu(node) { this.setState({ style_popup : { top : d3.event.clientY, left : d3.event.clientX, position : 'absolute' }, render_on_click : true }); } render() { return ( <div id="tree"> <div id ="tree-container" ref={(tree) => { this.tree = tree; }}> </div> {this.state.render_on_click && <div><PopUp popup_style={this.state.style_popup} /></div>} </div> ); } } // 修改renderTree函数,接收contextmenu作为参数,每次渲染时重新绑定 function renderTree(treeData, mountNode, nodeName, contextmenu) { // 这里保留你原来的d3渲染逻辑,最后绑定事件时用传入的contextmenu // 示例: // const tree = d3.select(mountNode)...; // ...其他渲染代码... // 绑定右键事件,确保每次渲染都用最新的组件实例方法 tree.selectAll('.node').on('contextmenu', contextmenu); }
关键说明
- 现在
contextmenu是组件的实例方法,每个D3Tree实例都有自己的版本,不会再出现全局变量覆盖的问题。 renderTree现在接收contextmenu作为参数,每次渲染时都会重新绑定事件,确保指向当前组件实例。componentWillUnmount里清理了所有d3绑定的右键事件,避免旧事件影响新组件。- 把DOM操作移到了
componentDidUpdate,符合React生命周期的最佳实践。
内容的提问来源于stack exchange,提问作者Akash Sateesh

