coderain guide

How to Transition from JavaScript to React Frontend

If you’re comfortable with JavaScript and looking to level up your frontend development skills, React is a natural next step. Developed by Facebook (now Meta), React has become the most popular frontend library for building dynamic, interactive user interfaces. Unlike vanilla JavaScript—where you manually manipulate the DOM and manage state—React introduces a **declarative, component-based paradigm** that simplifies building complex UIs. This blog will guide you through the transition from JavaScript to React, breaking down core concepts, tools, and best practices with clear examples. Whether you’re building a simple app or a large-scale project, by the end, you’ll have the foundation to write clean, efficient React code.

Table of Contents

  1. Understanding React’s Core Paradigm
  2. Setting Up Your React Environment
  3. JSX: The HTML-in-JS Syntax
  4. Components: The Building Blocks of React
  5. Props: Passing Data Between Components
  6. State: Managing Dynamic Data
  7. Lifecycle & Side Effects with Hooks
  8. Advanced Hooks: useContext, useReducer, and More
  9. Routing with React Router
  10. State Management Beyond useState
  11. Best Practices for React Development
  12. Conclusion
  13. References

1. Understanding React’s Core Paradigm

Before diving into code, it’s critical to grasp how React differs from vanilla JavaScript. Here are the key mental models:

Declarative vs. Imperative

  • Vanilla JS is imperative: You write step-by-step instructions to manipulate the DOM (e.g., document.getElementById('root').innerHTML = 'Hello').
  • React is declarative: You describe what the UI should look like for a given state, and React handles how to update the DOM.

Example: Updating a counter

// Vanilla JS (Imperative)
let count = 0;
const button = document.querySelector('button');
const display = document.querySelector('span');

button.addEventListener('click', () => {
  count++;
  display.textContent = count; // Manually update DOM
});

// React (Declarative)
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Component-Based Architecture

React apps are built from reusable, independent components (e.g., Button, Card, Navbar). Components can be nested, making UIs modular and easy to maintain.

Virtual DOM

React uses a lightweight in-memory representation of the DOM called the Virtual DOM. When state changes, React compares the Virtual DOM with the real DOM (a process called “reconciliation”) and updates only the necessary parts—optimizing performance.

2. Setting Up Your React Environment

To start building React apps, you’ll need:

  • Node.js (v14.0.0+ recommended) and npm (or Yarn) for package management.
  • A code editor (VS Code with the ES7+ React Snippets extension is popular).

Option 1: Create React App (CRA)

CRA is a zero-configuration toolchain for bootstrapping React apps:

npx create-react-app my-first-react-app
cd my-first-react-app
npm start

This spins up a development server at http://localhost:3000 with hot reloading.

Option 2: Vite (Faster Alternative)

Vite is a modern build tool with faster startup and HMR (Hot Module Replacement). Use it for larger projects:

npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

3. JSX: The HTML-in-JS Syntax

JSX (JavaScript XML) is React’s syntax for writing HTML-like code within JavaScript. It’s not HTML, but a syntax extension that gets transpiled to JavaScript (via Babel) before runtime.

Key JSX Rules

  • Expressions in JSX: Use {} to embed JavaScript expressions (variables, functions, calculations).

    const name = "Alice";
    <h1>Hello, {name}!</h1> {/* Renders: <h1>Hello, Alice!</h1> */}
  • Attributes: Use camelCase for HTML attributes (e.g., className instead of class, onClick instead of onclick).

    <div className="container" onClick={handleClick}>Click me</div>
  • Self-closing tags: Tags without children (e.g., <img>, <input>) must close with />.

    <img src="logo.png" alt="React Logo" />
  • Single root element: JSX expressions must return a single root element. Use fragments (<></> or <React.Fragment>) to group elements without adding extra DOM nodes:

    // Valid (with fragment)
    <>
      <h1>Title</h1>
      <p>Paragraph</p>
    </>
    
    // Invalid (multiple roots)
    <h1>Title</h1>
    <p>Paragraph</p>

Why JSX?

JSX makes code more readable than vanilla JS’s document.createElement calls. Compare:

// Vanilla JS
const element = document.createElement('div');
element.className = 'greeting';
element.textContent = `Hello, ${name}`;

// React with JSX
const element = <div className="greeting">Hello, {name}</div>;

4. Components: The Building Blocks

React components are reusable, self-contained pieces of UI. There are two types: functional components (modern, preferred) and class components (legacy).

Functional Components

A functional component is a JavaScript function that returns JSX. It accepts props (data) as input and returns what to render.

Example: A Simple Greeting Component

