Search code examples
reactjsreact-functional-component

Why are React props not updated in event handler


I have a functional component using the useEffect hook to subscribe to an external event and that external event is using a value in the components props. I dont understand why the component itself, when it renders, is using the updated prop value but my callback is using the original value.

I thought maybe I needed to change the second parameter of useEffect to specify props however this doesnt make any difference.

In the below example, the onSave callback is invoked and attempts to use the current value of props.modelName however, even though the component itself has an updated value the callback seems to only see the original value of that property.

Why is that? Does it have something to do with closure or am I missing something else?

useEffect(() => {
    EventBus.on(EventIds._designerSaveCommand, onSave);

    return () => {
      EventBus.remove(EventIds._designerSaveCommand, onSave);
    };
  }, [reactFlowInstance,props]);

And I have my event handler like this:

const onSave = () => {
    try {
      const object = reactFlowInstance?.toObject();

      if(object) {
        const exportedModel: IExportedModel = {
          modelName: props.modelName,  <---- This is not the current value in the component props
          model: object
        };

        const json = JSON.stringify(exportedModel);
        var blob = new Blob([json], { type: "application/json" });
        FileSaver.saveAs(blob, `${props.modelName}.json`);
      }
    }
    catch(e) {
      setMessage({text: e.message, type: MessageBarType.error});
    }
  };

Solution

  • You have the right idea by adding the prop to the dependency array. You also need to move the function onSave() to inside the hook. It will then be referring to the latest modelName.

    useEffect(() => {
      const onSave = () => {
        try {
          const object = reactFlowInstance?.toObject();
          if(object) {
            const exportedModel: IExportedModel = {
              modelName: modelName,
              model: object
            };
    
          const json = JSON.stringify(exportedModel);
            var blob = new Blob([json], { type: "application/json" });
            FileSaver.saveAs(blob, `${modelName}.json`);
          }
        }
        catch(e) {
          setMessage({text: e.message, type: MessageBarType.error});
        }
      };
    
      EventBus.on(EventIds._designerSaveCommand, onSave);
    
      return () => {
        EventBus.remove(EventIds._designerSaveCommand, onSave);
      };
    }, [reactFlowInstance, modelName]);
    
    

    If you don't like the useEffect so big, you can separate just the file logic to a new function, and call it inside the hook like so:

      useEffect(() => {
        const onSave = () => {
          saveFile(reactFlowInstance, modelName)
        };
        // ... handlers go here
      }, [reactFlowInstance, modelName]);