├── LICENSE ├── README.md ├── Router.js └── websockets-and-rest.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ashok Khanna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ashok's React Snippets 2 | Welcome to this Repo! Here I will save some useful high quality react snippets that I use in my code. The goal here is either to replicate functionality (as much as possible in terms of the key features) of large dependencies like React Router (refer Router.js) or some useful design patterns I want to bookmark for the future (I will add to this repository over time). 3 | 4 | I will also add some TypeScript & React snippets here as well since I like TypeScript :) 5 | 6 | 7 | If you like the repo, then star it ;) 8 | 9 | Thanks! 10 | -------------------------------------------------------------------------------- /Router.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Implements React Routing in Plain React, without reliance on React-Router or any other libraries. 4 | 5 | To use: 6 | 7 | In your top-level React Component (e.g. App or Index.js): 8 | 9 | - Import Router (e.g.: import Router from './Router') 10 | - Create a const with your routes and their associated component 11 | - Create a const with the component to show on urls not routed (i.e. 404 page) 12 | - Return a Router component as the top-level component of your App 13 | 14 | Example: 15 | 16 | ```function App() { 17 | ... 18 | const routes = [{path:"/", component:}, {path:"/register", component:}] 19 | ... 20 | const defaultComponent = 21 | 22 | return ( 23 | 24 | ) 25 | } 26 | ``` 27 | 28 | Then to use routes: 29 | 30 | - Use as you would normally do, e.g. Register 31 | 32 | - If you want to add an onClick event handler to buttons etc. use the `navigate` function, e.g.: 33 | 34 | 35 | 36 | And that's it! 37 | 38 | */ 39 | 40 | 41 | /* Code Starts Here */ 42 | 43 | import React from 'react'; 44 | import { useEffect, useState } from 'react'; 45 | 46 | // Global Event Listener on "click" 47 | // Credit Chris Morgan: https://news.ycombinator.com/item?id=31373486 48 | window.addEventListener("click", function (event) { 49 | // Only run this code when an link is clicked 50 | const link = event.target.closest("a"); 51 | // Correctly handle clicks to external sites and 52 | // modifier keys 53 | if ( 54 | !event.button && 55 | !event.altKey && 56 | !event.ctrlKey && 57 | !event.metaKey && 58 | !event.shiftKey && 59 | link && 60 | link.href.startsWith(window.location.origin + "/") && 61 | link.target !== "_blank" 62 | ) { 63 | // prevent full page reload 64 | event.preventDefault(); 65 | // Main routing function 66 | navigate(link.href); 67 | } 68 | }); 69 | 70 | /* Main Component */ 71 | 72 | export default function Router ({routes, defaultComponent}) { 73 | 74 | // state to track URL and force component to re-render on change 75 | const [currentPath, setCurrentPath] = useState(window.location.pathname); 76 | 77 | useEffect(() => { 78 | // define callback as separate function so it can be removed later with cleanup function 79 | const onLocationChange = () => { 80 | // update path state to current window URL 81 | setCurrentPath(window.location.pathname); 82 | } 83 | 84 | // listen for popstate event 85 | window.addEventListener('popstate', onLocationChange); 86 | 87 | // clean up event listener 88 | return () => { 89 | window.removeEventListener('popstate', onLocationChange) 90 | }; 91 | }, []) 92 | return routes.find(({path, component}) => path === currentPath)?.component || defaultComponent 93 | } 94 | 95 | /* Use the below in buttons and programmatically to navigate to pages */ 96 | 97 | export function navigate (href) { 98 | 99 | // update url 100 | window.history.pushState({}, "", href); 101 | 102 | // communicate to Routes that URL has changed 103 | const navEvent = new PopStateEvent('popstate'); 104 | window.dispatchEvent(navEvent); 105 | } 106 | -------------------------------------------------------------------------------- /websockets-and-rest.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Module: WebSocket 4 | Author: Ashok Khanna 5 | Last Update: 09-04-2022 6 | License: MIT 7 | 8 | Based on Bergi's Solution on Stack Overflow: 9 | https://stackoverflow.com/questions/60512129/websocket-waiting-for-server-response-with-a-queue 10 | 11 | How to use: 12 | 13 | 1. Import the module and create a socket instance: 14 | 15 | ``` 16 | import WebSocket from './Components/Websocket' 17 | 18 | export const ws = new WebSocket("wss://www.url.com/socket-point"); 19 | ``` 20 | 21 | 2. Then simply use it in your functions as following (first import below 22 | is to import the `ws' instance created above into the module where you are 23 | using the socket: 24 | 25 | ``` 26 | import {ws} from './index' 27 | 28 | ... 29 | 30 | function login() { 31 | ... 32 | ws.sendRequest(someMessageInJSONFormat, 33 | (value) => { 34 | ...> 35 | )} 36 | } 37 | ``` 38 | 39 | Usually I like to create some sort of JSON object as in the above, 40 | but if you read the below code then you can see there is a `sendMessage' 41 | variant that can handle plain strings 42 | 43 | */ 44 | 45 | export default class WebSocket { 46 | 47 | constructor(url) { 48 | 49 | // Here we create an empty array [] which we will add to later 50 | // Note that we can use {} also, which is for empty objects 51 | // (arrays are objects) 52 | this.waitingResponse = []; 53 | 54 | // Here we create an empty array [] that represents the queue of 55 | // messages that didn't send because the socket was closed and 56 | // are queued up to be sent during the onopen handler (which iterates 57 | // through this array) 58 | this.messageQueue = []; 59 | 60 | this.url = url; 61 | 62 | // We separate out the socket initialisation into its own function 63 | // as we will also call it during a reconnect attempt 64 | this.createSocket(); 65 | 66 | } 67 | 68 | 69 | // The reconnection logic is that whenever a message fails to send, the 70 | // message is added to messageQueue and a reconnection attempt is made. 71 | // So, when a connection is lost, it is reconnected to after a certain 72 | // time, but rather only when the user initiates an action that must 73 | // message (i.e.) interact with the WebSocket 74 | createSocket() { 75 | this.socket = new WebSocket(this.url); 76 | 77 | // Iterate through the queue of messages that haven't been sent 78 | // If this queue is empty then no messages are sent 79 | 80 | // All messages in the message queue arise from a previous 81 | // sendPayload event, thus are parsed in the correct JSON form 82 | // and have an associated request object in waitingResponse 83 | this.socket.onopen = () => { 84 | this.messageQueue.forEach(item => this.socket.send(item)) 85 | this.messageQueue = []; 86 | } 87 | 88 | this.socket.onclose = () => console.log("ws closed"); 89 | 90 | this.socket.onmessage = e => { this.processMessage(e); } 91 | } 92 | 93 | // Creates a new socket and adds any unsent 94 | // messages onto the message queue 95 | recreateSocket(message) { 96 | console.log("Reconnection Attempted"); 97 | this.messageQueue.push(message); 98 | this.createSocket(); 99 | } 100 | 101 | // Closes a socket, which can take a bit 102 | // of time (few seconds) since a roundtrip to 103 | // the server is done 104 | closeSocket(){ 105 | this.socket.close(); 106 | console.log("Socket closed manually.") 107 | } 108 | 109 | // Exposes a function for users to start a new 110 | // socket - there is no way to 'reconnect' to 111 | // a socket, a new websocket needs to be created 112 | openSocket(){ 113 | this.createSocket(); 114 | console.log("Socket opened manually.") 115 | } 116 | 117 | async sendPayload(details) { 118 | // Create a request where request = { sent: + new Date()} and this.waiting... = request 119 | // this means both request and waitingResponse[details.requestid] point to the same thing 120 | // so that changing request.test will also result in waitingResponse[details.requestid].test 121 | // having the same value 122 | 123 | // Note that details.requestid here is an index = the timestamp. Later when we process 124 | // messages received, we will check the timestamp of the requestid of the message received 125 | // against this waitingResponse array and resolve the request if a match is found 126 | 127 | let requestid = +new Date(); 128 | const request = this.waitingResponse[requestid] = { sent: requestid }; 129 | 130 | // Here we combine the request (which at this point is just { sent: ...} with the 131 | // actual data to be sent to form the final message to send 132 | const message = { ...request, ...details } 133 | 134 | // If Socket open then send the details (message) in String Format 135 | try { 136 | if (this.socket.readyState === WebSocket.OPEN) { 137 | this.socket.send(JSON.stringify(message)); 138 | } else { 139 | // Otherwise we try to recreate the socket and send the message 140 | // after recreating the socket 141 | this.recreateSocket(JSON.stringify(message)); 142 | } 143 | 144 | // Here we create a new promise function 145 | // We set the resolve property of request [which is also referenced 146 | // by waitingResponse[details.requestid] to the Promise's resolve function 147 | 148 | // Thus we can resolve the promise from processMessage (refer below) 149 | 150 | // We reject after 5 seconds of not receiving the associated message 151 | // with the same requestid 152 | const result = await new Promise(function(resolve, reject) { 153 | // This will automatically run, allowing us to access 154 | // the resolve function from outside this function 155 | request.resolve = resolve; 156 | 157 | console.log(request); 158 | // This will take 5 seconds to run, which becomes the lifecycle 159 | // of this Promise function - the resolve function must be 160 | // called before this point 161 | setTimeout(() => { 162 | reject('Timeout'); // or resolve({action: "to"}), or whatever 163 | }, 5000); 164 | }); 165 | 166 | console.info("Time took", (+new Date() - request.sent) / 1000); 167 | 168 | // function returns result 169 | return result; // or {...request, ...result} if you care 170 | } 171 | 172 | // code to run regardless of whether try worked or error thrown 173 | finally { 174 | console.log("Exit code ran successfully") 175 | 176 | delete this.waitingResponse[requestid]; 177 | } 178 | } 179 | 180 | 181 | // Message Receiver, we attach this to the onmessage handler 182 | // Expects message to be in JSON format, otherwise throws 183 | // an error and simply logs the message to console 184 | 185 | // The message must also have a requestid property (we 186 | // use lowercase "i" here because Common Lisp's JZON library 187 | // lowercases property names in JSON messages 188 | 189 | // Test if the requestid passed in has an entry in the waitingResponse 190 | // queue (data.requestid is the array index and the sendPayload function 191 | // sets a value in this array for various id indexes to { sent: .. } 192 | // This index also has a reference to the resolve function for the 193 | // associated promise for that request id 194 | 195 | // If that is true ('truthy' via if (request)), then resolve the 196 | // associated promise via request.resolve(data), where data is 197 | // the value resolved by the promise 198 | 199 | // Otherwise pass a variety of console warnings / logs - the message 200 | // will not be handled and disappear from the future (i.e. every 201 | // message needs a requestid set in waitingResponse to be caught 202 | 203 | // We could probably add in a router for server initiated messages 204 | // to be handled (under the second warning below) 205 | async processMessage(msg) { 206 | 207 | try { 208 | let data = JSON.parse(msg.data); 209 | 210 | if (data.hasOwnProperty("requestid")) { 211 | const request = this.waitingResponse[data.requestid] 212 | if (request) 213 | request.resolve(data) 214 | else 215 | console.warn("Got data but found no associated request, already timed out?", data) 216 | } else { 217 | // Add handlers here for messages without request ID 218 | console.warn("Got data without request id", data); 219 | } 220 | } catch { 221 | console.log(msg.data); 222 | } 223 | 224 | } 225 | 226 | // Main entry point for calling functions with a simple 227 | // callback to action to perform on the received data 228 | // Exists here to reduce boilerplate for the calling function 229 | async sendRequest(details, resolution, rejection = (error) => {console.log(error)}) { 230 | this.sendPayload(details).then( 231 | function(value) { 232 | resolution(value); 233 | }, 234 | function(error) { 235 | rejection(error); 236 | }) 237 | } 238 | 239 | // Second entry point for one direction messages 240 | // i.e. not expecting any responses. This bypasses 241 | // the request-response promise functions above 242 | 243 | // Attempts to JSON.stringify the object first, 244 | // and just sends the object if cannot be made 245 | // into a JSON string 246 | 247 | sendMessage(details) { 248 | // Example of an Immediately-Invoked Function Expression 249 | const message = (() => { 250 | try { 251 | return JSON.stringify(details) 252 | } 253 | catch (e) { 254 | return details 255 | } 256 | })() 257 | 258 | if (this.socket.readyState === WebSocket.OPEN) { 259 | this.socket.send(message); 260 | } else { 261 | // Otherwise we try to recreate the socket and send the message 262 | // after recreating the socket 263 | this.recreateSocket(message); 264 | } 265 | } 266 | 267 | 268 | } 269 | --------------------------------------------------------------------------------