surveyjs / survey-library

Free JavaScript form builder library with integration for React, Angular, Vue, jQuery, and Knockout.
https://surveyjs.io/form-library
MIT License
4.11k stars 801 forks source link

onAfterRender events removing event issue #7921

Closed ajlee-kp closed 2 months ago

ajlee-kp commented 6 months ago

Are you requesting a feature, reporting a bug or asking a question?

Bug report

What is the current behavior?

Survey Instance onAfterRenderQuestion, onAfterRenderPanel, onAfterRenderSurvey event cannot be cleaned up in useEffect and if you add them in return statement event won't be added in.

What is the expected behavior?

I'm expecting to be able to removing event inside return statement and be able to add in the events. This is ran in Strict Mode and should still be able to add events on re-render after initial render.

How would you reproduce the current behavior (if this is a bug)?

  useEffect(() => {
    const onComplete= (sender: Model) => <<TASK>>;
    const onRender= (_: Model, { question }: FocusInQuestionEvent) => <<TASK>>;
    survey.onComplete.add(onComplete);
    survey.onAfterRenderQuestion.add(onRender);
    return () => {
      survey.onComplete.remove(onComplete);
      survey.onAfterRenderQuestion.remove(onRender);
    };
  }, [
    survey.onComplete,
    survey.onAfterRenderQuestion
  ]);

This will result in onAfterRenderQuestion getting removed and never getting added back on rerender.

Specify your

JaneSjs commented 2 months ago

Hello @ajlee-kp,

Please accept my apologies for the delayed reply.

This issue occurs due to the way dependencies are handled in the useEffect hook and the component lifecycle in React's Strict Mode. React Strict Mode can trigger multiple renders in development to help identify potential problems. When survey.onComplete and survey.onAfterRenderQuestion are used as dependencies, they can cause the effect to run multiple times, leading to unexpected behavior.

To address this, the useEffect hook should not directly depend on the event handlers themselves. Instead, it should only depend on the survey instance. You can solve this by using useRef for the survey instance:

import React, { useEffect, useRef } from "react";
import { Model } from "survey-core";
import { Survey } from "survey-react-ui";
import "survey-core/defaultV2.min.css";
import "./index.css";
import { json } from "./json";

function SurveyComponent() {
    const survey = useRef(new Model(json)).current;

    useEffect(() => {
        const onComplete = (sender) => {
            console.log(JSON.stringify(sender.data, null, 3));
        };

        const onRender = (_, { question }) => {
            //...
        };

        survey.onComplete.add(onComplete);
        survey.onAfterRenderQuestion.add(onRender);

        return () => {
            survey.onComplete.remove(onComplete);
            survey.onAfterRenderQuestion.remove(onRender);
        };
    }, [survey]);

    return (<Survey model={survey} />);
}

export default SurveyComponent;

The useRef ensures that the survey instance remains stable across renders. This prevents the effect from running unnecessarily. In the updated code, the useEffect hook now only depends on the survey, which does not change between renders.

Please feel free to reactivate this ticket if you have further questions.