// Greeting.js
function Greeting() {
  return <h1>Hello, React!</h1>;
}

export default Greeting;

To use it in another component:

// App.js
import Greeting from './Greeting';

function App() {
  return (
    <div className="App">
      <Greeting /> {/* Renders the Greeting component */}
    </div>
  );
}

Class Components (Legacy)

Class components use ES6 classes and render() methods. They’re less common today (replaced by functional components and hooks), but you may encounter them in older code:

class Greeting extends React.Component {
  render() {
    return <h1>Hello, React!</h1>;
  }
}

5. Props: Passing Data Between Components

Props (short for “properties”) are how components pass data to each other. Think of props as function arguments for components.

Passing and Accessing Props

To pass props, add attributes to the component when rendering it. To access props, use the props parameter in functional components.

Example: Passing a Name to a Component

// User.js (Child component)
function User(props) {
  return <p>Welcome, {props.name}!</p>; // Access props.name
}

// App.js (Parent component)
function App() {
  return (
    <div>
      <User name="Alice" /> {/* Pass "Alice" as a prop */}
      <User name="Bob" /> {/* Pass "Bob" as a prop */}
    </div>
  );
}

Output:

Welcome, Alice!
Welcome, Bob!

Destructuring Props

For cleaner code, destructure props directly in the function parameters:

function User({ name }) { // Destructure props.name
  return <p>Welcome, {name}!</p>;
}

Default Props

Set default values for props if they’re not provided:

function User({ name = "Guest" }) { // Default to "Guest"
  return <p>Welcome, {name}!</p>;
}

// Usage: <User /> → "Welcome, Guest!"

6. State: Managing Dynamic Data

In vanilla JavaScript, variables update, but the DOM doesn’t automatically reflect changes. React’s state solves this: it’s a built-in way to manage dynamic data that triggers UI re-renders when updated.

Introducing the useState Hook

Hooks are functions that let you “hook into” React state and lifecycle features from functional components. The useState hook is used to declare state variables.

Syntax:

const [stateVariable, setStateFunction] = useState(initialValue);
  • stateVariable: The current state value.
  • setStateFunction: A function to update the state (triggers re-render).
  • initialValue: The initial value of the state (optional).

Example: Counter Component

import { useState } from 'react';

function Counter() {
  // Declare a state variable "count" with initial value 0
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}
  • When the “Increment” button is clicked, setCount(count + 1) updates count.
  • React re-renders the Counter component with the new count value, updating the UI.

State vs. Vanilla JS Variables

A vanilla JS variable won’t trigger a re-render when updated. Only state variables (declared with useState) cause React to re-render the component.

7. Lifecycle & Side Effects with Hooks

In vanilla JS, you might use DOMContentLoaded or event listeners for setup/cleanup. In React, side effects (e.g., data fetching, subscriptions, manually updating the DOM) are handled with the useEffect hook.

The useEffect Hook

useEffect lets you perform side effects in functional components. It replaces legacy class lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.

Syntax:

useEffect(() => {
  // Effect code (runs after render)
  return () => {
    // Cleanup code (runs before re-render or unmount)
  };
}, [dependencies]); // Re-run effect only if dependencies change

Common Use Cases

1. Run Once on Mount (Like componentDidMount)

Pass an empty dependencies array ([]) to run the effect once after the initial render:

useEffect(() => {
  console.log("Component mounted!");
}, []); // Empty array → runs once

2. Data Fetching

Fetch data from an API and update state:

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Fetch user data when userId changes
    const fetchUser = async () => {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      const data = await response.json();
      setUser(data);
    };

    fetchUser();

    // Cleanup: Abort fetch if component unmounts or userId changes
    return () => {
      // (Optional) Use AbortController for fetch cleanup
    };
  }, [userId]); // Re-run effect when userId changes

  if (!user) return <p>Loading...</p>;
  return <div>Name: {user.name}</div>;
}

3. Cleanup Side Effects

Use the cleanup function to avoid memory leaks (e.g., removing event listeners, canceling subscriptions):

useEffect(() => {
  const handleResize = () => console.log(window.innerWidth);
  window.addEventListener('resize', handleResize);

  // Cleanup: Remove event listener when component unmounts
  return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array → runs once, cleanup on unmount

8. Advanced Hooks

Beyond useState and useEffect, React provides several advanced hooks for common patterns.

useContext: Share State Across Components

useContext lets components access context (global state) without prop drilling (passing props through multiple levels).

Example: Theme Context

// ThemeContext.js
import { createContext, useContext, useState } from 'react';

// 1. Create a context
const ThemeContext = createContext();

// 2. Create a provider component
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children} {/* Children can access context */}
    </ThemeContext.Provider>
  );
}

