React



Describing the UI



JSX



React Rendering Steps



Styling


# 1. Inline Styles

//use the style prop
//The value is a JavaScript object, not a string.
//no support for pseudo-classes (like :hover) or media queries
<button style={{ backgroundColor: 'blue', color: 'white' }}>

# 2. CSS Stylesheets
import './styles.css';

function Button() {
  return <button className="btn">Click Me</button>;
}

# 3. CSS Modules

//Scoped CSS: styles are locally scoped to the component.
//Filename ends with .module.css. -> Button.module.css
//Good for: component-based architecture; avoids style conflicts

import styles from './Button.module.css';

function Button() {
  return <button className={styles.btn}>Click Me</button>;
}

Props



React Fragments



Rendering a List



Keeping Components Pure



Adding Interactivity



Responding to Events



A Component’s Memory and State in React


  1. State vs. Local Variables:

    • State retains information between renders, while local variables reset each time a component re*renders.
    • State updates trigger re*renders, whereas changes to local variables do not.
  2. Understanding State:

    • State is private to a component and cannot be changed by parent components.
    • Use useState to declare and manage state in a functional component.
    • State updates are asynchronous and processed after event handlers complete (called batching).
    • This means React waits until the current event handler or lifecycle method finishes before processing state changes, improving performance by minimizing re-renders.
const [text, setText] = useState("");

const handleClick = () => {
  setText("ddd");
  console.log(text); // 👀
};
//setText("ddd") schedules the state update — it does not immediately update text.
//console.log(text) will still print the old value (before the update), not "ddd".
// React batches state updates during event handlers to optimize performance. It applies all state changes after the handler finishes — //so text hasn't changed yet at the time of the console.log.
  1. How React Handles Renders:

    • React calls the component function, which returns a JSX snapshot based on the state (at that time).
    • React updates the UI to match this snapshot.
    • State remains fixed during a render; updates apply only in the next render.
  2. Managing Multiple State Updates:

    • Calling setState multiple times with the same value may not behave as expected due to state snapshots.

    • Use updater functions (e.g., setNumber(n => n + 1)) to ensure updates are based on the latest state.

      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);}}>+3</button>
      //In each function React prepares to change number to 1 on the next render since initial value is 0
      //Its value was “fixed” when React “took the snapshot” of the UI by calling your component.
      
      • To summarize, here’s how you can think of what you’re passing to the setNumber state setter:
        1. An updater function (e.g. n => n + 1) gets added to the queue.
        2. Any other value (e.g. number 5) adds “replace with 5” to the queue, ignoring what’s already queued.
  3. Updating Objects and Arrays in State:

    • State objects and arrays should be treated as immutable. Do not modify them directly; instead, create a copy and update it.
    • Example for objects:
      setPerson({
        ...person,
        firstName: e.target.value,
      });
      
    • Example for arrays:
      setArtists([
        ...artists,
        { id: nextId++, name: name },
      ]);
      
  4. Key Patterns for State Management:

    • Objects: Use the spread operator (...) to copy and update fields.
    • Arrays: Use array methods like map, filter, or slice to create a new array and apply changes (e.g., adding, removing, or transforming items).
  5. Best Practices:

    • Keep state values immutable to ensure React re*renders correctly.
    • Use updater functions for sequential updates in event handlers.
    • Avoid defining hooks inside conditional blocks or nested functions.

Managing State



Best Practices for Managing State in React



Declarative UI



State Design Principles



Lifting State Up



Component Identity and State Preservation



Resetting state at the same position


There are two ways to reset state when switching between them:

  1. Render components in different positions
  2. Give each component an explicit identity with key NOTE: Remember that keys are not globally unique. They only specify the position within the parent.

Extracting State Logic into a Reducer



Why Use a Reducer?


Key Points:

Reducers must be pure: No side effects—just compute and return the new state based on current state + action.

Actions reflect user interactions: One action should represent one meaningful interaction (e.g., reset_form instead of multiple set_fields).

Better for complex state: Especially when state updates are spread across many handlers.

Steps to convert useState to useReducer:

  1. Dispatch actions from handlers.
  2. Write a pure reducer function.
  3. Replace useState with useReducer. 👉 Use Immer if you prefer writing reducers in a mutating style but want to preserve immutability.
# From
const [tasks, setTasks] = useState(initialTasks);

function handleAddTask(text) {
  setTasks([
    ...tasks, {id: nextId++,text: text, done: false, },
  ]);
}

