【问题标题】:React hook useEffect failed to read new useState value that is updated with firebase's firestore realtime dataReact hook useEffect 无法读取使用 firebase 的 firestore 实时数据更新的新 useState 值
【发布时间】:2020-10-08 18:04:42
【问题描述】:

我有一个要渲染的数据对象数组。这个数据数组由我在 React 钩子中声明的 Firestore onSnapshot 函数填充:useEffect。这个想法是当新数据添加到firestore时应该更新dom,并且当从firestore db修改数据时应该修改dom。 添加新数据可以正常工作,但是在修改数据时会出现问题。 下面是我的代码:

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,整个效果就会开始无限运行。

请问有什么办法吗?

【问题讨论】:

  • 你可以做setDocList(current=>current.map(item=>...
  • 我知道这不是你想听到的,但我可能会建议使用useReducerreactjs.org/docs/hooks-reference.html#usereducer,而不是useState 来跟踪对象数组。它可以使更新更容易跟踪。至于你的错误,我不认为setDocList,即使使用 prevState 函数,在你进入 if 语句时也能保证是最新的。您是否考虑过仅添加所有文档 (type === 'added' || type === 'modified'),并在 useEffect 之外进行排序/过滤?
  • 看来你需要使用setLoading方法来管理来自Firebase的响应,我找到了这篇文章,希望对medium.com/javascript-in-plain-english/…有帮助
  • @HMR 谢谢,但是当我尝试这个时, current 的值是空数组,所以没有什么可以映射。
  • @BrettEast 我会试试这个,希望它能解决问题

标签: reactjs firebase google-cloud-firestore use-effect use-state


【解决方案1】:

正如评论的那样,您可以使用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 更改时运行,因此您需要删除侦听器并每次都使用它。

在您的代码中,您没有添加依赖项,因此 docList 是 stale closure。但最简单的方法是执行我的建议并为 setDocList 使用回调:setDocList(current=&gt;current.map... 所以 docList 不是效果的依赖项。

评论:

我不认为 setDocList,即使使用 prevState 函数,也不能保证在你进入 if 语句时是最新的

根本不正确,当您将回调传递给状态设置器时,当前状态将传递给该回调。

【讨论】:

  • 你说得非常对,我已经检查过了,它确实有效。我在下面的答案中添加了工作代码。非常感谢。
【解决方案2】:

基于@BrettEast 的建议;

我知道这不是您想听到的,但我可能会建议使用 useReducer reactjs.org/docs/hooks-reference.html#usereducer,而不是 useState 来跟踪对象数组。它可以使更新更容易跟踪。至于您的错误,我认为即使使用 prevState 函数,setDocList 也不能​​保证在您进入该 if 语句时是最新的。

我使用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));

谢谢,希望对遇到类似问题的人有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-02-02
    • 1970-01-01
    • 2023-01-05
    • 1970-01-01
    • 2022-12-01
    • 1970-01-01
    • 2021-09-22
    相关资源
    最近更新 更多