NOTIFYY FRONTEND INTERVIEW — COMPLETE PREPARATION HANDBOOK

Gopinath C | React.js + Next.js | 2+ Years | Immediate Joiner

Prepared: May 2025


SECTION 1 — COMPANY ANALYSIS: NOTIFYY

What Notifyy Does

Notifyy (notifyy.io) is a WhatsApp Business SaaS platform — a Meta Business Provider building an all-in-one suite for sales, support, and marketing teams. Their core product stack includes:

Feature Module What It Is
Unified Inbox Shared multi-agent WhatsApp team inbox with real-time messaging
Built-in CRM Contact/company management, deal pipelines, CSV import
Campaign Manager Bulk WhatsApp broadcasts with audience segmentation
Workflow Automation No-code visual chatbot/automation builder
WhatsApp Template Engine Meta-approved template creation and management

Tech evidence from the site: The app domain is app.notifyy.io, the site is clearly built on Next.js (_next/image CDN URLs, opengraph-image meta), and their backend is hosted on crm.notifyy.io suggesting a separate CRM API layer. The theme color #003D8A and structured layout suggest a design-system-driven frontend.

What Kind of Frontend Developer They Need

Notifyy is a product SaaS startup with a live product and paying customers. They need a developer who can:

Key Areas to Prepare (Notifyy-Specific)

  1. Real-time UI patterns — handling live conversation updates (polling/WebSocket in React)
  2. Multi-step form wizards — campaign creation flows (React Hook Form patterns)
  3. Data tables with filters — contact lists, deal pipelines (virtualization, pagination)
  4. Dashboard components — campaign analytics, delivery stats (chart rendering)
  5. Auth flows — OAuth (Google Sign-in is live on their site), JWT token handling
  6. Optimistic UI — sending a WhatsApp message should feel instant before API confirms
  7. RBAC — agent assignment, team inbox permission visibility

SECTION 2 — HR ROUND QUESTIONS & POLISHED ANSWERS


Q1: Tell me about yourself.

❌ Beginner mistake: "I am Gopinath, I have 2 years of experience in React..."
(Too short, no narrative, no hook)

✅ Best answer:

"I'm Gopinath, a Frontend Engineer with 2+ years of production experience building enterprise-grade applications using React.js and Next.js. At Rugaa, I've been the primary frontend developer for two products — Lease Portfolio 365, a financial dashboard handling 10,000+ lease records with optimized rendering, and NamSchool, a school management admin suite. I specialize in high-performance dashboards, complex form-driven workflows, and RBAC systems — which I understand are directly relevant to what Notifyy builds. I use AI tools like GitHub Copilot and ChatGPT as part of my daily workflow to accelerate delivery, and I'm an immediate joiner."

Why this is strong: Opens with impact, ties experience to their product domain, and ends with a practical selling point (immediate joiner + AI fluency).


Q2: Why are you looking to change jobs?

✅ Best answer:

"I've had great learning at Rugaa — I took ownership of two products end-to-end. But I'm ready to work on a live SaaS product with a larger user base, faster iteration cycles, and more cross-functional exposure. Notifyy builds exactly the kind of product I want to grow with — real-time messaging, CRM, multi-agent workflows — these are engineering problems I find genuinely interesting and haven't fully explored yet."

Red flag to avoid: Never say "salary isn't enough" or criticize your current company.


Q3: Why Notifyy specifically?

✅ Best answer:

"I looked at the product carefully before this conversation. Notifyy is a Next.js SaaS platform — I can see that in the architecture. The problem space — combining a CRM, real-time inbox, and campaign automation into one product — is genuinely complex from a frontend perspective. You're dealing with live data, multi-agent state, role-based views, and performance-sensitive tables — these are exactly the challenges I've been solving. The startup stage also means I can contribute to architecture decisions, not just execute tickets."

What this signals: You did your homework. Startup hiring managers love this.


Q4: Walk me through your most complex project.

✅ Best answer (Lease Portfolio 365):

"The most technically demanding project was Lease Portfolio 365 — a financial dashboard for a lease management SaaS. The core challenge was rendering and managing 10,000+ lease records. My first approach was naive — we were rendering everything at once and the table was lagging badly. I fixed this in two layers: first, I moved to paginated API calls with controlled state updates so we never load more than needed. Second, for the aggregate views, I applied useMemo to derived calculations so they didn't recompute on every unrelated state change. I also implemented SSR for the initial data load so the page wasn't blank on first render, which improved both perceived performance and SEO. This brought our dashboard load time down significantly."

Follow-up the interviewer will ask: "How did you measure the performance improvement?"
Your answer: "I used Chrome DevTools Performance tab and React DevTools profiler. I measured component render counts before and after memoization and tracked Time to Interactive through Lighthouse."


