Refactor a functional component with React hooks - reactjs

I have several functional components which share the same logic. So I would like to refactor them using React hooks. All of them make some calls to the server on mount to check if the order has been paid. If yes, paid state is set to true , and a file is being downloaded. On submit I check if paid state is set to true, if yes, the same file is being downloaded, if not, a new order is created and a user is being redirected to a page with a payment form.
I have already extracted all functions (getOrder(), getPaymentState(), createOrder(), initPayment() and downloadFile()) which make API calls to the server. How can I further optimize this code, so that I could move checkOrder(), checkPayment(), downloadPDF() and newOrder() outside the component to use the same logic with other components as well?
Here is my component:
const Form = () => {
const [paid, setPaid] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [loading, setLoading] = useState(false);
const [data, setData] = useState({});
const checkOrder = async () => {
let search = new URLSearchParams(window.location.search);
let success = search.get("Success");
if (success) {
try {
const data = await getOrder();
setData(data);
checkPayment(data);
} catch (err) {
alert(err.message)
}
}
};
const checkPayment = async values => {
try {
const paid = await getPaymentState();
setPaid(paid);
downloadPDF(values);
} catch (err) {
alert(err.message)
}
};
const downloadPDF = async values => {
setLoading(true);
let downloadData = {
email: values.email,
phone: values.phone
}
const response = await downloadFile(downloadData, sendURL);
setLoading(false);
window.location.assign(response.pdf);
}
const newOrder = async values => {
setSubmitting(true);
const order = await createOrder(values, description, sum);
const paymentUrl = await initPayment(order, description, sum, returnURL);
setSubmitting(false);
window.location.assign(paymentUrl);
}
const onSubmit = async values => {
if (paid) {
try {
downloadPDF(data);
} catch (err) {
console.log(err);
}
} else {
try {
newOrder(values)
} catch (err) {
alert(err.message)
}
}
};
useEffect(() => {
checkOrder();
}, []);
return (
)
}
EDIT 1: I also need to be able to pass some data to this hook: downloadData, sendURL, description, sum and returnURL, which will be different in each case. downloadData then needs to be populated with some data from the values.
I would appreciate if you could point me at the right direction. I'm just learning React and I would really like to find the correct way to do this.
EDIT 2: I've posted my own answer with the working code based on the previous answers. It's not final, because I still need to move downloadPDF() outside the component and pass downloadData to it, but when I do so, I get an error, that values are undefined. If anybody can help me with that, I will accept it as an answer.

I made a quick refactor of the code and put it in a custom hook, it looks like search param is the key for when the effect needs to run.
const useCheckPayment = (search) => {
const [paid, setPaid] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [loading, setLoading] = useState(false);
const [data, setData] = useState({});
const checkOrder = useCallback(async () => {
let paramSearch = new URLSearchParams(search);
let success = paramSearch.get('Success');
if (success) {
try {
//why not just pass it, makes getOrder a little less impure
const data = await getOrder(paramSearch);
setData(data);
checkPayment(data);
} catch (err) {
alert(err.message);
}
}
}, [checkPayment, search]);
const checkPayment = useCallback(async (values) => {
try {
const paid = await getPaymentState();
setPaid(paid);
downloadPDF(values);
} catch (err) {
alert(err.message);
}
}, []);
const downloadPDF = async (values) => {
setLoading(true);
const response = await downloadFile();
setLoading(false);
window.location.assign(response.pdf);
};
const newOrder = async (values) => {
setSubmitting(true);
const order = await createOrder();
const paymentUrl = await initPayment(order);
setSubmitting(false);
window.location.assign(paymentUrl);
};
const onSubmit = useCallback(
async (values) => {
if (paid) {
try {
downloadPDF(data);
} catch (err) {
console.log(err);
}
} else {
try {
newOrder(values);
} catch (err) {
alert(err.message);
}
}
},
[data, paid]
);
useEffect(() => {
checkOrder();
}, [checkOrder]); //checkOrder will change when search changes and effect is called again
return { onSubmit, submitting, loading };
};
const Form = () => {
const { onSubmit, submitting, loading } = useCheckPayment(
window.location.search
);
return '';
};

