React hook useEffect не смог прочитать новое значение useState, которое обновлено данными firebase в реальном времени - PullRequest
1 голос
/ 18 июня 2020

У меня есть массив объектов данных для визуализации. и этот массив данных заполняется функцией Firestore onSnapshot, которую я объявил в хуке React: useEffect. Идея состоит в том, что dom должен обновляться, когда новые данные добавляются в firestore, и должен изменяться, когда данные изменяются из базы firestore db. добавление новых данных работает нормально, но проблема возникает при изменении данных. вот мой код ниже:

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();
        }
    }, []);

по-видимому, я знаю, как я объявил useEffect с пустым массивом deps, чтобы заставить его запускаться один раз, если я должен включить docList в массив deps, весь эффект начинает работать бесконечно.

пожалуйста, как обойтись?

Ответы [ 2 ]

0 голосов
/ 19 июня 2020

Как уже отмечалось, вы могли использовать setDocList(current=>current.map(item=>..., вот рабочий пример с поддельной базой огня:

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 был устаревшим закрытием . Но проще всего было бы сделать то, что я предложил, и использовать обратный вызов для setDocList: setDocList(current=>current.map..., поэтому docList не зависит от эффекта.

Комментарий:

Я не Не думаю, что setDocList, даже с функцией prevState, гарантированно будет актуальным к тому моменту, когда вы войдете в этот оператор if

Это просто неверно, когда вы передаете обратный вызов установщику состояния текущее состояние передается этому обратному вызову.

0 голосов
/ 19 июня 2020

На основе предложения @BrettEast;

Я знаю, что это не то, что вы хотите услышать, но я бы, вероятно, предложил использовать useReducer reactjs .org / docs / hooks-reference.html #usereducer, а не useState для отслеживания массива объектов. Это может упростить отслеживание обновлений. Что касается вашей ошибки, я не думаю, что setDocList, даже с функцией prevState, гарантированно будет актуальным к тому времени, когда вы войдете в этот оператор 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));

Спасибо, надеюсь, эта помощь поможет тем, у кого возникла подобная проблема.

...