ReactJS Hooks dependency an async operation - javascript

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>
)
}

Related

onSnapshot in firebase fires multiple times [duplicate]

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

Initialize state with async function data NextJS? [duplicate]

My component relies on local state (useState), but the initial value should come from an http response.
Can I pass an async function to set the initial state? How can I set the initial state from the response?
This is my code
const fcads = () => {
let good;
Axios.get(`/admin/getallads`).then((res) => {
good = res.data.map((item) => item._id);
});
return good;
};
const [allads, setAllads] = useState(() => fcads());
But when I try console.log(allads) I got result undefined.
If you use a function as an argument for useState it has to be synchronous.
The code your example shows is asynchronous - it uses a promise that sets the value only after the request is completed
You are trying to load data when a component is rendered for the first time - this is a very common use case and there are many libraries that handle it, like these popular choices: https://www.npmjs.com/package/react-async-hook and https://www.npmjs.com/package/#react-hook/async. They would not only set the data to display, but provide you a flag to use and show a loader or display an error if such has happened
This is basically how you would set initial state when you have to set it asynchronously
const [allads, setAllads] = useState([]);
const [loading, setLoading] = useState(false);
React.useEffect(() => {
// Show a loading animation/message while loading
setLoading(true);
// Invoke async request
Axios.get(`/admin/getallads`).then((res) => {
const ads = res.data.map((item) => item._id);
// Set some items after a successful response
setAllAds(ads):
})
.catch(e => alert(`Getting data failed: ${e.message}`))
.finally(() => setLoading(false))
// No variable dependencies means this would run only once after the first render
}, []);
Think of the initial value of useState as something raw that you can set immediately. You know you would be display handling a list (array) of items, then the initial value should be an empty array. useState only accept a function to cover a bit more expensive cases that would otherwise get evaluated on each render pass. Like reading from local/session storage
const [allads, setAllads] = useState(() => {
const asText = localStorage.getItem('myStoredList');
const ads = asText ? JSON.parse(asText) : [];
return ads;
});
You can use the custom hook to include a callback function for useState with use-state-with-callback npm package.
npm install use-state-with-callback
For your case:
import React from "react";
import Axios from "axios";
import useStateWithCallback from "use-state-with-callback";
export default function App() {
const [allads, setAllads] = useStateWithCallback([], (allads) => {
let good;
Axios.get("https://fakestoreapi.com/products").then((res) => {
good = res.data.map((item) => item.id);
console.log(good);
setAllads(good);
});
});
return (
<div className="App">
<h1> {allads} </h1>
</div>
);
}
Demo & Code: https://codesandbox.io/s/distracted-torvalds-s5c8c?file=/src/App.js

how to save array with useEffect React Native

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)

Why does my useEffect react function run when the page loads although I am giving it a second value array

Why is my useEffect react function running on every page load although giving it a second value array with a query variable?
useEffect( () => {
getRecipes();
}, [query]);
Shouldn't it only run when the query state variable changes? I have nothing else using the getRecipes function except of the useEffect function.
import React, {useEffect, useState} from 'react';
import './App.css';
import Recipes from './components/Recipes/Recipes';
const App = () => {
// Constants
const APP_ID = '111';
const APP_KEY = '111';
const [recipes, setRecipes] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('');
const [showRecipesList, setShowRecipesList] = useState(false);
// Lets
let recipesList = null;
// Functions
useEffect( () => {
getRecipes();
}, [query]);
// Get the recipie list by variables
const getRecipes = async () => {
const response = await fetch(`https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=3&calories=591-722&health=alcohol-free`);
const data = await response.json();
console.log(data.hits);
setRecipes(data.hits);
}
// Update the search constant
const updateSearch = e => {
console.log(e.target.value);
setSearch(e.target.value);
}
const runQuery = e => {
e.preventDefault();
setQuery(search);
}
// List recipes if ready
if (recipes.length) {
console.log(recipes.length);
recipesList = <Recipes recipesList={recipes} />
}
return (
<div className="App">
<form className='search-app' onSubmit={ runQuery }>
<input
type='text'
className='search-bar'
onChange={ updateSearch }
value={search}/>
<button
type='submit'
className='search-btn' > Search </button>
</form>
<div className='recipesList'>
{recipesList}
</div>
</div>
);
}
export default App;
Following this: https://www.youtube.com/watch?v=U9T6YkEDkMo
A useEffect is the equivalent of componentDidMount, so it will run once when the component mounts, and then only re-run when one of the dependencies defined in the dependency array changes.
If you want to call getRecipes() only when the query dependency has a value, you can call it in a conditional like so:
useEffect(() => {
if(query) {
getRecipes()
}
}, [query])
Also, as your useEffect is calling a function (getRecipes) that is declared outside the use effect but inside the component, you should either move the function declaration to be inside the useEffect and add the appropriate dependencies, or wrap your function in a useCallback and add the function as a dependency of the useEffect.
See the React docs for information on why this is important.
UseEffect hook work equivalent of componentDidMount, componentDidUpdate, and componentWillUnmount combined React class component lifecycles.but there is a different in time of acting in DOM.componentDidMount and useEffect run after the mount. However useEffect runs after the paint has been committed to the screen as opposed to before. This means you would get a flicker if you needed to read from the DOM, then synchronously set state to make new UI.useLayoutEffect was designed to have the same timing as componentDidMount. So useLayoutEffect(fn, []) is a much closer match to componentDidMount() than useEffect(fn, []) -- at least from a timing standpoint.
Does that mean we should be using useLayoutEffect instead?
Probably not.
If you do want to avoid that flicker by synchronously setting state, then use useLayoutEffect. But since those are rare cases, you'll want to use useEffect most of the time.

