React

Build for Cloud Phone with React JS

React is one of the most popular JavaScript framework in 2024. Learn how to build a Cloud Phone widget using React, or skip ahead to the finished sample code and live demo hosted on GitHub Pages.

Need a primer on Cloud Phone?

Getting started with Cloud Phone

Learn how Cloud Phone brings compelling web apps to the next billion users

Setup

Prerequisites

Dependency Versions

This sample was developed using the following libraries and frameworks.

DependencyVersion
Nodev20.11.0 (LTS)
npm10.9.0
Reactv19
react-router7
i18next24
Vite6

Getting Started with Vite

Use Vite to scaffold a new React project.

Scaffolding with Vite
npm create vite@latest cloudphone-react-sample -- --template react

You can also use . for the project name to scaffold in the current directory.

Then follow the prompts to install dependencies into the node_modules/ directory.

Scaffolding project in ./cloudphone-react-sample...

Done. Now run:

  cd cloudphone-react-sample
  npm install
  npm run dev

The command npm run dev is aliased in the scripts section of the package.json file to run vite. This command starts a local HTTP server on port 5173 (by default), available at http://localhost:5173/

package.json
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  }

Development

Routing

React Router by Shopify is one of the most popular packages for routing React apps. It supports both single-page apps (SPAs) as a library, as well as full-stack development and deployment including static and server-side rendering (SSR) as a framework.

Since Cloud Phone is already rendered on powerful servers, server-side rendering (SSR) is redundant and unnecessary.

In this demo, React Router is used as a library. Install react-router using NPM:

npm install react-router

Then in main.jsx, wrap the <App /> element with <HashRouter>.

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { HashRouter } from 'react-router'
import './index.css'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <HashRouter>
      <App />
    </HashRouter>
  </StrictMode>
);

The default <BrowserRouter> does not work with GitHub Pages but can still be used for self-hosted widgets. For GitHub Pages, use <HashRouter> instead.

Internationalization

react-i18next is a popular package for handling internationalization (abbreviated i18n) in React apps.

npm install react-i18next i18next i18next-browser-languagedetector --save

For this demo, store translations as JSON files located at src/assets/locales/<language_code>/translation.json. Define a configuration like i18n.js below.

i18n.js
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import translationEN from '../assets/locales/en/translation.json'
import translationES from '../assets/locales/es/translation.json'

// Translations
const resources = {
  en: {
    translation: translationEN
  },
  es: {
    translation: translationES,
  }
};

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: 'en',
  });

export default i18n;

The demo includes two translations–English and Spanish–and uses the LanguageDetector package to automatically choose the default language based on the browser language.

Keyboard Navigation

Cloud Phones have directional pads (D-pads) to support keyboard navigation. Unlike desktop that uses the tabindex property and Tab (⇥) key to the next focusable element, Cloud Phone uses Arrow keys (↑↓←→).

OptionsMenu Screenshot
OptionsMenu
Settings Page Screenshot
Settings: Language Selection

Autofocus on the first element in a page, list, or grid to set the user’s bearings. Use the autoFocus prop or a callback ref like below.

AutofocusedLink.jsx
const autoFocus = (element) => element?.focus();

function AutofocusedLink({ url, text }) {
  return (
    <a
      href={url}
      autoFocus
      ref={autoFocus}>
      {text}
    </a>
  )
}

export default AutofocusedLink

Extend the callback ref approach to autofocus on the first item in a list using this example from Tiger Oakes.

findFirstFocusable.js
// CSS selector to find focusable elements
const FOCUSABLE_CSS_SELECTOR = `a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), *[tabindex], *[contenteditable]`;

// Find the first focusable element in a container (i.e. list)
function findFirstFocusable(container) {
  return container.querySelector(FOCUSABLE_CSS_SELECTOR);
}

// Auto-focus the first focusable item in element when its mounted
function autoFocusFirstFocusable(element) {
  if (element) {
    findFirstFocusable(element)?.focus();
  }
}

This technique is used in the OptionsMenu component. Here is a simplified example:

OptionsMenu.jsx
import React, { useEffect, useRef, useState } from 'react'
import { autoFocusFirstFocusable } from '../utils/focus'

function OptionsMenu({
  children = [ ],
}) {
  const menuRef = useRef(null);
  const [focusedIndex, setFocusedIndex] = useState(0);

  // Autofocus on the first element in the list
  useEffect(() => autoFocusFirstFocusable(menuRef?.current), [menuRef]);

  return (
    <>
      <menu
        ref={menuRef}
        role="menu">
        {React.Children.map(children, (child, index) =>
          React.cloneElement(child, {
            role: 'menuitem',
            className: ((index === focusedIndex) ? 'focused' : '')
          })
        )}
      </menu>
    </>
  )
}

export default OptionsMenu

The OptionsMenu component can have children of any type, including React Router’s <Link> component or standard the HTML anchor <a> tag, to create a navigation menu triggered by the Left Soft Key (LSK)

const [menuVisible, setMenuVisible] = useState(false);

const onSoftKeyClick = (position) => {
  // Toggle menu visibility with Escape (␛)
  if (position === 'start')
    setMenuVisible(!menuVisible);
}

return (
  <OptionsMenu visible={menuVisible}>
    <Link to="about" replace>{t('About')}</Link>
    <Link to="settings" replace>{t('Settings')}</Link>
    <a href="https://www.cloudfone.com/dev-privacy" target="_self">{t('Privacy')}</a>
  </OptionsMenu>

  <SoftKeyBar
    buttons = {{
      start: { icon: 'menu' },
      center: { icon: 'select' },
      end: { icon: 'back' },
    }}
    onSoftKeyClick={onSoftKeyClick} />
);

Hosting on GitHub Pages

GitHub Pages provides free hosting for public, open-source repositories. This demo uses the github-pages-deploy-action GitHub Action to build and deploy HTML, CSS, and JS to the gh-pages branch. The action is defined in pages.yml.

pages.yml
name: Deploy Pages
on:
  push:
    branches:
      - main
permissions:
  contents: write
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout 🛎️"
        uses: actions/checkout@v2
      - name: "Setup Node 🚧"
        uses: actions/setup-node@v4
        with:
          node-version: ">=20.11.0"
      - name: "Dependencies 🏗️"
        run: |
          npm ci
      - name: "Build 🔧"
        run: |
          npm run build
      - name: "Deploy 🚀"
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          folder: dist

Wrap Up

Tips

Consider these tips if you are starting a React project from scratch.

  • Use an accessible component library like Material UI (MUI) or Chakra UI
  • Lazy-load translations dynamically over HTTPS with i18next-http-backend
  • Reduce package size and page load time by switching to Preact
  • Develop reliably with testing utilities like Enzyme
  • Manage global state predictably with Redux

Next Steps

Register for the Cloud Phone Developer Program to try the live demo on the Cloud Phone Simulator or a physical device. Fork us on GitHub to start coding your first Cloud Phone widget using React and set up a custom domain name to point to GitHub Pages.

Contact Us

Reach out when you are ready to publish your first Cloud Phone widget

Get Support