Openspace Services Pvt Ltd.
Openspace Services Pvt Ltd.
  • About Us
  • Solutions
  • Contact Us
  • Careers
  • Blogs
  • Case Study

© 2025 Openspace Services Pvt. Ltd. All rights reserved.

Privacy Policy

| India MapIndia | USA MapUSA

electron-banner.png

27-03-2025

Converting Your React App to a Desktop Application with Electron

Converting a React web application into a desktop application using Electron allows you to leverage your existing codebase while accessing native desktop features. This guide will walk you through the process with practical examples.

What is Electron?

Electron is a framework that allows you to build cross-platform desktop applications using web technologies like JavaScript, HTML, and CSS. It combines Chromium (for rendering) and Node.js (for backend operations) into a single runtime.

Prerequisites

Before we begin, ensure you have:

  • A functioning React application
  • Node.js and npm installed
  • Basic familiarity with JavaScript and React

Step 1: Set Up Your Project

Let's start by installing Electron in your existing React application:

# Navigate to your React project
cd my-react-app

# Install Electron and necessary dependencies
npm install --save-dev electron electron-builder concurrently wait-on cross-env

Step 2: Create Electron Main Process File

Create a new file called electron.js in your project's public directory:

const { app, BrowserWindow } = require('electron');
const path = require('path');

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true
    }
  });

  mainWindow.loadURL(
    !app.isPackaged 
      ? "http://localhost:3000/" 
      : `file://${path.join(__dirname, "../build/index.html")}`
  );

  if (!app.isPackaged) {
    mainWindow.webContents.openDevTools();
  }

  mainWindow.on('closed', () => mainWindow = null);
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});

Step 3: Update package.json

Modify your package.json file to include Electron-specific configurations:

{
  "name": "my-electron-react-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1"
  },
  "devDependencies": {
    "electron": "^30.0.0",
    "electron-builder": "^24.9.0",
    "concurrently": "^8.2.2",
    "wait-on": "^7.2.0",
    "cross-env": "^7.0.3"
  },
  "main": "public/electron.js",
  "homepage": "./",
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "electron:dev": "concurrently \"cross-env BROWSER=none npm start\" \"wait-on http://localhost:3000 && electron .\"",
    "electron:build": "npm run build && electron-builder -c.extraMetadata.main=build/electron.js"
  }
}

Step 4: Accessing Native Features

One of the main advantages of Electron is the ability to access desktop features. Let's create a simple example that interacts with the file system:

import React, { useState } from 'react';
const { dialog } = window.require('electron').remote;
const fs = window.require('fs');

function FileSystemComponent() {
  const [fileContent, setFileContent] = useState('');
  
  const openFile = async () => {
    const { filePaths } = await dialog.showOpenDialog({
      properties: ['openFile'],
      filters: [{ name: 'Text Files', extensions: ['txt'] }]
    });
    
    if (filePaths && filePaths.length > 0) {
      const content = fs.readFileSync(filePaths[0], 'utf8');
      setFileContent(content);
    }
  };
  
  return (
    <div>
      <button onClick={openFile}>Open File</button>
      {fileContent && (
        <div>
          <h3>File Content:</h3>
          <pre>{fileContent}</pre>
        </div>
      )}
    </div>
  );
}

export default FileSystemComponent;

However, for newer Electron versions, you'll need to use IPC (Inter-Process Communication) instead of directly accessing Electron APIs from the renderer process:

Step 5: Setting Up IPC Communication

First, update your electron.js file:

const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const fs = require('fs');
// ... existing code ...

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, 'preload.js')
    }
  });
  
  // ... existing code ...
}

// Handle file open request
ipcMain.handle('file:open', async () => {
  const { filePaths } = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [{ name: 'Text Files', extensions: ['txt'] }]
  });
  
  if (filePaths && filePaths.length > 0) {
    const content = fs.readFileSync(filePaths[0], 'utf8');
    return content;
  }
  return null;
});

Then create a preload.js file in your public directory:

const { contextBridge, ipcRenderer } = require('electron');

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
  'electron', {
    openFile: () => ipcRenderer.invoke('file:open')
  }
);

Finally, update your React component:

import React, { useState } from 'react';

function FileSystemComponent() {
  const [fileContent, setFileContent] = useState('');
  
  const openFile = async () => {
    const content = await window.electron.openFile();
    if (content) {
      setFileContent(content);
    }
  };
  
  return (
    <div>
      <button onClick={openFile}>Open File</button>
      {fileContent && (
        <div>
          <h3>File Content:</h3>
          <pre>{fileContent}</pre>
        </div>
      )}
    </div>
  );
}

export default FileSystemComponent;

Step 6: Building Your Application

Now that your application is set up, you can build it for distribution:

# Run in development mode
npm run electron:dev

# Build for production
npm run electron:build

The production build will be created in the dist directory, typically containing installers for your chosen platforms.

Real-World Example: Creating a Markdown Editor

Let's create a more practical example - a simple markdown editor that saves files to the desktop:

