Повторные отправки компонентов с неправильным состоянием redux?

#reactjs #redux

#reactjs #redux

Вопрос:

У меня очень странная ошибка, которую я пытаюсь понять уже 1,5 дня. Проблема с этой ошибкой в том, что ее очень сложно отобразить, не показав около 2000 строк кода — я попытался восстановить простой пример в codesandbox, но не смог воспроизвести ошибку.

Ошибка может быть легко описана, хотя:

У меня есть родительский компонент A и дочерний компонент B. Оба подключены к одному и тому же хранилищу redux и подписаны на вызываемый редуктор active . Оба компонента выводят одно и то же activeQuestion свойство состояния. Оба компонента подключены к хранилищу redux по отдельности через connect()

Я отправляю действие SET_ACTIVE_QUESTION и компоненты повторно отображаются (я не уверен, почему происходит каждый повторный рендеринг), и компонент B теперь имеет обновленное состояние из хранилища, а компонент A — нет… и я, кажется, не могу понять, почему это так.

Реальное приложение довольно большое, но есть пара странных вещей, которые я заметил:

  • Ошибка исчезает, когда я подписываю родительский компонент A на активное состояние (компонент A подписан сам).

  • Действие по изменению активного вопроса запрашивается до его запуска с помощью setTimeout(() => doAction(), 0) . Если я удалю setTimeout, ошибка исчезнет.

Вот почему я думаю, что этот вопрос актуален даже без кода: как вообще возможно, что действие отправляется в хранилище redux (первый журнал консоли поступает непосредственно из reducer), а при последующем рендеринге отображается неправильное состояние? Я не уверен, как это вообще может быть возможно, если это не закрытие или что-то в этом роде.

введите описание изображения здесь


Функции обновления (mapStateToProps):

Компонент A (неправильное состояние):

 const mapStateToProps = (state: AppState) => ({
    active: state.active,
    answerList: state.answerList,
    surveyNotifications: state.surveyNotifications,
    activeDependencies: state.activeDependencies,
});
  

Компонент B (правильное состояние):

 const mapStateToProps = (state: AppState) => ({
    surveyNotifications: state.surveyNotifications,
    active: state.active,
    answerList: state.answerList,
    activeDependencies: state.activeDependencies,
});
  

Обновить:

Переход состояния инициируется компонентом B (правильное состояние) с помощью этой функции:

 const goToNextQuestionWithTransition = (
    where: string,
    shouldPerformValidation?: boolean
) => {
    setInState(false);
    setTimeout(() => {
        props.goToQuestion(where, shouldPerformValidation);
    }, 200);
};
  

Удаление setTimeout устраняет ошибку (но я не знаю почему)

Обновить (показать reducer):

 export const INITIAL_SATE = {
    activeQuestionUUID: '',
    ...
};

export default function (state = INITIAL_SATE, action) {
    switch (action.type) {
        case actionTypes.SET_ACTIVE_QUESTION:
            console.log('Action from reducer', action)
            return { ...state, activeQuestionUUID: action.payload };
        ...
        default:
            return {...state};
    }
}
  