Q5: What is your expected salary?

✅ Best answer:

"Based on my experience with production-grade Next.js applications, RBAC systems, and dashboard engineering — and considering Notifyy's SaaS product stage — I'm looking at ₹6 to ₹8 LPA. I'm open to discussing based on the full package and growth trajectory."


Q6: What are your strengths and weaknesses?

✅ Strength:

"My strongest skill is performance debugging. When something is slow in production, I know how to isolate whether it's an unnecessary re-render, an expensive computation, a poorly batched API call, or a DOM-level issue. I've done this on real dashboards with 10k+ records, not toy examples."

✅ Weakness (honest + growth-framed):

"I haven't yet worked deeply with TypeScript in production — I've read through it and understand interfaces and generics, but my current projects use JavaScript. I'm actively learning it and have been converting components to TypeScript in a side project. I know it's standard in serious SaaS codebases."


Q7: Describe a conflict with a teammate.

✅ Best answer:

"On NamSchool, there was a disagreement with the backend developer about where validation should live — they wanted server-side only, I argued for client-side too for a better UX. We talked it through and agreed on both: client-side for immediate feedback, server-side as the source of truth for security. That's now our standard pattern. The key was separating the 'what's right for the user' question from personal preference."


SECTION 3 — JAVASCRIPT DEEP QUESTIONS


Q1: Explain closures with a real-world frontend example.

✅ Production-level answer:

"A closure is when a function retains access to variables from its outer scope even after the outer function has returned. A very common real-world issue: event listeners inside loops."

// Bug — all listeners log the same value (i = 5 after loop ends)
for (var i = 0; i < 5; i++) {
  button[i].addEventListener('click', () => console.log(i)); 
}

// Fix — use let (block-scoped) OR use a closure via IIFE
for (let i = 0; i < 5; i++) {
  button[i].addEventListener('click', () => console.log(i)); // ✅ correct
}

"In React, closures are the reason stale state bugs happen inside useEffect — the effect captures the value of a variable at the time it was created, not the latest value."

Follow-up: "What is a stale closure in React?"

"When useEffect captures an old value of state or props in its closure, but the component has since re-rendered with new values. Fix: add the variable to the dependency array, or use a useRef to hold a mutable reference."


Q2: What is the event loop? Why does it matter for UI performance?

✅ Answer:

"JavaScript is single-threaded. The event loop is what allows async operations — like API calls or timers — to run without blocking the main thread. It has a call stack, a Web API layer, a callback/task queue, and a microtask queue. Microtasks (Promises) always run before macrotasks (setTimeout)."

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2

"For UI performance, this matters because long synchronous operations block rendering. If I have a for loop processing 50,000 records synchronously, the browser can't paint anything during that time. The fix is to chunk the work using requestAnimationFrame or setTimeout chunks, or move it to a Web Worker."


Q3: Debouncing vs. Throttling — when do you use each?

✅ Answer:

"Debouncing delays execution until after a pause in events. Throttling limits execution to at most once per time window."

// Debounce — search input: only fire after user stops typing
const debounce = (fn, delay) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
};

// Throttle — scroll handler: fire at most once every 100ms
const throttle = (fn, limit) => {
  let inThrottle;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
};

"At Notifyy, I'd debounce the contact search in the CRM list (fire API only after user pauses). I'd throttle a scroll event listener on a long conversation thread."


Q4: Promises — async/await error handling pattern.

✅ Production pattern:

// ❌ Common mistake — forgetting error handling
const fetchContacts = async () => {
  const data = await axios.get('/api/contacts');
  setContacts(data);
};

// ✅ Production pattern
const fetchContacts = async () => {
  try {
    const { data } = await axios.get('/api/contacts');
    setContacts(data);
  } catch (error) {
    if (error.response?.status === 401) {
      // token expired — redirect to login
      router.push('/login');
    } else {
      setError('Failed to load contacts. Please try again.');
    }
  } finally {
    setLoading(false);
  }
};

Q5: How do you detect and fix a memory leak in a React component?

✅ Answer:

"The most common React memory leak is calling setState on an unmounted component — for example, inside a useEffect that makes an API call, but the user navigates away before it resolves."

// ❌ Memory leak
useEffect(() => {
  fetchData().then(data => setData(data)); // component may be gone
}, []);

// ✅ Fix — AbortController
useEffect(() => {
  const controller = new AbortController();
  
  fetchData({ signal: controller.signal })
    .then(data => setData(data))
    .catch(err => {
      if (err.name !== 'AbortError') setError(err);
    });

  return () => controller.abort(); // cleanup
}, []);

"In DevTools, I detect this via the Memory tab — take heap snapshots before and after navigation, and look for components that remain in the snapshot after they should be gone."