How can I use a provider value to useMutation and also dispatch the changes to state afterwards?

I would like to use a Context.Provider value to handle both mutating and dispatching similar changes. I have read about React-Apollo's onComplete method, but I'm not sure which approach will cover more cases where I need to both mutate and dispatch state. Here's what I have:
const CartContext = React.createContext<{
state: State
dispatch: Dispatch<AnyAction>
cartApi: any
}>({ state: initialState, dispatch: () => null, cartApi: mutateUserProductsAndUpdateCart })
function CartProvider({ children }: { children?: React.ReactNode }) {
const [state, dispatch] = useReducer<Reducer<State, AnyAction>>(reducer, initialState)
// feel like i need to do something with the hook here to avoid invariant violation side effects
const [updateUserProducts] = useUpdateUserProducts()
return (
<CartContext.Provider value={{ state, dispatch, cartApi: mutateUserProductsAndUpdateCart}}>
{children}
</CartContext.Provider>
)
}
export const useCartState = () => useContext(CartContext)
And here's what I would like to do with my mutateUserProductsAndUpdateCart:
const mutateUserProductsAndUpdateCart = async (_mutation: any, _mutationParams: any, _dispatchObject: AnyObject) => {
// apollo mutation
const updateUserProductsResult = await updateUserProducts(_mutationParams)
if (updateUserProductsResult.error) throw Error("wtf")
// useReducer dispatch
dispatch(_dispatchObject)
return
}
and here is how I would like to access this on another component:
const { cartApi } = useCartState()
const addProductToCart = async () => {
const result = await cartApi({
mutation,
mutationVariables,
dispatchObject})
}
I feel like this article is sort of the direction I should be taking, but I'm very lost on implementation here. Thanks for reading.
I'm not sure this directly answers your question, but have you considered just using Apollo Client? It looks like you are trying to do two things:
Save items added to the cart to the server
Update the cart locally in the cache
It seems like you could skip creating your own context altogether and just create a hook for mutating (saving your cart items) and then update your local cache for cart items. Something like this:
import gql from 'graphql-tag';
import useMutation from '#apollo/client';
export const mutation = gql`
mutation($items: [CartItem]!) {
saveCartItems(items: $items) {
id
_list_of_properties_for_cache_update_
}
}
`;
export const useSaveCartItems = mutationProps => {
const [saveCartItems, result] = useMutation(
mutation,
mutationProps
);
return [
items => {
saveCartItems({
update: (cache, { data }) => {
const query = getCartQuery; // Some query to get the cart items from the cache
const results = cache.readQuery({ query });
// We need to add new items only; existing items will auto-merge
// Get list of new items from results
const data = []; // List of new items; IMPLEMENT ME!!!
cache.writeQuery({ query, data });
},
variables: { items },
});
},
result,
];
};
Then in your useCartState hook you can just query the local cache for the items using the same query you used for the update and return that. By using the update function you can fix your local cache and anybody can access it from anywhere, just use the hook. I know that isn't exactly what you asked for, but I hope it helps.
Apollo client documentation on handling this may be found here.

Categories