React hook useEffect 无法读取使用 firebase 的 firestore 实时数据更新的新 useState 值
React hook useEffect failed to read new useState value that is updated with firebase's firestore realtime data
我有一组要呈现的数据对象。这个数据数组由我在 React 挂钩中声明的 Firestore onSnapshot 函数填充:useEffect。这个想法是 dom 应该在新数据添加到 firestore 时更新,并且应该在从 firestore 数据库修改数据时进行修改。
添加新数据可以正常工作,但是修改数据时会出现问题。
下面是我的代码:
import React, {useState, useEffect} from 'react'
...
const DocList = ({firebase}) => {
const [docList, setDocList] = useState([]);
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
setDocList(docList => [{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}, ...docList]);
console.log('document added: ', docSnap.doc.data());
} // this works fine
if (docSnap.type === 'modified') {
console.log('try docList from Lists: ', docList); //this is where the problem is, this returns empty array, i don't know why
console.log('document modified: ', docSnap.doc.data()); //modified data returned
}
})
})
return () => {
unSubListener();
}
}, []);
显然,我知道我用空 deps 数组声明 useEffect 的方法是让它 运行 一次,如果我应该在 deps 数组中包含 docList
整个效果开始 运行无限。
请问有什么办法吗?
基于@BrettEast 的建议;
I know this isn't what you want to hear, but I would probably suggest using useReducer reactjs.org/docs/hooks-reference.html#usereducer, rather than useState for tracking an array of objects. It can make updating easier to track. As for your bug, I don't think setDocList, even with the the prevState function, is guaranteed to be up to date by the time you get into that if statement.
我使用 useReducer
而不是 useState
,这里是工作代码:
import React, {useReducer, useEffect} from 'react'
import { withAuthorization } from '../../Session'
import DocDetailsCard from './Doc';
const initialState = [];
/**
* reducer declaration for useReducer
* @param {[*]} state the current use reducer state
* @param {{payload:*,type:'add'|'modify'|'remove'}} action defines the function to be performed and the data needed to execute such function in order to modify the state variable
*/
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return [action.payload, ...state]
case 'modify':
const modIdx = state.findIndex((doc, idx) => {
if (doc.id === action.payload.id) {
console.log(`modified data found in idx: ${idx}, id: ${doc.id}`);
return true;
}
return false;
})
let newModState = state;
newModState.splice(modIdx,1,action.payload);
return [...newModState]
case 'remove':
const rmIdx = state.findIndex((doc, idx) => {
if (doc.id === action.payload.id) {
console.log(`data removed from idx: ${idx}, id: ${doc.id}, fullData: `,doc);
return true;
}
return false;
})
let newRmState = state;
newRmState.splice(rmIdx,1);
return [...newRmState]
default:
return [...state]
}
}
const DocList = ({firebase}) => {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
dispatch({type:'add', payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
if (docSnap.type === 'modified') {
dispatch({type:'modify',payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
if (docSnap.type === 'removed'){
dispatch({type:'remove',payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
})
})
return () => {
unSubListener();
}
}, [firebase]);
return (
<div >
{
state.map(eachDoc => (
<DocDetailsCard key={eachDoc.id} details={eachDoc} />
))
}
</div>
)
}
const condition = authUser => !!authUser ;
export default React.memo(withAuthorization(condition)(DocList));
同样根据@HMR,使用setState回调函数:
这是更新后的代码,如果您要使用 useState()
.
也可以使用
import React, { useState, useEffect} from 'react'
import { withAuthorization } from '../../Session'
import DocDetailsCard from './Doc';
const DocList = ({firebase}) => {
const [docList, setDocList ] = useState([]);
const classes = useStyles();
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
setDocList(current => [{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}, ...current]);
console.log('document added: ', docSnap.doc.data());
}
if (docSnap.type === 'modified') {
setDocList(current => current.map(item => item.id === docSnap.doc.id ? {
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()} : item )
)
}
if (docSnap.type === 'removed'){
setDocList(current => {
const rmIdx = current.findIndex((doc, idx) => {
if (doc.id === docSnap.doc.id) {
return true;
}
return false;
})
let newRmState = current;
newRmState.splice(rmIdx, 1);
return [...newRmState]
})
}
})
})
return () => {
unSubListener();
}
}, [firebase]);
return (
<div >
{
docList.map(eachDoc => (
<DocDetailsCard key={eachDoc.id} details={eachDoc} />
))
}
</div>
)
}
const condition = authUser => !!authUser ;
export default React.memo(withAuthorization(condition)(DocList));
谢谢希望这对遇到类似问题的人有所帮助。
如评论所述,您可以使用 setDocList(current=>current.map(item=>...
,这是使用假 firebase 的工作示例:
const firebase = (() => {
const createId = ((id) => () => ++id)(0);
let data = [];
let listeners = [];
const dispatch = (event) =>
listeners.forEach((listener) => listener(event));
return {
listen: (fn) => {
listeners.push(fn);
return () => {
listeners = listeners.filter((l) => l !== fn);
};
},
add: (item) => {
const newItem = { ...item, id: createId() };
data = [...data, newItem];
dispatch({ type: 'add', doc: newItem });
},
edit: (id) => {
data = data.map((d) =>
d.id === id ? { ...d, count: d.count + 1 } : d
);
dispatch({
type: 'edit',
doc: data.find((d) => d.id === id),
});
},
};
})();
const Counter = React.memo(function Counter({ up, item }) {
return (
<button onClick={() => up(item.id)}>
{item.count}
</button>
);
});
function App() {
const [docList, setDocList] = React.useState([]);
React.useEffect(
() =>
firebase.listen(({ type, doc }) => {
if (type === 'add') {
setDocList((current) => [...current, doc]);
}
if (type === 'edit') {
setDocList((current) =>
current.map((item) =>
item.id === doc.id ? doc : item
)
);
}
}),
[]
);
const up = React.useCallback(
(id) => firebase.edit(id),
[]
);
return (
<div>
<button onClick={() => firebase.add({ count: 0 })}>
add
</button>
<div>
{docList.map((doc) => (
<Counter key={doc.id} up={up} item={doc} />
))}
</div>
</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>
您可以 setDocList(docList.map...
但这会使 docList 成为效果的依赖项:useEffect(function,[docList])
每次 docList 更改时效果都会 运行 因此您需要删除侦听器和 idd每次都是。
在您的代码中您没有添加依赖项,因此 docList 是 stale closure。但最简单的方法是按照我的建议执行并使用 setDocList 的回调:setDocList(current=>current.map...
所以 docList 不是效果的依赖项。
评论:
I don't think setDocList, even with the the prevState function, is guaranteed to be up to date by the time you get into that if statement
根本不正确,当您将回调传递给状态 setter 时,当前状态将传递给该回调。
我有一组要呈现的数据对象。这个数据数组由我在 React 挂钩中声明的 Firestore onSnapshot 函数填充:useEffect。这个想法是 dom 应该在新数据添加到 firestore 时更新,并且应该在从 firestore 数据库修改数据时进行修改。 添加新数据可以正常工作,但是修改数据时会出现问题。 下面是我的代码:
import React, {useState, useEffect} from 'react'
...
const DocList = ({firebase}) => {
const [docList, setDocList] = useState([]);
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
setDocList(docList => [{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}, ...docList]);
console.log('document added: ', docSnap.doc.data());
} // this works fine
if (docSnap.type === 'modified') {
console.log('try docList from Lists: ', docList); //this is where the problem is, this returns empty array, i don't know why
console.log('document modified: ', docSnap.doc.data()); //modified data returned
}
})
})
return () => {
unSubListener();
}
}, []);
显然,我知道我用空 deps 数组声明 useEffect 的方法是让它 运行 一次,如果我应该在 deps 数组中包含 docList
整个效果开始 运行无限。
请问有什么办法吗?
基于@BrettEast 的建议;
I know this isn't what you want to hear, but I would probably suggest using useReducer reactjs.org/docs/hooks-reference.html#usereducer, rather than useState for tracking an array of objects. It can make updating easier to track. As for your bug, I don't think setDocList, even with the the prevState function, is guaranteed to be up to date by the time you get into that if statement.
我使用 useReducer
而不是 useState
,这里是工作代码:
import React, {useReducer, useEffect} from 'react'
import { withAuthorization } from '../../Session'
import DocDetailsCard from './Doc';
const initialState = [];
/**
* reducer declaration for useReducer
* @param {[*]} state the current use reducer state
* @param {{payload:*,type:'add'|'modify'|'remove'}} action defines the function to be performed and the data needed to execute such function in order to modify the state variable
*/
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return [action.payload, ...state]
case 'modify':
const modIdx = state.findIndex((doc, idx) => {
if (doc.id === action.payload.id) {
console.log(`modified data found in idx: ${idx}, id: ${doc.id}`);
return true;
}
return false;
})
let newModState = state;
newModState.splice(modIdx,1,action.payload);
return [...newModState]
case 'remove':
const rmIdx = state.findIndex((doc, idx) => {
if (doc.id === action.payload.id) {
console.log(`data removed from idx: ${idx}, id: ${doc.id}, fullData: `,doc);
return true;
}
return false;
})
let newRmState = state;
newRmState.splice(rmIdx,1);
return [...newRmState]
default:
return [...state]
}
}
const DocList = ({firebase}) => {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
dispatch({type:'add', payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
if (docSnap.type === 'modified') {
dispatch({type:'modify',payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
if (docSnap.type === 'removed'){
dispatch({type:'remove',payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
})
})
return () => {
unSubListener();
}
}, [firebase]);
return (
<div >
{
state.map(eachDoc => (
<DocDetailsCard key={eachDoc.id} details={eachDoc} />
))
}
</div>
)
}
const condition = authUser => !!authUser ;
export default React.memo(withAuthorization(condition)(DocList));
同样根据@HMR,使用setState回调函数:
这是更新后的代码,如果您要使用 useState()
.
import React, { useState, useEffect} from 'react'
import { withAuthorization } from '../../Session'
import DocDetailsCard from './Doc';
const DocList = ({firebase}) => {
const [docList, setDocList ] = useState([]);
const classes = useStyles();
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
setDocList(current => [{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}, ...current]);
console.log('document added: ', docSnap.doc.data());
}
if (docSnap.type === 'modified') {
setDocList(current => current.map(item => item.id === docSnap.doc.id ? {
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()} : item )
)
}
if (docSnap.type === 'removed'){
setDocList(current => {
const rmIdx = current.findIndex((doc, idx) => {
if (doc.id === docSnap.doc.id) {
return true;
}
return false;
})
let newRmState = current;
newRmState.splice(rmIdx, 1);
return [...newRmState]
})
}
})
})
return () => {
unSubListener();
}
}, [firebase]);
return (
<div >
{
docList.map(eachDoc => (
<DocDetailsCard key={eachDoc.id} details={eachDoc} />
))
}
</div>
)
}
const condition = authUser => !!authUser ;
export default React.memo(withAuthorization(condition)(DocList));
谢谢希望这对遇到类似问题的人有所帮助。
如评论所述,您可以使用 setDocList(current=>current.map(item=>...
,这是使用假 firebase 的工作示例:
const firebase = (() => {
const createId = ((id) => () => ++id)(0);
let data = [];
let listeners = [];
const dispatch = (event) =>
listeners.forEach((listener) => listener(event));
return {
listen: (fn) => {
listeners.push(fn);
return () => {
listeners = listeners.filter((l) => l !== fn);
};
},
add: (item) => {
const newItem = { ...item, id: createId() };
data = [...data, newItem];
dispatch({ type: 'add', doc: newItem });
},
edit: (id) => {
data = data.map((d) =>
d.id === id ? { ...d, count: d.count + 1 } : d
);
dispatch({
type: 'edit',
doc: data.find((d) => d.id === id),
});
},
};
})();
const Counter = React.memo(function Counter({ up, item }) {
return (
<button onClick={() => up(item.id)}>
{item.count}
</button>
);
});
function App() {
const [docList, setDocList] = React.useState([]);
React.useEffect(
() =>
firebase.listen(({ type, doc }) => {
if (type === 'add') {
setDocList((current) => [...current, doc]);
}
if (type === 'edit') {
setDocList((current) =>
current.map((item) =>
item.id === doc.id ? doc : item
)
);
}
}),
[]
);
const up = React.useCallback(
(id) => firebase.edit(id),
[]
);
return (
<div>
<button onClick={() => firebase.add({ count: 0 })}>
add
</button>
<div>
{docList.map((doc) => (
<Counter key={doc.id} up={up} item={doc} />
))}
</div>
</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>
您可以 setDocList(docList.map...
但这会使 docList 成为效果的依赖项:useEffect(function,[docList])
每次 docList 更改时效果都会 运行 因此您需要删除侦听器和 idd每次都是。
在您的代码中您没有添加依赖项,因此 docList 是 stale closure。但最简单的方法是按照我的建议执行并使用 setDocList 的回调:setDocList(current=>current.map...
所以 docList 不是效果的依赖项。
评论:
I don't think setDocList, even with the the prevState function, is guaranteed to be up to date by the time you get into that if statement
根本不正确,当您将回调传递给状态 setter 时,当前状态将传递给该回调。