React Native I can not store an array with AsyncStorage - reactjs

I am newbie in React Native and I am trying to store and get an array with AsyncStorage in ReactNative.
I have two problems.
First, I do not know why but when I storage data, it only works the second time but I am calling first the set of useState.
const handleAddTask = () => {
Keyboard.dismiss();
setTaskItems([...taskItems, task]);
storeData(taskItems);
};
Second, how can I call the getData function to get all the data and show it? Are there something like .onInit, .onInitialize... in ReactNative? Here is my full code
const [task, setTask] = useState();
const [taskItems, setTaskItems] = useState([]);
const handleAddTask = () => {
Keyboard.dismiss();
setTaskItems([...taskItems, task]);
storeData(taskItems);
};
const completeTask = (index) => {
var itemsCopy = [...taskItems];
itemsCopy.splice(index, 1);
setTaskItems(itemsCopy);
storeData(taskItems);
}
const storeData = async (value) => {
try {
await AsyncStorage.setItem('#tasks', JSON.stringify(value))
console.log('store', JSON.stringify(taskItems));
} catch (e) {
console.log('error');
}
}
const getData = async () => {
try {
const value = await AsyncStorage.getItem('#tasks')
if(value !== null) {
console.log('get', JSON.parse(value));
}
} catch(e) {
console.log('error get');
}
}

Updating state in React is not super intuitive. It's not asynchronous, and can't be awaited. However, it's not done immediately, either - it gets put into a queue which React optimizes according to its own spec.
That's why BYIRINGIRO Emmanuel's answer is correct, and is the easiest way to work with state inside functions. If you have a state update you need to pass to more than one place, set it to a variable inside your function, and use that.
If you need to react to state updates inside your component, use the useEffect hook, and add the state variable to its dependency array. The function in your useEffect will then run whenever the state variable changes.

Even if you're update state setTaskItems([...taskItems, task]) before save new data in local storage, storeData(taskItems) executed before state updated and save old state data.
Refactor handleAddTask as below.
const handleAddTask = () => {
Keyboard.dismiss();
const newTaskItems = [...taskItems, task]
setTaskItems(newTaskItems);
storeData(newTaskItems);
};

Related

useEffect with recyclerlistview (React Native) and exhaustive-deps

I am using a useEffect to fetch data on component render and set it to React Native RecyclerView as a data provider. I then set the state locally by appending to the existing dataProviderState. The problem is when I add the state as a dependency to the array the performance is terrible, as now the useEffect is called every time the list changes, so whenever the list is scrolled. How can I rewrite this hook to make this more performant while also following the exhaustive deps rule?
const dataProvider = new DataProvider((r1, r2) => r1 !== r2);
const rows = dataProvider.cloneWithRows([]);
const [dataProviderState, setDataProviderState] = useState(rows);
useEffect(() => {
const handleFetchContacts = async () => {
try {
const fetchedContacts = await fetchContacts();
setContacts(fetchedContacts);
setDataProviderState(dataProviderState.cloneWithRows(fetchedContacts));
} catch (error) {
logger.error(error);
}
};
const requestPermissionAndFetchContacts = async () => {
try {
const permission = await requestPermissionToContacts();
setPermissions({contacts: permission});
if (permission) {
handleFetchContacts();
}
} catch (err) {
logger.error(err);
setTimeout(() => requestPermissionAndFetchContacts(), 2000);
}
};
requestPermissionAndFetchContacts();
}, [handleFetchContacts]);
^adding dataProviderState to the dependency array here makes the list slow
I was able to fix this by referencing the previous state in the callback, so exhaustive-deps stopped complaining about me referencing the state value directly inside the component, but not having it in the array:
setDataProviderState(state => state.cloneWithRows(fetchedContacts));

Use Effect and Firebase causing infinite loop

I am using firebase and trying to load all my data at the start of the app using this code:
const [books, setBooks] = useState<BookType[]>([]);
const bookCollectionRef = collection(db, "books");
useEffect(() => {
const getBooks = async () => {
const data = await getDocs(bookCollectionRef);
const temp: BookType[] = data.docs.map((doc) => {
const book: BookType = {
//set properties
};
return book;
});
setBooks(temp);
};
getBooks();
}, [bookCollectionRef]);
This useEffect is getting run constantly leading me to believe that I have made an infinite loop. I don't see why this would be happening because I don't think I am updating bookCollectionRef inside the useEffect hook. Is there possibly a problem where firebase collection references constantly get updated? Any ideas help!
From what I can tell it may be that collection(db, "books") returns a new collection reference each time the component rerenders. Any time the component renders (triggered by parent rerendering, props updating, or updating the local books state) the new bookCollectionRef reference triggers the useEffect hook callback and updates the books state, thus triggering a rerender. Rinse and repeat.
If you don't need to reference the collection outside of the useEffect hook then simply omit bookCollectionRef and reference the collection directly. Trigger the useEffect only when the db value updates.
const [books, setBooks] = useState<BookType[]>([]);
useEffect(() => {
const getBooks = async () => {
const data = await getDocs(collection(db, "books"));
const temp: BookType[] = data.docs.map((doc) => {
const book: BookType = {
//set properties
};
return book;
});
setBooks(temp);
};
getBooks();
}, [db]);
If you only need to run the effect once when the component mounts then remove all dependencies, i.e. use an empty dependency array.