You can extract out all the generic things from within the Form component into a custom Hook and return the required values from this hook
The values which are dependencies and will vary according to the component this is being called from can be passed as arguments to the hook. Also the hook can return a onSubmit function to which you can pass on the downloadData
const useOrderHook = ({returnURL, sendURL, }) => {
const [paid, setPaid] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [loading, setLoading] = useState(false);
const [data, setData] = useState({});
const checkOrder = async () => {
let search = new URLSearchParams(window.location.search);
let success = search.get("Success");
if (success) {
try {
const data = await getOrder();
setData(data);
checkPayment(data);
} catch (err) {
alert(err.message)
}
}
};
const checkPayment = async values => {
try {
const paid = await getPaymentState();
setPaid(paid);
downloadPDF(values);
} catch (err) {
alert(err.message)
}
};
const downloadPDF = async values => {
setLoading(true);
let downloadData = {
email: values.email,
phone: values.phone
}
const response = await downloadFile(downloadData, sendURL);
setLoading(false);
window.location.assign(response.pdf);
}
const newOrder = async (values, description, sum) => {
setSubmitting(true);
const order = await createOrder(values, description, sum);
const paymentUrl = await initPayment(order, description, sum, returnURL);
setSubmitting(false);
window.location.assign(paymentUrl);
}
const onSubmit = async ({values, downloadData: data, description, sum}) => {
if (paid) {
try {
downloadPDF(data);
} catch (err) {
console.log(err);
}
} else {
try {
newOrder(values, description, sum)
} catch (err) {
alert(err.message)
}
}
};
useEffect(() => {
checkOrder();
}, []);
return {onSubmit, loading, submitting, paid, data };
}
Now you can use this hook in component like Form as follows
const Form = () => {
const {onSubmit, newOrder, loading, submitting, paid, data } = useOrderHook({returnUrl: 'someUrl', sendURL: 'Some send URL'})
const handleSubmit = (values) => {
// since this function is called, you can get the values from its closure.
const data = {email: values.email, phone: values.phone}
onSubmit({ data, values, description, sum})// pass in the required values for onSubmit here. you can do the same when you actually call newOrder from somewhere
}
// this is how you pass on handleSubmit to React-final-form
return <Form
onSubmit={handleSubmit }
render={({ handleSubmit }) => {
return <form onSubmit={handleSubmit}>...fields go here...</form>
}}
/>
}

Based on the answers above I came up with the following code.
The hook:
const useCheckPayment = ({initialValues, sendUrl, successUrl, description, sum, downloadPDF}) => {
const [paid, setPaid] = useState(false);
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [data, setData] = useState(initialValues);
const checkOrder = useCallback(
async () => {
let search = new URLSearchParams(window.location.search);
let success = search.get('Success');
if (success) {
try {
const data = await getOrder(search);
setData(data);
checkPayment(search);
} catch (err) {
alert(err.message);
}
}
}, [checkPayment]
);
const checkPayment = useCallback(
async (search) => {
try {
const paid = await getPaymentState(search);
setPaid(paid);
document.getElementById('myForm').dispatchEvent(new Event('submit', { cancelable: true }))
} catch (err) {
alert(err.message);
}
}, []
);
const newOrder = useCallback(
async (values) => {
setSubmitting(true);
const order = await createOrder(values, description, sum);
const paymentUrl = await initPayment(order, description, sum, successUrl);
setSubmitting(false);
window.location.assign(paymentUrl);
}, [description, sum, successUrl]
);
const downloadPDF = async (values, downloadData) => {
setLoading(true);
const response = await downloadFile(downloadData, sendUrl);
setLoading(false);
window.location.assign(response.pdf);
};
const onSubmit = useCallback(
async ({ values, downloadData }) => {
if (paid) {
try {
downloadPDF(values, downloadData);
} catch (err) {
console.log(err);
}
} else {
try {
newOrder(values);
} catch (err) {
alert(err.message);
}
}
},
[paid, downloadPDF, newOrder]
);
useEffect(() => {
checkOrder();
}, [checkOrder]);
return { onSubmit, submitting };
};
The component:
const sendUrl = 'https://app.example.com/send'
const successUrl = 'https://example.com/success'
const description = 'Download PDF file'
const sum = '100'
const Form = () => {
const handleSubmit = (values) => {
const downloadData = {
email: values.email,
phone: values.phone
}
onSubmit({ downloadData, values })
}
const { onSubmit, submitting } = useCheckPayment(
{sendUrl, successUrl, description, sum}
);
return (
<Form
onSubmit={handleSubmit}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}></form>
)}
/>
)
}

Related

I can't get Axios post information