Обновить
компонент A — правильное состояние

 const Survey: React.FC<IProps> = (props) => {
    const {
        survey,
        survey: { tenantModuleSet },
    } = props;
    const [isComplete, setIsComplete] = React.useState(false);
    const classes = useStyles();
    const surveyUtils = useSurveyUtils();

    console.log('Log from component A', props.active.activeQuestionUUID)

    React.useEffect(() => {
        const firstModule = tenantModuleSet[0];
        if (firstModule) {
            props.setActiveModule(firstModule.uuid);
        } else {
            setIsComplete(true);
        }
    }, []);

    const orderedLists: IOrderedLists = useMemo(() => {
        let orderedQuestionList: Array<string> = [];
        let orderedModuleList: Array<string> = [];

        tenantModuleSet.forEach((module) => {
            orderedModuleList.push(module.uuid);
            module.tenantQuestionSet.forEach((question) => {
                orderedQuestionList.push(question.uuid);
            });
        });

        return {
            questions: orderedQuestionList,
            modules: orderedModuleList,
        };
    }, [survey]);

    const validateQuestion = (question: IQuestion) => {
        ...
    };

    const findModuleForQuestion = (questionUUID: string) => {
        ...
    };

    const { setActiveQuestion, setActiveModule, active } = props;
    const { activeQuestionUUID, activeModuleUUID } = props.active;
    const currentQuestionIndex = orderedLists.questions.indexOf(
        activeQuestionUUID
    );

    const currentModuleIndex = orderedLists.modules.indexOf(activeModuleUUID);

    const currentModule = props.survey.tenantModuleSet.filter(
        (module) => module.uuid === active.activeModuleUUID
    )[0];

    if (!currentModule) return null;

    const currentQuestion = currentModule.tenantQuestionSet.filter(
        (question) => question.uuid === activeQuestionUUID
    )[0];

    const handleActiveSurveyScrollDirection = (destination: string) => {
    ...
    };

    const isQuestionLastInModule = ...

    const moveToNextQuestion = (modules: string[], questions: string[]) => {
        if (isQuestionLastInModule) {
            if (currentModule.uuid === modules[modules.length - 1]) {
                props.setActiveSurveyView("form");
            } else {
                setActiveQuestion("");
                setActiveModule(modules[currentModuleIndex   1]);
            }
        } else {
            console.log('this is the move function')
            setActiveQuestion(questions[currentQuestionIndex   1]);
        }
    };


    const goToQuestiton = (destination: string, useValidation = true) => {
            ....
            moveToNextQuestion(modules, questions);
            
    };

    return (
        <section className={classes.view}>
            {isComplete ? (
                <SurveyComplete />
            ) : (
                <div className={classes.bodySection}>
                    <Module
                        // adding a key here is nessesary
                        // or the Module will not unmount when the module changes
                        key={currentModule.uuid}
                        module={currentModule}
                        survey={props.survey}
                        goToQuestion={goToQuestiton}
                    />
                </div>
            )}
            {!isComplete amp;amp; (
                <div className={classes.footerSection}>
                    <SurveyFooter
                        tenantModuleSet={props.survey.tenantModuleSet}
                        goToQuestion={goToQuestiton}
                        orderedLists={orderedLists}
                    />
                </div>
            )}
        </section>
    );
};

const mapStateToProps = (state: AppState) => ({
    active: state.active,
    answerList: state.answerList,
    surveyNotifications: state.surveyNotifications,
    activeDependencies: state.activeDependencies,
});

const mapDispatchToProps = (dispatch: Dispatch) =>
    bindActionCreators(
        {
            removeQuestionNotification,
            setActiveQuestion,
            setActiveModule,
            setActiveSurveyScrollDirection,
        },
        dispatch
    );

export default connect(mapStateToProps, mapDispatchToProps)(Survey);
  

