· 7 min read

Unconventional React Tricks: Beyond the Basics

A practical guide to unconventional but practical React techniques - from useRef hacks and portals to transitions, Suspense patterns and off-main-thread work - with when-to-use guidance, pitfalls and examples.

Introduction

React gives you a well-defined toolbox: components, props, state and hooks. But once you know the basics, real productivity (and delight) comes from small, unconventional tricks that simplify code, improve perceived performance, or unlock patterns that would otherwise feel clumsy.

This post walks through a set of such tricks - practical, battle-tested, and explained with when-to-use guidance and caveats.

Why “unconventional” matters

Most teams reach for useState/useEffect/useMemo and good linting. But unconventional approaches are about solving common problems in new ways: reducing re-renders, shaping UI responsiveness, enabling rich interactions, or keeping code maintainable as complexity grows.

Core tricks (and how to apply them)

  1. useRef as a mutable instance (beyond DOM refs)

Problem: you need to store a value across renders but updating it shouldn’t rerender the component (e.g., timers, last-known-pointer position, cancellable tokens).

Trick: store mutable values in a ref and treat it as instance storage.

Example:

function useInterval(callback, delay) {
  const savedCb = React.useRef();
  React.useEffect(() => {
    savedCb.current = callback;
  }, [callback]);
  React.useEffect(() => {
    if (delay == null) return;
    const id = setInterval(() => savedCb.current?.(), delay);
    return () => clearInterval(id);
  }, [delay]);
}

Why it helps: you avoid binding callbacks in intervals or cancelling/recreating intervals on every callback change.

Caveat: refs are mutable and bypass React’s render model - use them when you explicitly want no render side effect.

  1. Expose imperative APIs with forwardRef + useImperativeHandle

Problem: consumers need to trigger ephemeral behavior (e.g., start/stop) on a child component without lifting every bit of state up.

Trick: use forwardRef + useImperativeHandle to offer a small imperative surface.

Example:

const Timer = React.forwardRef(function Timer(_, ref) {
  const idRef = React.useRef();
  React.useImperativeHandle(ref, () => ({
    start: () => {
      /* start logic */
    },
    stop: () => {
      /* stop */
    },
  }));
  return <div>Timer</div>;
});

// usage:
const ref = React.useRef();
ref.current?.start();

When to use: small, well-documented imperative APIs (modals, canvas controls, animation timelines). Avoid turning everything into imperative code.

  1. Portals for isolated UIs (modals, tooltips, traps)

Problem: modals or overlays need to break out of parent stacking context/overflow rules, but you want to keep logical ownership in React tree.

Trick: render into a DOM node outside the parent using React portals.

Example:

function Modal({ children }) {
  const el = React.useMemo(() => document.createElement('div'), []);
  React.useEffect(() => {
    document.body.appendChild(el);
    return () => document.body.removeChild(el);
  }, [el]);
  return ReactDOM.createPortal(children, el);
}

Why it helps: you keep component encapsulation and still ensure proper stacking and isolation.

  1. useTransition + useDeferredValue for perceived performance

Problem: expensive renders or large lists make typing or UI interactions feel janky.

Trick: mark low-priority updates as transitions or defer heavy parts of the UI so interactive parts stay snappy. See useTransition/useDeferredValue in the official docs.

Example:

const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(
  () => expensiveFilter(data, deferredQuery),
  [data, deferredQuery]
);

Or use useTransition to keep a pending state for heavy updates and show fallbacks.

When to use: large lists, complex tree updates, or when filtering/sorting blocks the UI.

Caveat: these APIs are about perceived performance - they don’t make work cheaper, they reorder priority. Test across devices.

  1. Render-as-you-fetch (Suspense-friendly data fetching)

Problem: coordinating loading states across components can produce lots of “isLoading” booleans.

Trick: adopt a “render-as-you-fetch” approach where each component suspends until it has data using a small resource wrapper (an established pattern in community examples and the React Suspense docs). See the Suspense conceptual docs: React Suspense.

Simplified pattern:

function wrapPromise(promise) {
  let status = 'pending';
  let result;
  const suspender = promise.then(
    r => {
      status = 'success';
      result = r;
    },
    e => {
      status = 'error';
      result = e;
    }
  );
  return {
    read() {
      if (status === 'pending') throw suspender;
      if (status === 'error') throw result;
      return result;
    },
  };
}

Then a component doing const data = resource.read() will suspend until data is ready.

When to use: complex UIs that can benefit from coordinated loading spinners and progressive hydration. Caveat: some Suspense uses are still experimental; check compatibility.

  1. Avoid layout thrash with useLayoutEffect + FLIP

Problem: measure-then-change cycles cause layout thrashing and flicker.

Trick: use useLayoutEffect to measure DOM before the browser paints and apply transforms that animate layout changes (FLIP: First, Last, Invert, Play).

Example sketch:

useLayoutEffect(() => {
  const rectBefore = elRef.current.getBoundingClientRect();
  // ... update layout (set state)
  const rectAfter = elRef.current.getBoundingClientRect();
  const dx = rectBefore.left - rectAfter.left;
  elRef.current.style.transform = `translateX(${dx}px)`;
  requestAnimationFrame(() => {
    elRef.current.style.transition = 'transform 300ms';
    elRef.current.style.transform = '';
  });
}, [deps]);

When to use: animating list reorders, smooth insert/remove animations.

  1. Offload non-urgent work with requestIdleCallback or setTimeout fallbacks

Problem: heavy non-UI tasks (indexing, preprocessing) block the main thread and drag interactions.

Trick: schedule these tasks with requestIdleCallback (with a setTimeout fallback for broader browser support) so they run when the main thread is idle.

Example:

function scheduleWork(cb) {
  if ('requestIdleCallback' in window) return requestIdleCallback(cb);
  return setTimeout(cb, 50);
}

scheduleWork(() => {
  /* expensive indexing */
});

Reference: MDN docs for requestIdleCallback.

When to use: background indexing, analytics batching, large non-urgent computations. Do not use for user-critical updates.

  1. Stable object references without overusing useMemo/useCallback

Problem: passing inline objects or functions breaks memoization in children and causes re-renders.

Trick A: store stable values in a ref and mutate them when needed.

const handlersRef = useRef({});
handlersRef.current.onClick = useCallback(
  () => {
    /* uses latest state */
  },
  [
    /* maybe nothing */
  ]
);
// pass handlersRef.current to children - stable reference.

Trick B: memoize whole props objects once and update inner values via refs or state specifically tracked for changes.

When to use: performance-sensitive wrappers (list item renderers, pure components).

Caveat: avoid premature optimization - prefer clear code unless you’ve identified a bottleneck.

  1. Compound components with context + useId for accessible patterns

Problem: building accessible component primitives (Tabs, Selects) while keeping a developer-friendly API.

Trick: combine context to share internal state and useId to generate stable, SSR-friendly ids for ARIA attributes.

Pattern sketch:

const TabsContext = createContext();
function Tabs({ children }) {
  const id = useId();
  const [active, setActive] = useState(0);
  return (
    <TabsContext.Provider value={{ id, active, setActive }}>
      {children}
    </TabsContext.Provider>
  );
}

When to use: building component libraries and complex forms.

  1. Micro-optimizations: custom comparators in React.memo

Problem: React.memo shallowly compares props. Small differences in nested objects cause wasted re-renders.

Trick: pass a custom comparator to React.memo for tight control.

export default React.memo(
  MyComp,
  (prev, next) => prev.user.id === next.user.id && prev.count === next.count
);

Caveat: custom comparators add maintenance overhead - use them where profiling shows gains.

Practical patterns and toy toolbelt

  • Lazy-load heavy components with React.lazy + Suspense across route boundaries.
  • Use libraries for virtualization (react-window) when lists are large.
  • Combine IntersectionObserver (see MDN: Intersection Observer API) with deferred loading to load images or items when they approach the viewport.
  • Keep accessibility in mind: modals rendered in portals still need focus management and aria attributes.

When to avoid these tricks

  • Premature optimization: don’t add complexity until you have measurable problems.
  • Avoid mixing imperative and declarative styles without strong reason - it increases cognitive load.
  • For tiny apps, the cost of micro-optimizations often outweighs benefits.

Testing and observability

  • Profile with the React DevTools Profiler and browser performance tools to find bottlenecks before applying advanced techniques.
  • Use synthetic events and deterministic rendering in tests; for suspend-based data fetching, provide mocks or loaders in test harnesses.

References and further reading

Closing: a developer mindset

These tricks are tools - not silver bullets. The right time to apply them is when they solve a noticeable problem (sluggish UI, fragile components, awkward APIs). Start with profiling, adopt one idea at a time, and document the reasoning for future maintainers. When used judiciously, unconventional techniques let your apps feel faster and your code feel cleaner.

Enjoy experimenting - and remember: clarity + measurement beats cleverness every time.

Back to Blog

Related Posts

View All Posts »

React and State Management: Expert Tricks for Efficiency

A strategic, in-depth guide to state management patterns and advanced tricks in React. Covers local vs global state, server-state libraries, normalization, selectors, immutability helpers, concurrency techniques, and practical code patterns to reduce re-renders and simplify data flow.

Using React with TypeScript: Tips and Tricks

Practical, example-driven guide to using TypeScript with React. Covers component typing, hooks, refs, generics, polymorphic components, utility types, and tooling tips to make your React code safer and more maintainable.

Integrating Third-Party Libraries: Effective Tricks in React

Practical guidelines and tricks for integrating third‑party libraries into React apps safely and efficiently - covering dynamic imports, SSR guards, wrapper components, performance tuning, bundler strategies, TypeScript typings, testing, and security.