Q6: Array methods — map, filter, reduce with a real dashboard example.

✅ Production example:

// Scenario: Notifyy campaign list — filter active, transform, then count by status
const campaigns = [
  { id: 1, status: 'sent', delivered: 480, total: 500 },
  { id: 2, status: 'failed', delivered: 0, total: 200 },
  { id: 3, status: 'sent', delivered: 990, total: 1000 },
];

// Get delivery rate for sent campaigns only
const deliveryStats = campaigns
  .filter(c => c.status === 'sent')
  .map(c => ({
    ...c,
    deliveryRate: ((c.delivered / c.total) * 100).toFixed(1) + '%'
  }));

// Aggregate total delivered
const totalDelivered = campaigns.reduce((sum, c) => sum + c.delivered, 0);

SECTION 4 — REACT DEEP QUESTIONS


Q1: Explain the Virtual DOM and reconciliation.

✅ Answer:

"React maintains a virtual DOM — a lightweight JavaScript representation of the real DOM. When state changes, React creates a new virtual DOM tree, diffs it against the previous one (this is reconciliation), and only applies the minimal set of real DOM changes needed. The diffing algorithm uses keys to identify list items and avoids re-rendering siblings that haven't changed. This is why keys in lists must be stable and unique — if you use array index as key, React may re-render or mismatch items when the list order changes."

Follow-up: "What triggers a re-render in React?"

"State change, props change, parent re-render (child re-renders even if its own props didn't change — unless wrapped in React.memo), or a context value change."


Q2: useEffect — the dependency array rules and common mistakes.

✅ Answer:

// Mistake 1 — missing dependency (stale closure)
const [count, setCount] = useState(0);
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count); // always logs 0 — stale closure
  }, 1000);
  return () => clearInterval(timer);
}, []); // ❌ count missing from deps

// Fix — use functional update or add count to deps
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1); // ✅ functional update, no stale closure
  }, 1000);
  return () => clearInterval(timer);
}, []);

// Mistake 2 — object/function as dependency (infinite loop)
useEffect(() => {
  fetchData(options); // refetches every render if options is a new object
}, [options]); // ❌ new object reference on every render

// Fix — useMemo or primitive deps
const stableOptions = useMemo(() => ({ page, limit }), [page, limit]);
useEffect(() => {
  fetchData(stableOptions);
}, [stableOptions]); // ✅

Q3: useMemo vs. useCallback — when to use each and when NOT to.

✅ Answer:

"useMemo memoizes a computed value. useCallback memoizes a function reference. Use useMemo when you have an expensive computation that shouldn't re-run on every render. Use useCallback when you pass a function as a prop to a memoized child component — otherwise the child re-renders because it receives a new function reference every time."

// useMemo — expensive filter on 10,000 records
const filteredLeases = useMemo(() => {
  return leases.filter(l => l.status === activeFilter && l.amount > minAmount);
}, [leases, activeFilter, minAmount]);

// useCallback — stable function for memoized child
const handleDelete = useCallback((id) => {
  dispatch(deleteContact(id));
}, [dispatch]);

// ❌ DON'T over-memoize — this is wasteful
const name = useMemo(() => user.firstName + ' ' + user.lastName, [user]);
// Just do: const name = user.firstName + ' ' + user.lastName;

Interviewer trap: "Should you always use useMemo and useCallback?"
Answer: "No. They have overhead — memory and comparison cost. Only use them when you can measure a real performance problem, or when passing stable references to memoized children."


Q4: Context API — when to use it vs. Redux.

✅ Answer:

"Context API is ideal for low-frequency global data — auth state, theme, locale. It's NOT designed for high-frequency updates like a chat inbox where messages arrive every few seconds. Every consumer re-renders when the context value changes, so a busy inbox using context naively would cause massive performance problems. For that, you'd use Redux with selectors that only subscribe to the specific slice you need, or a more modern solution like Zustand."

// ✅ Good Context usage — auth state (rarely changes)
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  return <AuthContext.Provider value={{ user, setUser }}>{children}</AuthContext.Provider>;
};

// ❌ Bad Context usage — real-time messages (updates constantly)
// Use Redux + RTK or a dedicated message store instead

Q5: How do you handle forms in React — controlled vs. uncontrolled?

✅ Answer:

"Controlled components store form state in React state — every keystroke updates state, React is the source of truth. Uncontrolled use refs to access DOM values. For complex forms like Notifyy's campaign wizard, I use react-hook-form — it avoids re-rendering on every keystroke by using uncontrolled inputs internally, while still giving you validation and submission handling."

// react-hook-form — production pattern for multi-step campaign wizard
const { register, handleSubmit, formState: { errors }, watch } = useForm({
  defaultValues: { campaignName: '', audience: 'all', template: '' }
});

