Array and Objects of states in React - reactjs

So I have an array of objects and each property in the object comes from the user. User chooses an object that he wants to edit and I am passing it using context to component that takes data from user. Now after getting data I am inserting it back to array but it is giving me wrong data.
Here is component that takes data from user.
const SelectedSymptom = () => {
const [
selected,
setSelected,
selectedSymptom,
setSelectedSymptom,
] = useContext(symptomContext);
const [note, setNote] = useState("");
const [since, setSince] = useState("");
const [severity, setSeverity] = useState("");
const [feature, setFeature] = useState("");
const [place, setPlace] = useState("");
const [colour, setColour] = useState("");
useEffect(() => {
return async () => {
await setSelectedSymptom((prev) => {
return {
...prev,
feature,
since,
severity,
place,
colour,
note,
};
});
await setSelected((prev) => {
const cur = prev.filter((item) => item.name !== selectedSymptom.name);
if (selectedSymptom !== "") cur.push(selectedSymptom);
return cur;
});
console.log(selectedSymptom, selected);
};
}, [since, feature, severity, place, colour]);
}
Data from form is coming correctly but I guess due to async nature of setState call, I am getting error.

First remove the return from useEffect. Return is a cleanup method for useEffect, example to terminate intervals, listeners etc.
I changed to a constant instead update() which I run in the end of the useEffect.
Also the parameters in the code below, you only had set the key, not the value to the keys
await setSelectedSymptom((prev) =>
return {
...prev,
feature: feature,
since: since,
severity: severity,
place: place,
colour: colour,
note: note,
};
});
I hope this brings some clarification
const SelectedSymptom = () => {
const [
selected,
setSelected,
selectedSymptom,
setSelectedSymptom,
] = useContext(symptomContext);
const [note, setNote] = useState("");
const [since, setSince] = useState("");
const [severity, setSeverity] = useState("");
const [feature, setFeature] = useState("");
const [place, setPlace] = useState("");
const [colour, setColour] = useState("");
useEffect(() => {
const update = async () => {
await setSelectedSymptom((prev) => {
return {
...prev,
feature: feature,
since: since,
severity: severity,
place: place,
colour: colour,
note: note,
};
});
await setSelected((prev) => {
const cur = prev.filter((item) => item.name !== selectedSymptom.name);
if (selectedSymptom !== "") cur.push(selectedSymptom);
return cur;
});
update();
};
}, [since, feature, severity, place, colour]);
}

Related

Map data on runtime after post request

I have three apis in all. GetAssets is the first, followed by assetsOptionsList and getAssetsLibrary. The issue I'm having is that when I post the data on getAssetsLibrary, I want to be able to present it on get Assets at runtime.Everything is working fine but i want to show assets on runtime.
I'm setting the runTime state true on get request but the problem is it works only for one time.Second time, it does not map on runtime. Actually, i want to know is there any alternative so that i can achieve the goal.
In the below code the one function is getting the assets. And i want to run the one function when the post request successfully sent.
const [images, setImages] = useState([]);
const [assetOptions, setAssetOptions] = useState([]);
const [faqOpened, setToggleFaq] = useState(false);
const [runTime, setRunTime] = useState(false)
const [assetID, setAssetID] = useState()
const [isLoading, setIsLoading] = useState(false);
const handleForm = (e) => {
const index = e.target.selectedIndex;
const el = e.target.childNodes[index]
const option = el.getAttribute('id');
setAssetID(option)
}
const formHandler = (e) => {
e.preventDefault()
let formData = new FormData();
formData.append('media', e.target.media.files[0]);
formData.append('assetListId', assetID)
formData.append('name', e.target.name.value);
console.log(Object.fromEntries(formData))
const res = axios.post('api/asset-library',
formData
).then((response) => {
showSuccessToaster(response?.data?.message)
setRunTime(true)
setToggleFaq(false)
})
.catch((error) => {
showErrorToaster(error?.response?.data?.message)
})
}
const showSuccessToaster = (response) => {
return uploadToasterSuccess.show({ message: response });
}
const showErrorToaster = (error) => {
return uploadToasterError.show({ message: error });
}
const one = async () => {
setIsLoading(true)
const data = await axios.get('api/assets').then((res) => {
return res?.data?.data
})
setImages(data)
setIsLoading(false)
}
const two = async () => {
const data = await axios.get('/api/asset-list').then((res) => {
return res?.data?.data
})
setAssetOptions(data)
}
useEffect(() => {
one()
two()
}, [runTime]);

