React函数式组件中如何切换作为变量传入的按钮的className?
React按钮className切换失败问题解决
核心错误点
- 混淆了外部传入的原生DOM按钮和组件内部返回的React按钮,两者逻辑完全脱节
- useState异步更新逻辑错误:在setState的updater函数中重复调用同状态的set方法,且直接使用更新前的旧状态值赋值给DOM,导致类名无法同步
- 事件传参错误:给原生DOM绑定点击事件时,回调函数的第一个参数是事件对象,并非你预期的按钮DOM实例
- 组件返回的冗余按钮无实际作用:绑定的
launchButton.toggle_get_places在原生DOM上不存在,无任何交互逻辑
修正方案
方案1:保留外部传入原生按钮的逻辑(无需组件内部返回按钮)
import React from 'react'; import { useInput } from '../hooks/useInput'; import { placesSource, places_lyr, } from "../qgis2web/layers"; import { increaseThrobberCounter, decreaseThrobberCounter } from "../qgis2web/throbberCounter"; import { addLayer, getErrorMessage, removeLayer } from "../qgis2web/qgis2web"; import { add_geojson_feats } from "../qgis2web/addGeojsonFeatures"; declare const ol, $, toastr; const GetPlacesControl = ({ map, launchButton }) => { const [btnActive, setBtnState] = React.useState<boolean>(false); function clear_places_features(map) { placesSource.clear(); removeLayer(places_lyr); } const toggle_get_places = () => { // 直接更新激活状态,不要嵌套setState setBtnState(prev => !prev); } const runCallback = React.useCallback((evt) => { clear_places_features(map); increaseThrobberCounter(); // 补全遗漏的计数增加逻辑 $.ajax({ type: "POST", contentType: "application/json;charset=utf-8", url: "api/get_places?x=" + evt.coordinate[0] + "&y=" + evt.coordinate[1], traditional: "true", datatype: "json", mimeType: 'application/json', }).done(function (response) { const places_feats = new ol.format.GeoJSON().readFeatures(response); if (places_feats.length == 0) { toastr.warning('no records found, try again'); } else { placesSource.addFeatures(places_feats); map.getView().fit(placesSource.getExtent()); places_lyr.setVisible(true); addLayer(places_lyr); } }).fail(function (jqXHR) { let msg = getErrorMessage(jqXHR); toastr.error(msg); }).always(function () { decreaseThrobberCounter(); }); }, [map]); // 监听btnActive状态变化,同步更新外部按钮类名和地图交互 React.useEffect(() => { if (btnActive) { launchButton.className = "fas fa-ban"; $('html,body').css('cursor', 'crosshair'); map.on('click', runCallback); } else { launchButton.className = "fas fa-globe-americas"; $('html,body').css('cursor', 'default'); map.un('click', runCallback); clear_places_features(map); } }, [btnActive, map, runCallback, launchButton]); React.useEffect(() => { const get_coords_on_click = (evt) => evt.coordinate; launchButton.addEventListener('click', toggle_get_places, false); map.on('singleclick', get_coords_on_click); return () => { launchButton.removeEventListener('click', toggle_get_places); map.un('singleclick', get_coords_on_click); // 组件销毁时重置样式和事件 $('html,body').css('cursor', 'default'); map.un('click', runCallback); }; }, [launchButton, map, runCallback]); // 不需要返回额外按钮,直接使用外部传入的按钮 return null; }; export default GetPlacesControl;
方案2:更符合React规范的写法(不需要外部传入按钮,组件内部渲染)
父组件调用时直接传map参数即可,不需要提前创建按钮:
const GetPlacesControl = ({ map }) => { const [btnActive, setBtnState] = React.useState<boolean>(false); // 其余逻辑和方案1完全一致,仅替换返回的组件内容 // ...省略重复逻辑 return ( <button className={btnActive ? 'fas fa-ban' : 'fas fa-globe-americas'} onClick={toggle_get_places} /> ); };
注意事项
- React的状态更新是批量异步的,不要在setState的updater函数里调用同个state的set方法,也不要在状态更新后立刻读取该状态值,应该通过useEffect监听状态变化执行后续逻辑
- 尽量不要直接操作原生DOM的属性,优先通过React的状态绑定DOM属性,符合单向数据流的设计规范
- useCallback的依赖项要补全,用到的外部变量都要放到依赖数组里,避免闭包陷阱
内容的提问来源于stack exchange,提问作者kflaw
相关产品推荐
相关产品推荐

