null is not an object (using async storage) - react-native

I'm trying to implement secure store (like async storage) into my testproject and it works. The only thing that doesn't work appears to be my load() useEffect that runs every time I start a new session.
So every time I start the session "null is not an object" appears and throws errors at every part where I'm looking for my list on my home-screen (my state is called goals).
but if I temporarily turn it off, add a goal to my list, and then turn it on - it works fine every time I restart the app
I feel like I need to write a condition in my load() statement but I can't figure out what, I think the app runs the load() and gets stuck at the statements where I use goals before I've added any. Can someone help me here? I've tried if (goals !== null) but it doesn't work.
const [goals, setGoals] = useState([])
const load = async() => {
if (goals !== null) {
try {
const goalsValue = await SecureStore.getItemAsync('Goals');
setGoals(JSON.parse(goalsValue))
} catch (err) {
alert(err)
}
}
}
useEffect(()=> {
load()
},[])

So what is happening is that there's nothing inside goalsValue so when you try parsing it you get an error. To avoid that you should add an if statement that checks if it's empty or not
const [goals, setGoals] = useState([])
const load = async() => {
if (goals !== null) {
try {
const goalsValue = await SecureStore.getItemAsync('Goals');
if(goalsValue) setGoals(JSON.parse(goalsValue))
}catch (err) {alert(err)}
}}
useEffect(()=> {
load()
},[])
Try this and let me know if it works ☺️

Related

React-Native async-storage putting functions in common js file?

In React-native (to which I'm relatively new), I use async storage to store a list of favourites between sessions.
eg
const StoreFavourites = async (sFavourites) => {
try {
await AsyncStorage.setItem(storageKeyFavourites, sFavourites);
} catch (e) {
// saving error
Alert.alert('Unable to store favourites')
}
}
const getFavourites = async () => {
try {
const value = await AsyncStorage.getItem(storageKeyFavourites)
if (value !== null) {
checkPageIsFavourite(value, false)
}
} catch (e) {
// err
}
}
As this will be used on multiple pages, I thought it would be good to store the code once in an external file, so I moved it to
PersistenStorage.js
and in the page, added
import * as pStorage from './PersistentStorage.js'
From my main component, I can then call
pStorage.StoreFavourites('somevalue')
and
pStorage.getFavourites()
which work, but I can't find a way for the getFavourites to call functions in the main component (ie which imports the external code)
checkPageIsFavourite(value, false)
can't run as it can't access this function from the external file.
Hope this is clear and is there a way around this? in other words, once the async get has completed, call functions in the main file.

Async Storage / Secure Store help for React Native

I'm doing a test app to learn react native, and I'm trying to use secure store (a bit like async storage) to store my individual goals and save them. So far it's working, however when I refresh the app only the last goal I entered gets loaded.
Where am I going wrong here? In my console log the full array is shown with both the old and the new ones I add, then I refresh and I only have one left.
const [goals, setGoals] = useState([])
const addGoal = async (goal) => {
try{
const goalJson = JSON.stringify({text: goal, id:`${Math.random()}`, todos:[], date: Date.now(), percentage:0})
await SecureStore.setItemAsync("Goal", goalJson)
load()
}
catch (err) {alert(err)}
}
const load = async() => {
try {
const goalValue = await SecureStore.getItemAsync("Goal")
const parsed = JSON.parse(goalValue)
if(goals !== null) {
setGoals([...goals, parsed])
console.log(goals)
}
}catch (err) {alert(err)}
}
useEffect(()=> {
load()
},[])
SecureStore is like a key-value database, so currently you're always writing to the same key Goal and your addGoal function is erasing the previous value with goalJson content.
Instead, load once the goals from storage, then update the goals state when a new goal is added, and write them all to on storage each time goals value is updated.
This how effects works, by "reacting" to a change of value. This is just a little bit more complicated because of SecureStorage async functions.
Here is my (untested) improved code. I renamed the storage key from Goal to Goals.
const [goals, setGoals] = useState([])
const [loaded, setLoaded] = useState(false)
useEffect(()=> {
async function load() {
try {
const goalsValue = await SecureStore.getItemAsync("Goals")
const goalsParsed = JSON.parse(goalsValue)
if (goalsParsed !== null) {
setGoals(goalsParsed)
}
setLoaded(true)
} catch (err) { alert(err) }
}
load()
}, []) // load only when component mount
const addGoal = (text) => {
const goal = { text, id:`${Math.random()}`, todos:[],
date: Date.now(), percentage:0 }
setGoals([...goals, goal])
})
useEffect(() => {
async function saveGoals() {
try {
// save all goals to storage
const goalsJson = JSON.stringify(goals)
await SecureStore.setItemAsync("Goals", goalsJson)
}
catch (err) {alert(err)}
}
if (loaded) { // don't save before saved goals were loaded
saveGoals();
}
}, [goals, loaded]) // run the effect each time goals is changed

nextTick() not triggering DOM update