# To
import tasksReducer from "./reducer";
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

function handleAddTask(text) {
  dispatch({type: 'added', id: nextId++,text: text,});
}

// when dispatch is called, the reducer function is called with the current state and the action
// the reducer function returns the new state


// In reducer

// reducer function should take two arguments, the current state and the action

function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    return [...tasks, {id: action.id,text: action.text,done: false,},
    ];
  } else if (action.type === 'changed') {
    ...
  }
}

//If reducer function is in the same file, then it should be declared before the useReducer hook

Passing Data Deeply with Context



When to Use Context



Example


#Example 1

#STEP 1
#create a context with initial/default value/s

//LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(0);

#STEP 2
#Provide Context to a Subtree
#Wrap part of the component tree with the `LevelContext.Provider`, and pass the context value.
#accept value/s as props, so in the place that this file is used can pass the value/s

//Section.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

function Section({ level, children }) {
  return (
    <LevelContext.Provider value={level}>
      {children}
    </LevelContext.Provider>
  );
}

#STEP 3
#Consume Context with `useContext`
#Use `useContext()` to read the current value of the context.

//Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

function Heading({ children }) {
  const level = useContext(LevelContext);
  const Tag = `h${level}`;
  return <Tag>{children}</Tag>;
}

#STEP 4
#Compose Nested Components
#You can now use `Section` and `Heading` components to build a dynamic heading structure:

<Section level={1}>
	<Heading>Heading 1</Heading>

	<Section level={2}>
		<Heading>Heading 2</Heading>

		<Section level={3}>
			<Heading>Heading 3</Heading>
        </Section>
    </Section>
</Section>

#Example 2

//UserContext.ts
interface UserContextType {
  userName: string;
  setUserName: (name: string) => void;
}

const UserContext = createContext<UserContextType | null>(null);
export default UserContext;

//Wrapper.tsx
const [userName, setUserName] = useState<string>("some initial value");

<UserContext.Provider value={{ userName, setUserName }}>	
	<Parent />
</UserContext.Provider>


//Parent.tsx
<div>
	<Child1 />
    <Child2 />
</div>

//Child1.tsx
const context = useContext(UserContext);
if (!context) {	throw new Error("Error");}
const { userName } = context;

<div>
    <>{userName}</>
</div>

//Child2.tsx
const context = useContext(UserContext);
if (!context) {	throw new Error("Error");}
const { userName, setUserName } = context;

<div>
	<input
		type="text"
		value={userName}
		onChange={(e) => setUserName(e.target.value)}
		placeholder="Enter your name"
	/>
</div>

ReactContextAPI.png


Scaling Up with Reducer and Context


const initialState = {};
const [state, dispatch] = useReducer(reducerFunction, initialState);

<UserContext.Provider value={{ ...state, dispatch }}>
  <Parent />
</UserContext.Provider>

This approach is especially useful when:


React Routers


npm install react-router-dom

//In App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
<BrowserRouter>
	<Nav /> //Nav should be rendered under BrowserRouter

	<Routes>
		<Route path="/" element={<Home />} />
		<Route path="/about" element={<About />} />

    	// Nested routes
		<Route path="/dashboard" element={<Dashboard />}>
			<Route path="settings" element={<Settings />} /> //no forward slash before route
			<Route path="profile" element={<Profile />} />
		</Route>

    	// Dynamic routes
    	<Route path="/product/:id" element={<About />} />    
	</Routes>
</BrowserRouter>

//Nav.tsx
<>
	<Link to="/">Home</Link>
	<Link to="/dashboard">Dashboard</Link>
	<Link to="/about">About</Link>
</>

//Dashboard.tsx
<>
	<Link to="settings">Settings</Link> //no forward slash before route
	<Link to="profile">Profile</Link>
	<Outlet /> //This component needs to be there to render the nested route component properly
</>

//To access the dynamic route params use const {id} = useParams();

//Progamitic navigation

const navigate = useNavigate();
const onclickHandler = ()=>{
    navigate("/home",{state:{someState}})
}

<button onClick={onclickHandler}>navigate to homepage</button>

//To access the passing state from the component
const location = useLocation();
const {someState} = location.state || {}

Escape Hatches



Referencing values with refs


import { useRef } from 'react';
let ref = useRef(0);

When to use refs:
Use refs when your component needs to interact with external APIs (like browser APIs) without affecting rendering. Common cases include:


Manipulating the DOM with Refs



Getting a ref to the node


