Summary
A custom dropdown property in SurveyJS was displaying the selected value in the UI but cleared it after clicking outside the dropdown. The JSON editor still retained the value, indicating an issue with the render lifecycle of the custom property. The fix involves ensuring that the component’s state is updated when the dropdown loses focus.
Root Cause
- The
choicescallback dynamically populates options based on the current survey state. - When the dropdown loses focus, SurveyJS re-evaluates the property but the choice list is regenerated before the selected value is committed.
- The regenerated list does not include the previously selected value, causing the UI to render it as empty.
Why This Happens in Real Systems
- Asynchronous state updates in React can out‑of‑sync with SurveyJS’s property callbacks.
- Dynamic choice generation on every render leads to accidental race conditions.
- Complex surveys with many custom properties exacerbate the timing mismatch.
Real-World Impact
- Users lose the ability to see what they selected after clicking away.
- Data appears missing to end‑users, creating confusion and requiring additional validation steps.
- Leads to increased support tickets and lower survey completion rates.
Example or Code (if necessary and relevant)
Serializer.addProperty("question", {
name: "feedbackQuestionFor",
category: "FeedbackModule",
displayName: "Feedback voor vraag",
type: "dropdown",
choices: function (
obj: QuestionCustomModel,
choicesCallback: (choices: { value: string; text: string }[]) => any
) {
let surveyModel = obj.getSurvey() as SurveyModel;
let questionTitles = surveyModel
.getAllQuestions()
.filter(
(question) =>
question.getPropertyValue("feedbackQuestionFor") === undefined &&
question.id !== obj.id
)
.map((question) => ({
value: question["title"],
text: question["title"],
}));
choicesCallback(questionTitles);
},
});
How Senior Engineers Fix It
-
Cache the options on the first render and reuse them until the survey changes.
-
Use
isReadOnlyoronValueChangedhandlers to persist the selected value before the dropdown is re‑rendered. -
Wrap the property definition in a React hook that synchronizes with SurveyJS state:
const [options, setOptions] = useState([]); useEffect(() => { const titles = surveyModel.getAllQuestions() .filter(/* same filter */) .map(q => ({ value: q.title, text: q.title })); setOptions(titles); }, [surveyModel]); Serializer.addProperty("question", { /* ... */ choices: () => options, }); -
Ensure the callback that updates
choicesdoes not run on every focus/blur event.
Why Juniors Miss It
- They often assume that dynamic callbacks are called only once, not on every focus event.
- They may overlook the fact that SurveyJS and React state are separate and can conflict.
- Lack of experience with asynchronous lifecycle events leads to missing race conditions.