const onSubmit = async (data) => {
  try {
    await api.post('/campaigns', data);
    toast.success('Campaign created!');
  } catch (err) {
    toast.error(err.response?.data?.message || 'Error creating campaign');
  }
};

// Input with validation
<input
  {...register('campaignName', { 
    required: 'Campaign name is required',
    minLength: { value: 3, message: 'Minimum 3 characters' }
  })}
/>
{errors.campaignName && <span>{errors.campaignName.message}</span>}

Q6: Error Boundaries — what they are and how to implement.

✅ Answer:

"Error boundaries are class components that catch JavaScript errors in their child component tree during rendering, in lifecycle methods, and in constructors. They don't catch errors in event handlers or async code."

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // Log to your monitoring service (Sentry, etc.)
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <div className="error-state">Something went wrong. <button onClick={() => this.setState({ hasError: false })}>Try again</button></div>;
    }
    return this.props.children;
  }
}

// Usage — wrap major sections
<ErrorBoundary>
  <CampaignDashboard />
</ErrorBoundary>

Q7: Code splitting and lazy loading in React.

✅ Answer:

// Lazy load heavy components — e.g., a campaign analytics chart
const CampaignChart = React.lazy(() => import('./CampaignChart'));

function Dashboard() {
  return (
    <Suspense fallback={<Skeleton height={300} />}>
      <CampaignChart />
    </Suspense>
  );
}

"In Next.js, you also have next/dynamic which gives you more control — you can disable SSR for components that rely on browser APIs."

import dynamic from 'next/dynamic';

const ReactFlowEditor = dynamic(() => import('./WorkflowBuilder'), {
  ssr: false, // doesn't work server-side
  loading: () => <div>Loading workflow editor...</div>
});

Q8: React state batching — React 18 changes.

✅ Answer:

"Before React 18, state updates were batched only inside React event handlers. If you called setState twice inside a setTimeout or a Promise, they'd cause two re-renders. React 18 introduced automatic batching — all state updates, regardless of where they happen, are batched into a single re-render."

// React 18 — both setState calls cause only ONE re-render
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // Only 1 re-render now, not 2
}, 1000);

// If you need to opt out (rare):
import { flushSync } from 'react-dom';
flushSync(() => setCount(c => c + 1)); // forces immediate re-render

SECTION 5 — NEXT.JS QUESTIONS


Q1: SSR vs. SSG vs. ISR — when do you use each?

✅ Answer with Notifyy context:

Strategy When to Use Notifyy Example
SSR (getServerSideProps) Data changes per request, must be fresh, user-specific CRM contact page — each user sees their own data
SSG (getStaticProps) Data rarely changes, same for all users Marketing pages, pricing page
ISR Data changes periodically, not per-request Blog posts, campaign templates gallery
Client-side fetch Real-time, user-interaction driven Inbox messages, live conversation updates
// SSR — user-specific CRM dashboard
export async function getServerSideProps(context) {
  const { req } = context;
  const token = req.cookies.auth_token;
  
  if (!token) {
    return { redirect: { destination: '/login', permanent: false } };
  }
  
  const contacts = await fetchContacts(token);
  return { props: { contacts } };
}

// ISR — campaign template gallery (revalidate every 10 min)
export async function getStaticProps() {
  const templates = await fetchPublicTemplates();
  return { props: { templates }, revalidate: 600 };
}

Q2: Next.js App Router vs. Pages Router — key differences.

✅ Answer:

"The App Router (Next.js 13+) uses React Server Components by default. Components are server-rendered unless you opt in with 'use client'. This means you can fetch data directly in the component — no need for getServerSideProps. It also introduces nested layouts, loading.js, error.js colocated with routes, and streaming with Suspense."

// App Router — server component (no 'use client')
// app/contacts/page.js
async function ContactsPage() {
  const contacts = await fetchContacts(); // runs on server, no useEffect needed
  return <ContactList contacts={contacts} />;
}

// Client component for interactivity
'use client';
function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('');
  return <input onChange={e => onSearch(e.target.value)} />;
}

Q3: What is hydration? What causes hydration errors?

✅ Answer:

"Hydration is the process where React attaches event listeners and state to the static HTML that the server already sent. The server renders HTML, the browser receives it and displays it immediately (fast first paint), then React's JS loads and 'hydrates' the static HTML into a live React app. A hydration error occurs when the server-rendered HTML doesn't match what React expects to render on the client — for example, rendering new Date() on the server produces a different timestamp than on the client, or accessing window on the server."

// ❌ Hydration error — window doesn't exist on server
const width = window.innerWidth; // crashes on server

// ✅ Fix — check environment or use useEffect
const [width, setWidth] = useState(null);
useEffect(() => {
  setWidth(window.innerWidth); // runs client-side only
}, []);