const myRef = useRef(null);
<div ref={myRef}> //This tells React to put this <div>’s DOM node into myRef.current.
//You can then access this DOM node from your event handlers and use the built-in browser APIs defined on it.
myRef.current.scrollIntoView();

Synchronizing with Effects


Example:

useEffect(() => {
  // This runs after every render
});

useEffect(() => {
  // This runs only on mount (when the component appears)
}, []);

useEffect(() => {
  // This runs on mount *and also* if either a or b have changed since the last render
}, [a, b]);
//Some APIs may not allow you to call them twice in a row. For example, the showModal method of the built-in <dialog> element throws if you call it twice. Implement the cleanup function and make it close the dialog:

useEffect(() => {
  const dialog = dialogRef.current;
  dialog.showModal();
  return () => dialog.close();
}, []);

//In development, your Effect will call showModal(), then immediately close(), and then showModal() again. This has the same user-visible behavior as calling showModal() once, as you would see in production.

You Might Not Need an Effect



useMemo


🔹 What is useMemo?

🔹 Why use it?

//How to use it?
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
//The function runs only when a or b changes.
//React skips recalculating if dependencies are the same as the last render.

🔹 When to use it?


Race Condition in useEffect


useEffect(() => {
  fetchData().then(setData);
}, [query]);
//If query changes quickly, older fetchData() results might overwrite the newer ones.

#Solution :  Use AbortController or a flag Or use a flag to ignore outdated responses.

useEffect(() => {
  const controller = new AbortController();
  fetchData({ signal: controller.signal }).then(setData).catch(() => {});

  return () => controller.abort(); // cancels previous request
}, [query]);

When You Do Need useEffect



Lifecycle of Reactive Effects


function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
  const settings = useContext(SettingsContext); // settings is reactive
  const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
  // ...
}

Separating Events from Effects


#Note: This is an experiment tool yet

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  const ontick = useEffectEvent(()=>{
    setCount(c => c + increment);
  });

  useEffect(() => {
    const id = setInterval(() => {
      ontick()
    }, 1000);
    return () => {
      clearInterval(id);
    };
  }, []);
//if you put `increment` in the dependence arry, every change to increment causes the Effect to re-synchronize,
//which causes the interval to clear. If you keep 
//clearing the interval every time before it has a chance to fire, it will appear as if the timer has stalled.

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}>–</button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

Removing Effect Dependencies



Core Principle


Dependencies should always match the code


Key Questions to Ask Before Fixing Dependencies


Before adjusting dependencies, evaluate your Effect by asking these critical questions:

Should this code move to an event handler?

Is your Effect doing several unrelated things?

Are you reading state to calculate the next state?

Do you want to read a value without "reacting" to its changes?

Does some reactive value change unintentionally?


The Object and Function Problem


Why Objects and Functions Cause Issues:

Impact on Performance:


Solutions for Different Scenarios


Move Code to Event Handlers:

Split Effects:

Use Updater Functions:

Extract Effect Events:

Restructure Object/Function Dependencies:


Best Practices


Avoid Suppressing the Linter:

Prefer Primitive Dependencies:

Structure for Success:


Debugging Strategy


When an Effect runs too often:

  1. Check if objects/functions are being recreated unnecessarily
  2. Verify each dependency is actually needed for the Effect's logic
  3. Consider if the Effect is doing too many unrelated things
  4. Evaluate whether some code belongs in event handlers instead
  5. Look for opportunities to extract primitive values from complex dependencies

This systematic approach ensures Effects run only when necessary while maintaining code clarity and performance.


Reusing Logic with Custom Hooks



Extracting your own custom Hook from a component


function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

# Custome hook
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

# Example 2
# This is not necessary form form inputs like these, this is just for demostration purpose

import { useFormInput } from './useFormInput.js';

export default function Form() {
  const firstNameProps = useFormInput('Mary');
  const lastNameProps = useFormInput('Poppins');

  return (
    <>
      <label>
        First name:
        <input {...firstNameProps} />
       //This is similar to <input value={firstNameProps.value} onChange={firstNameProps.onChange} />
      </label>
      <label>
        Last name:
        <input {...lastNameProps} />
      </label>
      <p><b>Good morning, {firstNameProps.value} {lastNameProps.value}.</b></p>
    </>
  );
}

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps; //Returns an object { value, onChange } — just like the props an <input> expects.
}
function Form() {
  const firstNameProps = useFormInput('Mary');
  const lastNameProps = useFormInput('Poppins');

Portal



Suspension



ErrorBoundary