Here is one of many possible ways to achieve this kind of logic.
[{"id":"c0e8eb09.9b41f8","type":"function","z":"b633308d.4a91a","name":"State","func":"var target = \"my-relay\";\nvar d = global.get(target);\n\nvar label = 'ON'\nvar color = 'green';\nif(d.state === false){\n label = \"OFF\";\n color = 'red';\n}\nmsg = {topic:label,background:color,enabled:true};\nreturn msg;","outputs":"1","noerr":0,"x":530,"y":840,"wires":[["5f73d08.a254a3"]]},{"id":"5f73d08.a254a3","type":"ui_button","z":"b633308d.4a91a","name":"on/off","group":"28d6c95.1b4c936","order":2,"width":2,"height":1,"passthru":false,"label":"{{msg.topic}}","tooltip":"","color":"","bgcolor":"{{msg.background}}","icon":"","payload":"","payloadType":"str","topic":"","x":670,"y":840,"wires":[["1f5603b5.74185c"]]},{"id":"1f5603b5.74185c","type":"function","z":"b633308d.4a91a","name":"setOnOff","func":"var target = \"my-relay\";\nvar d = global.get(target);\nvar label = \"ON\";\nvar color = \"green\";\nvar action = \"ON\";\nif(d.state === true){\n label = \"OFF\";\n color = \"red\";\n action = \"OFF\";\n}\n\nmsg = {payload:action};\nvar btn = {enabled:false,background:color,topic:label};\nreturn [btn, msg];","outputs":"2","noerr":0,"x":830,"y":840,"wires":[["5f73d08.a254a3"],["a88e362.276acc8"]]},{"id":"61d1c6de.d26058","type":"change","z":"b633308d.4a91a","name":"","rules":[{"t":"set","p":"my-relay","pt":"global","to":"{\"state\":true}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":720,"wires":[[]]},{"id":"cba457f2.bbbc98","type":"inject","z":"b633308d.4a91a","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":360,"y":720,"wires":[["61d1c6de.d26058"]]},{"id":"866f2925.b0be58","type":"function","z":"b633308d.4a91a","name":"","func":"var r = global.get('my-relay')\nr.state = msg.payload == \"OFF\" ? false : true\nglobal.set('my-relay',r)\n\n// content of message does not matter, we need only \n// signal that actual state is changed\nmsg.payload = \"state is changed\"\nreturn msg;","outputs":1,"noerr":0,"x":750,"y":960,"wires":[["c0e8eb09.9b41f8"]]},{"id":"a88e362.276acc8","type":"delay","z":"b633308d.4a91a","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":620,"y":960,"wires":[["866f2925.b0be58"]]},{"id":"63bbe7a4.168f38","type":"ui_ui_control","z":"b633308d.4a91a","name":"","events":"all","x":340,"y":840,"wires":[["c0e8eb09.9b41f8"]]},{"id":"1178ffa7.833ed","type":"comment","z":"b633308d.4a91a","name":"initialize device properties","info":"There can be more tha just state","x":390,"y":680,"wires":[]},{"id":"261fc881.69f708","type":"comment","z":"b633308d.4a91a","name":"feed current state when dashboard connects","info":"","x":230,"y":800,"wires":[]},{"id":"60a2cdea.f355d4","type":"comment","z":"b633308d.4a91a","name":"Fake switching ","info":"Actual state will be read somewhere in your flow. \nWrite state to object in global (\"my-relay\")\nAter that, send signal to button telling that actual state is just changed.","x":680,"y":1000,"wires":[]},{"id":"c25c3bb1.458208","type":"comment","z":"b633308d.4a91a","name":"read state","info":"","x":520,"y":800,"wires":[]},{"id":"ba7adaf5.82f8c8","type":"comment","z":"b633308d.4a91a","name":"prepare outgoing messages","info":"First output is to disable button until actual switching is taking place. No need to send out messages when you don't know the actual state of physical device.\nSecond output should be sent out to physical device switching logic. (MQTT, HTTP request ...)","x":880,"y":800,"wires":[]},{"id":"28d6c95.1b4c936","type":"ui_group","z":"","name":"VALGUSTUS","tab":"46cbb0d9.1f57d","order":10,"disp":false,"width":"9","collapse":false},{"id":"46cbb0d9.1f57d","type":"ui_tab","z":"","name":"MAIN","icon":"dashboard","order":1}]