Not awaiting for data in useEffect

I have a useEffect in my component that is waiting for data from the context so that it can set it in state. But its not waiting for the state and is moving on to the next line of code to set isLoading to false.
I'd like it to wait for the data so that I can render the loading.
I tried setting the isFetchingData in the context but I had run into problems where if another component calls it first it would set the isFetchingData state to false.
First call to ReactContext is setting the isLoading sate to false
It is fine for results to come back with no records. The component would render 'No records found'. Therefore, I cannot check the length on state to say if length is zero then keep loading.
Following is my code:
Context
const [activeEmployees, setActiveEmployees] = useState([]);
const [terminatedEmployees, setTerminatedEmployees] = useState([]);
useEffect(() => {
getEmployees()
.then(response => {
/// some code...
setActiveEmployees(response.activeEmployees)
setTerminatedEmployees(response.terminatedEmployees)
});
});
Component
const EmployeesTab = () => {
const { activeEmployees, terminatedEmployees } = useContext(BlipContext);
//Component states
const [isFetchingData, setIsFetchingData] = useState(true);
const [newEmployees, setNewEmployees] = useState([]);
const [oldEmployees, setOldEmployees] = useState([]);
useEffect(() => {
async function getData() {
await setNewEmployees(activeEmployees);
await setOldEmployees(terminatedEmployees);
setIsFetchingData(false);
}
getData();
}, [activeEmployees, terminatedEmployees, isFetchingData]);
if(isFetchingData) {
return <p>'Loading'</p>;
}
return (
// if data is loaded render this
);
};
export default EmployeesTab;
Since you have useState inside your useContext, I don't see the point of storing yet again the activeEmployees in another state.
If you want to have a local loading variable it could something like:
const loading = !(activeEmployees.length && terminatedEmployees.length);
This loading will update whenever getEmployees changes.
And to answer you question, the reason await is not having an effect is because setState is synchronous.

How to wait for multiple state updates in multiple hooks?