SECTION 6 — SCENARIO-BASED PRODUCTION PROBLEMS


Scenario 1: The CRM contact table is extremely slow when there are 5,000+ contacts.

Root cause: Rendering all rows to the DOM at once causes massive layout, paint, and scripting time.

Debugging approach:

  1. Open Chrome DevTools → Performance tab → record while scrolling
  2. Look for long "Rendering" and "Painting" tasks
  3. Check if React is re-rendering the entire list on every filter change

Fix:

// Option 1 — Server-side pagination (best for data integrity)
const [page, setPage] = useState(1);
const { data } = useQuery(['contacts', page], () => 
  api.get(`/contacts?page=${page}&limit=50`)
);

// Option 2 — Virtualization (for client-side lists)
import { FixedSizeList } from 'react-window';

<FixedSizeList height={600} itemCount={contacts.length} itemSize={60} width="100%">
  {({ index, style }) => (
    <div style={style}>
      <ContactRow contact={contacts[index]} />
    </div>
  )}
</FixedSizeList>

Interview-quality explanation:

"Virtualization only renders the visible rows in the DOM — instead of 5,000 DOM nodes, you have maybe 15. As the user scrolls, old rows are unmounted and new ones are mounted. The perceived experience is smooth and the real DOM stays lean."


Scenario 2: API calls are being made on every keystroke in the search bar.

Root cause: No debounce — onChange fires immediately, triggering API calls at 5-10 per second.

Fix:

// Custom debounce hook
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
}

// Usage
const [searchQuery, setSearchQuery] = useState('');
const debouncedSearch = useDebounce(searchQuery, 400);

useEffect(() => {
  if (debouncedSearch) fetchContacts(debouncedSearch);
}, [debouncedSearch]);

Scenario 3: Infinite re-render loop in a component.

Root cause: A useEffect dependency changes during the effect itself, triggering the effect again.

// ❌ Infinite loop
const [data, setData] = useState([]);
useEffect(() => {
  setData([...data, newItem]); // setData triggers re-render → useEffect runs again
}, [data]); // ❌ data in deps causes loop

// ✅ Fix — functional update, no need for data in deps
useEffect(() => {
  setData(prev => [...prev, newItem]); // reads latest without subscribing
}, [newItem]); // ✅ only runs when newItem changes

Another common cause:

// ❌ Object created inline — new reference every render
useEffect(() => {
  fetchData({ page, limit });
}, [{ page, limit }]); // new object reference = always triggers

// ✅ Fix — primitive deps
useEffect(() => {
  fetchData({ page, limit });
}, [page, limit]); // stable primitives

Scenario 4: Auth token expired — user sees broken API responses.

Root cause: JWT expired, all API calls return 401, but app doesn't handle this gracefully.

Production fix — Axios interceptor:

// axiosInstance.js
import axios from 'axios';

const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL });

// Request interceptor — attach token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth_token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Response interceptor — handle 401
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const original = error.config;
    
    if (error.response?.status === 401 && !original._retry) {
      original._retry = true;
      try {
        const { data } = await axios.post('/auth/refresh', {
          refreshToken: localStorage.getItem('refresh_token')
        });
        localStorage.setItem('auth_token', data.accessToken);
        original.headers.Authorization = `Bearer ${data.accessToken}`;
        return api(original); // retry original request
      } catch {
        // Refresh also failed — force logout
        localStorage.clear();
        window.location.href = '/login';
      }
    }
    return Promise.reject(error);
  }
);

export default api;

Scenario 5: White screen in production — component crashed.

Debugging steps:

  1. Check browser console for the error message
  2. Check network tab — did an API call fail?
  3. Check if a prop is undefined that the component doesn't handle
  4. Look at Sentry/error monitoring for stack trace

Root cause pattern:

// ❌ Crashes if user is null
<div>{user.name}</div>

// ✅ Safe access
<div>{user?.name ?? 'Unknown'}</div>

Prevention:

// Error boundary + fallback UI
<ErrorBoundary fallback={<ErrorScreen />}>
  <Dashboard />
</ErrorBoundary>

Scenario 6: Dashboard table re-renders on every parent state change, even when data is the same.

Root cause: Child component not memoized. Parent re-render causes child to re-render.

Fix:

// ❌ Re-renders every time parent state changes
function Dashboard() {
  const [filter, setFilter] = useState('all');
  return (
    <>
      <FilterBar onChange={setFilter} />
      <ContactTable contacts={contacts} /> {/* re-renders on every filter change */}
    </>
  );
}

// ✅ Memoize when props haven't changed
const ContactTable = React.memo(({ contacts }) => {
  return contacts.map(c => <ContactRow key={c.id} contact={c} />);
});

