├── 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 |
--------------------------------------------------------------------------------