I use a lot of firestore snapshots in my react native application. I am also using React hooks. The code looks something like this:
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// When the component initially loads, add all the loaded data to state.
// When data changes on firestore, we receive that update here in this
// callback and then update the UI based on current state
});;
}, []);
At first I assumed useState would be the best hook to store and update the UI. However, based on the way my useEffect hook is set up with an empty dependency array, when the snapshot callback gets fired with updated data and I try to modify the current state with the new changes, the current state is undefined. I believe this is because of a closure. I am able to get around it using useRef with a forceUpdate() like so:
const dataRef = useRef(initialData);
const [, updateState] = React.useState();
const forceUpdate = useCallback(() => updateState({}), []);
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// if snapshot data is added
dataRef.current.push(newData)
forceUpdate()
// if snapshot data is updated
dataRef.current.find(e => some condition) = updatedData
forceUpdate()
});;
}, []);
return(
// JSX that uses dataRef.current directly
)
My question is am I doing this correct by using useRef along with a forceUpdate instead of useState in a different way? It doesn't seem right that I'm having to update a useRef hook and call forceUpdate() all over my app. When trying useState I tried adding the state variable to the dependency array but ended up with an infinite loop. I only want the snapshot function to be initialized once and the stateful data in the component to be updated over time as things change on the backend (which fires in the onSnapshot callback).
It would be better if you combine useEffect and useState. UseEffect will setup and detach the listener, useState can just be responsible for the data you need.
const [data, setData] = useState([]);
useEffect(() => {
const unsubscribe = someFirestoreAPICall().onSnapshot(snap => {
const data = snap.docs.map(doc => doc.data())
this.setData(data)
});
//remember to unsubscribe from your realtime listener on unmount or you will create a memory leak
return () => unsubscribe()
}, []);
Then you can just reference "data" from the useState hook in your app.
A simple useEffect worked for me, i don't need to create a helper function or anything of sorts,
useEffect(() => {
const colRef = collection(db, "data")
//real time update
onSnapshot(colRef, (snapshot) => {
snapshot.docs.forEach((doc) => {
setTestData((prev) => [...prev, doc.data()])
// console.log("onsnapshot", doc.data());
})
})
}, [])
I found that inside of the onSnapshot() method I was unable to access state(e.g. if I console.log(state) I would get an empty value.
Creating a helper function worked for, but I'm not sure if this is hack-y solution or not but something like:
[state, setState] = useState([])
stateHelperFunction = () => {
//update state here
setState()
}
firestoreAPICall.onSnapshot(snapshot => {
stateHelperFunction(doc.data())
})
use can get the currentState using callback on set hook
const [state, setState] = useState([]);
firestoreAPICall.onSnapshot(snapshot => {
setState(prevState => { prevState.push(doc.data()) return prevState; })
})
prevState will have Current State Value
Related
I have a state named chats in which i am storing all messages from firebase and i'm trying to render it in app. but it does not render
here is my component's state:
const [chats, setChats] = useState([]);
I am bringing messages in UseEffect hook to get it on running of app
useEffect(() => {
let merged_uid = uid_merger(current_user.id, chat_user.uid);
database()
.ref('/')
.child(`chats/${merged_uid}`)
.on('child_added', (msgs) => {
console.log(msgs);
chats.push(msgs.val());
setChats(chats);
});
}, []);
but id does not render.
since chats is a state you can't mutate it using chats.push(msgs.val()); instead what you need to do is to replace
chats.push(msgs.val());
setChats(chats);
with
setChats([...chats, msgs.val()])
I want save array data using react useEffect. Follow Example with class:
async componentDidMount() {
const users = await AsyncStorage.getItem('users');
if (users) {
this.setState({ users: JSON.parse(users) });
}
}
componentDidUpdate(_, prevState) {
const { users } = this.state;
if (prevState.users !== users) {
AsyncStorage.setItem('users', JSON.stringify(users));
}
}
how to implement the logic with React Hooks?
For componentDidMount logic you can use useEffect hook:
useEffect(() => {
const asyncFetch = async () => {
const users = await AsyncStorage.getItem("users");
if (users) {
// setter from useState
setUsers(JSON.parse(users));
}
};
asyncFetch();
}, []);
For componentDidMount use useEffect with dep array and useRef reference.
const prevUsers = useRef();
useEffect(() => {
const prevUsers = prevUsers.current;
// Some equal check function
if (!areEqual(prevUsers, users)) {
AsyncStorage.setItem("users", JSON.stringify(users));
}
prevUsers.current = users;
}, [users]);
Notice that in your current code, prevState.users !== users is always truley, you comparing two objects and in JS {} !== {} always results true.
You can try like below and you can use hooks in functional based component not class based component
//state declaration similar to class based component
const [usersdata,setUsers] = useState([]);
const users = await JSON.parse(AsyncStorage.getItem('users'));
//whenever the value of users changes useEffect will reset the value of users in state useEffect handle the lifecycle in function based component
useEffect(()=>{
if(users){
setUsers(JSON.parse(users));
}
},[users])
For hooks the logic changes slightly, you would have to "hook" your effect with a state in order to update the component, so the component would update (componentDidUpdate) when the hooked state has been updated, you can obviously hook multiple states.
If you choose to not hook any state, the effect would execute only at the mounting of the component just like (componentDidMount())
I don't see the logic that makes you decide when to update the user state since you always get it from the storage, so I will assume that you have some kind of a trigger that makes you verify if the users value has changed in the storage.
so you can refactor your code like this:
const [users, setUsers] = useState([]);
const [userHasChanged, setUserHasChanged] = useState(false);
usEffect(async () => {
// comparing the old users with the new users is not useful since you always fetch the users from the storage, so the optimal is to always set the new array/ object to users, this way you avoid comparing the two objects which is a bit costly.
const newUsers = await AsyncStorage.getItem("users");
setUsers(JSON.parse(newUsers));
setUserHasChanged(false);
}, [userHasChanged])
// some code that triggers userHasChanged, you use setUserHasChaned(true)
So I have a hook that on mount, reads data from an indexedDB and stores that data in its internal state
The problem that I have, is that the indexedDB data gets changed from another component (added/removed), and I need to react to those changes in this hook. I'm not 100% familiar with hooks and how this would be done, but the first thought would be to have as hook dependency the value from the indexedDB.
HOWEVER, the reading of the data from the indexedDB is an async operation and the hook dependency would be a.. promise.
So basically, the flow is as follows:
Component 1 calls the hook like so:
const eventItems = useEventListItems({
sortBy,
sortGroupedBy,
eventTimestamp,
events,
assets,
touchedEventIds,
unsyncedEvents, // <--- this is the one that we need
order,
});
The useEventListItems hook, on mount, reads the data from the indexed DB, stores it in its internal state and returns it:
const { readUnsyncedEvents } = useDebriefStore();
const [unsyncedEvents, setUnsyncedEvents] = useState<number[]>([]);
useEffectAsync(async () => {
const storedUnsyncedEventIds = await readUnsyncedEvents<number[]>();
if (storedUnsyncedEventIds?.data) {
setUnsyncedEvents(storedUnsyncedEventIds.data);
}
}, [setUnsyncedEvents]);
where readUnsyncedEvents is:
export const readUnsyncedEvents = <T>(type: Type): Promise<DebriefStoreEntry<T>> =>
debriefStore
.get(type)
.then((entry) => entry && { data: entry.data, timestamp: entry.timestamp });
The unsyncedEvents from the indexedDB are then changed from another component.
What should happen now, is that the useEventListItems hook should listen to the changes in the IDB and update the unsyncedEvents in its internal state, passing them to the component that uses this hook. How would I achieve this?
My first thought was to have something like this in the useEventListItems hook:
useEffect(() => {
setUnsyncedEvents(..newValueFromIdb);
}, [ await readUnsyncedEvents()]);
but that won't work since it'll be a promise. Is there anyway I can have as hook dependency, a value returned by an async operation?
You can use Context API to refetch the data from IDB.
The idea here is to create a context with a counter variable which will be updated after each IDB update operation. And useEventListItems hook will read that counter variable from context and trigger the useEffect hook.
export const IDBContext = React.createContext({
readFromIDB: null,
setReadFromIDB: () => {}
});
export const IDBContextProvider = ({ children }) => {
const [readFromIDB, setReadFromIDB] = useState(0);
return (
<IDBContext.Provider value={{ readFromIDB, setReadFromIDB }}>
{children}
</IDBContext.Provider>
);
};
This is how your useEventListItems will look like.
const { readUnsyncedEvents } = useDebriefStore();
const [unsyncedEvents, setUnsyncedEvents] = useState<number[]>([]);
const {readFromIDB} = useContext(IDBContext); // this variable will be updated after each IDB update.
useEffectAsync(async () => {
const storedUnsyncedEventIds = await readUnsyncedEvents<number[]>();
if (storedUnsyncedEventIds?.data) {
setUnsyncedEvents(storedUnsyncedEventIds.data);
}
}, [readFromIDB,setUnsyncedEvents]); // added that to dependency array to trigger the hook on value change.
And here are the components:
const IDBUpdateComponent = ()=>{
const {readFromIDB,setReadFromIDB} = useContext(IDBContext);
const updateIDB = ()=>{
someIDBUpdateOpetation().then(res=>{
setReadFromIDB(readFromIDB+1) // update the context after IDB update is successful.
}).catch(e=>{})
}
return(
<div>IDBUpdateComponent</div>
);
}
const IDBConsumerComponent = ()=>{
return (
<div>IDBConsumerComponent</div>
)
}
Just make sure that both the components are wrapped inside the context so that they can access the values.
const App = ()=>{
return(
<div>
<IDBContextProvider>
<IDBUpdateComponent />
<IDBConsumerComponent />
</IDBContextProvider>
</div>
)
}
I've been loving getting into hooks and dealing with all the new fun issues that come up with real-world problems :) Here's one I've run into a couple of times and would love to see how you "should" solve it!
Overview: I have created a custom hook to capsulate some of the business logic of my app and to store some of my state. I use that custom hook inside a component and fire off an event on load.
The issue is: my hook's loadItems function requires access to my items to grab the ID of the last item. Adding items to my dependency array causes an infinite loop. Here's a (simplified) example:
Simple ItemList Component
//
// Simple functional component
//
import React, { useEffect } from 'react'
import useItems from '/path/to/custom/hooks/useItems'
const ItemList = () => {
const { items, loadItems } = useItems()
// On load, use our custom hook to fire off an API call
// NOTE: This is where the problem lies. Since in our hook (below)
// we rely on `items` to set some params for our API, when items changes
// `loadItems` will also change, firing off this `useEffect` call again.. and again :)
useEffect(() => {
loadItems()
}, [loadItems])
return (
<ul>
{items.map(item => <li>{item.text}</li>)}
</ul>
)
}
export default ItemList
Custom useItems Hook
//
// Simple custom hook
//
import { useState, useCallback } from 'react'
const useItems = () => {
const [items, setItems] = useState([])
// NOTE: Part two of where the problem comes into play. Since I'm using `items`
// to grab the last item's id, I need to supply that as a dependency to the `loadItems`
// call per linting (and React docs) instructions. But of course, I'm setting items in
// this... so every time this is run it will also update.
const loadItems = useCallback(() => {
// Grab our last item
const lastItem = items[items.length - 1]
// Supply that item's id to our API so we can paginate
const params = {
itemsAfter: lastItem ? lastItem.id : nil
}
// Now hit our API and update our items
return Api.fetchItems(params).then(response => setItems(response.data))
}, [items])
return { items, loadItems }
}
export default useItems
The comments inside the code should point out the problem, but the only solution I can come up with right now to make linters happy is to supply params TO the loadItems call (ex. loadItems({ itemsAfter: ... })) which, since the data is already in this custom hook, I am really hoping to not have to do everywhere I use the loadItems function.
Any help is greatly appreciated!
Mike
If you plan to run an effect just once, omit all dependencies:
useEffect(() => {
loadItems();
}, []);
You could try with useReducer, pass the dispatch as loadItems as it never changes reference. The reducer only cares if the action is NONE because that is what the cleanup function of useEffect does to clean up.
If action is not NONE then state will be set to last item of items, that will trigger useEffect to fetch using your api and when that resolves it'll use setItems to set the items.
const NONE = {};
const useItems = () => {
const [items, setItems] = useState([]);
const [lastItem, dispatch] = useReducer(
(state, action) => {
return action === NONE
? NONE
: items[items.length - 1];
},
NONE
);
useEffect(() => {
//initial useEffect or after cleanup, do nothing
if (lastItem === NONE) {
return;
}
const params = {
itemsAfter: lastItem ? lastItem.id : Nil,
};
// Now hit our API and update our items
Api.fetchItems(params).then(response =>
setItems(response)
);
return () => dispatch(NONE); //clean up
}, [lastItem]);
//return dispatch as load items, it'll set lastItem and trigger
// the useEffect
return { items, loadItems: dispatch };
};
I was starting to build some of my new components with the new and shiny React Hooks. But I was using a lot of async api calls in my components where I also show a loading spinner while the data is fetching. So as far as I understood the concept this should be correct:
const InsideCompontent = props => {
const [loading, setLoading] = useState(false);
useEffect(() => {
...
fetchData()
...
},[])
function fetchData() {
setFetching(true);
apiCall().then(() => {
setFetching(false)
})
}
}
So this is just my initial idea of how this might work. Just a small example.
But what happens if the parent component has now a condition changed that this component gets unmounted before the async call is finished.
Is there somehow a check where I can check if the component is still mounted before I call the setFetching(false) in the api callback?
Or am I missing something here ?
Here is working example :
https://codesandbox.io/s/1o0pm2j5yq
EDIT:
There was no really issue here. You can try it out here:
https://codesandbox.io/s/1o0pm2j5yq
The error was from something else, so with hooks you don't need to check if the component is mounted or not before doing a state change.
Another reason why to use it :)
You can use the useRef hook to store any mutable value you like, so you could use this to toggle a variable isMounted to false when the component is unmounted, and check if this variable is true before you try to update the state.
Example
const { useState, useRef, useEffect } = React;
function apiCall() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Foo");
}, 2000);
});
}
const InsideCompontent = props => {
const [state, setState] = useState({ isLoading: true, data: null });
const isMounted = useRef(true);
useEffect(() => {
apiCall().then(data => {
if (isMounted.current) {
setState({ isLoading: false, data });
}
});
return () => {
isMounted.current = false
};
}, []);
if (state.isLoading) return <div>Loading...</div>
return <div>{state.data}</div>;
};
function App() {
const [isMounted, setIsMounted] = useState(true);
useEffect(() => {
setTimeout(() => {
setIsMounted(false);
}, 1000);
}, []);
return isMounted ? <InsideCompontent /> : null;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Here's a Hook for fetching data that we use internally. It also allows manipulating the data once it's fetched and will throw out data if another call is made prior to a call finishing.
https://www.npmjs.com/package/use-data-hook
(You can also just include the code if you don't want an entire package)
^ Also this converts to JavaScript by simply removing the types.
It is loosely inspired by this article, but with more capabilities, so if you don't need the data-manipulation you can always use the solution in that article.
Assuming that this is the error you've encountered:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
React complains and hints you at the same time. If component has to be unmounted but there is an outstanding network request, it should be cancelled. Returning a function from within useEffect is a mechanism for performing any sort of cleanup required (docs).
Building on your example with setTimeout:
const [fetching, setFetching] = useState(true);
useEffect(() => {
const timerId = setTimeout(() => {
setFetching(false);
}, 4000);
return () => clearTimeout(timerId)
})
In case component unmounts before the callback fires, timer is cleared and setFetching won't be invoked.