I'm creating a messaging app and I'm having some trouble with scrolling to the bottom of an ion-content element when a new message is added to an array. I'm using the scrollToBottom() method that comes with ion-content, and I'm using the Composition API in Vue 3.
Consider this snippet:
setup(props) {
const replyContent = ref("")
const messages = ref([])
// References to ion-content in the template
const ionContent = ref(null)
const reply = async () => {
const message = await replyToThread(props.threadId, replyContent.value).then((message) => message)
messages.value.push(message)
nextTick(() => {
console.log("DOM updated!")
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
})
}
return { replyContent, messages, ionContent, reply }
}
replyToThread() performs an API call and returns the new message, and nextTick() should ensure me that the DOM has been updated so that I can have my way with it. The console does successfully log "DOM updated!", but no scrolling to the bottom happens.
But, and somehow this works every time nextTick() doesn't, when I replace the nextTick() code block with the following, it works flawlessly:
setTimeout(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
}, 200)
I have to set the timeout at around 200 ms, otherwise it doesn't work. But relying on this when something fancy like nextTick() should do the trick feels quite dirty. Does anyone know why this is happening?
That is because nextTick() only guarantees that the actual DOM has been updated: it doesn't mean that the browser has actually finished the layout of the page. That is the reason why you need an arbitrary timeout to ensure the scrolling works, because after 200ms the browser is likely to be done laying things out based on the updated DOM.
To fix this you will probably need to rely on window.requestAnimationFrame:
nextTick(() => {
window.requestAnimationFrame(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
});
});
If this feels like too much nesting for you, you can create a method that returns a promise based on rAF:
methods: {
rAF: function() {
return new Promise(r => window.requestAnimationFrame(r));
}
}
Then it's a matter of ensuring promises returned by both nextTick() and rAF() are resolved before scrolling:
await nextTick();
await this.rAF();
if (ionContent.value) {
ionContent.value.$el.scrollToBottom();
}

Simplest way to merge two useState values

I have a react native hook where trips must be updated every time a createdTrips is added to the state:
const [trips, setTrips] = useState([]);
function fetchCreatedTrips() {
try {
API.graphql(graphqlOperation(onCreateTrip)).subscribe({
next: (result) => {
console.log(result);
const updatedTrips = [...trips, result.value.data.onCreateTrip]
setTrips(updatedTrips)
}
})
} catch (err) { console.log(err) }
}
Now, when i first open the screen, it renders all the trips items of the list.
However with the current code, after i create a trip and go back to that screen, it doesn't currently return all the trips + the newly created one, but only one, that is the newly created one. How can i return all the items of the list? Sorry in advance, i'm a beginner.
While setting the new state on your effect you should include the previous values of the trips. You can use the spread operator to do the same.
const createdTrips = [result.value.data.onCreateTrip];
setTrips([...createdTrips, ...trips]);
or better merge the newly created trip in the updateTrips variable and then set it as the state value,
const udpatedTrips = [...trips,result.value.data.onCreateTrip];
setTrips(udpatedTrips);
The problem in your code is you are firing those two methods at the same time and there can be race conditions, so you see random updates on the screen.
Ideally, you need to bring in some consistency in the API calls and the state update. So first fetchTrips()->then->fetchCreatedTrips(). Try the below code wherein I don't update the state immediately in fetchTrips() but rather pass on the results to fetchCreatedTrips() which completes the API call and updates the state together.
const [trips, setTrips] = useState([]);
useEffect(() => {
fetchTrips();
}, [])
async function fetchTrips() {
try {
const tripData = await API.graphql(graphqlOperation(listTrips));
const trips = tripData.data.listTrips.items
fetchCreatedTrips(trips);
} catch (err) { console.log(err) }
}
function fetchCreatedTrips(fetchedTrips) {
try {
API.graphql(graphqlOperation(onCreateTrip)).subscribe({
next: (result) => {
console.log(result);
const updatedTrips = [...fetchedTrips,...updatedTrips];
setTrips(updatedTrips)
}
})
} catch (err) { console.log(err) }
}
PS: Please handle exceptions correctly.

The code below AsyncStorage does not run

I am working on my first React Native app. I am trying to learn how AsyncStorage works, but I somehow can't make it work even though it is said to be simple.
I am trying to save the data to storage, whenever the store is updated.
The problem is that the code below the line:
"await AsyncStorage.setItem("TODOS", jTodo)" does not seem to run. I don't know what the problem is...
const unsubscribe = store.subscribe(save);
async function save(){
try {
const todos = store.getState().todos
console.log(todos)
const jTodo = JSON.stringify(todos)
await AsyncStorage.setItem("TODOS", jTodo)
console.log("saving 2: " + todos);
} catch (e) {
console.error('Failed to save todos.' + todos)
}
}
The same is the case when I try to load data from storage. Again the code below the line: "const jTodos = await AsyncStorage.getItem('TODOS')" does not seem to run.
async function load() {
try {
console.log("so far even better")
const jTodos = await AsyncStorage.getItem('TODOS')
const todos = JSON.parse(jTodos);
console.log(todos);
todos.map((todo) => this.props.addTodo(todo))
} catch (e) {
console.error('Failed to load todos.')
}
}
load();
I hope some of you can point out what the problem is! Thank you in advance!!
If you are using android there is known issues with it not working.
"cold boot" your emulator from android studio.
https://github.com/facebook/react-native/issues/14101