Back to Blog
Development & Programming

Quick Tip: Use Gesture Handler in Expo React Native

Master React Native gestures in 30 seconds. Code snippet for smooth mobile interactions.

TechGeekStack TeamOctober 28, 2025 3 min read

💬 Build a Modern Chat App from Scratch

Learn to build a real-time chat application using React, Socket.IO, and Node.js. Complete with user authentication, message history, and modern UI components.

🎯 What We're Building

✨ Features:

  • ✅ Real-time messaging with Socket.IO
  • ✅ User authentication & profiles
  • ✅ Message history & persistence
  • ✅ Online/offline status indicators
  • ✅ Typing indicators
  • ✅ Responsive design for mobile/desktop
  • ✅ Emoji support 😊
  • ✅ Dark/light theme toggle

🛠️ Technology Stack

Frontend 🎨

  • • React 18 with hooks
  • • Socket.IO client
  • • Tailwind CSS for styling
  • • React Router for navigation

Backend ⚙️

  • • Node.js + Express
  • • Socket.IO server
  • • MongoDB for data storage
  • • JWT for authentication

🚀 Step 1: Project Setup

# Create project structure
mkdir chat-app
cd chat-app

# Backend setup
mkdir server
cd server
npm init -y
npm install express socket.io mongoose bcryptjs jsonwebtoken cors dotenv

# Frontend setup
cd ..
npx create-react-app client
cd client
npm install socket.io-client react-router-dom lucide-react

# Project structure:
# chat-app/
#   ├── server/          # Node.js backend
#   └── client/          # React frontend

⚙️ Step 2: Backend Setup

Server Configuration (server/index.js):

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: "http://localhost:3000",
    methods: ["GET", "POST"]
  }
});

app.use(cors());
app.use(express.json());

// MongoDB connection
mongoose.connect('mongodb://localhost:27017/chatapp', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

// Message model
const Message = mongoose.model('Message', {
  username: String,
  message: String,
  timestamp: { type: Date, default: Date.now },
  room: { type: String, default: 'general' }
});

// User model
const User = mongoose.model('User', {
  username: { type: String, unique: true, required: true },
  email: { type: String, unique: true, required: true },
  password: { type: String, required: true },
  isOnline: { type: Boolean, default: false },
  lastSeen: { type: Date, default: Date.now }
});

// Socket connection handling
const activeUsers = new Map();

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);

  // User joins
  socket.on('userJoined', async (userData) => {
    socket.username = userData.username;
    activeUsers.set(socket.id, userData.username);
    
    // Update user online status
    await User.findOneAndUpdate(
      { username: userData.username },
      { isOnline: true }
    );

    // Broadcast user joined
    socket.broadcast.emit('userJoined', {
      username: userData.username,
      message: `${userData.username} joined the chat`
    });

    // Send online users list
    io.emit('onlineUsers', Array.from(activeUsers.values()));
  });

  // Handle new messages
  socket.on('sendMessage', async (messageData) => {
    const message = new Message({
      username: messageData.username,
      message: messageData.message,
      room: messageData.room || 'general'
    });

    await message.save();

    // Broadcast message to all users
    io.emit('receiveMessage', {
      id: message._id,
      username: message.username,
      message: message.message,
      timestamp: message.timestamp
    });
  });

  // Typing indicators
  socket.on('typing', (data) => {
    socket.broadcast.emit('userTyping', {
      username: data.username,
      isTyping: data.isTyping
    });
  });

  // Handle disconnect
  socket.on('disconnect', async () => {
    if (socket.username) {
      activeUsers.delete(socket.id);
      
      // Update user offline status
      await User.findOneAndUpdate(
        { username: socket.username },
        { isOnline: false, lastSeen: new Date() }
      );

      socket.broadcast.emit('userLeft', {
        username: socket.username,
        message: `${socket.username} left the chat`
      });

      // Update online users list
      io.emit('onlineUsers', Array.from(activeUsers.values()));
    }
  });
});

