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 时,当前状态将传递给该回调。