我遇到了react的这种非常奇怪的行为,并在下面创建了一个简单的演示。
其行为是,当我像第182行一样直接执行this.renderPortal()
时,可以关闭对话框。但是何时从setState回调中执行以下操作:
return <div onClick={()=>{
// this.renderPortal() //worker fine
this.setState({
a : 1
}, ()=>{
this.renderPortal()
})
}}> click me</div>
我无法关闭它,控制台将提示Cannot read property 'destroy' of null
。
似乎React认为从setState回调呈现的Dialog是无状态的,为什么?
let rootDialogContainer = document.createElement("div");
document.body.appendChild(rootDialogContainer);
function createConfig(config, defaults) {
return Object.assign(
{},
defaults,
typeof config === "string" ? { content: config } : config
);
}
class Modal extends React.Component {
constructor(props) {
super(props);
this.state = {
show: true
};
this.cancel = this.cancel.bind(this);
}
ok() {
if (this.props.autoClose) {
this.setState({ show: false });
}
const { onOk, afterClose } = this.props;
onOk && onOk();
afterClose();
}
cancel() {
if (this.mounted && this.props.autoClose) {
this.setState({ show: false });
}
const { onCancel, afterClose } = this.props;
onCancel && onCancel();
afterClose();
}
destroy() {
this.props.forceClose();
}
componentDidMount() {
this.mounted = true
}
componentWillUnmount() {
this.mounted = false
}
render() {
let DialogType = this.props.dialog || Dialog;
return (
<DialogType
{...this.props}
show={this.state.show}
onOk={this.ok}
onCancel={this.cancel}
/>
);
}
}
class Dialog extends React.Component{
constructor(props) {
super(props)
}
renderDialogWrap(params) {
let {
contentElm,
onCancel
} = params;
return (
<div className={"dialog"}>
<div style={{}} className={"inner-dialog"}>
{contentElm}
<div className="button" onClick={onCancel}>
cancel
</div>
</div>
</div>
);
}
renderPortal() {
return <Modal />;
}
render() {
let props = this.props;
let {
show,
className,
title,
content,
onCancel = () => {},
onOk = () => {},
children,
renderAsHiddenIfNotShow = false,
} = props;
return ReactDOM.createPortal(
<div
style={{ display: show ? "" : "none" }}
>
{this.renderDialogWrap({
onCancel,
contentElm: children,
show,
renderAsHiddenIfNotShow
})}
</div>,
document.body
);
}
}
Dialog.show = function(params) {
const config = createConfig(params, {
show: true,
autoClose: true,
onOk: () => {},
onCancel: () => {}
});
config.content = config.content || config.desc;
let container = rootDialogContainer
if (config.id) {
let containerId = `wrapper`
container = document.getElementById(containerId)
if (!container) {
container = document.createElement('div')
container.setAttribute('id', containerId)
document.body.appendChild(container)
}
}
config.forceClose = function(){
ReactDOM.unmountComponentAtNode(container);
};
config.afterClose = function() {
config.autoClose && config.forceClose();
};
return ReactDOM.render(this.buildModal(config), container);;
};
class Wrapper extends React.Component{
constructor(props){
super(props)
}
renderPortal(){
const destroy = () => {
this.myDialog.destroy();
};
this.myDialog = Dialog.show({
onOk: ()=>{},
onCancel: destroy,
onClose: destroy,
autoClose: false,
content: <div>
<p>Test</p>
</div>
});
}
render(){
return <div onClick={()=>{
// this.renderPortal() //worker fine
this.setState({
a : 1
}, ()=>{
this.renderPortal()
})
}}> click me</div>
}
}
Dialog.buildModal = function(config) {
return <Modal {...config} />;
};
function App() {
return (
<div className="App">
<Wrapper />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
.dialog{
HEIGHT: 50px;
background: white;
border:1px solid gray;
top: 20px;
width: 500px;
position: absolute;
}
.inner-dialog{
width: 100%;
height: 100%;
}
.button{
position: absolute;
bottom: 0;
border:1px solid gray;
}
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
答案 0 :(得分:-1)
原因很简单,虽然对话框本身是无状态的,但在呈现有状态的组件时会呈现它,因此ReactDom.render
会将其视为有状态的。
绕过此问题的简单技巧是替换:
this.setState({
a : 1
}, ()=>{
this.renderPortal()
})
})
与
this.setState({
a : 1
}, ()=>{
setTimeout(() => {
this.renderPortal()
}, 1)
})
通过这种方式,您的组件被视为无状态。
完整代码:
let rootDialogContainer = document.createElement("div");
document.body.appendChild(rootDialogContainer);
function createConfig(config, defaults) {
return Object.assign(
{},
defaults,
typeof config === "string" ? { content: config } : config
);
}
class Modal extends React.Component {
constructor(props) {
super(props);
this.state = {
show: true
};
this.cancel = this.cancel.bind(this);
}
ok() {
if (this.props.autoClose) {
this.setState({ show: false });
}
const { onOk, afterClose } = this.props;
onOk && onOk();
afterClose();
}
cancel() {
if (this.mounted && this.props.autoClose) {
this.setState({ show: false });
}
const { onCancel, afterClose } = this.props;
onCancel && onCancel();
afterClose();
}
destroy() {
this.props.forceClose();
}
componentDidMount() {
this.mounted = true
}
componentWillUnmount() {
this.mounted = false
}
render() {
let DialogType = this.props.dialog || Dialog;
return (
<DialogType
{...this.props}
show={this.state.show}
onOk={this.ok}
onCancel={this.cancel}
/>
);
}
}
class Dialog extends React.Component{
constructor(props) {
super(props)
}
renderDialogWrap(params) {
let {
contentElm,
onCancel
} = params;
return (
<div className={"dialog"}>
<div style={{}} className={"inner-dialog"}>
{contentElm}
<div className="button" onClick={onCancel}>
cancel
</div>
</div>
</div>
);
}
renderPortal() {
return <Modal />;
}
render() {
let props = this.props;
let {
show,
className,
title,
content,
onCancel = () => {},
onOk = () => {},
children,
renderAsHiddenIfNotShow = false,
} = props;
return ReactDOM.createPortal(
<div
style={{ display: show ? "" : "none" }}
>
{this.renderDialogWrap({
onCancel,
contentElm: children,
show,
renderAsHiddenIfNotShow
})}
</div>,
document.body
);
}
}
Dialog.show = function(params) {
const config = createConfig(params, {
show: true,
autoClose: true,
onOk: () => {},
onCancel: () => {}
});
config.content = config.content || config.desc;
let container = rootDialogContainer
if (config.id) {
let containerId = `wrapper`
container = document.getElementById(containerId)
if (!container) {
container = document.createElement('div')
container.setAttribute('id', containerId)
document.body.appendChild(container)
}
}
config.forceClose = function(){
ReactDOM.unmountComponentAtNode(container);
};
config.afterClose = function() {
config.autoClose && config.forceClose();
};
return ReactDOM.render(this.buildModal(config), container);;
};
class Wrapper extends React.Component{
constructor(props){
super(props)
}
renderPortal(){
const destroy = () => {
this.myDialog.destroy();
};
this.myDialog = Dialog.show({
onOk: ()=>{},
onCancel: destroy,
onClose: destroy,
autoClose: false,
content: <div>
<p>Test</p>
</div>
});
}
render(){
return <div onClick={()=>{
// this.renderPortal() //worker fine
this.setState({
a : 1
}, ()=>{
setTimeout(() => this.renderPortal(), 1)
})
}}> click me</div>
}
}
Dialog.buildModal = function(config) {
return <Modal {...config} />;
};
function App() {
return (
<div className="App">
<Wrapper />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
.dialog{
HEIGHT: 500px;
background: white;
border:1px solid gray;
top: 20px;
width: 500px;
position: absolute;
}
.inner-dialog{
width: 100%;
height: 100%;
}
.button{
position: absolute;
bottom: 0;
border:1px solid gray;
}
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>