DEV Community

Sabber Hossain
Sabber Hossain

Posted on

Level Up Your React Skills: 10 Advanced Techniques for Senior Devs

As React applications grow more complex, the patterns that were “just fine” when you were starting out might start to feel limiting. Maybe you’ve built a successful MVP, but now you’re noticing subtle performance issues. Or perhaps your state management has gotten tangled, and your data fetching logic has mushroomed into something unrecognisable.

1. Use useCallback with a Persistent Service Reference
We often see useCallback used to memoize inline arrow functions in event handlers. We do this to make sure that the function reference remains stable and does not trigger unnecessary re-renders when passed as a prop.

But as you level up, you’ll find you can use it to maintain stable references to more complex services—like WebSockets, workers, or other persistent resources—so they aren’t re-created unnecessarily on each render.

This approach builds on the basics of useRef and useCallback: you’re ensuring a long-lived service connection remains stable. This can save performance overhead and avoid unintended reconnections.

Example:

function createExpensiveService() {
  // Pretend this sets up a WebSocket or a shared worker
  return { send: (msg) => console.log('Sending:', msg) };
}

function usePersistentService() {
  const serviceRef = React.useRef(createExpensiveService());
  // Memoize the send function so it never changes
  const stableSend = React.useCallback((msg) => {
    serviceRef.current.send(msg);
  }, []);
  return stableSend;
}
function MyComponent() {
  const send = usePersistentService();
  return <button onClick={() => send('HELLO')}>Send Message</button>;
}
Enter fullscreen mode Exit fullscreen mode

2. Using a Ref for Simplicity Instead of State
Sometimes we make the mistake to put all changing data in a state. But sometimes, you just need a value that won’t trigger a re-render. In these cases, using a ref is actually simpler and more efficient.

For example, imagine a counter that you just need to read and update internally without affecting the UI. A ref is perfect. No need for useState or fancy renders—just a stable box to store a changing value.

Example:

function MyCounter() {
  const counterRef = React.useRef(0);
  const increment = () => {
    counterRef.current++;
    console.log('Ref count is now:', counterRef.current);
  };
  return <button onClick={increment}>Increment (Check Console)</button>;
}
Enter fullscreen mode Exit fullscreen mode

3. Using Suspense for Data Fetching with a Global Resource Cache
In many React apps, fetching data involves useEffect, loading states, and a bunch of manual checks. Suspense can simplify all of this by allowing components to “read” from a special data resource. If the data isn’t ready, the component automatically suspends, and React shows a fallback UI until the data arrives. This approach centralises loading logic, making your components cleaner and more focused on rendering.

Example (Conceptual):

function createResource(fetchFn) {
  let status = 'pending';
  let result;
  const promise = fetchFn().then(
    data => { status = 'success'; result = data; },
    err => { status = 'error'; result = err; }
  );
  return {
    read() {
      if (status === 'pending') throw promise;
      if (status === 'error') throw result;
      return result;
    }
  };
}

const userResource = createResource(() => fetch('/api/user').then(r => r.json()));

function UserProfile() {
  const user = userResource.read();
  return <div>Hello, {user.name}!</div>;
}

