Can't add object to existing array (useState react native) - javascript

I have addText() that runs on click event
const [list, setList] = useState([])
const [value, setValue] = useState("")
useEffect(() => {
getObjectItem("tasks")
.then(t => setList(t.item))
.catch(e => { console.log(e) })
}), []
// A function that add data to the list array
function addText(text) {
console.log(list);
if (value !== "") {
setList(prev =>
[...prev,
{ text: text, isSelected: false }] // Adding a JS Object
)
setObjectItem("tasks", list);
setValue("")
} else {
alert("Please type in something!")
}
}
Output from console.log(list):
Array [
Object {
"isSelected": true,
"text": "Test",
}
]
getObjectItem("tasks") function:
const getObjectItem = async (name) => {
try {
const jsonItem = await AsyncStorage.getItem(name)
const item = JSON.parse(jsonItem)
return {
status: 'success',
name: name,
item: item
}
} catch (err) {
return {
status: 'error',
name: name,
error: err
}
}
}
Why can't I add values to the existing list array with setList() in addText() function?

Setting state is asynchronous.
In addText you write:
setObjectItem("task", list)
which will set the value in AsyncStorage to whatever list was, not what it will be after the state has been updated. The easiest solution is to create the new array then set it to state and AsyncStorage.

Try to put
.then(t => setList([t.item]))
instead of what you wrote

Related

React & Typescript - custom hook to pull status updates resets back to initial status