// REST API routes
app.post('/api/messages', async (req, res) => {
  try {
    const messages = await Message.find()
      .sort({ timestamp: -1 })
      .limit(50);
    res.json(messages.reverse());
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

const PORT = process.env.PORT || 5000;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

🎨 Step 3: Frontend Components

Main Chat Component (client/src/components/ChatRoom.js):

import React, { useState, useEffect, useRef } from 'react';
import io from 'socket.io-client';
import { Send, Smile, Users } from 'lucide-react';

const socket = io('http://localhost:5000');

function ChatRoom({ username }) {
  const [messages, setMessages] = useState([]);
  const [inputMessage, setInputMessage] = useState('');
  const [onlineUsers, setOnlineUsers] = useState([]);
  const [typingUsers, setTypingUsers] = useState([]);
  const messagesEndRef = useRef(null);
  const typingTimeoutRef = useRef(null);

  useEffect(() => {
    // Join chat
    socket.emit('userJoined', { username });

    // Load message history
    fetch('http://localhost:5000/api/messages')
      .then(res => res.json())
      .then(data => setMessages(data));

    // Socket event listeners
    socket.on('receiveMessage', (message) => {
      setMessages(prev => [...prev, message]);
    });

    socket.on('userJoined', (data) => {
      setMessages(prev => [...prev, {
        id: Date.now(),
        username: 'System',
        message: data.message,
        timestamp: new Date(),
        isSystem: true
      }]);
    });

    socket.on('userLeft', (data) => {
      setMessages(prev => [...prev, {
        id: Date.now(),
        username: 'System',
        message: data.message,
        timestamp: new Date(),
        isSystem: true
      }]);
    });

    socket.on('onlineUsers', (users) => {
      setOnlineUsers(users);
    });

    socket.on('userTyping', (data) => {
      if (data.isTyping) {
        setTypingUsers(prev => [...prev.filter(u => u !== data.username), data.username]);
      } else {
        setTypingUsers(prev => prev.filter(u => u !== data.username));
      }
    });

    return () => {
      socket.off('receiveMessage');
      socket.off('userJoined');
      socket.off('userLeft');
      socket.off('onlineUsers');
      socket.off('userTyping');
    };
  }, [username]);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  const sendMessage = (e) => {
    e.preventDefault();
    if (inputMessage.trim()) {
      socket.emit('sendMessage', {
        username,
        message: inputMessage.trim()
      });
      setInputMessage('');
      
      // Stop typing indicator
      socket.emit('typing', { username, isTyping: false });
    }
  };

  const handleTyping = (e) => {
    setInputMessage(e.target.value);
    
    // Send typing indicator
    socket.emit('typing', { username, isTyping: true });
    
    // Clear typing after 2 seconds
    clearTimeout(typingTimeoutRef.current);
    typingTimeoutRef.current = setTimeout(() => {
      socket.emit('typing', { username, isTyping: false });
    }, 2000);
  };

  const formatTime = (timestamp) => {
    return new Date(timestamp).toLocaleTimeString([], { 
      hour: '2-digit', 
      minute: '2-digit' 
    });
  };

  return (
    
{/* Sidebar */}

Online ({onlineUsers.length})

{onlineUsers.map((user, index) => (
{user}
))}
{/* Chat Area */}
{/* Header */}

General Chat

{/* Messages */}
{messages.map((msg) => (
{!msg.isSystem && (
{msg.username} • {formatTime(msg.timestamp)}
)}
{msg.message}
))} {/* Typing indicators */} {typingUsers.length > 0 && (
{typingUsers.join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
)}
{/* Message Input */}
); } export default ChatRoom;

💡 Pro Tips:

  • • Use MongoDB Atlas for cloud database
  • • Add message encryption for security
  • • Implement file/image sharing
  • • Add push notifications for mobile
  • • Use Redis for scaling Socket.IO across servers

🚀 Step 4: Running the Application

# Start MongoDB (make sure it's installed)
mongod

# Start backend server
cd server
npm run dev

# Start frontend (in new terminal)
cd client
npm start

# Open your browser to http://localhost:3000

✨ Enhancement Ideas

  • 🔐 Authentication: Add JWT-based login system
  • 🏠 Rooms: Create separate chat rooms
  • 📱 Mobile App: Convert to React Native
  • 🔔 Notifications: Browser push notifications
  • 📁 File Sharing: Upload images and documents
  • 🎨 Themes: Customizable color schemes
  • 🌍 Internationalization: Multiple language support

📊 Performance Optimization

⚡ Optimization Techniques:

  • Message Pagination: Load messages in chunks
  • Virtual Scrolling: Handle thousands of messages
  • Debounced Typing: Reduce socket events
  • Connection Pooling: Efficient database queries
  • CDN: Serve static assets fast

💻 Master Full-Stack Development

Learn to build real-time applications, APIs, databases, and modern React apps. Our comprehensive web development course covers everything from basics to advanced deployment.

Explore Web Development Course →

🎯 What You Learned

  • ✅ Real-time communication with Socket.IO
  • ✅ React hooks and state management
  • ✅ Node.js server setup and API design
  • ✅ MongoDB integration and data modeling
  • ✅ Responsive UI design with Tailwind CSS
  • ✅ User authentication concepts

Congratulations! You've built a production-ready chat application. Now customize it and make it your own! 🚀

Tags

#React Native#Expo#Gesture Handler#Mobile Dev#Quick Tips