Skip to main content

useMounted

The useMounted hook tracks whether a component is currently mounted to the DOM. It returns false initially and on unmount, and true when the component is mounted. This is useful for preventing state updates on unmounted components.

Installation

npm install @craft-ui/hooks

Import

import { useMounted } from "@craft-ui/hooks";

Usage

import { useMounted } from "@craft-ui/hooks";
import { useState, useEffect } from "react";

function DataFetcher() {
  const mounted = useMounted();
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("/api/data")
      .then(res => res.json())
      .then(result => {
        // Only update state if component is still mounted
        if (mounted) {
          setData(result);
        }
      });
  }, [mounted]);

  return <div>{data ? JSON.stringify(data) : "Loading..."}</div>;
}

Returns

mounted
boolean
A boolean value that is false initially and on unmount, and true when the component is mounted.

Type Definition

function useMounted(): boolean;

Examples

Preventing Memory Leaks

import { useMounted } from "@craft-ui/hooks";
import { useState, useEffect } from "react";

function AsyncComponent() {
  const mounted = useMounted();
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("/api/data");
        const result = await response.json();
        
        if (mounted) {
          setData(result);
          setLoading(false);
        }
      } catch (error) {
        if (mounted) {
          console.error(error);
          setLoading(false);
        }
      }
    };

    fetchData();
  }, [mounted]);

  if (loading) return <div>Loading...</div>;
  return <div>{JSON.stringify(data)}</div>;
}

Delayed Actions

import { useMounted } from "@craft-ui/hooks";
import { useState, useEffect } from "react";

function NotificationBanner({ message, duration = 3000 }) {
  const mounted = useMounted();
  const [visible, setVisible] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      if (mounted) {
        setVisible(false);
      }
    }, duration);

    return () => clearTimeout(timer);
  }, [mounted, duration]);

  if (!visible) return null;

  return (
    <div className="notification">
      {message}
    </div>
  );
}

Animation Delays

import { useMounted } from "@craft-ui/hooks";
import { useState, useEffect } from "react";

function FadeInComponent({ children }) {
  const mounted = useMounted();
  const [show, setShow] = useState(false);

  useEffect(() => {
    if (mounted) {
      // Delay to trigger CSS transition
      const timer = setTimeout(() => setShow(true), 10);
      return () => clearTimeout(timer);
    }
  }, [mounted]);

  return (
    <div
      style={{
        opacity: show ? 1 : 0,
        transition: "opacity 0.3s ease-in",
      }}
    >
      {children}
    </div>
  );
}

Conditional Event Listeners

import { useMounted } from "@craft-ui/hooks";
import { useState, useEffect } from "react";

function KeyboardListener() {
  const mounted = useMounted();
  const [key, setKey] = useState("");

  useEffect(() => {
    const handleKeyPress = (e: KeyboardEvent) => {
      if (mounted) {
        setKey(e.key);
      }
    };

    if (mounted) {
      window.addEventListener("keypress", handleKeyPress);
    }

    return () => {
      window.removeEventListener("keypress", handleKeyPress);
    };
  }, [mounted]);

  return <div>Last key pressed: {key}</div>;
}

WebSocket Connection

import { useMounted } from "@craft-ui/hooks";
import { useState, useEffect } from "react";

function WebSocketComponent() {
  const mounted = useMounted();
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    const ws = new WebSocket("ws://localhost:8080");

    ws.onmessage = (event) => {
      if (mounted) {
        setMessages(prev => [...prev, event.data]);
      }
    };

    return () => {
      ws.close();
    };
  }, [mounted]);

  return (
    <ul>
      {messages.map((msg, i) => (
        <li key={i}>{msg}</li>
      ))}
    </ul>
  );
}

Polling Data

import { useMounted } from "@craft-ui/hooks";
import { useState, useEffect } from "react";

function PollingComponent() {
  const mounted = useMounted();
  const [data, setData] = useState(null);

  useEffect(() => {
    const pollData = async () => {
      const response = await fetch("/api/status");
      const result = await response.json();
      
      if (mounted) {
        setData(result);
      }
    };

    if (mounted) {
      pollData();
      const interval = setInterval(pollData, 5000);
      
      return () => clearInterval(interval);
    }
  }, [mounted]);

  return <div>{data ? JSON.stringify(data) : "Loading..."}</div>;
}

Form Submission

import { useMounted } from "@craft-ui/hooks";
import { useState } from "react";

function FormComponent() {
  const mounted = useMounted();
  const [submitting, setSubmitting] = useState(false);
  const [success, setSuccess] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitting(true);

    try {
      await fetch("/api/submit", { method: "POST" });
      
      if (mounted) {
        setSuccess(true);
        setSubmitting(false);
      }
    } catch (error) {
      if (mounted) {
        setSubmitting(false);
      }
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={submitting}>
        {submitting ? "Submitting..." : "Submit"}
      </button>
      {success && <p>Success!</p>}
    </form>
  );
}

Common Patterns

Safe State Updates

Wrap state updates in mounted checks:
const mounted = useMounted();

useEffect(() => {
  fetchData().then(result => {
    if (mounted) {
      setState(result);
    }
  });
}, [mounted]);

Cleanup Pattern

Use with cleanup functions:
const mounted = useMounted();

useEffect(() => {
  const subscription = subscribe(data => {
    if (mounted) {
      handleData(data);
    }
  });

  return () => subscription.unsubscribe();
}, [mounted]);

Custom Async Hook

import { useMounted } from "@craft-ui/hooks";
import { useState, useEffect } from "react";

function useAsync<T>(asyncFn: () => Promise<T>) {
  const mounted = useMounted();
  const [state, setState] = useState<{
    loading: boolean;
    data: T | null;
    error: Error | null;
  }>({ loading: true, data: null, error: null });

  useEffect(() => {
    asyncFn()
      .then(data => {
        if (mounted) {
          setState({ loading: false, data, error: null });
        }
      })
      .catch(error => {
        if (mounted) {
          setState({ loading: false, data: null, error });
        }
      });
  }, [mounted]);

  return state;
}

Use Cases

  • Preventing state updates after component unmounts
  • Avoiding “Can’t perform a React state update on an unmounted component” warnings
  • Safely handling async operations (fetch, setTimeout, setInterval)
  • Managing WebSocket connections
  • Controlling animations and transitions
  • Conditional event listener attachment
  • Polling and real-time data updates

Notes

  • The hook returns false on initial render and after unmount
  • The hook returns true after the component mounts (after useEffect runs)
  • This pattern helps prevent memory leaks and React warnings
  • Always check the mounted value before calling state setters in async callbacks
  • The cleanup function automatically sets mounted to false
  • This is particularly useful for components that fetch data or use timers
  • Consider using AbortController for fetch requests as an alternative cancellation method

Build docs developers (and LLMs) love