For my posts
in component AboutUsers.jsx
const [users, setUsers] = useState([]);
if I write like this, it's working, I see posts in users:
in component AboutUsers.jsx
useEffect(()=> {
const getUsers = axios.get('https://jsonplaceholder.typicode.com/todos',{
params:{
_limit:limitPage,
_page:currentPage
}
})
.then(response => setUsers(response.data))
},[])
but I created other component PostMyServise.js with:
export default class PostMyServise {
static async getPost(limit=10, page=1) {
const result = await axios.get('https://jsonplaceholder.typicode.com/todos',{
params: {
_limit: limit,
_page: page,
}
})
.then(response => {
return response
})
return result;
}
}
And one yet component useCreatePosts.js:
export const usePosts = (callback) => {
const [isTrue, setIsTrue] = useState('')
const [error, setError] = useState('')
const createPost = async () => {
try {
setIsTrue(false);
await callback;
} catch (e) {
setError(e.message);
} finally {
setIsTrue(true);
}
}
return [createPost, isTrue, error];
}
export default usePosts;
I wrote this, and I see empty array in console.log(users):
I don't understand why array is empty
const [createPost, isTrue, error] = usePosts (async ()=> {
const response = await PostMyServise.getPost(limitPage, currentPage);
setUsers(response.data)
})
useEffect(() => {
createPost();
},[currentPage])
You are not calling the callback. You need to add the parentheses.
const createPost = async () => {
try {
setIsTrue(false);
await callback();
} catch (e) {
setError(e.message);
} finally {
setIsTrue(true);
}
}
I feel like something about your code is over-engineered and too complex but that's outside the scope of the question. This change will at least get it working. Also, I suggest changing the name of isTrue and setIsTrue to something more meaningful as those names do not tell you what they are for.

Stop invoking custom hook on first render

I started having fun with custom hooks recently. I am mostly using them to fetch from api. The thing is that since I cannot really put useFetchLink inside of functions or useEffect i dont know how to prevent it from fetching after website first render. I could put some ifs in the hook but isn't there any other way?
***component***
export default function LinkShortener({ setLinkArr }) {
const [nextLink, setNextLink] = useState();
const inputRef = useRef(null);
const handleClick = () => {
setNextLink(inputRef.current.value);
};
const { shortLink, loading, error } = useFetchLink(nextLink);
useEffect(() => {
setLinkArr((prev) => [
...prev,
{
id: prev.length === 0 ? 1 : prev[prev.length - 1].id + 1,
long: nextLink,
short: shortLink,
},
]);
inputRef.current.value = "";
}, [shortLink, error]);
return (
<LinkShortenerContainer>
<InputContainer>
<LinkInput ref={inputRef} type="text" />
</InputContainer>
<Button
size={buttonSize.medium}
text={
loading ? (
<Loader />
) : (
<FormattedMessage
id="linkShortener.shortenItBtn"
defaultMessage="Shorten It !"
/>
)
}
onClick={handleClick}
></Button>
</LinkShortenerContainer>
);
}
***hook***
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchLink = async () => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${linkToShorten}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchLink(linkToShorten);
}, [linkToShorten]);
const value = { shortLink, loading, error };
return value;
};```
Why not using directly fetchLink function and calling it whenever you need inside the component? I would change the hook in this way without useEffect inside
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchLink = async () => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${linkToShorten}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const value = { shortLink, loading, error, fetchLink };
return value;
};
Generally speaking - the standard way to avoid useEffect from running of 1st render is to use a boolean ref initialized with false, and toggled to true after first render - see this answer.
However, in your case, you don't want to call the function if linkToShorten is empty, even if it's not the 1st render, so use an if inside useEffect.
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchLink = useCallback(async (linkToShorten) => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${linkToShorten}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if(linkToShorten) fetchLink(linkToShorten);
}, [fetchLink, linkToShorten]);
const value = { shortLink, loading, error };
return value;
};

Getting a undefined value when trying to match fetch results to people objects

Im working on a star wars api app. I am getting an array of people objects, 10 characters. Who all are their own object with different values. However homeworld, and species are urls. So I have to fetch them and store that data to the correct place. I figured out a way to get the homeworld values to each character. However when I try to do it with species I receive undefined. Would appreciate any help this has been kind of a pain thanks ahead of time !
const [people, setPeople] = useState([]);
const [homeWorld, setHomeWorld] = useState([]);
const [species, setSpecies] = useState([]);
const [nextPageUrl, setNextPageUrl] = useState("https://swapi.dev/api/people/");
const [backPageUrl, setBackPageUrl] = useState('');
const [test, setTest] = useState([]);
const fetchPeople = async () => {
const { data } = await axios.get(nextPageUrl);
setNextPageUrl(data.next);
setBackPageUrl(data.previous);
return data.results;
}
const backPage = async () => {
const { data } = await axios.get(backPageUrl);
setCharacters(data.results);
setNextPageUrl(data.next);
setBackPageUrl(data.previous);
}
// Get People
async function getPeople() {
const persons = await fetchPeople();
const homeWorldUrl= await Promise.all(
persons.map((thing) => axios.get(thing.homeworld)),
);
const newPersons = persons.map((person) => {
return {
...person,
homeworld: homeWorldUrl.find((url) => url.config.url === person.homeworld)
};
});
const newPersons2 = newPersons.map((person) => {
return {
...person,
homeWorld: person.homeworld.data.name
};
});
setPeople(newPersons2);
}
// Get Species
async function getSpecies() {
const persons = await fetchPeople();
const speciesUrl = await Promise.all(
persons.map((thing) => axios.get(thing.species)),
);
const newSwapi = persons.map((person) => {
return {
...person,
species: speciesUrl.find((info) => info.data.url === person.species)
};
});
setTest(newSwapi);
// const newPersons2 = newPersons.map((person) => {
// return {
// ...person,
// homeWorld: person.homeworld.data.name
// };
// });
}
useEffect(() => {
getPeople();
getSpecies();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); ```
Species property of person is a array, so your getSpecies() should be like
async function getSpecies() {
const persons = await fetchPeople();
const speciesUrl = await Promise.all(
persons
.filter((thing) => thing.species.length)
.map((thing) => axios.get(thing.species[0]))
);
const newSwapi = persons.map((person) => {
return {
...person,
species: speciesUrl.find((info) => info.data.url === person.species[0])
};
});
setTest(newSwapi);
}

