如何在ActionFunction中获取useLoaderData的client对象并避免重复触发loader
问题解答
一、优化Action中使用MQTT Client的方案
核心误区
你当前把服务器端的MQTT Client序列化后传到客户端,再通过表单提交回服务器的做法完全错误:MQTT Client是带网络连接上下文的服务器端对象,序列化会丢失所有连接状态,既无效又存在安全风险,而且违背了Remix“服务器端函数共享资源”的设计逻辑。
正确实现方式
1. 服务器端缓存MQTT Client实例
用单例模式封装MQTT连接,避免每次Loader都新建连接(浪费资源),同时让Loader和Action能共享同一个实例:
// utils/mqtt-client.ts import mqtt from "mqtt"; let mqttClient: mqtt.MqttClient | null = null; let connectionPromise: Promise<mqtt.MqttClient> | null = null; export async function getMqttClient() { if (mqttClient) return mqttClient; if (connectionPromise) return connectionPromise; connectionPromise = new Promise((resolve, reject) => { const client = mqtt.connect('172.31.100.181', options); client.on('connect', () => { mqttClient = client; resolve(client); }); client.on('error', (err) => { connectionPromise = null; reject(err); }); }); return connectionPromise; }
2. 简化Loader函数
Loader不再返回Client对象,只返回连接状态即可(Client在服务器端通过工具类获取):
export const loader: LoaderFunction = async ({ request }: LoaderArgs) => { let error = null; let connected = false; try { await getMqttClient(); // 仅确认连接状态 connected = true; } catch (e: any) { error = `Client connect error: ${e.message}`; console.error('Client connect error: ', e.message); } return json({ error, connected }); };
3. 简化表单与Action逻辑
表单无需传递Client,直接用标准submit按钮;Action中通过工具类获取Client执行操作:
<fetcher.Form method="post" action="/mqtt"> <button type="submit" name="_action" value="publish_all">All devices</button> <button type="submit" name="_action" value="publish_bedroom">Bedroom devices</button> </fetcher.Form>
Action函数:
export const action: ActionFunction = async ({ request }) => { const formData = await request.formData(); const action = formData.get('_action'); try { const client = await getMqttClient(); switch (action) { case 'publish_all': client.publish('iotbso/feeds/test', 'publish all message'); break; case 'publish_bedroom': client.publish('bedroom/feeds', 'bedroom message'); break; default: return json({ success: false, message: 'Unknown action' }, { status: 400 }); } return json({ success: true, message: 'Publish successful' }); } catch (e: any) { return json({ success: false, message: `Publish error: ${e.message}` }, { status: 500 }); } };
二、解决Loader重复触发的问题
原因分析
你之前用fetcher.submit手动提交表单时,默认会触发当前路由的Loader重新执行;同时旧Loader每次都会新建MQTT连接,放大了重复触发的负面影响。
解决方法
- 使用标准submit按钮:上面优化后的表单用了标准
<button type="submit">,无需手动调用fetcher.submit,Remix会自动处理表单提交,且不会无意义触发Loader(因为Loader不再返回动态资源,仅返回状态)。 - 手动提交时禁用Loader刷新:如果必须用
fetcher.submit,添加reloadDocument: false选项阻止Loader触发:
onClick={() => { fetcher.submit( { '_action': 'publish_all' }, { method: "post", action: "/mqtt", reloadDocument: false } ); }}
额外优化
- 资源泄漏防护:在服务器关闭时断开MQTT连接:
// server.ts 或项目入口文件 process.on('exit', async () => { const client = await getMqttClient(); client.end(); });
- 多用户隔离:如果是多用户应用,可改用Remix会话存储为每个用户保存独立Client实例,并在会话过期时自动断开连接。
内容的提问来源于stack exchange,提问作者Roland
相关产品推荐
相关产品推荐

