Some comments about React Lifecycle Methods and bi-directional communication with Sockets

Dean Gladish
10 min readAug 11, 2022

It’s pretty easy to understand the built-in methods which get called when a component is mounted (or when it updates). This is certain. What isn’t clear is the relationship between these React lifecycle methods, especially when you’re including additional stuff like componentDidUpdate.

One project which exemplifies how React lifecycle methods and two-way communication can be extrapolated into anything, such as online multi-player table-top games such as at Fullstack, where we’d seen stuff like Pong and learned about cool APIs like Moment.js, yet implementing these in the sense of using Sockets to allow the server to notify clients of certain types of changes… furthermore, if you’re storing stuff like chat data and/or past game data, instead of just having stuff show up client-side and disappear when you refresh the page, then you probably want to better understand the interaction between the React Lifecycle and specifically what happens server-side, and how clients will react; that is why this article originally modeled after a Civ-style game where you can develop squares and then have other players interact with your civilization and economy, developing territory and conquering it. After realizing the extent to which this functionality depends on React lifecycle methods, it seems fit to make a simpler game which exemplifies some of the issues which developers might run into, and aside from that it seems fit to establish the basics of bi-directional game-play (being able to download the game history directly from the database instead of from local storage, adding each move to the data-base) versus client-side stuff (examples like clicking a piece and highlighting all possible moves, click & drag on mobile, Moment.js to show the time limit, and being able to replay the game with sound effects and simple animations.

After creating three different game rooms with Socket I/O (in which, by the way, you can play just by having two tabs open), creating NavLinks to those rooms based on the URL slug, and fixing an online player over-counting issue caused by multiple instances of the client socket listening for the same connect event, setting it up such that different game rooms correspond to different game IDs represented by the URL slug, we got to the point where we can post players’ moves to the correct row of the games table based on that ID. How would you post players’ moves to the games table?

Initially, we’d been taught to use Sockets in the sense of determining local time server-side and then transmitting this information, used to calculate time differences which are displayed by the timer for Player 1 and the timer for Player 2! Moreover, the number of online players is also showed.

With this in place, we could notify each other not just of new chat messages but also of each person’s individual move. The prototype functions for movement of each game piece are verified client-side, and since we’re accessing the session id (connect.sid) cookie using the react-cookie package, making sure cookies are not HTTP-only so that we can extract connect.sid using react-cookie and then use it client-side, we can also verify that the session ID corresponds to the actual session ID representing a player registration on the database. So given a global chat box for which the text field clears when you submit a chat message as well, and the fact that we can count online users, you’d think that Socket I/O would permit a notification & emit message whenever someone makes a move. Socket Data received by the client also passes down as properties which could then be sent back to the database. The goal is to send a peer-to-peer message, and it’s also to want data to be persistent when you close the page; the beforeUnload listener deals with page unloads later in this article. As “lifecycle methods”, unload handlers have to be bound to the component.

New board data converts into a JSON string before making a PUT request to the database. in order to read this data, it turns out that you can just convert the JSON string back into an array, which doesn’t even contain the original board object. Every time a new checkers board is initialized, mapping the relevant elements only (.name and .player) to a board object which contains some additional, quite crucial attributes such as prototype functions, if you were ever wondering what to do with prototype functions on objects which you are posting to a database, you could reconstitute them based on objectName: value. How do you maintain prototype functions in JSON format?

After successfully recreating the board, based on a JSON string retrieved from the database which had inadvertently lost the necessary prototype methods, when someone makes a move, the data is sent to the database and is retrievable. So when the data is retrieved, functions which determine whether a move is valid can be mapped to the appropriate pieces. By modifying the location of a piece name in the array, we determine where pieces are actually located. Essentially, because prototype functions are removed when objects are stringified into JSON format, we want to create a new checkers board each time we reload the game.

And finally when we update the game board which causes a socket.emit which clientSocket.on identifies as a cue to reload a particular game’s data from the database, we also inadvertently update all components because they depend on Redux. So componentDidUpdate immediately puts the new board data on local state. Look out for the infinite loops which may occur if you’re not careful with this lifecycle method! When someone makes a move, a dummy socket.emit message is sent which indicates that they should refetch the data. React components only update when their state changes, and in this case the Redux state, which is being mapped to local state, only changes once. The effect is the appearance that every time someone makes a move, the other person refetches the data.

Observe that fetching from the database is a little quicker than putting to the database; in the context of this website, having the framework in place to pass down props through socket.emit whenever you need to, say, modify the time stored in the database or record the specific move that is passed to a thunk creator and PUT to the database at the moment, is as obvious as is having the framework to start and stop timers; instead of using a button to tell the timer whose turn it was, the value “black” or “white” returns directly from Redux which is also client-side.

Speaking of infinite loops and timers, PUT requests to the time remaining for each player with the criterion, stored on the database as a boolean, prevent multiple requests from being made, that is checked within the API route. But you could check it anywhere.

The earlier reason that we want a beforeUnload listener is that we need to un-register players’ nicknames and more importantly, their session IDs when they close the page (as long as they are on a specific game page and not just on the homepage). There are in fact all kinds of listeners, such as the onMouseDown function which highlights squares showing possible directions a piece can take.

It might be helpful to think of what happens every time the database changes, and what kind of things you want to have show up client-side if so, and to understand when the client re-renders. For example, one thing you want to have show up client-side is the number of players registered for a game room. The way it’s set up, the io and connect.sid cookies are what keep the checkers game together. They’re what identify a person so that no one else can say, play as black in game room 3. the io is like a temporary session id; once you refresh the page, it changes completely. It’s just a temporary ID for server-client communication, and it does not persist when you refresh the page. However, the connect.sid identifies the client itself and does not change across refreshes.

And to mention identifying clients for a brief time, on GitHub when deleting login credentials in Keychain Access you can log in as a different user. That’s not a problem; for quite some time, you can push to a private repository for which you are neither an owner nor a contributor, logging back in as the right or wrong Git user (git config user.name) all makes things accessible and recognizes the usefulness of cookies as well as their limitations (there aren’t any, unless you delete them).

Given how great the react-cookie package is and how nice it is to be able to access cookies client-side and send them to the API routes as req.sessionID for player verification, there is one use case for cookies. Furthermore, it exemplified how things mostly happen client-side (nothing changes on the server when we just play the game, unless we refresh the page which causes the middleware to reload).

The client is important! Without a front-end, testing API routes (GET, PUT, POST) using Postman comes in handy — Postman is nice because it allows you to see both the request you’re about to send and the response you get back from the server. Being able to make a proper POST request in Postman, specifically understanding the difference between JSON and JavaScript syntax (for posting a single row, or for posting multiple rows in one request. A lot of semicolons!) is essential. There may be a similarly easy way to test action creators.

There are a ton of things which are evaluated in addition to the sessionID, some of them automatically — Sequelize as an ORM does check the createdAt and updatedAt columns to make sure they are valid dates. There’s a saying that we don’t really need to use ORMs, and yet this is one use case (if you like the framework it provides). Furthermore, it’s worth it to mention that generally once you get past the reducers, importing them into index.js and then combining them, console logs are going to show up server-side. You could look at server logs or Postman.

Shout out to VS Code’s autocomplete (which shows you where you are in the directory when importing stuff, as well as what’s being exported from that file so that you know whether or not you’re making a valid import). It truly helped me in this. Additional advice would be consistency — consistency of model names for instance, which Sequelize requires to be spelled the same whether you’re seeding the database or whether you’re looking at the model itself.

Keeping Sessions separate from API requests means that we can’t make API requests in the main index.js server file (where the Sessions are being run). Moreover, we don’t need to POST anything to the Sessions database because session data is already in the database and so what you actually want to do is get the session ID from anywhere within the app itself, specifically from one of the components.

One thing to note about the call stack/React lifecycle, is that making componentDidMount asynchronous (async componentDidMount and then await someAction, awaitSomeOtherAction) significantly puts things down the call stack, so that componentDidUpdate is able to run before everything else is even finished, before the rest of the componentDidMount block finishes. The reason for this is the flow of data (async componentDidMount -> awaited action call -> server response -> reducer -> update component -> update child components). All of this happens before the next line on componentDidMount is called. Child components are updated even if we aren’t passing them anything relevant, and even if we aren’t passing it anything at all (not from props or anywhere else). Remember that the component mounts before any of the functions in componentDidMount actually run. So much for lifecycle methods.

Anyway, the game data is stored client-side using Redux, which is a reducer and processor (for-loops and break/continue!). The if/else (either/or) statements determine whether data can be sent to the database. The reason is Redux — it may not be necessary to reduce every action per se onto the Redux state (suppose you’re modifying a game by ID and don’t need to collect anything on Redux), however it’s sometimes needed when you need to update components and/or retrieve data immediately. Now, modifying games by ID itself was something else — when you’re handling events, and you want to access e.target.id, you need to define id={3} for instance on the div — if the event occurs on a NavLink, it can’t be on the NavLink either — it has to be <NavLink>on the div enclosed by that link</Navlink>. It’s easier to do it this way — otherwise if you’re referring to e.target.value within your event handler, you’ll be accessing not the value attribute of that div but the inner HTML, and in that sense you should only use e.target.value when you’re dealing with, for example, drop-down menus. Furthermore, you shouldn’t preventDefault with NavLinks because they’re a different class. It’s easier to deal with events this way because accessing e.target.className for instance, using dev tools, just represents another quandary.

So you’ve got this stuff about lifecycle methods, and it’s important to remember that the primary use of async/await is to make sure that things are finished before you move on. If you want to re-render child components, you can pass them special properties like ref and key which won’t even show up on props officially.

On the reducer, you have several options. Let’s say you’re reducing a single game, fetched by id, onto one of the redux reducers which forms a portion of the Redux state. In that case, you want to make sure that you’re not adding more games onto that reducer. You want to actually replace that part of the Redux state, so no de-structuring of the Redux state is necessary. You shouldn’t return anything to do with the Redux state, but instead should return action.data or action.singleGame or whatever you called it.

If you look closely you might see type=”module” in the main index.html file. If you’re using an import statement outside a module, you need to do this.

On the API routes, computed property names [propertyName] are particularly important when you’re passing a property name within req.body and want to use it to specify the column name to be updated (by Sequelize), so if you’re ever using a query parameter as a column name on a Sequelize query then keep that in mind.

There are two main places where things are stored — props and state.

--

--