Getting additional data in firebase/auth - onAuthStateChanged

I want to get extra data from a users collection in firestore when user loggs in. I do this in a useEffect function in a AuthContext. This is my code:
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
const fetchUserData = async () => {
if (!user) {
setCurrentUser(null);
setLoading(false);
return;
}
const userData = await fetchUserDataFromFirestore(user.uid);
setCurrentUser({ ...user, ...userData });
setLoading(false);
};
fetchUserData();
});
return unsubscribe;
}, [currentUser]);
This kind of works as I do get the data but messages are piling up in the console as can be seen in my screenshot:
The fetchUserDataFromFirestore function is implemented like this:
export const fetchUserDataFromFirestore = async (id) => {
const docRef = doc(db, "users", id);
const docSnap = await getDoc(docRef);
if (docSnap.exists) {
const userData = docSnap.data();
return userData;
}
return null;
};
What can I do about this?
For future reference this is how I did it
const [uid, setUid] = useState(null)
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user) {
setUid(user.uid)
} else {
setUid(null)
}
})
return () => {
unsubscribe()
}
}, [])
// set currentUser state
useEffect(() => {
if (uid) {
const userRef = doc(db, "users", uid)
getDoc(userRef)
.then((docSnapshot) => {
const data = docSnapshot.data()
setCurrentUser(data)
})
}
}, [uid])

Trigger useEffect again after API call

I have the following useEffect hook which runs on the initial loading of the page
useEffect(() => {
const getLeads = () => axios.get("/api/leads");
const setLeadState = async () => {
try {
const res = await getLeads();
res.data.map(lead => setLeads(prevState => [...prevState, lead]));
} catch (err) {
console.error(err);
}
};
setLeadState();
}, []);
and then I have the following function to add new data to the api. This function is called on a form submit.
const addLead = async (firstName, lastName, email) => {
try {
const body = {
firstName,
lastName,
email
};
const res = await axios.post("api/leads", body);
} catch (err) {
console.error(err);
}
};
How can I call my useEffect hook again after the addLead function was executed? I tried something like this
const [test, setTest] = useState("");
useEffect(() => {
const getLeads = () => axios.get("/api/leads");
const setLeadState = async () => {
try {
const res = await getLeads();
res.data.map(lead => setLeads(prevState => [...prevState, lead]));
} catch (err) {
console.error(err);
}
};
setLeadState();
}, [test]);
And then changed "test" in the addLead function, however this leads to an infinite loop with the page not rendering at all.
Move the function that fetches/sets the data outside of the effect and then call that function after addLead
const getLeads = () => axios.get("/api/leads");
const setLeadState = async () => {
try {
const res = await getLeads();
res.data.map(lead => setLeads(prevState => [...prevState, lead]));
} catch (err) {
console.error(err);
}
};
// Initial load
useEffect(() => {
setLeadState();
}, []);
const addLead = async (firstName, lastName, email) => {
try {
const body = {
firstName,
lastName,
email
};
const res = await axios.post("api/leads", body);
// Load again
setLeadState();
} catch (err) {
console.error(err);
}
};

Resources