反应功能组件正在订阅时拍摄状态快照。
例如PFB代码。
如果我单击setSocketHandler按钮,然后按setWelcomeString按钮。现在,如果我在登录welcomestring时通过套接字收到消息,则该消息为空。
但是如果我单击setWelcomeString按钮,然后单击setSocketHandler按钮。现在,如果我在套接字上收到消息,欢迎在控制台上登录。
我在项目中也看到了相同的行为,所以只创建了这个简单的应用程序来证明。
如果我使用下面评论的类组件,则一切正常。
所以我的问题是,为什么反应功能组件在注册时处于一种状态,而不在收到消息时处于实际状态。
这很奇怪。如何使其在功能组件中正常工作。
import React, {useEffect, useState} from 'react';
import logo from './logo.svg';
import './App.css';
const io = require('socket.io-client');
const socket = io.connect('http://localhost:3000/');
const App : React.FunctionComponent = () => {
const [welcomeString, setWelcomeString] = useState("");
const buttonCliecked = () => {
console.log("clocked button");
setWelcomeString("Welcome")
}
const onsockethandlerclicked = () => {
console.log("socket handler clicked");
socket.on('out', () => {
console.log("Recived message")
console.log(welcomeString);
});
}
return (
<div>
<header className="component-header">User Registration</header>
<label>{welcomeString}</label>
<button onClick={buttonCliecked}>setWelcomeString</button>
<button onClick={onsockethandlerclicked}>setSocketHandler</button>
</div>
);
}
/*class App extends React.Component {
constructor(props) {
super(props);
this.state = {
welcomeString:""
}
}
buttonCliecked = () => {
console.log("clocked button");
this.setState({ welcomeString:"Welcome"})
}
onsockethandlerclicked = () => {
console.log("socket handler clicked");
socket.on('out', () => {
console.log("Recived message")
console.log(this.state.welcomeString);
});
}
render() {
return (
<div>
<header className="component-header">User Registration</header>
<label>{this.state.welcomeString}</label>
<button onClick={this.buttonCliecked}>setwelcomestring</button>
<button onClick={this.onsockethandlerclicked}>setSocketHandler</button>
</div>
);
}
}*/
export default App;
对于那些来自Redux背景的人来说,useReducer看起来看似复杂且不必要。在useState和上下文之间,很容易陷入这样的陷阱:对于大多数较简单的用例,reduce会增加不必要的复杂性;但是,事实证明useReducer可以大大简化状态管理。让我们看一个例子。
与我的其他帖子一样,此代码来自我的图书清单项目。用例是屏幕允许用户扫描书籍。记录ISBN,然后将其发送到一个查找图书信息的限速服务。由于查询服务受速率限制,因此无法保证您的图书很快就会被查询,因此设置了网络套接字;随着更新的到来,消息将沿着ws发送,并在ui中进行处理。 ws的api很简单:数据包上具有_messageType属性,其余对象用作有效负载。显然,更认真的项目会设计出更坚固的产品。
对于组件类,用于设置ws的代码非常简单:在componentDidMount中创建了ws订阅,在componentWillUnmount中将其拆除。考虑到这一点,很容易陷入尝试使用钩子进行跟踪的陷阱
const BookEntryList = props => {
const [pending, setPending] = useState(0);
const [booksJustSaved, setBooksJustSaved] = useState([]);
useEffect(() => {
const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
ws.onmessage = ({ data }) => {
let packet = JSON.parse(data);
if (packet._messageType == "initial") {
setPending(packet.pending);
} else if (packet._messageType == "bookAdded") {
setPending(pending - 1 || 0);
setBooksJustSaved([packet, ...booksJustSaved]);
} else if (packet._messageType == "pendingBookAdded") {
setPending(+pending + 1 || 0);
} else if (packet._messageType == "bookLookupFailed") {
setPending(pending - 1 || 0);
setBooksJustSaved([
{
_id: "" + new Date(),
title: `Failed lookup for ${packet.isbn}`,
success: false
},
...booksJustSaved
]);
}
};
return () => {
try {
ws.close();
} catch (e) {}
};
}, []);
//...
};
我们将ws的创建放入一个依赖列表为空的useEffect调用中,这意味着它永远不会重新触发,并且我们返回一个函数来进行拆解。首次安装组件时,我们将设置ws,而在卸载组件时,将其拆下,就像使用类组件一样。
问题
此代码严重失败。我们正在访问useEffect闭包内的状态,但不将该状态包含在依赖项列表中。例如,在useEffect内部,待处理的值将绝对始终为零。当然,我们可以在ws.onmessage处理程序中调用setPending,这将导致状态更新以及组件重新呈现,但是当它重新呈现时,我们的useEffect将不会重新触发(同样,由于依赖关系为空)列表)—结果,关闭将继续关闭当前过期的未决值。
很明显,使用下面讨论的Hooks掉毛规则,很容易发现这一点。从根本上说,从上课日起就必须打破旧习惯。不要从componentDidMount / componentDidUpdate / componentWillUnmount的思想框架中访问这些依赖项列表。仅仅因为它的类组件版本将在componentDidMount中设置一次Web套接字,并不意味着您可以将其直接转换为带有空依赖项列表的useEffect调用。
不要考虑太多,不要太聪明:效果回调中使用的渲染函数作用域中的任何值都需要添加到依赖项列表中:这包括道具,状态等。
解决方案
虽然我们可以将所有需要的状态添加到我们的useEffect依赖关系列表中,但这将导致Web套接字被拆除,并在每次更新时重新创建。如果ws在创建时发送了初始状态的数据包(可能已经在我们的用户界面中进行了说明和更新),这将很难有效,并且可能实际上会引起问题。
但是,如果我们仔细观察,可能会发现一些有趣的事情。我们执行的每项操作始终处于先前状态。我们总是说“增加待处理的书的数量”,“将这本书添加到已完成的清单中”之类的意思。实际上,发送将先前状态转换为新状态的命令是化简器的全部目的。
将整个状态管理移到reducer会消除useEffect回调中对本地状态的任何引用;让我们看看如何。
function scanReducer(state, [type, payload]) {
switch (type) {
case "initial":
return { ...state, pending: payload.pending };
case "pendingBookAdded":
return { ...state, pending: state.pending + 1 };
case "bookAdded":
return {
...state,
pending: state.pending - 1,
booksSaved: [payload, ...state.booksSaved]
};
case "bookLookupFailed":
return {
...state,
pending: state.pending - 1,
booksSaved: [
{
_id: "" + new Date(),
title: `Failed lookup for ${payload.isbn}`,
success: false
},
...state.booksSaved
]
};
}
return state;
}
const initialState = { pending: 0, booksSaved: [] };
const BookEntryList = props => {
const [state, dispatch] = useReducer(scanReducer, initialState);
useEffect(() => {
const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
ws.onmessage = ({ data }) => {
let packet = JSON.parse(data);
dispatch([packet._messageType, packet]);
};
return () => {
try {
ws.close();
} catch (e) {}
};
}, []);
//...
};
虽然行数稍多,但我们不再具有多个更新功能,我们的useEffect主体更加简单易读,我们不再需要担心过时的状态被封闭在闭包中:我们所有的更新都是通过针对我们的调度进行的单个减速器。这也有助于提高可测试性,因为我们的异径管非常容易测试。它只是一个普通的JavaScript函数。正如React团队的Sunil Pai所说,使用reducer有助于将读取与写入分开。我们的useEffect主体现在只担心会产生新状态的调度动作;在涉及到既要读取现有状态又要写入新状态之前。
您可能已经注意到动作以数组的形式发送到reducer,类型在零槽中,而不是作为带有类型键的对象。都可以使用useReducer;这只是丹·阿布拉莫夫(Dan Abramov)向我展示的减少技巧的技巧:)函数setState()呢?
最后,你们中的有些人可能想知道为什么在原始代码中我不只是这样做
setPending(pending =>未决-1 || 0);
而不是
setPending(pending-1 || 0);
这样可以消除关闭问题,并且对于这个特定用例可以很好地工作;但是,对booksJustSaved的最新更新需要访问待定的值,反之亦然,因此该解决方案将无法正常运行,从而使我们立于不败之地。而且,我发现reducer版本更干净一些,状态管理在其自己的reducer函数中很好地分开了。
总而言之,我认为useReducer()目前使用率极低。它远没有您想象的那么可怕。试试看!
快乐编码!