UseEffect not filling all states

SO this is my code, i'm trying to filter my offers by there users , i have called all my offers and all my user and there states are full but when i try to filter offers by there users the state stay empty but when i hit spacebar on my keyboard the state get full like it's the spacebar is triggering useEffect to fill the state
const [offer, setOffer] = useState([]);
const [user, setUser] = useState({});
const[useroffers,setUseroffer]=useState([]);
const isOffer = async () => {
const oflg = await GetAllOff();
setOffer(oflg);
};
const isLoggedIn = async () => {
const userLg = await CurrentUser();
setUser(userLg.data.user);
};
const isUseroffer = async()=>{
setUseroffer(offer.filter((el)=>el.createdbyId === user._id));
};
useEffect( () => {
isOffer();
isLoggedIn();
isUseroffer();
}, []);
console.log(offer);
console.log(user)
console.log(useroffers);
So useEffect is filling the offers and user States but not filling the useroffers state intil i click on the spacebar
useroffers is dependent on both user and offer but you are trying to set it in the same render cycle as those two. Updated state values aren't available until the render cycle after they are set, so setUseroffers doesn't have access to the values it needs to update properly. To solve this you can declare a second useEffect which is dependent on user and offer so that as those values update so does your filtered array.
const [offer, setOffer] = useState([]);
const [user, setUser] = useState({});
const [useroffers, setUseroffer] = useState([]);
const isOffer = async () => {
const oflg = await GetAllOff();
setOffer(oflg);
};
const isLoggedIn = async () => {
const userLg = await CurrentUser();
setUser(userLg.data.user);
};
useEffect(() => {
isOffer();
isLoggedIn();
}, []);
useEffect(() => {
setUseroffer(offer.filter((el) => el.createdbyId === user._id));
}, [user, offer]);
codesandbox
Alternatively you can do it all in a single useEffect by awaiting the offer and user values and using them directly to set all three states once they are available. (This will result in only a single rerender rather than the possible four in the previous example)
const [offer, setOffer] = useState([]);
const [user, setUser] = useState({});
const [useroffers, setUseroffer] = useState([]);
useEffect(() => {
const login = async () => {
const userLg = await CurrentUser();
const ofLg = await GetAllOff();
setUser(userLg.data.user);
setOffer(ofLg);
setUseroffer(
ofLg.filter((el) => el.createdbyId === userLg.data.user._id)
);
};
login();
}, []);
codesandbox

How to avoid triggering useEffect() from another useEffect()?