// Also memoize the contacts array if it's derived
const filteredContacts = useMemo(() => 
  contacts.filter(c => filter === 'all' || c.status === filter),
  [contacts, filter]
);

SECTION 7 — CODING ROUND QUESTIONS


Coding Q1: Build a debounced search input that fetches contacts from an API.

import { useState, useEffect, useCallback } from 'react';

function ContactSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!query.trim()) {
      setResults([]);
      return;
    }

    const timer = setTimeout(async () => {
      setLoading(true);
      setError(null);
      try {
        const res = await fetch(`/api/contacts?search=${query}`);
        const data = await res.json();
        setResults(data);
      } catch {
        setError('Search failed. Please try again.');
      } finally {
        setLoading(false);
      }
    }, 400);

    return () => clearTimeout(timer); // cleanup on next keystroke
  }, [query]);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search contacts..." />
      {loading && <p>Searching...</p>}
      {error && <p className="error">{error}</p>}
      <ul>{results.map(c => <li key={c.id}>{c.name}</li>)}</ul>
    </div>
  );
}

Coding Q2: Implement a reusable paginated data table.

function DataTable({ columns, fetchFn, pageSize = 20 }) {
  const [page, setPage] = useState(1);
  const [data, setData] = useState([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    fetchFn({ page, pageSize })
      .then(({ rows, total }) => {
        setData(rows);
        setTotal(total);
      })
      .finally(() => setLoading(false));
  }, [page, pageSize, fetchFn]);

  const totalPages = Math.ceil(total / pageSize);

  return (
    <div>
      <table>
        <thead>
          <tr>{columns.map(col => <th key={col.key}>{col.title}</th>)}</tr>
        </thead>
        <tbody>
          {loading 
            ? <tr><td colSpan={columns.length}>Loading...</td></tr>
            : data.map((row, i) => (
                <tr key={row.id ?? i}>
                  {columns.map(col => <td key={col.key}>{col.render ? col.render(row) : row[col.key]}</td>)}
                </tr>
              ))
          }
        </tbody>
      </table>
      <div className="pagination">
        <button disabled={page === 1} onClick={() => setPage(p => p - 1)}>Prev</button>
        <span>Page {page} of {totalPages}</span>
        <button disabled={page === totalPages} onClick={() => setPage(p => p + 1)}>Next</button>
      </div>
    </div>
  );
}

Coding Q3: Implement a simple RBAC guard component.

// roles.js
export const ROLES = { ADMIN: 'admin', AGENT: 'agent', OWNER: 'owner' };

// RoleGuard.jsx
function RoleGuard({ allowedRoles, children, fallback = null }) {
  const { user } = useAuth();
  
  if (!user || !allowedRoles.includes(user.role)) {
    return fallback;
  }
  
  return children;
}

// Usage — only admins see the "Delete Campaign" button
<RoleGuard allowedRoles={[ROLES.ADMIN, ROLES.OWNER]} fallback={<span>No permission</span>}>
  <button onClick={deleteCampaign}>Delete Campaign</button>
</RoleGuard>

Coding Q4: Write a JavaScript function to flatten a deeply nested object.

// Input: { a: 1, b: { c: 2, d: { e: 3 } } }
// Output: { a: 1, 'b.c': 2, 'b.d.e': 3 }

function flattenObject(obj, prefix = '') {
  return Object.keys(obj).reduce((acc, key) => {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
      Object.assign(acc, flattenObject(obj[key], fullKey));
    } else {
      acc[fullKey] = obj[key];
    }
    return acc;
  }, {});
}

Coding Q5: Implement a custom hook for API fetching with loading/error states.

function useFetch(url, options = {}) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    if (!url) return;
    
    const controller = new AbortController();
    setState({ data: null, loading: true, error: null });

    fetch(url, { ...options, signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => setState({ data, loading: false, error: null }))
      .catch(err => {
        if (err.name !== 'AbortError') {
          setState({ data: null, loading: false, error: err.message });
        }
      });

    return () => controller.abort();
  }, [url]);

  return state;
}

// Usage
const { data: contacts, loading, error } = useFetch('/api/contacts');

SECTION 8 — RBAC & AUTHENTICATION DEEP QUESTIONS


Q1: How did you implement RBAC in your projects?

✅ Production-level answer:

"In Lease Portfolio 365, we had three roles: Admin, Manager, and Viewer. I implemented it in two layers: route-level and component-level. For routes, I created a higher-order component that checked the user's role from the auth context and redirected unauthorized users to a 403 page. For component-level, I built a <PermissionGate> component — similar to the code I just showed — that takes a required permission prop and conditionally renders its children. Roles and permissions were fetched from the API on login and stored in context. I never stored permissions in localStorage — that's a security risk. The backend always re-validates."

