J'essaie d'utiliser un émetteur d'événements avec React useEffect et useState, mais il obtient toujours l'état initial au lieu de l'état mis à jour. Cela fonctionne si j'appelle directement le gestionnaire d'événements, même avec un setTimeout.

Si je passe la valeur au 2ème argument useEffect(), cela le fait fonctionner, mais cela provoque un réabonnement à l'émetteur d'événements chaque fois que la valeur change (qui est déclenchée par des frappes).

Qu'est-ce que je fais mal? J'ai essayé useState, useRef, useReducer et useCallback, et je n'ai rien pu faire.

Voici une reproduction :

import React, { useState, useEffect } from "react";
import { Controlled as CodeMirror } from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import EventEmitter from "events";

let ee = new EventEmitter();

const initialValue = "initial value";

function App(props) {
  const [value, setValue] = useState(initialValue);

  // Should get the latest value, both after the initial server load, and whenever the Codemirror input changes.
  const handleEvent = (msg, data) => {
    console.info("Value in event handler: ", value);
    // This line is only for demoing the problem. If we wanted to modify the DOM in this event, we would instead call some setState function and rerender in a React-friendly fashion.
    document.getElementById("result").innerHTML = value;
  };

  // Get value from server on component creation (mocked)
  useEffect(() => {
    setTimeout(() => {
      setValue("value from server");
    }, 1000);
  }, []);

  // Subscribe to events on component creation
  useEffect(() => {
    ee.on("some_event", handleEvent);
    return () => {
      ee.off(handleEvent);
    };
  }, []);

  return (
    <React.Fragment>
      <CodeMirror
        value={value}
        options={{ lineNumbers: true }}
        onBeforeChange={(editor, data, newValue) => {
          setValue(newValue);
        }}
      />
      {/* Everything below is only for demoing the problem. In reality the event would come from some other source external to this component. */}
      <button
        onClick={() => {
          ee.emit("some_event");
        }}
      >
        EventEmitter (doesnt work)
      </button>
      <div id="result" />
    </React.Fragment>
  );
}

export default App;

Voici un bac à sable de code avec le même dans App2 :

https://codesandbox.io/s/ww2v80ww4l

Le composant App a 3 implémentations différentes - EventEmitter, pubsub-js et setTimeout. Seul setTimeout fonctionne.

Éditer

Pour clarifier mon objectif, je veux simplement que la valeur dans handleEvent corresponde à la valeur Codemirror dans tous les cas. Lorsqu'un bouton est cliqué, la valeur actuelle du miroir codé doit être affichée. Au lieu de cela, la valeur initiale est affichée.

40
Tony R 14 mars 2019 à 05:44

3 réponses

Meilleure réponse

value est périmé dans le gestionnaire d'événements car il tire sa valeur de la fermeture où il a été défini. À moins que nous ne réinscrivons un nouveau gestionnaire d'événements à chaque fois que value change, il n'obtiendra pas la nouvelle valeur.

Solution 1 : créez le deuxième argument de l'effet de publication [value]. Cela permet au gestionnaire d'événements d'obtenir la valeur correcte, mais provoque également la réexécution de l'effet à chaque frappe.

Solution 2 : utilisez un ref pour stocker le dernier value dans une variable d'instance de composant. Ensuite, créez un effet qui ne fait que mettre à jour cette variable à chaque fois que l'état value change. Dans le gestionnaire d'événements, utilisez ref, pas value.

const [value, setValue] = useState(initialValue);
const refValue = useRef(value);
useEffect(() => {
    refValue.current = value;
});
const handleEvent = (msg, data) => {
    console.info("Value in event handler: ", refValue.current);
};

https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often

Il semble qu'il existe d'autres solutions sur cette page qui pourraient également fonctionner. Un grand merci à @Dinesh pour l'aide.

54
Carsten Führmann 20 janv. 2020 à 04:21

UseCallback aurait dû fonctionner ici.

import React, { useState, useEffect, useCallback } from "react";
import PubSub from "pubsub-js";
import { Controlled as CodeMirror } from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import EventEmitter from "events";

let ee = new EventEmitter();

const initialValue = "initial value";

function App(props) {
  const [value, setValue] = useState(initialValue);

  // Should get the latest value
  const handler = (msg, data) => {
    console.info("Value in event handler: ", value);
    document.getElementById("result").innerHTML = value;
  };

  const handleEvent = useCallback(handler, [value]);

  // Get value from server on component creation (mocked)
  useEffect(() => {
    setTimeout(() => {
      setValue("value from server");
    }, 1000);
  }, []);

  // Subscribe to events on component creation
  useEffect(() => {
    PubSub.subscribe("some_event", handleEvent);
    return () => {
      PubSub.unsubscribe(handleEvent);
    };
  }, [handleEvent]);
  useEffect(() => {
    ee.on("some_event", handleEvent);
    return () => {
      ee.off(handleEvent);
    };
  }, []);

  return (
    <React.Fragment>
      <CodeMirror
        value={value}
        options={{ lineNumbers: true }}
        onBeforeChange={(editor, data, newValue) => {
          setValue(newValue);
        }}
      />
      <button
        onClick={() => {
          ee.emit("some_event");
        }}
      >
        EventEmitter (works)
      </button>
      <button
        onClick={() => {
          PubSub.publish("some_event");
        }}
      >
        PubSub (doesnt work)
      </button>
      <button
        onClick={() => {
          setTimeout(() => handleEvent(), 100);
        }}
      >
        setTimeout (works!)
      </button>
      <div id="result" />
    </React.Fragment>
  );
}

export default App;

Vérifiez les codesandbox ici https://codesandbox.io/s/react-base-forked- i9ro7

0
Lokii 18 nov. 2020 à 05:11

Réponse mise à jour.

Le problème ne vient pas des crochets. La valeur de l'état initial a été fermée et transmise à EventEmitter et a été utilisée encore et encore.

Ce n'est pas une bonne idée d'utiliser des valeurs d'état directement dans handleEvent. Au lieu de cela, nous devons les passer en tant que paramètres lors de l'émission de l'événement.

import React, { useState, useEffect } from "react";
import { Controlled as CodeMirror } from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import EventEmitter from "events";

let ee = new EventEmitter();

const initialValue = "initial value";

function App(props) {
  const [value, setValue] = useState(initialValue);
  const [isReady, setReady] = useState(false);

  // Should get the latest value
  function handleEvent(value, msg, data) {
    // Do not use state values in this handler
    // the params are closed and are executed in the context of EventEmitter
    // pass values as parameters instead
    console.info("Value in event handler: ", value);
    document.getElementById("result").innerHTML = value;
  }

  // Get value from server on component creation (mocked)
  useEffect(() => {
    setTimeout(() => {
      setValue("value from server");
      setReady(true);
    }, 1000);
  }, []);

  // Subscribe to events on component creation
  useEffect(
    () => {
      if (isReady) {
        ee.on("some_event", handleEvent);
      }
      return () => {
        if (!ee.off) return;
        ee.off(handleEvent);
      };
    },
    [isReady]
  );

  function handleClick(e) {
    ee.emit("some_event", value);
  }

  return (
    <React.Fragment>
      <CodeMirror
        value={value}
        options={{ lineNumbers: true }}
        onBeforeChange={(editor, data, newValue) => {
          setValue(newValue);
        }}
      />
      <button onClick={handleClick}>EventEmitter (works now)</button>
      <div id="result" />
    </React.Fragment>
  );
}

export default App;

Voici un codesandbox fonctionnel

3
Dinesh Pandiyan 14 mars 2019 à 05:23