I'm implementing pagination with React. It generally works well, except one issue.
When I'm changing sorting criteria, I want to reset page to 1st. The problem is, changing the page number triggers data fetch again. So whenever I'm on page 2 or above and change sorting criteria, the data is being fetched twice. Once for the fact of changing the criteria (which trigger resetting the page to 1) and then again, as the page changed to one. Is there any clean way to avoid this clash and make the fetch only happen once?
Here's my simplified code:
import { useState, useEffect } from 'react';
export default function MyComponent() {
const [items, setItems] = useState([]);
const [column, setColumn] = useState();
const [direction, setDirection] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [perPage, setPerPage] = useState(10);
useEffect(
() => (async () => {
const response = await fetch('...');
const { items } = await response.json();
setItems(items);
})(),
[column, direction, currentPage, perPage]
);
useEffect(
() => setCurrentPage(1), // This triggers the useEffect() above
[column, direction, perPage]
);
return (
// Template code
);
}
How would a React guru do this?
You can add a state like shouldFetch that can be used to conditionally fetch and avoid multiple calls.
const [column, setColumn] = useState();
const [direction, setDirection] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [shouldFetch, setShouldFetch] = useState(true);
useEffect(() => {
(async () => {
if (shouldFetch) {
const response = await sleep(1000);
console.log(response);
// prevent fetch as we want to allow it later
setShouldFetch(false);
}
})();
}, [column, direction, currentPage, perPage, shouldFetch]);
useEffect(() => {
setCurrentPage(1);
// allow fetch
setShouldFetch(true);
}, [column, direction, perPage]);
const changeColumn = () => {
setColumn("new-col");
};
const changeCurrentPage = () => {
setCurrentPage(2);
// to fetch when currentPage changes
// this should not be added to other handlers as it is also present in the second useEffect that gets triggered when other params change
setShouldFetch(true);
};
const changePerPage = () => {
setPerPage(20);
};
const changeDirection = () => {
setDirection("descending");
};
Alternative:
To avoid unnecessary fetching and to make sure that items are fetched using the updated state values, you can remove the second useEffect and reset currentPage when you update other params.
This will only trigger the useEffect once because React will perform both state updates (setColumn and setCurrentPage) at once.
const sleep = (ms) => new Promise((res) => setTimeout(() => res("Hi Mom"), ms));
export default function App() {
// const [items, setItems] = useState([]);
const [column, setColumn] = useState();
const [direction, setDirection] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [perPage, setPerPage] = useState(10);
useEffect(() => {
(async () => {
const response = await sleep(1000);
console.log(response);
})();
}, [column, direction, currentPage, perPage]);
// remove this effect
// useEffect(() => setCurrentPage(1), [column, direction, perPage]);
const changeColumn = () => {
setColumn("new-col");
setCurrentPage(1);
};
const changeCurrentPage = () => {
setCurrentPage(2);
};
const changePerPage = () => {
setPerPage(20);
setCurrentPage(1);
};
const changeDirection = () => {
setDirection("descending");
setCurrentPage(1);
};
return (
<>
<button onClick={changeColumn}>change column</button>
<button onClick={changeDirection}>change direction</button>
<button onClick={changeCurrentPage}>change page</button>
<button onClick={changePerPage}>change perPage</button>
</>
);
}

How to wait for a useState update within onSubmit?

I want to fetch metadata from a website, then upload it to the database.
The website link comes from a form input field.
Since useState update is async, the data is not yet present in the formData object on submit.
What options do I have?
npm package in use: https://www.npmjs.com/package/suq
const [title, setTitle] = useState("");
const [url, setUrl] = useState("");
const [imgUrl, setImgUrl] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
suq(
`${urlToPost}`,
function (err, json, body) {
if (!err) {
setTitle(json.opengraph["og:title"]);
setUrl(json.opengraph["og:url"])
setImgUrl(json.opengraph["og:image"]);
}
}
);
const postData = {
title,
url,
imgUrl
};
db.collection("posts")
.doc()
.set(postData)
.then(() => {
console.log("Document successfully written!");
})
.catch((error) => {
console.error("Error writing document: ", error);
});
}
what about this:
const [title, setTitle] = useState("");
const [url, setUrl] = useState("");
const [imgUrl, setImgUrl] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
let submitTitle = '';
let submitUrl = '';
let submitImgUrl = '';
suq(
"https://www.space.com/first-structures-in-universe-revealed",
function (err, json, body) {
if (!err) {
submitTitle = json.opengraph["og:title"];
submitUrl = json.opengraph["og:url"];
submitImgUrl = json.opengraph["og:image"];
setTitle(submitTitle);
setUrl(submitUrl)
setImgUrl(submitImgUrl);
}
}
);
const formData = {
title: submitTitle,
url: submitUrl,
imgUrl: submitImgUrl
};
console.log(formData);
// title, url and imgUrl are still at initial value
}
the local state updates will be available in the next render but the values you need will be available to you in your handle submit function
We can't handle the wait for the state update directly in the same call.
As the state will update its value after the re-render cycle.
So it's upon you what would you like to do with the updated state so I can help you.