Follow-up: "What if a user manually changes their role in localStorage?"
Answer: "That's exactly why we never rely on client-side data for security. Every API request goes through server-side authorization. The UI hides buttons, but the API rejects unauthorized actions regardless. Defense in depth."


Q2: How does token refresh work? Walk me through the full flow.

✅ Full flow:

"The user logs in and receives an access token (short-lived, 15-60 min) and a refresh token (long-lived, 7-30 days). The access token is sent in every API request header. When it expires, the API returns 401. Our Axios interceptor catches that 401, pauses the original request, calls the refresh endpoint with the refresh token to get a new access token, stores it, then retries the original request with the new token. If the refresh token is also expired (user hasn't used the app in 30 days), we force logout. The refresh token should only be stored in an HTTP-only cookie — never in localStorage — because it's not accessible to JavaScript and is therefore safe from XSS attacks."


SECTION 9 — ANT DESIGN & COMPONENT SYSTEM QUESTIONS


Q1: How do you customize Ant Design components without breaking on upgrades?

✅ Answer:

"I never override Ant Design's internal CSS classes directly — those can change between versions. Instead, I use their theme token system (Ant Design 5) to customize colors, spacing, and typography at the design token level. For component-level customizations, I wrap the component in my own component and expose only the props I need."

// antd v5 theme customization
import { ConfigProvider } from 'antd';

<ConfigProvider
  theme={{
    token: {
      colorPrimary: '#003D8A', // Notifyy brand blue
      borderRadius: 6,
      fontFamily: 'Inter, sans-serif',
    },
    components: {
      Table: { headerBg: '#F0F5FF' },
    }
  }}
>
  <App />
</ConfigProvider>

Q2: How do you build a reusable component system that your team can trust?

✅ Answer:

"Three principles: (1) Single responsibility — each component does one thing. (2) Prop-driven — behavior is controlled by props, not internal state where possible. (3) Documented — every component has a clear prop interface, ideally with TypeScript or PropTypes. I build base components like <Table>, <FormField>, <StatusBadge> that wrap Ant Design and apply our design tokens. Product-specific components compose these — they never reach into Ant Design directly. This means if we switch UI libraries, only the base components change."


SECTION 10 — MOCK INTERVIEW SIMULATION


Interviewer: "Tell me about yourself."
You: (Use the polished answer from Section 2 — HR round)

Interviewer: "You mentioned 10,000+ records optimization. What exactly was the problem and how did you diagnose it?"
You: "The initial symptom was that the table took 4-6 seconds to become interactive after data loaded. I opened Chrome DevTools Performance tab and recorded a page load. I saw a 'Long Task' of over 2 seconds in the scripting phase — React was mounting thousands of DOM nodes. I then used React DevTools Profiler to see which components were rendering and how long each took. The table component was rendering all rows even those outside the viewport. The fix was react-window for virtualization, combined with moving the data fetch to SSR so the first meaningful paint was instant."

Interviewer (pressure): "Why didn't you use pagination instead of virtualization?"
You: "We actually used both. Server-side pagination for the initial data — we fetch 500 records at a time, not all 10k. Within that 500, react-window handles virtualization so only visible rows render. Pagination alone would have required a network request on every page turn, which felt slow for financial analysts who scan data quickly. The combination gave the best of both."

Interviewer: "Show me how you'd handle a form where submitting should be disabled while a previous submission is in flight."

const [submitting, setSubmitting] = useState(false);

const handleSubmit = async (data) => {
  if (submitting) return; // guard against double-submit
  setSubmitting(true);
  try {
    await api.post('/campaigns', data);
    toast.success('Campaign launched!');
    router.push('/campaigns');
  } catch (err) {
    toast.error(err.response?.data?.message || 'Failed to create campaign');
  } finally {
    setSubmitting(false); // always re-enable, even on error
  }
};

<button type="submit" disabled={submitting}>
  {submitting ? 'Launching...' : 'Launch Campaign'}
</button>

Interviewer: "What happens if the user refreshes while submitting?"
You: "The request in flight is abandoned — the browser cancels it. The record may or may not be created on the backend depending on timing. The solution is idempotency keys — send a unique request ID with each submission. The backend uses it to detect duplicate submissions and returns the same response for repeated requests with the same key."

Interviewer: "What's your understanding of Next.js middleware?"
You: "Next.js middleware runs before a request is processed — at the edge, before the server-side functions. You can use it to do auth checks, redirects, and request rewrites without going all the way to the server. For Notifyy's use case, middleware would be ideal for checking if a user has a valid session cookie on every request to /app/* routes — and redirecting to /login if not — without the redirect happening in each page's getServerSideProps."


SECTION 11 — TOP 50 HIGHEST PROBABILITY QUESTIONS