// Main Electron process (electron.js) 
const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron'); 

const path = require('path'); 

const fs = require('fs'); 

let mainWindow; 
let currentFilePath = null; 
function createWindow() { 
  mainWindow = new BrowserWindow({ 
    width: 1200, 
    height: 800, 
    webPreferences: { 
      nodeIntegration: false, 
      contextIsolation: true, 
      preload: path.join(__dirname, 'preload.js') 
    } 
  }); 
  mainWindow.loadURL( 
    !app.isPackaged 
      ? 'http://localhost:3000' 
      : `file://${path.join(__dirname, '../build/index.html')}` 
 ); 
  if (!app.isPackaged) { 
    mainWindow.webContents.openDevTools(); 
  }
  mainWindow.on('closed', () => mainWindow = null); 
  const template = [ 
    { 
      label: 'File', 
      submenu: [ 
        { 
          label: 'New', 
          accelerator: 'CmdOrCtrl+N', 
          click: () => { 
            mainWindow.webContents.send('file:new'); 
            currentFilePath = null; 
          } 
        }, 
        { 
          label: 'Open', 
          accelerator: 'CmdOrCtrl+O', 
          click: async () => { 
            await openFile(); 
          } 
        }, 
        { 
          label: 'Save', 
          accelerator: 'CmdOrCtrl+S', 
          click: async () => { 
            mainWindow.webContents.send('file:save-request'); 
          } 
        }, 
        { 
          label: 'Save As', 
          accelerator: 'CmdOrCtrl+Shift+S', 
          click: async () => { 
            mainWindow.webContents.send('file:save-as-request'); 
          } 
        }, 

        { type: 'separator' }, 
        { 
          label: 'Exit', 
          accelerator: 'CmdOrCtrl+Q', 
          click: () => { 
            app.quit(); 
          } 
        } 
      ] 
    }, 
    { 
      label: 'Edit', 
      submenu: [ 
        { role: 'undo' }, 
        { role: 'redo' }, 
        { type: 'separator' }, 
        { role: 'cut' }, 
        { role: 'copy' }, 
        { role: 'paste' }, 
        { role: 'delete' }, 
        { role: 'selectAll' } 
      ] 
    } 
  ]; 
  const menu = Menu.buildFromTemplate(template); 
  Menu.setApplicationMenu(menu); 
} 
async function openFile() { 

  const { filePaths } = await dialog.showOpenDialog({ 
    properties: ['openFile'], 
    filters: [ 
      { name: 'Markdown', extensions: ['md', 'markdown'] }, 
      { name: 'All Files', extensions: ['*'] } 
    ] 
  }); 
  if (filePaths && filePaths.length > 0) { 
    const content = fs.readFileSync(filePaths[0], 'utf8'); 
    currentFilePath = filePaths[0]; 

    mainWindow.webContents.send('file:opened', { filePath: currentFilePath, content }); 

    return content; 

  } 
  return null; 

} 
app.whenReady().then(createWindow); 

app.on('window-all-closed', () => { 

  if (process.platform !== 'darwin') { 

    app.quit(); 

  } 

}); 

app.on('activate', () => { 

  if (mainWindow === null) { 

    createWindow(); 

  } 

}); 
// IPC handlers 
ipcMain.handle('file:open', openFile); 

ipcMain.handle('file:save', async (event, { content, saveAs = false }) => { 
  let filePath = currentFilePath; 
  if (!filePath || saveAs) { 
    const { filePath: savePath, canceled } = await dialog.showSaveDialog({ 
      title: 'Save Markdown File', 
      defaultPath: filePath || 'untitled.md', 
      filters: [ 

        { name: 'Markdown', extensions: ['md'] }, 

        { name: 'All Files', extensions: ['*'] } 

      ] 
    }); 

    if (canceled || !savePath) { 
      return { success: false }; 
    } 
    filePath = savePath; 
  } 
  try { 
    fs.writeFileSync(filePath, content, 'utf8'); 
    currentFilePath = filePath; 
    return { success: true, filePath }; 
  } catch (error) { 
    return { success: false, error: error.message }; 
  } 
}); 
// Preload script (preload.js)
const { contextBridge, ipcRenderer } = require('electron'); 
contextBridge.exposeInMainWorld( 
  'electron', { 
    openFile: () => ipcRenderer.invoke('file:open'), 
    saveFile: (content, saveAs) => ipcRenderer.invoke('file:save', { content, saveAs }), 
    onFileOpened: (callback) => ipcRenderer.on('file:opened', (event, data) => callback(data)), 
    onNewFile: (callback) => ipcRenderer.on('file:new', () => callback()), 
    onSaveRequest: (callback) => ipcRenderer.on('file:save-request', () => callback()), 
    onSaveAsRequest: (callback) => ipcRenderer.on('file:save-as-request', () => callback()) 
  } 
); 
// React Component for the markdown editor (App.js) 

import React, { useState, useEffect } from 'react'; 
import ReactMarkdown from 'react-markdown'; 
import './App.css'; 

