Chat realtime sử dụng Nestjs + Socket.io và React + Redux-Saga

Chào mừng các bạn trở lại với series tutorial Nestjs của mình. Đến hẹn lại lên như đã nói ở bài viết trước bài viết này mình lại cùng xây dựng React App Chat realtime : Nestjs + Socket.io, React + Redux-Saga nhé . Bắt đầu thôi Index series Giới thiệu về setup repository +

Chào mừng các bạn trở lại với series tutorial Nestjs của mình.

Đến hẹn lại lên như đã nói ở bài viết trước bài viết này mình lại cùng xây dựng React App Chat realtime : Nestjs + Socket.io, React + Redux-Saga nhé . Bắt đầu thôi

Index series

  1. Giới thiệu về setup repository + typeorm.
  2. Xác thực người dùng trong Nestjs sử dụng Passport JWT.
  3. Nestjs – Create relationship với Typeorm + mysql
  4. Tiếp tục series mình lại cùng xây dựng React App Chat realtime : Nestjs + Socket.io, React + Redux-Saga.

1. Cấu trúc

  • Cấu trúc sẽ bao gồm :
    • Server Side: Nestjs + socket.io
    • Client Side: ReactJs + socket-client + redux saga

2. Hướng xử lý

  • Server :
    • Tạo mới table “devices”
    • Khi client connect đến socket gateway: thực hiện lưu socket_id vào table “devices”
    • Khi client disconnect thực hiện xóa socket_id trong table “devices”
    • Khi có tin nhắn được emit thì tìm tất cả các socket_id của user trong conversation để gửi tin nhắn
  • Client: emit message và receive message

3. Server side

  1. Cài đặt các packet
    Ở đây thì mình vẫn sử dụng các pakage của các bài trước và sẽ cần thêm 1 số pakage khác nữa

    > npm install @nestjs/websockets
    
     > npm i socket.io
    
    
  2. Xử lý validation cho socket
    app.gateway.ts

    @WebSocketGateway(3006,{ cors:true})exportclassAppGatewayimplementsOnGatewayInit,
      OnGatewayConnection,
      OnGatewayDisconnect
    {
      @WebSocketServer() server: Server;private logger: Logger =newLogger('MessageGateway');constructor(private userService: UsersService,private jwtService: JwtService,){}//function get user from tokenasyncgetDataUserFromToken(client: Socket): Promise<UserEntity>{const authToken: any = client.handshake?.query?.token;try{const decoded =this.jwtService.verify(authToken);returnawaitthis.userService.getUserByEmail(decoded.email);// response to function}catch(ex){thrownewHttpException('Not found', HttpStatus.NOT_FOUND);}}}
  3. Xử lý logic Client connect

    Vẫn là trong app.gateway.ts

    @WebSocketGateway(3006,{ cors:true})exportclassAppGatewayimplementsOnGatewayInit,
      OnGatewayConnection,
      OnGatewayDisconnect
    {
      @WebSocketServer() server: Server;private logger: Logger =newLogger('MessageGateway');constructor(private userService: UsersService,private deviceService: DeviceService,private jwtService: JwtService,){}...asynchandleConnection(client: Socket){this.logger.log(client.id,'Connected..............................');const user: UserEntity =awaitthis.getDataUserFromToken(client);const device ={
          user_id: user.id,
          type: TypeInformation.socket_id,
          status:false,
          value: client.id,};awaitthis.deviceService.create(information);}}...
  4. Xử lý logic client disconnect

    Vẫn là trong app.gateway.ts

    @WebSocketGateway(3006,{ cors:true})exportclassAppGateway{...asynchandleDisconnect(client: Socket){const user =awaitthis.getDataUserFromToken(client);awaitthis.deviceService.deleteByValue(user.id, client.id);// need handle remove socketId to tablethis.logger.log(client.id,'Disconnect');}awaitthis.deviceService.create(information);}}...
  5. Xử lý Listen message và emit message to client

       @WebSocketGateway(3006,{ cors:true})exportclassAppGateway{...
    
         @SubscribeMessage('messages')asyncmessages(client: Socket, payload: MessagesInterface){// get all user trong conversation bằng conversation_idconst conversation =awaitthis.conversationService.findById(
             payload.conversation_id,['users'],);// get all socket id đã lưu trước đó của các user thuộc conversationconst dataSocketId =awaitthis.deviceService.findSocketId(userId);// Lưu dữ liệu vào bảng messageconst message =awaitthis.messageService.create({
             user_id: payload.user_id,
             status:false,
             message: payload.message,
             conversation_id: payload.conversation_id,
             createdAt:newDate(),
             updatedAt:newDate(),});//emit message đến socket_id
           dataSocketId.map((value)=>{
             emit.to(value.value).emit('message-received',{
               id: message.id,
               message: message.message,
               conversation_id: message.conversation_id,
               user_id: message.user_id,
               status: message.status,
               createdAt: message.createdAt,
               updatedAt: message.updatedAt,});});}}...

4. Client Side

