#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
.