React (Top 20)

  1. What is the difference between state and props?
  2. Explain useEffect cleanup and when you need it.
  3. When would you use useRef instead of useState?
  4. What is React.memo and when does it not help?
  5. Explain the difference between useCallback and useMemo.
  6. What happens when you call setState inside a render?
  7. How does React handle keys in lists? What happens with wrong keys?
  8. What is the difference between controlled and uncontrolled inputs?
  9. How do you share state between sibling components?
  10. What is prop drilling and how do you solve it?
  11. Explain how Context API re-renders work.
  12. What is React.Suspense and how does it work?
  13. How do you implement code splitting in a Next.js app?
  14. Explain the difference between rendering and committing in React.
  15. What is a custom hook and when should you create one?
  16. How do you prevent a child component from re-rendering unnecessarily?
  17. What are the rules of hooks?
  18. How do you handle async operations in useEffect?
  19. What is the difference between mount, update, and unmount phases?
  20. How do you test a React component? (Even if basic — mention Jest + React Testing Library)

JavaScript (Top 15)

  1. What is the difference between == and ===?
  2. Explain this keyword in different contexts.
  3. What is the difference between null and undefined?
  4. What are Promises? How is async/await different?
  5. Explain event bubbling and event delegation.
  6. What is the difference between let, const, and var?
  7. How does destructuring work? Rest/spread operators?
  8. What is a higher-order function? Give an example.
  9. How does Array.prototype.reduce work?
  10. What is memoization in plain JavaScript?
  11. What is the difference between shallow copy and deep copy?
  12. Explain prototype chain.
  13. What is an IIFE and when would you use it?
  14. How does module system work (ES modules vs CommonJS)?
  15. What is tree shaking?

Next.js & Performance (Top 10)

  1. What is the difference between getStaticProps and getServerSideProps?
  2. What is ISR and when do you use it?
  3. How do you optimize images in Next.js?
  4. What is the _app.js file used for?
  5. How does routing work in Next.js App Router?
  6. What are Server Components vs. Client Components?
  7. How do you handle environment variables in Next.js?
  8. What is next/dynamic and when would you use it?
  9. How does Next.js handle CSS and styles?
  10. What causes hydration errors?

Architecture & Production (Top 5)

  1. How do you structure a large React application?
  2. How do you handle global error states in an app?
  3. How do you handle API versioning on the frontend?
  4. How do you approach performance optimization systematically?
  5. How do you handle WebSocket or real-time data in React?

SECTION 12 — TOP 20 MUST-STUDY CONCEPTS

# Concept Why Notifyy Will Ask
1 useEffect dependency rules + cleanup Real-time inbox polling
2 useMemo for derived state CRM contact filtering
3 useCallback for stable event handlers Table with memoized rows
4 React.memo — when it helps and when it doesn't Agent list components
5 Axios interceptors (request + response) Auth token refresh
6 JWT auth flow (access + refresh tokens) Notifyy has login + OAuth
7 React Context — proper usage and limits Auth context
8 Controlled forms + react-hook-form Campaign wizard
9 Debounce pattern CRM search
10 Virtualization (react-window) Contact/message lists
11 Error boundaries Inbox crash recovery
12 Code splitting + React.lazy Heavy chart components
13 SSR in Next.js Data-sensitive pages
14 Hydration errors + fixes Next.js app base
15 Redux Toolkit (createSlice, createAsyncThunk) Global campaign state
16 RBAC pattern (component + route level) Agent vs admin views
17 Optimistic UI updates Message send feel
18 AbortController in useEffect Cleanup on navigation
19 React 18 automatic batching Multiple state updates
20 next/dynamic + ssr:false Browser-only components

SECTION 13 — CONFIDENCE-BUILDING TIPS FOR NOTIFYY

  1. Frame everything around their product. When asked about forms, say "like the campaign wizard." When asked about tables, say "like a CRM contact list." This signals you understood what they build.

  2. Own your "basic" qualifications. You have "basic Jest" and "basic CI/CD." Don't say "I don't know much about testing." Say: "I've written component tests using Jest. I'm actively leveling up in RTL and would prioritize that at Notifyy."

  3. Your dashboard experience is rare at 2 years. Most 2-year developers haven't touched 10k-record data optimization. Lead with this story in technical rounds — it's a strong differentiator.

  4. The AI tools angle is a real advantage. Startup founders love this. If asked "how do you work faster," mention Copilot for scaffold generation and ChatGPT for debugging unfamiliar patterns — with the caveat that you always review and understand the output.

  5. Prepare one question for them: "What's the most complex frontend challenge your team is currently working on?" This signals technical curiosity and helps you understand the role's depth.


End of Handbook — Gopinath C | Notifyy Interview Preparation | React.js + Next.js | May 2025