我正在使用 React Context 来存储数据并提供修改这些数据的功能。
现在,我正在尝试使用 React Hooks 将类组件转换为功能组件。
虽然类中的一切都按预期工作,但我无法让它在功能组件中工作。
由于我的应用程序代码有点复杂,我创建了这个小示例(JSFiddle 链接(,它允许重现问题:
首先是上下文,对于类和功能组件都是相同的:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
constructor (props) {
super(props);
this.increase = this.increase.bind(this);
this.reset = this.reset.bind(this);
this.state = {
current: 0,
increase: this.increase,
reset: this.reset
}
}
render () {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase (step) {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset () {
this.setState({
current: 0
});
}
}
现在,这里是类组件,它工作得很好:
class MyComponent extends React.Component {
constructor (props) {
super(props);
this.increaseByOne = this.increaseByOne.bind(this);
}
componentDidMount () {
setInterval(this.increaseByOne, 1000);
}
render () {
const count = this.context;
return (
<div>{count.current}</div>
);
}
increaseByOne () {
const count = this.context;
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}
}
MyComponent.contextType = MyContext;
预期结果是,它在一秒的间隔内计为 5 - 然后从 0 再次开始。
这是转换后的功能组件:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, []);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
它不会将计数器重置为 5,而是恢复计数。
问题是,if (count.current === 5) {
行中的count.current
总是0
,因为它不使用最新值。
我让它工作的唯一方法是按以下方式调整代码:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, [count]);
React.useEffect(() => {
console.log('useEffect');
const interval = setInterval(increaseByOne, 1000);
return () => {
clearInterval(interval);
};
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
现在,increaseByOne
回调在上下文的每次更改时都会重新创建,这也意味着每秒调用一次效果。
结果是,它会在每次更改上下文时清除间隔并设置一个新间隔(您可以在浏览器控制台中看到(。
这可能适用于这个小示例,但它改变了原始逻辑,并且有更多的开销。
我的应用程序不依赖于间隔,但它正在侦听事件。删除事件侦听器并在稍后再次添加它,意味着,如果它们在侦听器的删除和绑定之间触发,我可能会丢失一些事件,这是由 React 异步完成的。
有人有一个想法,期望如何反应,在不改变一般逻辑的情况下解决这个问题?
我在这里创建了一个小提琴,以使用上面的代码:
https://jsfiddle.net/Jens_Duttke/78y15o9p/
第一个解决方案是将随时间变化的数据放入useRef
中,以便可以通过引用而不是闭包访问它(以及您在基于类的版本中访问实际this.state
(
const MyComponent = (props) => {
const countByRef = React.useRef(0);
countByRef.current = React.useContext(MyContext);
React.useEffect(() => {
setInterval(() => {
const count = countByRef.current;
console.log(count.current);
if (count.current === 5) {
count.reset();
} else {
count.increase(1);
}
}, 1000);
}, []);
return (
<div>{countByRef.current.current}</div>
);
}
另一种解决方案是修改reset
和increase
以允许函数参数,以及使用setState
和useState
的更新程序。
那么它会是
useEffect(() => {
setInterval(() => {
count.increase(current => current === 5? 0: current + 1);
}, 1000);
}, [])
PS也希望你没有错过真实代码中的清理功能:
useEffect(() => {
const timerId = setInterval(..., 1000);
return () => {clearInterval(timerId);};
}, [])
否则会有内存泄漏
如果increaseByOne
函数不需要知道实际的count.current
,则可以避免重新创建它。在上下文中创建一个名为is
的新函数,用于检查current
是否等于值:
is = n => this.state.current === n;
并在increaseByOne
函数中使用此函数:
if (count.is(5)) {
count.reset();
}
例:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
render() {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase = (step) => {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset = () => {
this.setState({
current: 0
});
}
is = n => this.state.current === n;
state = {
current: 0,
increase: this.increase,
reset: this.reset,
is: this.is
};
}
const MyComponent = (props) => {
const { increase, reset, is, current } = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
if (is(5)) {
reset();
} else {
increase(1);
}
}, [increase, reset, is]);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{current}</div>
);
}
const App = () => (
<MyContextProvider>
<MyComponent />
</MyContextProvider>
);
ReactDOM.render( <
App / > ,
document.querySelector("#app")
);
body {
background: #fff;
padding: 20px;
font-family: Helvetica;
}
<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="app"></div>