React hooks 函数组件防止在状态更新时重新渲染
React hooks function component prevent re-render on state update
我正在学习 React Hooks,这意味着我将不得不从 classes 转移到函数组件。以前在 classes 中,我可以有 class 独立于状态的变量,我可以在不重新渲染组件的情况下更新这些变量。现在我正在尝试将组件重新创建为带有钩子的函数组件,我遇到了 运行 我无法(据我所知)为该函数创建变量的问题,因此唯一的存储方式数据通过 useState
挂钩。然而,这意味着只要该状态更新,我的组件就会重新呈现。
我在下面的示例中对此进行了说明,我试图将 class 组件重新创建为使用挂钩的函数组件。我想在有人点击它时为 div 设置动画,但如果用户在它已经设置动画时点击它,则阻止再次调用动画。
class ClassExample extends React.Component {
_isAnimating = false;
_blockRef = null;
onBlockRef = (ref) => {
if (ref) {
this._blockRef = ref;
}
}
// Animate the block.
onClick = () => {
if (this._isAnimating) {
return;
}
this._isAnimating = true;
Velocity(this._blockRef, {
translateX: 500,
complete: () => {
Velocity(this._blockRef, {
translateX: 0,
complete: () => {
this._isAnimating = false;
}
},
{
duration: 1000
});
}
},
{
duration: 1000
});
};
render() {
console.log("Rendering ClassExample");
return(
<div>
<div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
</div>
);
}
}
const FunctionExample = (props) => {
console.log("Rendering FunctionExample");
const [ isAnimating, setIsAnimating ] = React.useState(false);
const blockRef = React.useRef(null);
// Animate the block.
const onClick = React.useCallback(() => {
if (isAnimating) {
return;
}
setIsAnimating(true);
Velocity(blockRef.current, {
translateX: 500,
complete: () => {
Velocity(blockRef.current, {
translateX: 0,
complete: () => {
setIsAnimating(false);
}
},
{
duration: 1000
});
}
},
{
duration: 1000
});
});
return(
<div>
<div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
</div>
);
};
ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
如果您单击 ClassExample 栏(粉红色),您将看到它在设置动画时不会重新呈现,但是如果您单击 FunctionExample 栏(红色),它会在设置动画时重新呈现两次。这是因为我使用 setIsAnimating
导致重新渲染。我知道这可能不是很能提高性能,但如果功能组件可能的话,我想阻止它。有没有suggestions/am我做错了什么?
更新(尝试修复,尚无解决方案):
下面用户 lecstor 建议可以将 useState 的结果更改为 let 而不是 const,然后直接设置它 let [isAnimating] = React.useState(false);
。不幸的是,这也不起作用,正如您在下面的代码片段中看到的那样。单击红色条将启动其动画,单击 o运行ge 方块将使组件重新渲染,如果再次单击红色条,它将打印 isAnimating
已重置为false 即使栏仍在动画。
const FunctionExample = () => {
console.log("Rendering FunctionExample");
// let isAnimating = false; // no good if component rerenders during animation
// abuse useState var instead?
let [isAnimating] = React.useState(false);
// Var to force a re-render.
const [ forceCount, forceUpdate ] = React.useState(0);
const blockRef = React.useRef(null);
// Animate the block.
const onClick = React.useCallback(() => {
console.log("Is animating: ", isAnimating);
if (isAnimating) {
return;
}
isAnimating = true;
Velocity(blockRef.current, {
translateX: 500,
complete: () => {
Velocity(blockRef.current, {
translateX: 0,
complete: () => {
isAnimating = false;
}
}, {
duration: 5000
});
}
}, {
duration: 5000
});
});
return (
<div>
<div
id = 'block'
onClick = {onClick}
ref = {blockRef}
style = {
{
width: '100px',
height: '10px',
backgroundColor: 'red'
}
}
>
{}
</div>
<div onClick={() => forceUpdate(forceCount + 1)}
style = {
{
width: '100px',
height: '100px',
marginTop: '12px',
backgroundColor: 'orange'
}
}/>
</div>
);
};
ReactDOM.render( < div > < FunctionExample / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
更新2(解决方案):
如果你想在函数组件中有一个变量,但不想让它在更新时重新渲染组件,你可以使用 useRef
而不是 useState
。 useRef
不仅仅可以用于 dom 元素,实际上建议用于实例变量。
参见:https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
为什么您认为不能像使用 classes 一样使用内部变量?
好的,感觉有点脏但是改变 useState 状态怎么样? 8)
不,这不符合预期
状态在重新渲染时重置
所以这与组件的实际渲染无关,也许我们的逻辑需要基于动画本身。这个特殊问题可以通过检查 class 动画时元素上的速度设置来解决。
const FunctionExample = ({ count }) => {
console.log("Rendering FunctionExample", count);
// let isAnimating = false; // no good if component rerenders during animation
// abuse useState var instead?
// let [isAnimating] = React.useState(false);
const blockRef = React.useRef(null);
// Animate the block.
const onClick = React.useCallback(() => {
// use feature of the anim itself
if (/velocity-animating/.test(blockRef.current.className)) {
return;
}
console.log("animation triggered");
Velocity(blockRef.current, {
translateX: 500,
complete: () => {
Velocity(blockRef.current, {
translateX: 0,
}, {
duration: 1000
});
}
}, {
duration: 5000
});
});
return (
<div>
<div
id = 'block'
onClick = {onClick}
ref = {blockRef}
style = {
{
width: '100px',
height: '10px',
backgroundColor: 'red'
}
}
>
{}
</div>
</div>
);
};
const Counter = () => {
const [count, setCount] = React.useState(0);
return <div>
<FunctionExample count={count} />
<button onClick={() => setCount(c => c + 1)}>Count</button>
</div>;
}
ReactDOM.render( < div > < Counter / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
使用 ref 在函数调用之间保留值而不触发渲染
class ClassExample extends React.Component {
_isAnimating = false;
_blockRef = null;
onBlockRef = (ref) => {
if (ref) {
this._blockRef = ref;
}
}
// Animate the block.
onClick = () => {
if (this._isAnimating) {
return;
}
this._isAnimating = true;
Velocity(this._blockRef, {
translateX: 500,
complete: () => {
Velocity(this._blockRef, {
translateX: 0,
complete: () => {
this._isAnimating = false;
}
},
{
duration: 1000
});
}
},
{
duration: 1000
});
};
render() {
console.log("Rendering ClassExample");
return(
<div>
<div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
</div>
);
}
}
const FunctionExample = (props) => {
console.log("Rendering FunctionExample");
const isAnimating = React.useRef(false)
const blockRef = React.useRef(null);
// Animate the block.
const onClick = React.useCallback(() => {
if (isAnimating.current) {
return;
}
isAnimating.current = true
Velocity(blockRef.current, {
translateX: 500,
complete: () => {
Velocity(blockRef.current, {
translateX: 0,
complete: () => {
isAnimating.current = false
}
},
{
duration: 1000
});
}
},
{
duration: 1000
});
});
return(
<div>
<div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
</div>
);
};
ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
我正在学习 React Hooks,这意味着我将不得不从 classes 转移到函数组件。以前在 classes 中,我可以有 class 独立于状态的变量,我可以在不重新渲染组件的情况下更新这些变量。现在我正在尝试将组件重新创建为带有钩子的函数组件,我遇到了 运行 我无法(据我所知)为该函数创建变量的问题,因此唯一的存储方式数据通过 useState
挂钩。然而,这意味着只要该状态更新,我的组件就会重新呈现。
我在下面的示例中对此进行了说明,我试图将 class 组件重新创建为使用挂钩的函数组件。我想在有人点击它时为 div 设置动画,但如果用户在它已经设置动画时点击它,则阻止再次调用动画。
class ClassExample extends React.Component {
_isAnimating = false;
_blockRef = null;
onBlockRef = (ref) => {
if (ref) {
this._blockRef = ref;
}
}
// Animate the block.
onClick = () => {
if (this._isAnimating) {
return;
}
this._isAnimating = true;
Velocity(this._blockRef, {
translateX: 500,
complete: () => {
Velocity(this._blockRef, {
translateX: 0,
complete: () => {
this._isAnimating = false;
}
},
{
duration: 1000
});
}
},
{
duration: 1000
});
};
render() {
console.log("Rendering ClassExample");
return(
<div>
<div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
</div>
);
}
}
const FunctionExample = (props) => {
console.log("Rendering FunctionExample");
const [ isAnimating, setIsAnimating ] = React.useState(false);
const blockRef = React.useRef(null);
// Animate the block.
const onClick = React.useCallback(() => {
if (isAnimating) {
return;
}
setIsAnimating(true);
Velocity(blockRef.current, {
translateX: 500,
complete: () => {
Velocity(blockRef.current, {
translateX: 0,
complete: () => {
setIsAnimating(false);
}
},
{
duration: 1000
});
}
},
{
duration: 1000
});
});
return(
<div>
<div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
</div>
);
};
ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
如果您单击 ClassExample 栏(粉红色),您将看到它在设置动画时不会重新呈现,但是如果您单击 FunctionExample 栏(红色),它会在设置动画时重新呈现两次。这是因为我使用 setIsAnimating
导致重新渲染。我知道这可能不是很能提高性能,但如果功能组件可能的话,我想阻止它。有没有suggestions/am我做错了什么?
更新(尝试修复,尚无解决方案):
下面用户 lecstor 建议可以将 useState 的结果更改为 let 而不是 const,然后直接设置它 let [isAnimating] = React.useState(false);
。不幸的是,这也不起作用,正如您在下面的代码片段中看到的那样。单击红色条将启动其动画,单击 o运行ge 方块将使组件重新渲染,如果再次单击红色条,它将打印 isAnimating
已重置为false 即使栏仍在动画。
const FunctionExample = () => {
console.log("Rendering FunctionExample");
// let isAnimating = false; // no good if component rerenders during animation
// abuse useState var instead?
let [isAnimating] = React.useState(false);
// Var to force a re-render.
const [ forceCount, forceUpdate ] = React.useState(0);
const blockRef = React.useRef(null);
// Animate the block.
const onClick = React.useCallback(() => {
console.log("Is animating: ", isAnimating);
if (isAnimating) {
return;
}
isAnimating = true;
Velocity(blockRef.current, {
translateX: 500,
complete: () => {
Velocity(blockRef.current, {
translateX: 0,
complete: () => {
isAnimating = false;
}
}, {
duration: 5000
});
}
}, {
duration: 5000
});
});
return (
<div>
<div
id = 'block'
onClick = {onClick}
ref = {blockRef}
style = {
{
width: '100px',
height: '10px',
backgroundColor: 'red'
}
}
>
{}
</div>
<div onClick={() => forceUpdate(forceCount + 1)}
style = {
{
width: '100px',
height: '100px',
marginTop: '12px',
backgroundColor: 'orange'
}
}/>
</div>
);
};
ReactDOM.render( < div > < FunctionExample / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
更新2(解决方案):
如果你想在函数组件中有一个变量,但不想让它在更新时重新渲染组件,你可以使用 useRef
而不是 useState
。 useRef
不仅仅可以用于 dom 元素,实际上建议用于实例变量。
参见:https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
为什么您认为不能像使用 classes 一样使用内部变量?
好的,感觉有点脏但是改变 useState 状态怎么样? 8)
不,这不符合预期 状态在重新渲染时重置
所以这与组件的实际渲染无关,也许我们的逻辑需要基于动画本身。这个特殊问题可以通过检查 class 动画时元素上的速度设置来解决。
const FunctionExample = ({ count }) => {
console.log("Rendering FunctionExample", count);
// let isAnimating = false; // no good if component rerenders during animation
// abuse useState var instead?
// let [isAnimating] = React.useState(false);
const blockRef = React.useRef(null);
// Animate the block.
const onClick = React.useCallback(() => {
// use feature of the anim itself
if (/velocity-animating/.test(blockRef.current.className)) {
return;
}
console.log("animation triggered");
Velocity(blockRef.current, {
translateX: 500,
complete: () => {
Velocity(blockRef.current, {
translateX: 0,
}, {
duration: 1000
});
}
}, {
duration: 5000
});
});
return (
<div>
<div
id = 'block'
onClick = {onClick}
ref = {blockRef}
style = {
{
width: '100px',
height: '10px',
backgroundColor: 'red'
}
}
>
{}
</div>
</div>
);
};
const Counter = () => {
const [count, setCount] = React.useState(0);
return <div>
<FunctionExample count={count} />
<button onClick={() => setCount(c => c + 1)}>Count</button>
</div>;
}
ReactDOM.render( < div > < Counter / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
使用 ref 在函数调用之间保留值而不触发渲染
class ClassExample extends React.Component {
_isAnimating = false;
_blockRef = null;
onBlockRef = (ref) => {
if (ref) {
this._blockRef = ref;
}
}
// Animate the block.
onClick = () => {
if (this._isAnimating) {
return;
}
this._isAnimating = true;
Velocity(this._blockRef, {
translateX: 500,
complete: () => {
Velocity(this._blockRef, {
translateX: 0,
complete: () => {
this._isAnimating = false;
}
},
{
duration: 1000
});
}
},
{
duration: 1000
});
};
render() {
console.log("Rendering ClassExample");
return(
<div>
<div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
</div>
);
}
}
const FunctionExample = (props) => {
console.log("Rendering FunctionExample");
const isAnimating = React.useRef(false)
const blockRef = React.useRef(null);
// Animate the block.
const onClick = React.useCallback(() => {
if (isAnimating.current) {
return;
}
isAnimating.current = true
Velocity(blockRef.current, {
translateX: 500,
complete: () => {
Velocity(blockRef.current, {
translateX: 0,
complete: () => {
isAnimating.current = false
}
},
{
duration: 1000
});
}
},
{
duration: 1000
});
});
return(
<div>
<div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
</div>
);
};
ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>