Компонент B (неправильное состояние)

 const Question: React.FC<IProps> = (props: IProps) => {
    const [showSubmitButton, setShowSubmitButton] = React.useState(false);
    const [inState, setInState] = React.useState(true);
    const classes = useStyles();

    const { question, module, goToQuestion, active } = props;

    const notifications: Array<IQuestionNotification> =
        props.surveyNotifications[question.uuid] || [];

    const answerArr = props.answerList[question.uuid];

    const dependency = props.activeDependencies.questions[question.uuid];


    useEffect(() => {
        /**
         * Function that moves to next or previous question based on the activeSurveyScrollDirection
         */
        const move =
            active.activeSurveyScrollDirection === "forwards"
                ? () => goToQuestion("next", false)
                : () => goToQuestion("prev", false); // backwards

        if (!dependency) {
            if (!question.isVisible) move();
        } else {
            const { type } = dependency;
            if (type === DependencyTypeEnum.SUBTRACT) {
                console.log('DEPENDENCY MOVE')
                move();
            }
        }
    }, [dependency, question, active.activeQuestionUUID]);

    console.log('Log from component B', active.activeQuestionUUID)

    const goToNextQuestionWithTransition = (
        where: string,
        shouldPerformValidation?: boolean
    ) => {
        // props.goToQuestion(where, shouldPerformValidation);
        setInState(false);
        setTimeout(() => {
            props.goToQuestion(where, shouldPerformValidation);
        }, 200);
    };

    /**
     * Questions that only accept one answer will auto submit
     * Questions that have more than one answer will display
     * complete button after one answer is passed.
     */
    const doAutoComplete = () => {
        if (answerArr?.length) {
            if (question.maxSelect === 1) {
                goToNextQuestionWithTransition("next");
            }

            if (question.maxSelect > 1) {
                setShowSubmitButton(true);
            }
        }
    };

    useDidUpdateEffect(() => {
        doAutoComplete();
    }, [answerArr]);

    return (
        <Grid container justify="center">
            <Grid item xs={11} md={8} lg={5}>
                <div className={clsx(classes.question, !inState amp;amp; classes.questionOut)}>
                    <QuestionBody
                        question={question}
                        notifications={notifications}
                        module={module}
                        answerArr={answerArr}
                    />
                </div>
                {showSubmitButton amp;amp;
                active.activeQuestionUUID === question.uuid ? (
                    <Button
                        variant="contained"
                        color="secondary"
                        onClick={() => goToNextQuestionWithTransition("next")}
                    >
                        Ok!
                    </Button>
                ) : null}
            </Grid>
        </Grid>
    );
};

const mapStateToProps = (state: AppState) => ({
    surveyNotifications: state.surveyNotifications,
    active: state.active,
    answerList: state.answerList,
    activeDependencies: state.activeDependencies,
});

const mapDispatchToProps = (dispatch: Dispatch) =>
    bindActionCreators(
        {
            setActiveQuestion,
        },
        dispatch
    );

export default connect(mapStateToProps, mapDispatchToProps)(Question);
  

Комментарии:

1. @RameshReddy, я полностью понимаю! Я очень не решался даже спрашивать об этом здесь, так как я не был уверен, что показывать. Если у вас есть какие-либо идеи, какая часть может быть особенно актуальной, я рад показать ее.

2. Можете ли вы показать reducer, а также какое значение вы регистрируете и где вы его регистрируете?

3. привет, @markerikson, я добавил reducer. Извините, за задержку.

4. SET_ACTIVE_QUESTION это также единственное действие, которое может изменить activeQuestion состояние. Между ними нет никаких действий, которые могли бы как-то изменить состояние. Я также проверил с помощью redux devtools, что нет другого действия, изменяющего состояние.

5. Можете ли вы показать, где вы выполняете эту регистрацию в компонентах? На самом деле, можете ли вы показать фактическую логику для обоих компонентов? (вы можете опустить все выходные данные рендеринга JSX — я просто хочу посмотреть, где регистрируются значения относительно жизненных циклов, и когда все обновляется)

Ответ №1:

Можете ли вы опубликовать копию mapStateToProps как компонента B, так и компонента A? Если вы используете reselect (или аналогичные библиотеки), можете ли вы также опубликовать определения селекторов? Куда вы помещаете вызов setTimeout()?

Если вы уверены, что в mapStateToProps нет побочных эффектов, то, похоже, вы изменяете activeQuestion свойство где-то до или после повторного рендеринга компонента B, присваивая старое значение. (Возможно, вам нужно искать какое-то назначение в conditions).

Также обратите внимание, что вы не всегда можете доверять журналу консоли, поскольку его значение может быть оценено позже, когда вы его вызовете.

Комментарии:

1. Я добавил функции mapStateToProps. Я не использую повторный выбор. Обычно я использую useMemo или useCallback, но не для двух компонентов здесь и, как правило, не в состоянии redux.

2. Можно было бы подумать, что я где-то мутирую, но ошибка исчезает, когда я просто добавляю активное состояние в контейнер component A .