In order to test our react app connected to a server, we need to run two servers:
- the first is our actual server, and takes care of answering requests
- the second is a server that we use only to refresh our react app every time we change something. This second server is not useful in production, but it is useful as long as we're developing.
Luckily, we don't have to invent the second one; create-react-app does it for us.
So:
- create a directory for the project (
> mkdir project) and enter it (> cd project) - use create-react-app to create a front end application (e.g.
> create-react-app front --use-npm) - create a new server project:
- create the directory (e.g.
> mkdir backand> cd back) - run
npm init - install the necessary stuff:
npm install --save express socket.io nodemon - create a new file
index.jsto store our server
- create the directory (e.g.
- you should have the following structure:
project
│
├── back
│ ├── node_modules
│ ├── index.js
│ ├── package.json
│ └── package-lock.json
└── front
├── node_modules
├── package.json
├── public
├── README.md
├── src
│ │
│ └── ... a few files
└── package-lock.json
- in
back/package.json, locate thescriptsarea. It should look like this:
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
...
}add a script to run nodemon:
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
...
}this will allow us to run npm start to start the project
- in
back/index.js, create a very simple server:
const http = require('http')
const server = http.createServer()
const handleRequest = (req, res) => {
res.end('ok')
}
server.on('request', handleRequest)
server.listen(8888, ()=> console.log(`server is ready`))We are using the port 8888 because the port 3000 will be used by create-react-app
Let's try it:
> npm startthen go to http://localhost:8888/. If everything works correctly, you should see "ok".
Then, edit index.js and change the "ok" to "ok!". Save; nodemon should restart the server automatically. Go to http://locahost:8888 again, and verify the text has changed.
- in
front, we installed:- React, to write dynamic front-end applications using HTML instead of javascript (JSX)
- Webpack, which recompiles our files everytime we save a file
- Webpack server, which runs our front-end and restarts it everytime we save a file
- in
back, we installed:- express, which will help us answer http requests (e.g,
http://localhost:3000/some-path). We will not really use it in this exercise. - socket.io, which will help us open permanent connections with browsers
- nodemon, which will restart our server everytime we save
- express, which will help us answer http requests (e.g,
When we will want to run our app, we will need to run both the front-end webpack server and the back end express server
let's prepare a script to do that:
- in
project/, runnpm init - then run
npm install --save concurrently - open
package.json, locate thescriptsobjects, and add:
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"server": "cd back && npm start",
"client": "cd front && npm start",
"start": "concurrently --kill-others-on-fail --names \"server client\" \"npm run server\" \"npm run client\""
},
...
}Now, running npm run server will run the server; npm run client will run create-react-app. And npm start will run both
- in the main directory ("project"), run
git init - in the main directory, create a file
.gitignore, and write in it:**/*node_modules. This instructs git to not look for what is in this folder - try it: run
git add -A -nto test what files would be added. It should be the files above. If you see a lot of files from node_modules, you're doing something wrong - if everything is ok, run
git add -Ato actually add the files. Then rungit commit -m "first commit". - congrats! you're ready to go
- use Razzle. Razzle comes pre-packaged with a server and a client and everything just works with one command. However, Razzle doesn't play nice with socket.io, so it would be a bother to use in this example
- use this repo :) but I prefer you do the work
in back/index.js, set up a super-simple socket server. Add to the bottom of the file:
const io = require('socket.io')(server);
let globalNumber = 0
io.on('connection', (socket) => {
console.log('a user connected')
socket.on('disconnect', () => {
console.log('user disconnected');
});
socket.on('increment', () => {
globalNumber++
io.emit('number:change',globalNumber)
});
socket.on('decrement', () => {
globalNumber--
io.emit('number:change',globalNumber)
});
socket.emit('number:change',globalNumber)
});In order for the client (browser) to communicate with the server, we're going to need to install socket.io in the client too. So, go to /front, and run:npm install --save socket.io-client
then, import it in front/src/App.js
add:
import io from 'socket.io-client'towards the top of the file- in the
Appclass, add:
class App extends Component{
state = { socket:null, globalNumber:0 }
componentDidMount(){
const socket = io('http://localhost:8888');
this.setState({socket})
socket.on('number:change', (globalNumber) => {
this.setState({globalNumber})
})
}
onIncrement = () => this.state.socket.emit('increment')
onDecrement = () => this.state.socket.emit('decrement')
render(){
// do something here to show the globalNumber and use increment and decrement
}
}At this state, you have a working server, a working client, and they communicate together
** don't forget to commit **
This would be a good moment to create a repository on github and backup your code
Let's make it so peeps can connect and write some text together.
Here are the specifications of the app:
1 - when a user connects, all users should be notified. 2 - when a user sends a text, all users should receive the letter 3 - the text should know who sent it.
BONUS: allow user to set a username for themselves
So, let's think a bit. We need to design our API.
When a user connects, he needs to send a signal to the server. We know this signal is already prepared in Socket.io, it's called 'connect'. You remember this part?
// back/index.js
const io = require('socket.io')(server);
io.on('connection', (socket) => {
// user has connected
}Good. But we don't have a way to tell other users that a user has connected.
Let's fix that. We need to re-send this signal to our users. We could just call it something simple like "connection", but it might be confusing to use something that is native to Socket.io. Let's call this user:new. This is a nice format, because this way, we can later use user:nickname for when the user changes their nickname, user:quit for when they quit, and so on.
important: this is just "a" way of writing those. It can be absolutely anything. If you decide to use
socket.emit('user has connected yeaaaah')No one is going to stop you (actually, I will stop you). But it's better to be organized.
So, when a user connects, you need to emit that he has connected.
Add this inside the io.on('connection') block:
// back/index.js
// ...
io.on('connection', (socket) => {
console.log('a user connected')
io.emit('user:new')
// ...Very good, now the server emits to all users that a new user has entered.
However, we aren't listening on the user side, so nothing will happen. Let's fix that.
// front/src/App.js
//...
componentDidMount(){
const socket = io('http://localhost:8888');
this.setState({socket})
socket.on('number:change', (globalNumber) => {
this.setState({globalNumber})
})
socket.on('user:new', ()=>{
console.log('a user has connected')
})
// ...Now, run the whole app and open your console. Open a second window, you should see "a user has connected"
What Happened?:
- A user connects. He sends the native Socket.io
connectionsignal to the server - The server receives the signal (
on('connection')) - The server sends sends to all users a signal we called
user:new - Every connected user receives
user:new - Every user uses console.log to write
a user has connectedto the console.
Now let's give a number, or name, to that user.
First, install the cat-names library:
npm install --save cat-namesThen, in the server's index.js, put somewhere at the top:
// back/index.js
const catNames = require('cat-names');and, below, where users connect:
// back/index.js
//...
io.on('connection', (socket) => {
const username = catNames.random()
//...Send the username to all the users with the signal: change io.emit('user:new') to io.emit('user:new',username).
Now, when a user connects, the server chooses a random cat name for him/her, and sends it to everyone.
But, and this is important, the user themselves doesn't know their own name. So let's invent a new signal, called user:me, to send the user info:
// back/index.js
io.emit('user:new',username)
socket.emit('user:me',username) Finally, catch those on the client:
// front/src/App.js
//...
socket.on('user:new', (username)=>{
console.log('a user called '+username+' has connected')
})
socket.on('user:me', (username)=>{
this.setState({username})
})
//...Don't forget to show this somewhere in the render function, or else nothing will show
What is happening?:
- The user connects and sends a "connect" signal
- The server receives the signal and chooses a random cat name
- The server sends to every users the new name
- Every user receives the signal that a new user has connected, and their name
- The server sends to the user themselves their own name
- The user receives their own name and sets in
state
Notice: on the server, there are TWO object that can emit:
io.emit(message)sends a message to ALL userssocket.emit(message)sends a message to the CONNECTED user
On the client, there's only socket.emit(message), which sends a message to the server.
Congrats! Now users can connect and acquire a user name!
important: Lastly, put the socket in the state so we can use it elsewhere in our Component:
this.setState({socket:socket})
Now, let's create the chat proper:
1 - in the React App, create a div to store the text 2 - in the Ract App, create a form, put inside the text input, and a button 3 - when the form is submit:
- acquire the value of the text box
- display it in console.log
- emit a
textmessage (this.state.socket.emit('text',theTextToSend)). 4 - put a listener for when a message is received (socket.on('text',(text)=>{ console.log(text) })) 5 - on the server, when a text is received, re-emit it for everyone 6 - Try it. Does it work? Do you see the message in your console? If yes, great!Add it to an array in the state, and display it inrender
...You almost have a chat.
Now, make it so the username is sent with the message