错误的 React 挂钩行为与事件监听器
Wrong React hooks behaviour with event listener
我在玩 React Hooks 并且遇到了问题。
当我尝试使用事件侦听器处理的按钮来控制台记录它时,它显示了错误的状态。
代码沙盒: https://codesandbox.io/s/lrxw1wr97m
- 点击 'Add card' 按钮 2 次
- 在第一张卡片中,点击 Button1 并在控制台中看到有 2 张卡片处于状态(正确行为)
- 在第一张卡片中,点击 Button2(由事件侦听器处理)并在控制台中看到只有一张卡片处于状态(错误行为)
为什么会显示错误的状态?
在第一张卡片中,Button2
应该在控制台中显示 2
卡片。有什么想法吗?
const { useState, useContext, useRef, useEffect } = React;
const CardsContext = React.createContext();
const CardsProvider = props => {
const [cards, setCards] = useState([]);
const addCard = () => {
const id = cards.length;
setCards([...cards, { id: id, json: {} }]);
};
const handleCardClick = id => console.log(cards);
const handleButtonClick = id => console.log(cards);
return (
<CardsContext.Provider
value={{ cards, addCard, handleCardClick, handleButtonClick }}
>
{props.children}
</CardsContext.Provider>
);
};
function App() {
const { cards, addCard, handleCardClick, handleButtonClick } = useContext(
CardsContext
);
return (
<div className="App">
<button onClick={addCard}>Add card</button>
{cards.map((card, index) => (
<Card
key={card.id}
id={card.id}
handleCardClick={() => handleCardClick(card.id)}
handleButtonClick={() => handleButtonClick(card.id)}
/>
))}
</div>
);
}
function Card(props) {
const ref = useRef();
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
}, []);
return (
<div className="card">
Card {props.id}
<div>
<button onClick={props.handleButtonClick}>Button1</button>
<button ref={node => (ref.current = node)}>Button2</button>
</div>
</div>
);
}
ReactDOM.render(
<CardsProvider>
<App />
</CardsProvider>,
document.getElementById("root")
);
<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>
<div id='root'></div>
我正在使用 React 16.7.0-alpha.0 和 Chrome 70.0.3538.110
顺便说一句,如果我使用类重写 CardsProvider,问题就消失了。
使用 class 的 CodeSandbox:https://codesandbox.io/s/w2nn3mq9vl
这是使用 useState
挂钩的功能组件的常见问题。同样的问题适用于任何使用 useState
状态的回调函数,例如.
事件处理程序在 CardsProvider
和 Card
组件中的处理方式不同。
handleCardClick
和 handleButtonClick
在 CardsProvider
功能组件中使用,在其范围内定义。每次运行都有新的函数,它们指的是在定义它们的那一刻获得的 cards
状态。每次呈现 CardsProvider
组件时都会重新注册事件处理程序。
在 Card
功能组件中使用的 handleCardClick
作为 prop 接收,并在 useEffect
的组件安装上注册一次。它在整个组件生命周期中都是相同的函数,并且指的是第一次定义 handleCardClick
函数时新鲜的陈旧状态。 handleButtonClick
作为道具接收并在每次 Card
渲染时重新注册,每次都是一个新函数并引用新状态。
可变状态
解决此问题的常用方法是使用 useRef
而不是 useState
。 ref 基本上是一个配方,它提供了一个可以通过引用传递的可变对象:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
在这种情况下,应该像 useState
预期的那样在状态更新时重新呈现组件,引用不适用。
可以单独保持状态更新和可变状态,但 forceUpdate
在 class 和功能组件(列出仅供参考)中都被认为是一种反模式:
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
状态更新函数
一个解决方案是使用状态更新函数,它从封闭范围接收新鲜状态而不是陈旧状态:
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
在这种情况下,像 console.log
这样的同步副作用需要一个状态,解决方法是 return 相同的状态以防止更新。
function eventListener() {
setState(freshState => {
console.log(freshState);
return freshState;
});
}
useEffect(() => {
// register eventListener once
return () => {
// unregister eventListener once
};
}, []);
这不适用于异步副作用,特别是 async
函数。
手动事件侦听器重新注册
另一种解决方案是每次都重新注册事件侦听器,这样回调总是从封闭范围获取最新状态:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
return () => {
// unregister eventListener
};
}, [state]);
内置事件处理
除非事件监听器注册在document
、window
或其他超出当前组件范围的事件目标上,否则React自己的DOM事件处理必须是尽可能使用,这样就不需要 useEffect
:
<button onClick={eventListener} />
在最后一种情况下,事件侦听器可以用 useMemo
或 useCallback
额外记忆,以防止在作为道具传递时不必要的重新渲染:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
- 此答案的上一版本建议使用适用于 React 16.7.0-alpha 版本中初始
useState
挂钩实现但在最终 React 16.8 实现中不可用的可变状态。 useState
目前仅支持不可变状态。*
解决这个问题的更简洁的方法是创建一个我称之为 useStateRef
的钩子
function useStateRef(initialValue) {
const [value, setValue] = useState(initialValue);
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return [value, setValue, ref];
}
您现在可以使用 ref
作为状态值的参考。
我的简短回答是 useState 对此有一个简单的解决方案:
function Example() {
const [state, setState] = useState(initialState);
function update(updates) {
// this might be stale
setState({...state, ...updates});
// but you can pass setState a function instead
setState(currentState => ({...currentState, ...updates}));
}
//...
}
查看控制台,你会得到答案:
React Hook useEffect has a missing dependency: 'props.handleCardClick'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
只需将 props.handleCardClick
添加到依赖项数组中,它就会正常工作。
我的简短回答
this WILL NOT not trigger re-render ever time myvar changes.
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, []);
This WILL trigger render -> putting myvar in []
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, [myvar]);
更改 index.js
文件中的以下行后,button2
运行良好:
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
- }, []);
+ });
你不应该使用 []
作为第二个参数 useEffect
除非你希望它 运行 一次。
这样您的回调将始终具有更新的状态值;)
// registers an event listener to component parent
React.useEffect(() => {
const parentNode = elementRef.current.parentNode
parentNode.addEventListener('mouseleave', handleAutoClose)
return () => {
parentNode.removeEventListener('mouseleave', handleAutoClose)
}
}, [handleAutoClose])
我在玩 React Hooks 并且遇到了问题。 当我尝试使用事件侦听器处理的按钮来控制台记录它时,它显示了错误的状态。
代码沙盒: https://codesandbox.io/s/lrxw1wr97m
- 点击 'Add card' 按钮 2 次
- 在第一张卡片中,点击 Button1 并在控制台中看到有 2 张卡片处于状态(正确行为)
- 在第一张卡片中,点击 Button2(由事件侦听器处理)并在控制台中看到只有一张卡片处于状态(错误行为)
为什么会显示错误的状态?
在第一张卡片中,Button2
应该在控制台中显示 2
卡片。有什么想法吗?
const { useState, useContext, useRef, useEffect } = React;
const CardsContext = React.createContext();
const CardsProvider = props => {
const [cards, setCards] = useState([]);
const addCard = () => {
const id = cards.length;
setCards([...cards, { id: id, json: {} }]);
};
const handleCardClick = id => console.log(cards);
const handleButtonClick = id => console.log(cards);
return (
<CardsContext.Provider
value={{ cards, addCard, handleCardClick, handleButtonClick }}
>
{props.children}
</CardsContext.Provider>
);
};
function App() {
const { cards, addCard, handleCardClick, handleButtonClick } = useContext(
CardsContext
);
return (
<div className="App">
<button onClick={addCard}>Add card</button>
{cards.map((card, index) => (
<Card
key={card.id}
id={card.id}
handleCardClick={() => handleCardClick(card.id)}
handleButtonClick={() => handleButtonClick(card.id)}
/>
))}
</div>
);
}
function Card(props) {
const ref = useRef();
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
}, []);
return (
<div className="card">
Card {props.id}
<div>
<button onClick={props.handleButtonClick}>Button1</button>
<button ref={node => (ref.current = node)}>Button2</button>
</div>
</div>
);
}
ReactDOM.render(
<CardsProvider>
<App />
</CardsProvider>,
document.getElementById("root")
);
<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>
<div id='root'></div>
我正在使用 React 16.7.0-alpha.0 和 Chrome 70.0.3538.110
顺便说一句,如果我使用类重写 CardsProvider,问题就消失了。 使用 class 的 CodeSandbox:https://codesandbox.io/s/w2nn3mq9vl
这是使用 useState
挂钩的功能组件的常见问题。同样的问题适用于任何使用 useState
状态的回调函数,例如
事件处理程序在 CardsProvider
和 Card
组件中的处理方式不同。
handleCardClick
和 handleButtonClick
在 CardsProvider
功能组件中使用,在其范围内定义。每次运行都有新的函数,它们指的是在定义它们的那一刻获得的 cards
状态。每次呈现 CardsProvider
组件时都会重新注册事件处理程序。
Card
功能组件中使用的 handleCardClick
作为 prop 接收,并在 useEffect
的组件安装上注册一次。它在整个组件生命周期中都是相同的函数,并且指的是第一次定义 handleCardClick
函数时新鲜的陈旧状态。 handleButtonClick
作为道具接收并在每次 Card
渲染时重新注册,每次都是一个新函数并引用新状态。
可变状态
解决此问题的常用方法是使用 useRef
而不是 useState
。 ref 基本上是一个配方,它提供了一个可以通过引用传递的可变对象:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
在这种情况下,应该像 useState
预期的那样在状态更新时重新呈现组件,引用不适用。
可以单独保持状态更新和可变状态,但 forceUpdate
在 class 和功能组件(列出仅供参考)中都被认为是一种反模式:
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
状态更新函数
一个解决方案是使用状态更新函数,它从封闭范围接收新鲜状态而不是陈旧状态:
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
在这种情况下,像 console.log
这样的同步副作用需要一个状态,解决方法是 return 相同的状态以防止更新。
function eventListener() {
setState(freshState => {
console.log(freshState);
return freshState;
});
}
useEffect(() => {
// register eventListener once
return () => {
// unregister eventListener once
};
}, []);
这不适用于异步副作用,特别是 async
函数。
手动事件侦听器重新注册
另一种解决方案是每次都重新注册事件侦听器,这样回调总是从封闭范围获取最新状态:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
return () => {
// unregister eventListener
};
}, [state]);
内置事件处理
除非事件监听器注册在document
、window
或其他超出当前组件范围的事件目标上,否则React自己的DOM事件处理必须是尽可能使用,这样就不需要 useEffect
:
<button onClick={eventListener} />
在最后一种情况下,事件侦听器可以用 useMemo
或 useCallback
额外记忆,以防止在作为道具传递时不必要的重新渲染:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
- 此答案的上一版本建议使用适用于 React 16.7.0-alpha 版本中初始
useState
挂钩实现但在最终 React 16.8 实现中不可用的可变状态。useState
目前仅支持不可变状态。*
解决这个问题的更简洁的方法是创建一个我称之为 useStateRef
function useStateRef(initialValue) {
const [value, setValue] = useState(initialValue);
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return [value, setValue, ref];
}
您现在可以使用 ref
作为状态值的参考。
我的简短回答是 useState 对此有一个简单的解决方案:
function Example() {
const [state, setState] = useState(initialState);
function update(updates) {
// this might be stale
setState({...state, ...updates});
// but you can pass setState a function instead
setState(currentState => ({...currentState, ...updates}));
}
//...
}
查看控制台,你会得到答案:
React Hook useEffect has a missing dependency: 'props.handleCardClick'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
只需将 props.handleCardClick
添加到依赖项数组中,它就会正常工作。
我的简短回答
this WILL NOT not trigger re-render ever time myvar changes.
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, []);
This WILL trigger render -> putting myvar in []
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, [myvar]);
更改 index.js
文件中的以下行后,button2
运行良好:
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
- }, []);
+ });
你不应该使用 []
作为第二个参数 useEffect
除非你希望它 运行 一次。
这样您的回调将始终具有更新的状态值;)
// registers an event listener to component parent
React.useEffect(() => {
const parentNode = elementRef.current.parentNode
parentNode.addEventListener('mouseleave', handleAutoClose)
return () => {
parentNode.removeEventListener('mouseleave', handleAutoClose)
}
}, [handleAutoClose])