// 3. Custom hook to use the context
export function useTheme() {
  return useContext(ThemeContext);
}

Now any component can access the theme:

import { useTheme } from './ThemeContext';

function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button 
      style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#333' : '#fff' }}
      onClick={toggleTheme}
    >
      Toggle Theme ({theme})
    </button>
  );
}

useReducer: Complex State Logic

For state logic with multiple sub-values or complex transitions, useReducer is a better alternative to useState. It works like Redux’s reducer pattern.

Example: Todo List

import { useReducer } from 'react';

// Reducer function: Takes current state and action, returns new state
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'TOGGLE_TODO':
      return state.map(todo => 
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    default:
      return state;
  }
}

function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []); // Initial state: empty array

  return (
    <div>
      <button onClick={() => dispatch({ type: 'ADD_TODO', text: 'Learn useReducer' })}>
        Add Todo
      </button>
      <ul>
        {todos.map(todo => (
          <li 
            key={todo.id} 
            onClick={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
            style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
          >
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

9. Routing with React Router

Single-page applications (SPAs) use client-side routing to navigate without reloading the page. React Router is the de facto library for this.

Setup

Install React Router:

npm install react-router-dom

Key Components

  • BrowserRouter: Wraps your app to enable routing (usually in index.js).
  • Routes: A container for Route components.
  • Route: Maps a URL path to a component.
  • Link: Navigation link (replaces <a> tags to avoid full page reloads).

Example: Basic Routing

1. Wrap your app with BrowserRouter (in index.js):

import { BrowserRouter } from 'react-router-dom';

ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

2. Define routes in App.js:

import { Routes, Route, Link } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Contact from './Contact';

function App() {
  return (
    <div>
      {/* Navigation */}
      <nav>
        <Link to="/">Home</Link> |{' '}
        <Link to="/about">About</Link> |{' '}
        <Link to="/contact">Contact</Link>
      </nav>

      {/* Routes */}
      <Routes>
        <Route path="/" element={<Home />} /> {/* Home page */}
        <Route path="/about" element={<About />} /> {/* About page */}
        <Route path="/contact" element={<Contact />} /> {/* Contact page */}
      </Routes>
    </div>
  );
}

10. State Management Beyond useState

For apps with complex state (e.g., user sessions, cart data), useState and useContext may not be enough. Here are common solutions:

Context API + useReducer

Ideal for medium-sized apps needing global state (e.g., theme, user auth). Combine useContext and useReducer to manage global state cleanly.

Redux

A predictable state container for large apps. It uses a single store, actions, and reducers to manage state. Libraries like react-redux and @reduxjs/toolkit simplify setup.

Zustand/Jotai

Lightweight alternatives to Redux with less boilerplate. Zustand, for example, lets you create a store with a simple hook:

// store.js
import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// Use in component
function Counter() {
  const { count, increment } = useStore();
  return <button onClick={increment}>{count}</button>;
}

11. Best Practices for React Development

1. Component Composition Over Inheritance

Use component composition (passing components as props) instead of inheritance for code reuse:

// Parent component passing a child component
function Card({ children }) {
  return <div className="card">{children}</div>;
}

// Usage
<Card>
  <h2>Card Title</h2>
  <p>Card content</p>
</Card>

2. Avoid Unnecessary Re-renders

  • Use React.memo to memoize components that don’t need to re-render.
  • Use the useCallback hook to memoize functions passed as props.
  • Use the useMemo hook to memoize expensive calculations.

3. Keep Components Small and Focused

A component should do one thing. Split large components into smaller, reusable ones.

4. Use Functional Components and Hooks

Class components are legacy. Stick to functional components and hooks for cleaner, more maintainable code.

5. Test Your Components

Use tools like Jest (testing framework) and React Testing Library (component testing) to write unit tests:

// Counter.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('increments count when button is clicked', () => {
  render(<Counter />);
  const button = screen.getByText('Increment');
  fireEvent.click(button);
  expect(screen.getByText('You clicked 1 times')).toBeInTheDocument();
});

Conclusion

Transitioning from JavaScript to React involves shifting from an imperative, DOM-manipulating mindset to a declarative, component-based one. By mastering core concepts like JSX, props, state, hooks, and routing, you’ll be able to build dynamic, scalable UIs efficiently.

Start small (e.g., a todo app or weather dashboard), experiment with hooks, and gradually explore advanced topics like state management and testing. With practice, React will feel like a natural extension of your JavaScript skills.

References