useState initial value don't use directly the value

I have an initial state that I never use directly in the code, only inside another set value state
Only a scratch example:
interface PersonProps {}
const Person: React.FC<PersonProps> = () => {
const [name, setName] = useState<string>("")
const [todayYear, setTodayYear] = useState<string>("")
const [birthYear, setBirthYear] = useState<string>("")
const [age, setAge] = useState<string>("")
const getPerson = async () => {
try {
const response = await getPersonRequest()
const data = await response.data
setName(data.name)
setTodayYear(data.today_year)
setBirthYear(data.future_year)
setAge(data.todayYear - data.birthYear)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getPerson()
})
return (
<h1>{name}</h1>
<h2>{age}</h2>
)
}
export default Person
In this case as you can see I will never use "todayYear" and "birthYear" on UI, so code give a warning
todayYear is assigned a value but never used
What can I do to fix this and/or ignore this warning?
If you don't use them for rendering, there's no reason to have them in your state:
const Person: React.FC<PersonProps> = () => {
const [name, setName] = useState<string>("")
const [age, setAge] = useState<string>("")
const getPerson = async () => {
try {
const response = await getPersonRequest()
const data = await response.data
setName(data.name)
setAge(data.todayYear - data.birthYear)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getPerson()
})
return (
<h1>{name}</h1>
<h2>{age}</h2>
)
}
Side note: In most cases, you can leave off the type argument to useState wen you're providing an intial value. There's no difference between:
const [name, setName] = useState<string>("")
and
const [name, setName] = useState("")
TypeScript will infer the type from the argument. You only need to be explicit when inference can't work, such as if you have useState<Thingy | null>(null).
As this other answer points out, unless you want your code to run every time your component re-renders (which would cause an infinite render loop), you need to specify a dependency array. In this case, probably an empty one if you only want to get the person information once.
Also, since it's possible for your component to be unmounted before the async action occurs, you should cancel your person request if it unmounts (or at least disregard the result if unmounted):
const Person: React.FC<PersonProps> = () => {
const [name, setName] = useState<string>("");
const [age, setAge] = useState<string>("");
const getPerson = async () => {
const response = await getPersonRequest();
const data = await response.data;
return data;
};
useEffect(() => {
getPerson()
.then(data => {
setName(data.name)
setAge(data.todayYear - data.birthYear)
})
.catch(error => {
if (/*error is not a cancellation*/) {
// (Probably better to show this to the user in some way)
console.log(error);
}
});
return () => {
// Cancel the request here if you can
};
}, []);
return (
<h1>{name}</h1>
<h2>{age}</h2>
);
};
If it's not possible to cancel the getPersonRequest, the fallback is a flag:
const Person: React.FC<PersonProps> = () => {
const [name, setName] = useState<string>("");
const [age, setAge] = useState<string>("");
const getPerson = async () => {
const response = await getPersonRequest();
const data = await response.data;
return data;
};
useEffect(() => {
let mounted = true;
getPerson()
.then(data => {
if (mounted) {
setName(data.name)
setAge(data.todayYear - data.birthYear)
}
})
.catch(error => {
// (Probably better to show this to the user in some way)
console.log(error);
});
return () => {
mounted = false;
};
}, []);
return (
<h1>{name}</h1>
<h2>{age}</h2>
);
};
I also would like to mention one more thing. It's not related to your question but I think it's important enough to talk about it.
you need to explicitly state your dependencies for useEffect
In your case, you have the following code
useEffect(() => {
getPerson()
})
it should be written as follow if you want to trigger this only one time when a component is rendered
useEffect(() => {
getPerson()
}, [])
or if you want to trigger your side effect as a result of something that has changed
useEffect(() => {
getPerson()
}, [name])
If this is not clear for I suggest read the following article using the effect hook

Resources