Saltar al contenido
home / blog / react-hooks-patrones
React Hooks JavaScript

Advanced Patterns with React Hooks

2026-04-16 · 11 min read
Advanced Patterns with React Hooks

Custom hooks, hook composition, and patterns like useReducer with Context to manage complex state without Redux.

Custom Hooks: Extracting Reusable Logic

The golden rule: if you have stateful logic that appears in more than one component, it’s a candidate for a custom hook.

useFetch — the classic

function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(url, { signal: controller.signal })
      .then(r => r.json())
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') setError(err);
      })
      .finally(() => setLoading(false));

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

  return { data, loading, error };
}

// Usage:
function UserProfile({ id }: { id: string }) {
  const { data, loading, error } = useFetch<User>(`/api/users/${id}`);
  if (loading) return <Spinner />;
  if (error) return <Error />;
  return <Profile user={data!} />;
}

useLocalStorage

function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initial;
    } catch {
      return initial;
    }
  });

  const set = useCallback((val: T | ((prev: T) => T)) => {
    setValue(prev => {
      const next = typeof val === 'function' ? (val as (p: T) => T)(prev) : val;
      localStorage.setItem(key, JSON.stringify(next));
      return next;
    });
  }, [key]);

  return [value, set] as const;
}

useReducer + Context: Global State Without Redux

For moderately complex global state, useReducer with Context is enough and doesn’t require external dependencies.

// store/theme.tsx
type Theme = 'dark' | 'light';
type Action = { type: 'TOGGLE_THEME' } | { type: 'SET_THEME'; payload: Theme };

function reducer(state: Theme, action: Action): Theme {
  switch (action.type) {
    case 'TOGGLE_THEME': return state === 'dark' ? 'light' : 'dark';
    case 'SET_THEME': return action.payload;
    default: return state;
  }
}

const ThemeContext = createContext<{
  theme: Theme;
  dispatch: Dispatch<Action>;
} | null>(null);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, dispatch] = useReducer(reducer, 'dark');
  return (
    <ThemeContext.Provider value={{ theme, dispatch }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be inside ThemeProvider');
  return ctx;
}

Hook Composition

Hooks compose naturally — you can build complex hooks from simple ones:

function useUserData(userId: string) {
  const { data: user, loading: userLoading } = useFetch<User>(`/api/users/${userId}`);
  const { data: posts, loading: postsLoading } = useFetch<Post[]>(`/api/users/${userId}/posts`);

  return {
    user,
    posts,
    loading: userLoading || postsLoading,
  };
}

useCallback and useMemo — When to Use Them

The rule: use them when the cost of recomputing is greater than the cost of comparing.

// ✅ Use useMemo — expensive calculation
const sortedItems = useMemo(
  () => items.sort((a, b) => b.score - a.score),
  [items]
);

// ✅ Use useCallback — function passed to memoized component
const handleSubmit = useCallback(
  (data: FormData) => {
    mutate(data);
  },
  [mutate]
);

// ❌ Not needed — calculation is trivial
const doubled = useMemo(() => count * 2, [count]);

useRef Beyond DOM Refs

useRef stores mutable values that should not trigger re-renders:

function useInterval(callback: () => void, delay: number) {
  const savedCallback = useRef(callback);

  // Update the ref without re-render
  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    const id = setInterval(() => savedCallback.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}

Conclusion

Hooks change how you think in React: instead of “which lifecycle method do I use here”, you think “what effect does this value have”. Composition is the real superpower — small, focused hooks that combine into complex behaviors.

The useReducer + Context pattern covers 80% of cases where you would have previously reached for Redux. Start simple and add complexity only when you need it.