Example
In my scenario I have a sidebar with filters.. each filter is created by a hook:
const filters = {
customerNoFilter: useFilterForMultiCreatable(),
dateOfOrderFilter: useFilterForDate(),
requestedDevliveryDateFilter: useFilterForDate(),
deliveryCountryFilter: useFilterForCodeStable()
//.... these custom hooks are reused for like 10 more filters
}
Among other things the custom hooks return currently selected values, a reset() and handlers like onChange, onRemove. (So it's not just a simple useState hidden behind the custom hooks, just keep that in mind)
Basically the reset() functions looks like this:
I also implemented a function to clear all filters which is calling the reset() function for each filter:
const clearFilters = () => {
const filterValues = Object.values(filters);
for (const filter of filterValues) {
filter.reset();
}
};
The reset() function is triggering a state update (which is of course async) in each filter to reset all the selected filters.
// setSelected is the setter comming from the return value of a useState statement
const reset = () => setSelected(initialSelected);
Right after the resetting I want to do stuff with the reseted/updated values and NOT with the values before the state update, e.g. calling API with reseted filters:
clearFilters();
callAPI();
In this case the API is called with the old values (before the update in the reset())
So how can i wait for all filters to finish there state updated? Is my code just badly structured? Am i overseeing something?
For single state updates I could simply use useEffect but this would be really cumbersome when waiting for multiple state updates..
Please don't take the example to serious as I face this issue quite often in quite different scenarios..
So I came up with a solution by implementing a custom hook named useStateWithPromise:
import { SetStateAction, useEffect, useRef, useState } from "react";
export const useStateWithPromise = <T>(initialState: T):
[T, (stateAction: SetStateAction<T>) => Promise<T>] => {
const [state, setState] = useState(initialState);
const readyPromiseResolverRef = useRef<((currentState: T) => void) | null>(
null
);
useEffect(() => {
if (readyPromiseResolverRef.current) {
readyPromiseResolverRef.current(state);
readyPromiseResolverRef.current = null;
}
/**
* The ref dependency here is mandatory! Why?
* Because the useEffect would never be called if the new state value
* would be the same as the current one, thus the promise would never be resolved
*/
}, [readyPromiseResolverRef.current, state]);
const handleSetState = (stateAction: SetStateAction<T>) => {
setState(stateAction);
return new Promise(resolve => {
readyPromiseResolverRef.current = resolve;
}) as Promise<T>;
};
return [state, handleSetState];
};
This hook will allow to await state updates:
const [selected, setSelected] = useStateWithPromise<MyFilterType>();
// setSelected will now return a promise
const reset = () => setSelected(undefined);
const clearFilters = () => {
const promises = Object.values(filters).map(
filter => filter.reset()
);
return Promise.all(promises);
};
await clearFilters();
callAPI();
Yey, I can wait on state updates! Unfortunatly that's not all if callAPI() is relying on updated state values ..
const [filtersToApply, setFiltersToApply] = useState(/* ... */);
//...
const callAPI = () => {
// filtersToApply will still contain old state here, although clearFilters() was "awaited"
endpoint.getItems(filtersToApply);
}
This happens because the executed callAPI function after await clearFilters(); is is not rerendered thus it points to old state. But there is a trick which requires an additional useRef to force rerender after filters were cleared:
useEffect(() => {
if (filtersCleared) {
callAPI();
setFiltersCleared(false);
}
// eslint-disable-next-line
}, [filtersCleared]);
//...
const handleClearFiltersClick = async () => {
await orderFiltersContext.clearFilters();
setFiltersCleared(true);
};
This will ensure that callAPI was rerendered before it is executed.
That's it! IMHO a bit messy but it works.
If you want to read a bit more about this topic, feel free to checkout my blog post.

setState with value of previously returned custom hook

I couldn't find a similar question here, so here it goes:
I created a custom hook useBudget to fetch some data.
const initalState = {
budget_amount: 0,
};
const useBudget = (resource: string, type: string) => {
const [budgetInfo, setBudget] = useState(initalState);
useEffect(
() => {
(async (resource, type) => {
const response = await fetchBudgetInfo(resource, type);
setBudget(response);
})(resource, type);
}, []);
return [budgetInfo];
};
And on the component that uses that hook, I have something like this:
const [budgetInfo] = useBudget(resource, type);
const [budgetForm, setBudgetForm] = useState({ warningMsg: null, errorMsg: null, budget: budgetInfo.budget_amount });
The problem is: The initial state of this component does not update after the fetching. budget renders with 0 initially and keeps that way. If console.log(budgetInfo) right afterwards, the budget is there updated, but the state is not.
I believe that this is happening due to the asynchronicity right? But how to fix this?
Thanks!
I could get to a fix, however, I am not 100% that this is the best/correct approach. As far as I could get it, due to the asynchronicity, I am still reading the old state value, and a way to fix this would be to set the state inside useEffect. I would have:
const [budgetInfo] = useBudget(resource, type);
const [appState, setAppState] = useState({ budget: budgetInfo.budget_amount });
useEffect(
() => {
setAppState({ budget: budgetInfo.budget_amount });
}, [budgetInfo]);
But it's working now!
Working example: https://stackblitz.com/edit/react-wiewan?file=index.js
Effects scheduled with useEffect don’t block the browser from updating the screen - that's why 0 (initialState) is displayed on the screen. After the value is fetched, the component stays the same as there is no change in its own state (budgetForm).
Your solution updates component's state once budgetInfo is fetched hence triggering a re-render, which works but seems to be rather a workaround.
useBudget could be used on its own:
const useBudget = (resource, type) => {
const [budgetInfo, setBudget] = useState(initalState);
const fetchBudgetInfo = async () => {
const response = await (new Promise((resolve) => resolve({ budget_amount: 333 })))
setBudget(response)
}
useEffect(() => {
fetchBudgetInfo(resource, type)
}, [])
return budgetInfo
};
const App = props => {
const { budget_amount } = useBudget('ok', 'ok')
return (
<h1>{budget_amount}</h1>
);
}
fetchBudgetInfo is split out since if we make effect function async (useEffect(async () => { ... })), it will be implicitly returning a promise and this goes against its design - the only return value must be a function which is gonna be used for cleaning up. Docs ref
Alternatively, consider retrieving data without a custom hook:
const fetchBudgetInfo = async (resource, type) => {
const response = await fetch(resource, type)
return response
}
useEffect(() => {
const response = fetchBudgetInfo(resource, type)
setAppState((prevState) => { ...prevState, budget: response.budget_amount });
}, []);
Notably, we are manually merging old state with the new value, which is necessary if appState contains several values - that's because useState doesn't shallowly merge objects (as this.setState would) but instead completely replaces state variable. Doc ref
On a somewhat related note, there is nothing wrong with using object to hold state, however using multiple state variables offers few advantages - more precise naming and the ability to do individual updates.

Resources