For my final project at Flatiron I decided to challenge myself by implementing WebSockets. And what a true challenge it was. It’s not necessarily that It’s hard to understand but more so that there is no abundance in documentation. Youtube videos usually only uses Rails, or the front end is different. Now don’t get me wrong, there is a few good articles that helped me get started. With this article I wanted to add one more to the list. I created a game with only one model. Hopefully a new point-of-view will be helpful to you. (I have simplified it for the purpose of this article.) Lets get started…
***A little background on this game. I created the “STOP” paper game. Meaning the players are given a random letter from the alphabet and with that letter they fill out every category with a word that starts with that letter. The username category is filled out with their same username each time so when the answers are displayed the players will be able to know who submitted it.***
BACKEND
This will go ahead and create your Rails backend.
rails new project-name --api -T --database=postgresql
Now let’s go ahead and add all your gems and bundle install.
gem 'active_model_serializers', '~> 0.10.0'gem 'redis', '~> 4.0'gem 'rack-cors'
Since this is with Postgres, don’t forget to run
rails db:create
Now we will go ahead and generate a resource for our model.
rails g resource Game username animal color thing
After that is created make sure to run
rails db:migrate
Now go to your routes.rb file and add this line of code. This will define the WebSocket end point.
mount ActionCable.server => '/cable'
Now lets create our channel to be able to stream
rails g channel games
Once that is created let’s add the name of the channel.
class GamesChannel < ApplicationCable::Channel
def subscribed
stream_from "games_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Go to your serializers file and make sure the attributes align with your schema like so…
class GameSerializer < ActiveModel::Serializer
attributes :id, :username, :animal, :color, :thing
end
Time to set up our GamesController
class GamesController < ApplicationController def index
games = Game.all
render json: games
end def create
game = Game.new(game_params)
if game.save
serialized_data = ActiveModelSerializers::Adapter::Json.new(
GameSerializer.new(game)).serializable_hash
ActionCable.server.broadcast 'games_channel', serialized_data
render json: serialized_data end endprivate
def game_params
params.require(:game).permit(:id, :username, :animal, :color, :thing)
end
end
All done for the backend set-up!
FRONTEND
Let’s create our frontend react client side.
create-react-app project-name-frontend
To make things easier for us, let’s install react-actioncable-provider to make the set-up easier.
npm install --save react-actioncable-provider
Now this next step is not necessary but I thought it was a good way to keep my code DRY. We will create a src/constants/index.js file. This is where we will store all of our constants, for later use.
export const API_ROOT = 'http://localhost:3000';
export const API_WS_ROOT = 'ws://localhost:3000/cable';
export const HEADERS = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
Now let’s wrap our main component which holds all the other components with the Action Cable Provider. I did this in my src/index.js file. Which looks like this. You will also notice that I connected my redux store here as well.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux'
import GameReducer from './redux/reducers/GameReducer'
import {createStore, applyMiddleware, compose} from 'redux'
import thunk from 'redux-thunk'
import { ActionCableProvider } from 'react-actioncable-provider';
import { API_WS_ROOT } from './constants';const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;const store = createStore(GameReducer,composeEnhancers(applyMiddleware(thunk)))ReactDOM.render(
<React.StrictMode>
<ActionCableProvider url={API_WS_ROOT}>
<Provider store={store}>
<App />
</Provider>
</ActionCableProvider>
</React.StrictMode>,
document.getElementById('root')
);// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Now we will create our Game component, which will be connected to the Redux Store
import React, { Component } from 'react'
import {connect} from 'react-redux'
import { gameAns } from '../redux/actions/gameActions'
import GameAnswers from './GameAnswers'
import { ActionCableConsumer } from 'react-actioncable-provider';
class Game extends Component { state = {
games: [],
};
handleReceivedGames = (games)=> {
const game = games.game
this.props.gameAns(game)
};
}
render() {
return (
<div>
<h1>Game</h1>
<ActionCableConsumer
channel={{ channel: 'GamesChannel' }}
onReceived={this.handleReceivedGames}
/>
<h2>Game Answers</h2>
{this.props.gameA.map((game) => {
return <GameAnswers game={game} key={game.id}
/>
})}
</div>
)
}
}const mapStateToProps = (games) => {
return {
gameA: games.gameAns
}
}
export default connect(mapStateToProps, { gameAns })(Game)
Above I reference <GameAnswers game={game} key={game.id} /> This is where I display the game attributes. (game.username, game.animal, etc..) You can do it right there in the same line or in a separate component as shown above. TIP: If by the end of this article you are not seeing the messages displayed on both screens then come back to your handleReceivedGames function, and debug from there.
Next our GameForm Component.
import React, { Component } from 'react'
import { createGame } from '../redux/actions/gameActions'
import { connect } from 'react-redux'
class GameForm extends Component {state = {
username: "",
animal: "",
color: "",
thing: ""
} submit = (e) => {
e.preventDefault();
this.props.createGame(this.state);
this.setState({
initials: "",
name: "",
place: "",
color: "",
animal: "",
thing: ""
});
}; render() {
return (
<div>
<form onSubmit={this.submit}>
Username:<input onChange={(e) => this.setState({
initials: e.target.value })} type="text" value=
{this.state.initials}/><br/><br/><br/><br/><br/>
Color:<input onChange={(e) => this.setState({
color: e.target.value })} type="text" value=
{this.state.color}/>
<br/><br/>
Animal:<input onChange={(e) => this.setState({
animal: e.target.value })} type="text" value=
{this.state.animal}/><br/><br/>
Thing:<input onChange={(e) => this.setState({
thing: e.target.value })} type="text" value=
{this.state.thing}/><br/><br/>
<input type="submit" value="STOP"/>
</form>
</div>
)
}
}export default connect(null, { createGame })(GameForm)
Jot something down
Now our Redux Store. First Action
export const createGame = (data) => {
return (dispatch) => {
return fetch(`http://localhost:3000/games`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ game: data })
})
.then((res) => res.json())
.then((game) => {
dispatch({ type: "CREATE_GAME_SUCCESS", payload:
game.game })
})
.catch((error) => console.log(error))
};
};export const gameAns = (game) => {
return dispatch => {
dispatch({ type: "GAME_ANSWERS", payload: game })
}
}
Now our Reducer.
const initialState = { gameAns: [] }
function gameReducer(state = initialState, action) {
switch(action.type) {
case "GAME_ANSWERS":
return { ...state, gameAns: [...state.gameAns,
action.payload] }; case "CREATE_GAME_SUCCESS":
return { ...state, allGames: [...state.allGames,
action.payload] };
default:
return state;
}
}export default gameReducer
With all of this in place, when you type npm start in your terminal your browser will open to the game. Now open another window(incognito) and they should look the same. go ahead and fill out the form and hit submit and you will see (in this case) the answer on both screens!!! Amazing congratulations! You now have a real time application.