我试图通过redux-thunk操作查询我的Firebase后端,但是,当我在使用useEffect()
的初始渲染中进行查询时,最终出现此错误:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
我的操作只是返回一个Firebase查询快照,然后在我的reducer中收到它。我使用钩子来调度我的动作:
export const useAnswersState = () => {
return {
answers: useSelector(state => selectAnswers(state)),
isAnswersLoading: useSelector(state => selectAnswersLoading(state))
}
}
export const useAnswersDispatch = () => {
const dispatch = useDispatch()
return {
// getAnswersData is a redux-thunk action that returns a firebase snapshot
setAnswers: questionID => dispatch(getAnswersData(questionID))
}
}
和以下选择器从快照和redux状态获取所需的数据:
export const selectAnswers = state => {
const { snapshot } = state.root.answers
if (snapshot === null) return []
let answers = []
snapshot.docs.map(doc => {
answers.push(doc.data())
})
return answers
}
export const selectAnswersLoading = state => {
return state.root.answers.queryLoading || state.root.answers.snapshot === null
}
然后在我的实际组件中,我尝试通过调度我的操作来首先查询我的后端,然后在按如下所示加载数据后尝试读取结果数据:
const params = useParams() // params.id is just an ID string
const { setAnswers, isAnswersLoading } = useAnswersDispatch()
const { answers } = useAnswersState()
useEffect(() => {
setAnswers(params.id)
}, [])
if (!isAnswersLoading)) console.log(answers)
为了明确起见,我正在使用useAnswersDispatch
调度一个redux-thunk操作,该操作返回一个Firebase数据快照。然后,我将在装入数据后使用useAnswersState
钩子访问数据。我试图在我的实际视图组件的useEffect
中调度查询,然后使用状态挂钩显示数据。
但是,当我尝试打印answers
的值时,出现了上面的错误。我将不胜感激任何帮助,并乐于提供任何更多信息(如果有帮助的话),但是,我已经测试了我的reducer和action本身,两者均按预期工作,因此我相信问题出在文件中如上所述。
答案 0 :(得分:0)
尝试重构动作创建者,以便在效果中调用dispatch
。您需要根据效果触发进行调度。
const setAnswers = (params.id) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(useAnswersDispatch(params.id));
}, [])
}
假设getAnswersData
是一个选择器,效果将触发调度到您的应用程序状态,当您返回响应时,选择器getAnswersData
将选择所需的字段。
我不确定params.id
的来源,但是您的组件依靠它来确定应用程序状态的答案。
触发调度后,仅更新应用程序状态,而不更新组件状态。使用useDispatch
设置变量,您可以在组件的生命周期中对Redux存储的调度功能进行变量引用。
要回答您的问题,如果您希望它处理多个调度,请将params.id
和dispatch
添加到效果中的依赖项数组中。
// Handle null or undefined param.id
const answers = (param.id) => getAnswersData(param.id);
const dispatch = useDispatch();
useEffect(() => {
if(params.id)
dispatch(useAnswersDispatch(params.id));
}, [params.id, dispatch]);
console.log(answers);
答案 1 :(得分:0)
如所评论;我认为您无限循环的实际代码依赖于setAnswers
。在您的问题中,您忘记添加此依赖关系,但是下面的代码显示了如何防止setAnswers
更改并引起无限循环:
const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
const { type, payload } = action;
console.log('in reducer', type, payload);
if (type === GOT_DATA) {
return { ...state, data: payload };
}
return state;
};
//I guess you imported this and this won't change so
// useCallback doesn't see it as a dependency
const getAnswersData = id => ({
type: GOT_DATA,
payload: id,
});
const useAnswersDispatch = dispatch => {
// const dispatch = useDispatch(); //react-redux useDispatch will never change
//never re create setAnswers because it causes the
// effect to run again since it is a dependency of your effect
const setAnswers = React.useCallback(
questionID => dispatch(getAnswersData(questionID)),
//your linter may complain because it doesn't know
// useDispatch always returns the same dispatch function
[dispatch]
);
return {
setAnswers,
};
};
const Data = ({ id }) => {
//fake redux
const [state, dispatch] = React.useReducer(reducer, {
data: [],
});
const { setAnswers } = useAnswersDispatch(dispatch);
React.useEffect(() => {
setAnswers(id);
}, [id, setAnswers]);
return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
const [id, setId] = React.useState(88);
return (
<div>
<button onClick={() => setId(id => id + 1)}>
increase id
</button>
<Data id={id} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
这是您的原始代码,导致无限循环,因为setAnswers不断变化。
const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
const { type, payload } = action;
console.log('in reducer', type, payload);
if (type === GOT_DATA) {
return { ...state, data: payload };
}
return state;
};
//I guess you imported this and this won't change so
// useCallback doesn't see it as a dependency
const getAnswersData = id => ({
type: GOT_DATA,
payload: id,
});
const useAnswersDispatch = dispatch => {
return {
//re creating setAnswers, calling this will cause
// state.data to be set causing Data to re render
// and because setAnser has changed it'll cause the
// effect to re run and setAnswers to be called ...
setAnswers: questionID =>
dispatch(getAnswersData(questionID)),
};
};
let timesRedered = 0;
const Data = ({ id }) => {
//fake redux
const [state, dispatch] = React.useReducer(reducer, {
data: [],
});
//securit to prevent infinite loop
timesRedered++;
if (timesRedered > 20) {
throw new Error('infinite loop');
}
const { setAnswers } = useAnswersDispatch(dispatch);
React.useEffect(() => {
setAnswers(id);
}, [id, setAnswers]);
return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
const [id, setId] = React.useState(88);
return (
<div>
<button onClick={() => setId(id => id + 1)}>
increase id
</button>
<Data id={id} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
答案 2 :(得分:0)
您只需要添加 params.id
作为依赖项。