Situation:
Im trying to write a custom hook that allows me to fetch data and launches a setInterval which polls status updates every two seconds. Once all data is processed, the interval is cleared to stop polling updates.
Problem:
My problem is that the function passed to setInterval only has the initial state (empty array) although it has already been updated. Either it is resetting back to the initial state or the function has an old reference.
Code:
Link to Codesandbox: https://codesandbox.io/s/pollingstate-9wziw7?file=/src/Hooks.tsx
Hook:
export type Invoice = {id: number, status: string};
export async function executeRequest<T>(callback: () => T, afterRequest: (response: {error: string | null, data: T}) => void) {
const response = callback();
afterRequest({error: null, data: response});
}
export function useInvoiceProcessing(formData: Invoice[]): [result: Invoice[], executeRequest: () => Promise<void>] {
const timer: MutableRefObject<NodeJS.Timer | undefined> = useRef();
const [result, setResult] = useState<Invoice[]>(formData);
// Mock what the API would return
const firstRequestResult: Invoice[] = [
{id: 1, status: "Success"},
{id: 2, status: "Pending"}, // This one needs to be polled again
{id: 3, status: "Failed"}
];
const firstPoll: Invoice = {id: 2, status: "Pending"};
const secondPoll: Invoice = {id: 2, status: "Success"};
// The function that triggers when the user clicks on "Execute Request"
async function handleFirstRequest() {
await executeRequest(() => firstRequestResult, response => {
if (!response.error) {
setResult(response.data)
if (response.data.some(invoice => invoice.status === "Pending")) {
// Initialize the timer to poll every 2 seconds
timer.current = setInterval(() => updateStatus(), 2000);
}
} else {
// setError
}
})
}
let isFirstPoll = true; // Helper variable to simulate a first poll
async function updateStatus() {
// Result has the initial formData values (NotUploaded) but should already have the values from the first request
console.log(result);
const newResult = [...result];
let index = 0;
for (const invoice of newResult) {
if (invoice.status === "Pending") {
await executeRequest(() => isFirstPoll ? firstPoll : secondPoll, response => {
if (!response.error) {
newResult[index] = response.data;
} else {
// Handle error
}
});
}
index++;
}
setResult(newResult);
isFirstPoll = false;
const areInvoicesPending = newResult.some(invoice => invoice.status === "Pending");
if (!areInvoicesPending) {
console.log("Manual clear")
clearInterval(timer.current);
}
};
useEffect(() => {
return () => {
console.log("Unmount clear")
clearInterval(timer.current);
}
}, [])
return [result, handleFirstRequest];
Usage:
const [result, executeRequest] = useInvoiceProcessing([
{ id: 1, status: "NotUploaded" },
{ id: 2, status: "NotUploaded" },
{ id: 3, status: "NotUploaded" }
]);
async function handleRequest() {
console.log("Start request");
executeRequest();
}
return (
<div className="App">
<button onClick={handleRequest}>Execute Request</button>
<p>
{result.map((invoice) => (
<Fragment key={invoice.id}>
{invoice.status}
<br />
</Fragment>
))}
</p>
</div>
);
EDIT1
I have one possible solution. This post helped me in the right direction: React hooks functions have old version of a state var
The closure that updateStatus uses is outdated. To solve that, I saved updateStatus in a useRef (updateStatus also needs useCallback). Although not necessary, I had to store result in a useRef as well but I'm not sure yet why.
const updateStatusRef = useRef(updateStatus);
useEffect(()=>{
updateStatusRef.current = updateStatus;
}, [updateStatus]);
Here's a working example: https://codesandbox.io/s/pollingstate-forked-njw4ct?file=/src/Hooks.tsx

React Native Firestore: How do I listen for database changes at the same time as using the .where() query?

I have made a FlatList that gets populated from a firestore database. I can currently do all the CRUD operations, but when I edit an entry, it doesn't change in the FlatList. It does change in the firestore database.
I suspect it's because I'm not using .onSnapshot(). My problem is that I need to filter the data using .where() and I haven't been able to find out how to combine the two operations.
My code looks like this:
export const Coach = () => {
const navigation = useNavigation();
const [user, setUser] = useState();
const [userName, setUserName] = useState('');
const [workoutIds, setWorkoutIds] = useState([]);
const [workouts, setWorkouts] = useState([]);
const userRef = firestore().collection('Users');
const workoutRef = firestore().collection('Workouts');
// Setting the user state
auth().onAuthStateChanged(userInstance => {
if (userInstance) {
setUser(userInstance);
}
});
// Getting coach id's from firestore - Started out at individual workout id's
useEffect(() => {
if (user) {
const subscriber = userRef.doc(user.uid).onSnapshot(userSnap => {
if (userSnap) {
setUserName(userSnap.data().Name);
setWorkoutIds(userSnap.data().Workouts);
}
});
return () => subscriber();
}
}, [user]);
// using the list of coach id's to get workouts
useEffect(() => {
if (workoutIds.length != 0) {
let workoutList = [];
workoutRef
.where(firestore.FieldPath.documentId(), 'in', workoutIds)
.get()
.then(query => {
query.forEach(snap => {
workoutList.push({...snap.data(), key: snap.id});
});
setWorkouts(workoutList);
});
}
}, [workoutIds]);
The problem should lie in the last useEffect block.
So how do I get it to listen for changes and update the FlatList, while still using the .where()?
----------------------------------------- Edit -----------------------------------------
I have tried to add an onSnapshot to my query:
Before:
// using the list of coach id's to get workouts
useEffect(() => {
if (workoutIds.length != 0) {
let workoutList = [];
workoutRef
.where(firestore.FieldPath.documentId(), 'in', workoutIds)
.get()
.then(query => {
query.forEach(snap => {
workoutList.push({...snap.data(), key: snap.id});
});
setWorkouts(workoutList);
});
}
}, [workoutIds]);
After:
// using the list of coach id's to get workouts
useEffect(() => {
if (workoutIds.length != 0) {
let workoutList = [];
workoutRef
.where(firestore.FieldPath.documentId(), 'in', workoutIds)
.onSnapshot(query => {
query.forEach(snap => {
workoutList.push({...snap.data(), key: snap.id});
});
setWorkouts(workoutList);
});
}
}, [workoutIds]);
It still doesn't update the view straight away and now I get an error about encountering two of the same keys.
To solve the issue I had to add .onSnapshot() to my query for it to listen to changes in the database. On top of that I accidentally put the temporary list that I added objects to, outside the onSnapshot(), so it just kept adding on. After moving the temporary list into the onSnapshot(), it now updates.
Before:
useEffect(() => {
if (workoutIds.length != 0) {
let workoutList = [];
workoutRef
.where(firestore.FieldPath.documentId(), 'in', workoutIds)
.get()
.then(query => {
query.forEach(snap => {
workoutList.push({...snap.data(), key: snap.id});
});
setWorkouts(workoutList);
});
}
}, [workoutIds]);
After:
useEffect(() => {
if (workoutIds.length != 0) {
workoutRef
.where(firestore.FieldPath.documentId(), 'in', workoutIds)
.onSnapshot(query => {
let workoutList = [];
query.forEach(snap => {
workoutList.push({...snap.data(), key: snap.id});
});
setWorkouts(workoutList);
});
}
}, [workoutIds]);

setState on empty field in JSON object

I have the empty field ModifiedBy. I need to populate it with a username stored in session storage, userInfo.name.
const [details, setDetails] = useState("");
const handleCreateData = (e) => {
setDetails((prev) => {
return { ...prev, ModifiedBy: userInfo.name };
});
}
This method only works when ModifiedBy is already populated in data. I need to be able to populate ModifiedBy when it is empty, and update it is populated.
A quick example of my JSON
{
"ModifiedBy": "Ciaran Crowley"
}
const handleCreateData = (e) => {
setDetails((prev) => {
prev.ModifiedBy = userInfo.name || ''
return prev;
});
}

What to return if no id found using react and typescript?

i have a query "details" which is like below
query details(
$id: ID!
) {
something(id: $id) {
id
itemId
typesId
itemDetails {
id
name
}
typesDetails {
id
name
}
}
}
i have defined the types like below
type itemDetails {
id: String,
name: String,
}
type typesDetails {
id: String,
name: String,
}
type something {
id: ID!
itemId: ID
typesId: [ID!]
itemDetails: itemDetails
typesDetails: [typesDetails]
}
on the resolvers side (graphql) i have to field resolve the itemDetails (with the itemId i recieve from backend). this itemId can be null or can have some string value like example '1'.
and typesDetails with the typesId i receive from backend. typesId can be null or array of ids like example ['1','2',...]
const resolvers: Resolvers = {
something: {
itemDetails: async(parent, args, { dataSources: { itemsAPI} }) => {
const itemId = get (parent, 'itemId'); //can be null or string value
if(itemId) {
const { data } = await itemsAPI.getItems();
const item = data.filter((item: any) =>
itemId === item.id
); //filter the data whose id is equal to itemId
return {
id: item[0].id,
name: item[0].name,
}
}else { // how to rewrite this else part
return {}:
}
},
typesDetails: async (parent, args, { dataSources: {assetTypesAPI} }) => {
const typesId = get(parent, 'typesId');
if (typesId) {
const allTypes = await typesAPI.getTypes();
const res = typesId.map((id: any) => allTypes.find((d) => d.id === id)); //filter
//allTypes that match typesId
const final = res.map(({id, name}: {id:string, name:string}) => ({id,name}));
//retreive the id and name fields from res array and put it to final array
return final;
} else { // how to rewrite this else part
return [{}];
}
}
}
The above code works. but the code looks clumsy in the way i return empty array if no itemId and typesId returned from backend.
how can i handle the case for itemDetails field if itemId is null from backend and typesDetails field if typesId is null from backend.
could someone help me with this. thanks.
The question is more about graphql than it is react and typescript. You do well to change the tag 🏷 of the question for proper matching.
The perfect solution for me would be validating the itemId in the case of itemDetails. If the value is null, throw an error or return an empty object like you're doing in the else section of the itemDetails.
I don't think itemId is coming from the backend in the case above, it should be coming from the argument passed to the query itemDetails
The same applies to the typesDetails.
Using try and catch can help catch any error during the async operations to the API (database).
const resolvers: Resolvers = {
something: {
itemDetails: async(parent, args, { dataSources: { itemsAPI} }) => {
try {
const itemId = get (parent, 'itemId'); //can be null or string value
if(itemId) {
const { data } = await itemsAPI.getItems();
const item = data.filter((item: any) =>
itemId === item.id
); //filter the data whose id is equal to itemId
return {
id: item[0].id,
name: item[0].name,
}
}else { // how to rewrite this else part
return {}:
}
} catch(error) {
throw new Error('something bad happened')
}
},
typesDetails: async (parent, args, { dataSources: {assetTypesAPI} }) => {
try {
const typesId = get(parent, 'typesId');
if (typesId) {
const allTypes = await typesAPI.getTypes();
const res = typesId.map((id: any) => allTypes.find((d) => d.id === id)); //filter
//allTypes that match typesId
const final = res.map(({id, name}: {id:string, name:string}) => ({id,name}));
//retreive the id and name fields from res array and put it to final array
return final;
} else { // how to rewrite this else part
return [{}];
}
} catch (error) {
throw new Error('something happened')
}
}
}

setState is not merging the values

I use the following code in react in order to update state. state should finally looks like this:
this.state.output = {
'abc':{
value: 10
},
'cde':{
value: 20
}
// new values are added and old kept (if not overwritten)
}
My handler:
handleChange = (data) => {
this.setState(prevState => {
return {
output: {
[data.id]: { ...data },
},
}
})
}
When the data is passed in to handleChage with a new data.id, output does not add the new key, instead completely replace all its content
this.state.output = {
'new':{
value: 2
},
}
I need instead keep the previous key also. What is wrong in my code?
Because you forgot to add the other property and their values, update the object like this:
handleChange = (data) => {
this.setState(prevState => {
return {
output: {
...prevState.output, // notice this
[data.id]: { ...data },
},
}
})
}
Or simply:
handleChange = (data) => {
this.setState(prevState => ({
output: {
...prevState.output,
[data.id]: { ...data },
},
})
)}
object spread syntax is recent spec. Here is the documentation for it: using-object-spread-operator
Below code uses Object.assign method instead :
handleChange = (data) => {
this.setState(prevState => ({
output: Object.assign({}, prevState, {
[data.id]: data
})
})
)}

Categories