function App() {
  return (
    <React.Suspense fallback={<div>Loading user data...</div>}>
      <UserProfile />
    </React.Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Suspense with Dynamic Imports and Preload Hints
Code splitting is common with React.lazy(), but you can take it further by preloading code before the user needs it. This reduces the wait time when they finally click a button or navigate to a certain route. Start loading your heavy components in the background so that they are instantly ready when needed.

Example:

const HeavyChart = React.lazy(() => import('./HeavyChart'));

function usePreloadHeavyChart() {
  React.useEffect(() => {
    import('./HeavyChart'); // start preloading on mount
  }, []);
}

function Dashboard() {
  usePreloadHeavyChart();
  return (
    <React.Suspense fallback={<div>Loading Chart...</div>}>
      <HeavyChart />
    </React.Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

5. Error Boundaries that Retry Automatically
At some point, something in your app may fail — maybe a network request or a lazy-loaded component. Traditional error boundaries show a fallback UI and stop there. By enhancing them, you can try recovering automatically, retrying after a short delay. This can be a great user experience improvement, turning a temporary glitch into a seamless recovery.

Example:

class AutoRetryErrorBoundary extends React.Component {
  state = { hasError: false, attempt: 0 };
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  componentDidUpdate() {
    if (this.state.hasError) {
      setTimeout(() => {
        this.setState(s => ({ hasError: false, attempt: s.attempt + 1 }));
      }, 2000);
    }
  }
  render() {
    if (this.state.hasError) return <div>Retrying...</div>;
    return this.props.children(this.state.attempt);
  }
}

function UnstableComponent({ attempt }) {
  if (attempt < 2) throw new Error('Simulated Crash!');
  return <div>Loaded on attempt {attempt}</div>;
}

// Usage
<AutoRetryErrorBoundary>
  {(attempt) => <UnstableComponent attempt={attempt} />}
</AutoRetryErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

6. Virtualising Lists with Dynamic Item Heights
When you have huge lists, rendering every item can kill performance. Virtualisation libraries (like react-window) render only what’s visible. But what if items have unpredictable heights? You can measure them dynamically and feed those measurements back into your virtualisation logic. This reduces both memory usage and rendering time, keeping scrolling silky smooth.

Example:

import { VariableSizeList as List } from 'react-window';

function useDynamicMeasurement(items) {
  const sizeMap = React.useRef({});
  const refCallback = index => el => {
    if (el) {
      const height = el.getBoundingClientRect().height;
      sizeMap.current[index] = height;
    }
  };
  const getSize = index => sizeMap.current[index] || 50;
  return { refCallback, getSize };
}

function DynamicHeightList({ items }) {
  const { refCallback, getSize } = useDynamicMeasurement(items);
  return (
    <List height={400} itemCount={items.length} itemSize={getSize} width={300}>
      {({ index, style }) => (
        <div style={style} ref={refCallback(index)}>
          {items[index]}
        </div>
      )}
    </List>
  );
}
Enter fullscreen mode Exit fullscreen mode

7. Using a State Machine for Complex UI Flows
When your component starts to feel like a spaghetti mess of if statements, a state machine can help. Tools like XState integrate nicely with React hooks. Instead of juggling multiple booleans, you define states and transitions in a single, clean chart. It’s a mental model shift: you’re writing a “map” of how your UI flows, making it easier to understand and debug.

Example:

import { useMachine } from '@xstate/react';
import { createMachine } from 'xstate';

const formMachine = createMachine({
  initial: 'editing',
  states: {
    editing: { on: { SUBMIT: 'validating' } },
    validating: {
      invoke: {
        src: 'validateForm',
        onDone: 'success',
        onError: 'error'
      }
    },
    success: {},
    error: {}
  }
});

function Form() {
  const [state, send] = useMachine(formMachine, {
    services: { validateForm: async () => {/* validation logic */} }
  });
  return (
    <button onClick={() => send('SUBMIT')}>
      {state.value === 'editing' ? 'Submit' : state.value.toString()}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

8. Controlled Concurrency with useTransition and Task Queues
React 18 introduced useTransition to help you mark certain state updates as “non-urgent.” This can be a game-changer for performance under heavy load. Imagine fetching large amounts of data or performing expensive calculations. By deferring non-urgent updates, you keep the UI responsive and avoid locking up the main thread.

Example:

function ComplexUI() {
  const [isPending, startTransition] = React.useTransition();
  const [data, setData] = React.useState([]);

  function loadMore() {
    startTransition(() => {
      setData(old => [...old, ...generateMoreData()]);
    });
  }
  return (
    <>
      <button onClick={loadMore}>Load More</button>
      {isPending && <span>Loading more data...</span>}
      <List data={data} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

9. useImperativeHandle to Create Controlled Component APIs
Sometimes you need a parent component to directly control a child — like calling childRef.current.focus() on a custom input. useImperativeHandle is a hook that lets you define what a parent sees when it uses ref on a child component. This is perfect for creating neat, controlled component APIs that feel like calling a method on a class instance, but in a React-friendly way.

Example:

const FancyInput = React.forwardRef((props, ref) => {
  const inputRef = React.useRef();
  React.useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    getValue: () => inputRef.current.value
  }));
  return <input ref={inputRef} {...props} />;
});

function Parent() {
  const fancyRef = React.useRef();
  return (
    <>
      <FancyInput ref={fancyRef} />
      <button onClick={() => fancyRef.current.focus()}>Focus Input</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

10. Progressive Hydration with a Custom Hook
Server-side rendering (SSR) can get your content to the user fast, but hydrating a huge app all at once can slow interactivity. By delaying hydration for non-critical parts of your page, you can keep the initial load snappy. A custom hook that gradually hydrates certain components after a delay can make SSR feel even more seamless.

Example:

function useProgressiveHydration(delay = 1000) {
  const [hydrated, setHydrated] = React.useState(false);
  React.useEffect(() => {
    const t = setTimeout(() => setHydrated(true), delay);
    return () => clearTimeout(t);
  }, [delay]);
  return hydrated;
}

function HeavyComponent() {
  const hydrated = useProgressiveHydration();
  return hydrated ? <ExpensiveTree /> : <Placeholder />;
}
Enter fullscreen mode Exit fullscreen mode

Final Thoughts
These techniques aren’t meant to be used all at once, and certainly not in every codebase. They’re tools — advanced ones — that become helpful as your applications and your skills grow. Consider these approaches when you start running into the limitations of simpler patterns.

Top comments (0)