no-use-effect

Par factory-ai · factory-plugins

Applique la règle d'interdiction de `useEffect` lors de l'écriture ou de la révision de code React. S'ACTIVE lors de l'écriture de composants React, du refactoring d'appels `useEffect` existants, de la revue de PRs contenant des `useEffect`, ou quand un agent ajoute un `useEffect` « par précaution ». Fournit les cinq patterns de remplacement et l'échappatoire `useMountEffect`.

npx skills add https://github.com/factory-ai/factory-plugins --skill no-use-effect

Pas de useEffect

N'appelle jamais useEffect directement. Utilise plutôt l'état dérivé, les gestionnaires d'événements, les bibliothèques de récupération de données, ou useMountEffect.

Référence rapide

À la place de useEffect pour… Utilise
Dériver l'état d'un autre état/props Calcul inline (Règle 1)
Récupérer des données useQuery / bibliothèque de récupération de données (Règle 2)
Réagir aux actions utilisateur Gestionnaires d'événements (Règle 3)
Sync externe unique au montage useMountEffect (Règle 4)
Réinitialiser l'état quand un prop change Prop key sur le parent (Règle 5)

Quand utiliser cette compétence

  • Écrire de nouveaux composants React
  • Refactoriser les appels useEffect existants
  • Examiner les PRs qui introduisent useEffect
  • Un agent ajoute useEffect « juste au cas où »

Workflow

1. Identifier le useEffect

Détermine ce que fait l'effet -- dériver l'état, récupérer des données, réagir à un événement, synchroniser avec un système externe, ou réinitialiser l'état.

2. Appliquer le bon motif de remplacement

Utilise les cinq règles ci-dessous pour choisir le bon remplacement.

3. Vérifier

npm run lint -- --filter=<package>
npm run typecheck -- --filter=<package>
npm run test -- --filter=<package>

L'échappatoire : useMountEffect

Pour le cas rare où tu dois synchroniser avec un système externe au montage :

L'implémentation enveloppe useEffect avec un tableau de dépendances vide pour rendre l'intention explicite :

export function useMountEffect(effect: () => void | (() => void)) {
  /* eslint-disable no-restricted-syntax */
  useEffect(effect, []);
}

Motifs de remplacement

Règle 1 : Dérive l'état, ne le synchronise pas

La plupart des effects qui définissent l'état à partir d'un autre état sont inutiles et ajoutent des rendus supplémentaires.

// MAUVAIS : Deux cycles de render - d'abord obsolète, puis filtré
function ProductList() {
  const [products, setProducts] = useState([]);
  const [filteredProducts, setFilteredProducts] = useState([]);

  useEffect(() => {
    setFilteredProducts(products.filter((p) => p.inStock));
  }, [products]);
}

// BON : Calcule inline en un seul render
function ProductList() {
  const [products, setProducts] = useState([]);
  const filteredProducts = products.filter((p) => p.inStock);
}

Signal d'alerte : Tu es sur le point d'écrire useEffect(() => setX(deriveFromY(y)), [y]), ou tu as un état qui ne fait que refléter un autre état ou des props.

Règle 2 : Utilise les bibliothèques de récupération de données

La récupération basée sur les effects crée des conditions de concurrence et duplique la logique de cache.

// MAUVAIS : Risque de condition de concurrence
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    fetchProduct(productId).then(setProduct);
  }, [productId]);
}

// BON : La bibliothèque de requêtes gère l'annulation/cache/obsolescence
function ProductPage({ productId }) {
  const { data: product } = useQuery(['product', productId], () =>
    fetchProduct(productId)
  );
}

Signal d'alerte : Ton effect fait fetch(...) puis setState(...), ou tu réimplémente le cache, les tentatives, l'annulation, ou la gestion de l'obsolescence.

Règle 3 : Gestionnaires d'événements, pas d'effects

Si un utilisateur clique sur un bouton, fais le travail dans le gestionnaire.

// MAUVAIS : Effect comme relais d'action
function LikeButton() {
  const [liked, setLiked] = useState(false);

  useEffect(() => {
    if (liked) {
      postLike();
      setLiked(false);
    }
  }, [liked]);

  return <button onClick={() => setLiked(true)}>Like</button>;
}

// BON : Action directe pilotée par les événements
function LikeButton() {
  return <button onClick={() => postLike()}>Like</button>;
}

Signal d'alerte : L'état est utilisé comme flag pour qu'un effect fasse la vraie action, ou tu construis une mécanique « définir flag → effect exécuté → réinitialiser flag ».

Règle 4 : useMountEffect pour la sync externe unique au montage

Bons cas d'usage : intégration DOM (focus, scroll), cycles de vie de widgets tiers, abonnements aux API du navigateur.

// MAUVAIS : Garde à l'intérieur du effect
function VideoPlayer({ isLoading }) {
  useEffect(() => {
    if (!isLoading) playVideo();
  }, [isLoading]);
}

// BON : Montage uniquement quand les préconditions sont remplies
function VideoPlayerWrapper({ isLoading }) {
  if (isLoading) return <LoadingScreen />;
  return <VideoPlayer />;
}

function VideoPlayer() {
  useMountEffect(() => playVideo());
}

Utilise useMountEffect pour les dépendances stables (singletons, refs, valeurs de contexte qui ne changent jamais) :

// MAUVAIS : useEffect avec dépendance qui ne change jamais
useEffect(() => {
  connectionManager.on('connected', handleConnect);
  return () => connectionManager.off('connected', handleConnect);
}, [connectionManager]); // connectionManager est un singleton du contexte

// BON : useMountEffect pour les dépendances stables

useMountEffect(() => {
  connectionManager.on('connected', handleConnect);
  return () => connectionManager.off('connected', handleConnect);
});

Signal d'alerte : Tu synchronises avec un système externe, et le comportement est naturellement « setup au montage, cleanup au démontage ».

Règle 5 : Réinitialise avec key, pas avec orchestration de dépendances

// MAUVAIS : Effect essaie d'émuler le comportement de remontage
function VideoPlayer({ videoId }) {
  useEffect(() => {
    loadVideo(videoId);
  }, [videoId]);
}

// BON : key force un remontage propre
function VideoPlayer({ videoId }) {
  useMountEffect(() => {
    loadVideo(videoId);
  });
}

function VideoPlayerWrapper({ videoId }) {
  return <VideoPlayer key={videoId} videoId={videoId} />;
}

Signal d'alerte : Tu écris un effect dont le seul rôle est de réinitialiser l'état local quand une ID/prop change, ou tu veux que le composant se comporte comme une toute nouvelle instance pour chaque entité.

Convention de structure de composant

Les valeurs calculées viennent après les hooks et l'état local, jamais via useEffect :

export function FeatureComponent({ featureId }: ComponentProps) {
  // Hooks d'abord
  const { data, isLoading } = useQueryFeature(featureId);

  // État local
  const [isOpen, setIsOpen] = useState(false);

  // Valeurs calculées (PAS useEffect + setState)
  const displayName = user?.name ?? 'Unknown';

  // Gestionnaires d'événements
  const handleClick = () => { setIsOpen(true); };

  // Retours précoces
  if (isLoading) return <Loading />;

  // Render
  return <Flex direction="column" gap="lg">...</Flex>;
}

Skills similaires