Rules of Hooks要求在每个渲染上调用相同的钩子和相同的顺序。如果你打破这个规则,就可以解释出现了什么问题。例如这段代码:
function App() {
console.log('render');
const [flag, setFlag] = useState(true);
const [first] = useState('first');
console.log('first is', first);
if (flag) {
const [second] = useState('second');
console.log('second is', second);
}
const [third] = useState('third');
console.log('third is', third);
useEffect(() => setFlag(false), []);
return null;
}
输出到控制台
render
first is first
second is second
third is third
render
first is first
third is second
并导致警告或错误。
但是在元素生命周期中不会改变的条件呢?
const DEBUG = true;
function TestConst() {
if (DEBUG) {
useEffect(() => console.log('rendered'));
}
return <span>test</span>;
}
这段代码并没有真正破坏规则,似乎工作正常。但它仍然会引发夹板警告。
此外,似乎可以编写基于道具的类似代码:
function TestState({id, debug}) {
const [isDebug] = useState(debug);
if (isDebug) {
useEffect(() => console.log('rendered', id));
}
return <span>{id}</span>;
}
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => setCounter(1), []);
return (
<div>
<TestState id="1" debug={false}/>
<TestState id="2" debug={true}/>
</div>
);
}
此代码按预期工作。
因此,当我确定它不会改变时,在一个条件内调用钩子是否安全?是否可以修改eslint规则来识别这种情况?
问题更多的是真正的要求,而不是实现类似行为的方式。据我所知,重要的是
确保每次组件渲染时都以相同的顺序调用Hook。这就是允许React在多个useState和useEffect调用之间正确保留Hook状态的原因
并且这个规则有一个例外的地方:“不要在循环,条件或嵌套函数中调用Hooks”。
虽然您可以像上面提到的那样有条件地编写钩子,但它可能目前正在工作,但它可能导致将来出现预期的行为。例如,在当前情况下,您没有修改isDebug
状态。
演示
const {useState, useEffect} = React;
function TestState({id, debug}) {
const [isDebug, setDebug] = useState(debug);
if (isDebug) {
useEffect(() => console.log('rendered', id));
}
const toggleButton = () => {
setDebug(prev => !prev);
}
return (
<div>
<span>{id}</span>
<button type="button" onClick={toggleButton}>Toggle debug</button>
</div>
);
}
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => setCounter(1), []);
return (
<div>
<TestState id="1" debug={false}/>
<TestState id="2" debug={true}/>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<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="app"/>
根据经验,您不应该违反规则,因为它可能是将来出现问题的原因。您可以通过以下方式处理上述方案而不违反规则
const {useState, useEffect} = React;
function TestState({id, debug}) {
const [isDebug, setDebug] = useState(debug);
useEffect(() => {
if(isDebug) {
console.log('rendered', id)
}
}, [isDebug]);
const toggleButton = () => {
setDebug(prev => !prev);
}
return (
<div>
<span>{id}</span>
<button type="button" onClick={toggleButton}>Toggle debug</button>
</div>
);
}
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => setCounter(1), []);
return (
<div>
<TestState id="1" debug={false}/>
<TestState id="2" debug={true}/>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<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="app"/>
对于你的用例,我没有看到问题,我不知道将来会如何破坏,你说它是正常的。
但是,我认为警告实际上是合法的,并且应该始终存在,因为这可能是您的代码中的潜在错误(不是在这个特定的错误中)
所以我在你的情况下做的是禁用该行的react-hooks/rules-of-hooks
规则。
此挂钩规则解决了条件挂钩调用可能出现问题时的常见情况:
不要在循环,条件或嵌套函数中调用Hook。相反,始终在React函数的顶层使用Hooks。通过遵循此规则,您可以确保每次组件呈现时都以相同的顺序调用Hook。
如果开发人员不完全了解后果,则此规则是安全的选择,可以作为经验法则使用。
但这里的实际规则是:
确保每次组件渲染时都以相同的顺序调用Hook
使用循环,条件和嵌套函数是完全没问题的,只要它保证在同一个组件实例中以相同的数量和顺序调用钩子。
如果在运行时重新分配process.env.NODE_ENV === 'development'
属性,甚至process.env.NODE_ENV
条件也可能在组件寿命期间发生变化。
如果条件是常量,则可以在组件外部定义它以保证:
const isDebug = process.env.NODE_ENV === 'development';
function TestConst() {
if (isDebug) {
useEffect(...);
}
...
}
如果条件来自动态值(特别是初始道具值),则可以记忆:
function TestConst({ debug }) {
const isDebug = useMemo(() => debug, []);
if (isDebug) {
useEffect(...);
}
...
}
或者,由于useMemo
isn't guaranteed to preserve values将来React发布,可以使用useState
(如问题所示)或useRef
;后者没有额外的开销和合适的语义:
function TestConst({ debug }) {
const isDebug = useRef(debug).current;
if (isDebug) {
useEffect(...);
}
...
}
如果有react-hooks/rules-of-hooks
ESLint规则,则可以每行禁用它。
请不要使用这种模式。它可能适用于您的示例,但它不是很好(或惯用)。
标准模式(有充分理由)是在构造函数中声明初始状态,然后更新以响应正文中的某些条件(setState)。 React Hooks在无状态组件中镜像这个功能 - 所以它应该工作相同。
其次,我看不出动态添加这段状态是如何有用的,并且可能会在以后引起渲染问题。在您的示例中,一个简单的const也可以正常工作 - 没有理由使用动态状态。
考虑一下:
return (<React.Fragment>{second}</React.Fragment>)
只要没有定义second
,就会出现Reference错误。