> npm i socket.io-client

       > npm i redux-saga

       > npm i @reduxjs/toolkit

       > npm i redux
  1. Setting Redux + Redux saga:

    store.ts :

    const rootReducer =combineReducers({
        router:connectRouter(history),
        auth: authReducer,
        chat: chatReducer,})const sagaMiddleware =createSagaMiddleware()exportconst store =configureStore({
        reducer: rootReducer,middleware:(getDefaultMiddleware)=>getDefaultMiddleware().concat(sagaMiddleware,routerMiddleware(history)),});
    
      sagaMiddleware.run(rootSaga)exportfunction*rootSaga(){yieldall([authSaga(),chatSaga(),])}export type AppDispatch =typeof store.dispatch;export type RootState = ReturnType<typeof store.getState>;export type AppThunk<ReturnType =void>= ThunkAction<
        ReturnType,
        RootState,
        unknown,
        Action<string>>;

    Tiếp đó cần use redux saga trong file App.tsx

    import{store}from'./store'
        
        ReactDOM.render(// <React.StrictMode><Provider store={store}><ConnectedRouter history={history}>// thay vì dùng redux các bạn cũng có thể dùng Connect API cũng dễ dàng cho việc code hơn nhé// trong source mình cũng có để nhé{/*<SocketContext.Provider value={{socket}}>*/}// trong đây mình có dùng style componet material-ui các bạn cũng có thể tham khảo nhé<MuiThemeProvider theme={themes}><App /><RouterComponent /></MuiThemeProvider>{/*</SocketContext.Provider>*/}</ConnectedRouter></Provider>,// </React.StrictMode>,
        document.getElementById('root'));
  2. Tiền hành xử lý logic khi 1 action được dispatch

    NOTE: ở đây mình có sử dụng redux toolkit mọi người có thể tìm hiều về nó trước khi đọc tiếp nhé

    exportinterfaceMessage{
     id: number;
     user_id: number | string;
     conversation_id: number | string;
     message: string;}exportinterfaceConversation{
     messages: Message[];
     id: number;
     title: string |null;
     sending: boolean;}const initialState: ListConversationState ={
         loading:false,
         error:'',
         conversations:[],
         loaded:false,}exportconst chatSlice =createSlice({
         name:'chat',
         initialState,
         reducers:{sendMessage(state, action: PayloadAction<Message>){
             state.conversations = state.conversations.map(conversation=>{if(conversation.id === action.payload.conversation.id ){// cái này để hiển thị sending
                        conversation.sending =true}return conversation;});return state;},sendMessageSuccess(state, action: PayloadAction<Message>){// ở đây ta cần push message received vào list message của conversation đang activce 
               state.conversations = state.conversations.map(conversation=>{if(conversation.id === action.payload.conversation.id ){// cái này để hiển thị sending
                        conversation.sending =false
                          conversation.messages = conversation.messages 
                          ?[ action.payload,...conversation.messages]:[action.payload]}return conversation;});}}})
  3. Saga middleware
    trong file chatSaga.ts :

    import{ io, Socket }from'socket.io-client';functionconnect(){const token =getAccessToken();const url = process.env.REACT_APP_SOCKET_URL??'';const socket =io(url,{
        query:{ token }});returnnewPromise(resolve=>{
        socket.on('connect',()=>{// socket.emit('room', 'room1');resolve(socket);});})}//receive messagefunction*read(socket: Socket){while(true){
            socket.on('message-received',(message)=>{// dispatch sendMessageSuccessyieldput(chatActions.sendMessageSuccess, message)});;}}//handle send messagefunction*send(socket: Socket){while(true){const{ payload }=yieldtake(chatActions.sendMessage.type)
    
        socket.emit('messages', payload)}}function*handleIO(socket: Socket){yieldfork(read, socket);yieldfork(send, socket);}function*flowSocket(){const socket: Socket =yieldcall(connect)// ta cần 1 task thực hiện send and receive messageconst task: Task =yieldfork(handleIO, socket)// ở đây nếu logout thì cần close connect socketyieldtake(authAction.logout.type)yieldcancel(task)}//flow function*flow(){while(true){const isLoggedIn =Boolean(getToken())const currentUser =Boolean(getUser());// ở đây mình cần check điều kiện đã đăng nhập chưa if(isLoggedIn && currentUser){// đã đăng nhập thì cho phép nextyieldcall(flowSocket)}else{// nếu chưa đăng nhạp thì cần nắng nghe việc loginSuccess thì nextyieldtake(authAction.loginSuccess)yieldcall(flowSocket)}}}//root handleexportdefaultfunction*chatSaga(){yieldfork(flow)}
  4. Sử dụng trong component

    const Index: React.FC=()=>{const dispatch =useDispatch();const chat: ListConversationState =useSelector((state: RootState)=> state.chat)// const { socket } = useContext(SocketContext);const[message, setMessage]=useState('')constsendData=()=>{dispatch(chatActions.sendMessage({
                   message,
                   conversation_id: conversationActive.id,
                   user_id:getUser().id,}))setMessage('')}return(<div>{
                chat.messages && chat.messages.map((message, index)=><p key={index}>{message.message}</p>)}<input type='text' value={message} onChange={e=>setMessage(e.target.value)}/><button onClick={sendData}>{/*{ chat.sending ? 'Sending.........' : 'Send'}*/}
               send
             </button></div>)}

5. Kết quả và kết luận

Cuối cùng kết quá sẽ là :

Cảm ơn các bạn đã theo dõi series của mình.

Server side: tại đây

Client side: tại đây

Sắp tới mình đang định xây dựng series về Nextjs mời các bạn đón đọc

Nguồn: viblo.asia

Bài viết liên quan

7 Cách Tăng Tốc Ứng Dụng React Hiệu Quả Mà Bạn Có Thể Làm Ngay

React là một thư viện JavaScript phổ biến trong việc xây dựng giao diện người d

Trung Quốc “thả quân bài tẩy”: hàng loạt robot hình người!

MỘT CUỘC CÁCH MẠNG ROBOT ĐANG HÌNH THÀNH Ở TRUNG QUỐC Thượng Hải, ngày 13/5 –

9 Mẹo lập trình Web “ẩn mình” giúp tiết kiệm hàng giờ đồng hồ

Hầu hết các lập trình viên (kể cả những người giỏi) đều tốn thời gian x

Can GPT-4o Generate Images? All You Need to Know about GPT-4o-image

OpenAI‘s GPT-4o, introduced on March 25, 2025, has revolutionized the way we create visual con