function App() { 
  const [markdown, setMarkdown] = useState('# Hello Electron Markdown\n\nStart typing...'); 
  const [currentFile, setCurrentFile] = useState(null); 
  useEffect(() => { 
    // Listen for file open events 
    window.electron.onFileOpened((data) => { 
      setMarkdown(data.content); 
      setCurrentFile(data.filePath); 
    }); 
    // Listen for new file events 
    window.electron.onNewFile(() => { 

      setMarkdown('# New Document\n\nStart typing...'); 
      setCurrentFile(null); 
    }); 

    // Listen for save requests 
   window.electron.onSaveRequest(() => { 
     saveFile(false); 
    }); 

    // Listen for save-as requests 
    window.electron.onSaveAsRequest(() => { 
      saveFile(true); 
    }); 
  }, []);

  const saveFile = async (saveAs) => { 
    const result = await window.electron.saveFile(markdown, saveAs); 
    if (result.success) { 
      setCurrentFile(result.filePath); 
    }
  }; 

return ( 
   <div className="app"> 
      <div className="file-info"> 
        {currentFile ? `Editing: ${currentFile}` : 'New Document'} 
      </div> 
      <div className="container"> 
        <div className="editor-pane"> 
        <textarea 
            value={markdown} 
            onChange={(e) => setMarkdown(e.target.value)} 
           className="markdown-input" 
          /> 
       </div> 
       <div className="preview-pane"> 
         <ReactMarkdown>{markdown}</ReactMarkdown> 
        </div> 
      </div> 
    </div> 
  ); } 

export default App; 
// CSS Styling (App.css) 
.app { 
  display: flex; 
  flex-direction: column; 
  height: 100vh; 
} 
.file-info { 
  background-color: #f0f0f0; 
  padding: 8px 16px; 
  font-size: 12px; 
  border-bottom: 1px solid #ddd; 
} 
.container { 
  display: flex; 
  flex: 1; 
} 
.editor-pane, .preview-pane { 
  flex: 1; 
 overflow: auto; 
  padding: 20px; 
} .editor-pane { 
  border-right: 1px solid #ccc; 
} 
.markdown-input { 
  width: 100%; 
  height: 100%; 
  border: none; 
  outline: none; 
  resize: none; 
  font-family: monospace; 
  font-size: 14px; 
} 
.preview-pane { 
  background-color: #fafafa; 
} 

Step 7: Packaging for Distribution

To distribute your application to users, you'll need to package it appropriately for each platform:

# Add platform-specific build configurations to package.json 

"build": { 
  "appId": "com.yourcompany.markdowneditor", 
  "productName": "Markdown Editor", 
  "mac": { 
    "category": "public.app-category.productivity" 
  }, 
  "win": { 
    "target": [ 
      "nsis" 
    ] 
  }, 
  "linux": { 
    "target": [ 
      "deb", 
      "AppImage" 
    ], 
    "category": "Office" 
  } 
} 

Common Challenges and Solutions

Challenge 1: Working with Native Modules

Native Node.js modules often need to be rebuilt for Electron:

# Install electron-rebuild
npm install --save-dev electron-rebuild

# Add to package.json scripts
"postinstall": "electron-rebuild"

Challenge 2: Optimizing Application Size

Electron apps can be large. Consider these optimizations in your package.json:

"build": {
  "files": [
    "build/**/*",
    "node_modules/**/*"
  ],
  "extraResources": [
    "./assets/**"
  ],
  "asar": true
}

Challenge 3: Handling Updates

For automatic updates, consider adding Electron Updater:

npm install electron-updater

Then implement it in your main process:

const { autoUpdater } = require('electron-updater'); 
// Check for updates 
autoUpdater.checkForUpdatesAndNotify(); 

// Listen for update events 
autoUpdater.on('update-available', () => { 
  mainWindow.webContents.send('update-available'); 
}); 

autoUpdater.on('update-downloaded', () => { 
  mainWindow.webContents.send('update-downloaded'); 
}); 

Conclusion

Converting a React application to a desktop app with Electron combines the best of both worlds: the rich ecosystem and development experience of React with the desktop capabilities of Electron. By following the steps in this guide, you can transform your web application into a fully featured desktop experience.

Remember that Electron applications give you access to file systems, notifications, system tray integration, and more - opening possibilities that simply aren't available in traditional web applications. Use these capabilities wisely to enhance your application's user experience.

avatar-icon.png

Written By

Ravindra Mistry

Leveraging PostgreSQL Row Level Security in Multi-Tenant Applications

Row Level Security (RLS) in PostgreSQL is a robust feature that provid.......

Ravindra Mistry

2025-03-28

Paperwork is Killing Productivity. AI is Saving It

Manual document processing wastes time, increases errors, and creates .......

Niket Shah

2025-03-28

Untitled-1.jpg
About Us

Learn about OpenSpace Services, a trusted end-to-end digital product development & consulting firm, delivering software, apps, CRM & web solutions.

    Category