I'm struggling with React Hooks here. I looked online, but couldn't figure out how to adapt the examples to my code. I have the following component which triggers a "Too many re-renders" error:
const EmailVerification = () => {
const [showMessage, setShowMessage] = useState(true);
const [text, setText] = useState("...Loading. Do not close.");
const { data, error } = useQuery(VERIFY_EMAIL);
if (error) {setText(genericErrorMessage);}
if (data) {setText(emailVerificationMessage);}
return (
<Wrapper>
<Message setShowMessage={setShowMessage} text={text} />
</Wrapper>
)
}
How can I reorganize my code to avoid this error? I know that the useEffect hook should be used to perform side effects, although I wouldn't know how to use it in this case (supposing it is necessary).
The error is triggered because you are using setText directly in the render function. This function renders the component after calling it. Because in the next render, data and error are still set, it calls setText again.
You are right about useEffect. With useEffect you can make sure that the setText function is only being called when a change occurs in the data. In your case, that is for the data and/or error variables.
import { useEffect } from 'react';
const EmailVerification = () => {
const [showMessage, setShowMessage] = useState(true);
const [text, setText] = useState("...Loading. Do not close.");
const { data, error } = useQuery(VERIFY_EMAIL);
useEffect(() => {
if (error) setText('message');
if (data) setText('emailVerificationMessage');
}, [error, data]);
return (
<Wrapper>
<Message setShowMessage={setShowMessage} text={text} />
</Wrapper>
)
}
However, since you are only changing the text variable using already existing props, you can also do this in JS(X) only:
const EmailVerification = () => {
const [showMessage, setShowMessage] = useState(true);
const { isLoading, data, error } = useQuery(VERIFY_EMAIL);
const text = isLoading ? 'Loading... Do not close' : error || !data ? 'Error message' : 'emailVerificationMessage';
return (
<Wrapper>
<Message setShowMessage={setShowMessage} text={text} />
</Wrapper>
)
}
This uses a nested ternary operator (not a fan) which can be replaced with any other method.
setText will cause a rerender and will be called again on the next render. As I understand, you want to set the text once the query returns either an error or the data.
To avoid this, either use onError and onCompleted that you can pass to useQuery like so :
const { data, error } = useQuery(VERIFY_EMAIL, {
onCompleted: () => setText(emailVerificationMessage),
onError: () => setText(genericErrorMessage)
});
and remove these two lines:
if (error) {setText(genericErrorMessage);}
if (data) {setText(emailVerificationMessage);}
or call setText in a useEffect:
useEffect(() => {
if (error) {
setText(genericErrorMessage)
}
}, [error])
Related
I am attempting to render either an Application or Login page depending on whether getUser() returns a user object.
However, in both development and production, a blank page is rendered.
This is the code
export default function index() {
supabase.auth.getUser().then((response) => {
const userData = response.data.user;
console.log(userData);
return userData != undefined || userData != null ? (
<>
<Shell />
<AppView />
</>
) : (
<NoSessionWarn />
);
});
}
I use NextJS's router.push('/application') to route the user to this page, in case that might have something to do with it.
Any idea why this could be showing a blank page? I've tried taking the return block out of the .then() block and still nothing.
Few things:
In React functional components, side effects must be handled inside
a useEffect hook
React components names should be capitalized (Index instead of index in your case).
Most of the time it's a better idea to use strict equality operator since it also checks for the type of the operands.
As a suggestion, you could abstract the logic of the auth checking process into a custom hook. This not only increases the readability of the component, but also makes this logic reusable and you now would have separation of concerns. Your component doesn't know and doesn't care about how the user data is being retrieved, it just uses it.
Putting it all together:
useAuth custom hook:
export const useAuth = () => {
const [user, setUser] = useState(null)
const [isAuthorizing, setIsAuthorizing] = useState(true)
useEffect(() => {
supabase.auth
.getUser()
.then((response) => {
setUser(response.data.user)
})
.catch((err) => {
console.error(err)
})
.finally(() => {
setIsAuthorizing(false)
})
}, [])
return { user, isAuthorizing }
}
Component:
export default function Index() {
const { user, isAuthorizing } = useAuth()
if (isAuthorizing) return <p>Loading</p>
// Being very explicit here about the possible falsy values.
if (user === null || user === undefined) return <NoSessionWarn />
return (
<>
<Shell />
<AppView />
</>
)
}
You need to use the useState hook to re-render when you receive the data.
You need to use the useEffect hook with an empty dependency array to execute getUser() once on mount.
You'll also probably want a loading mechanism while the request is made.
export default function index() {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
supabase.auth.getUser().then((response) => {
setUserData(response.data.user);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>
if (!userData) return <NoSessionWarn />;
return (
<>
<Shell />
<AppView />
</>
);
}
Example: https://stackblitz.com/edit/react-ts-neq5rh?file=App.tsx
I am rendering a react Component App.tsx which uses useEffect hook. Inside the useEffect, it makes an async call updates the state delivery defined in App.tsx. At first render, delivery is undefined, but after next re-render, it is defined.
const App = () => {
const [delivery, setDelivery] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setup()
}, [])
const setup = async () => {
try {
const response = await someAsyncCall()
setDelivery(response)
setLoading(true)
/// Other functionality
} catch (err) {
console.log(err)
}finally{
setLoading(false)
}
}
return (
<>
{loading? <div>Loading!!</div>
: <div>App has loaded with {delivery.displayName} {delivery.id}</div>
}
</>
)
}
How to write unit test so that it re-renders with right value? Right now I get Uncaught [TypeError: Cannot read property 'displayName' of undefined]
Unit Test I wrote :
describe('test', ()=>{
it("mounts", ()=>{
const wrapper = mount(<App />)
})
})
Any thoughts on how to re-render? I read wrapper.update() could be used. I was wondering how?
Shouldn't this solve the problem?
return (
<>
{loading || !delivery ? <div>Loading!!</div>
: <div>App has loaded with {delivery.displayName} {delivery.id}</div>
}
</>
)
I am having a weird issue inside useEffect() in my React component. I have to make 2 separate axios requests to get data when the page loads. I am trying to use a hook variable to see if the data objects are populated before passing them to the JSX. Here's my current configuration:
import React, { useState, useEffect } from 'react';
import Navbar from '../components/layout/Navbar';
import ContactsCard from '../components/layout/ContactsCard';
import EmailCard from '../components/layout/EmailCard';
import MeetingsCard from '../components/layout/MeetingsCard';
import { useParams } from "react-router-dom";
import config from './../config/config';
import axios from "axios";
function SummaryPageNew() {
let { selectName } = useParams();
const [contactData, setContactData] = useState();
const [meetingData, setMeetingData] = useState();
const [loadingData, setLoadingData] = useState(true);
//API calls
async function getContactData() {
axios
.get(config.apiURL + `/affiliations/name/${selectName}`)
.then((response) => {
return setContactData(response.data[0]);
});
}
async function getMeetingData() {
axios
.get(config.apiURL + `/meetings_attendees/name/${selectName}`)
.then((response) => {
return setMeetingData(response.data);
});
}
useEffect((loadingData) => {
getContactData();
getMeetingData();
setLoadingData(false);
if (loadingData) {
//if the result is not ready so you make the axios call
getContactData();
getMeetingData();
setLoadingData(false);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div>
<Navbar />
<div>
<div style={{ textAlign: "center" }}>
<h3>Contact Information</h3>
<h5>Profile: {selectName}</h5>
</div>
{loadingData ? (
<p>Loading Please wait...</p>
) : (
<div className="row">
<ContactsCard contactData={contactData} />
<EmailCard emailData={meetingData} />
<MeetingsCard meetingData={meetingData} />
</div>
)}
</div>
</div>
)
}
export default SummaryPageNew
I have tried moving the setLoadingData(false) method inside the axios calls. If I move it inside the getMeetingData() call. This works ... sometimes. Apparently, on some occasions, it loads first and then the contactData doesn't get returned. In the current configuration, the DOM renders with "Loading Please wait...". What am I doing wrong here? How can I resolve this issue?
There are many issues with your code.
useEffect functions don't take any parameters. Your declaration of loadingData as a parameter is covering the actual loadingData variable in your component, and React will not pass a value for this.
You're missing a dependency on loadingData in your call to useEffect. As is, the function will only execute once and then never again as long as the component stays mounted. So, loadingData never gets set to false. Generally, it is a bad idea to avoid warnings about useEffect dependencies unless you have a very good reason.
My recommended solution would be to avoid storing extra state for the "loading" status. Instead, I would just check whether the two state values have been populated yet, and show the "Loading..." text if either is not.
This leaves you with:
function SummaryPageNew() {
let { selectName } = useParams();
const [contactData, setContactData] = useState();
const [meetingData, setMeetingData] = useState();
const isReady = contactData !== undefined && meetingData !== undefined;
//API calls
async function getContactData() { ... }
async function getMeetingData() { ... }
useEffect((loadingData) => {
getContactData();
getMeetingData();
}, []);
return (
<div>
<Navbar />
<div>
<div style={{ textAlign: "center" }}>
<h3>Contact Information</h3>
<h5>Profile: {selectName}</h5>
</div>
{isReady ? (
<div className="row">
<ContactsCard contactData={contactData} />
<EmailCard emailData={meetingData} />
<MeetingsCard meetingData={meetingData} />
</div>
) : (
<p>Loading Please wait...</p>
)}
</div>
</div>
)
}
react-query is a very powerful library for fetching data asynchronously using hooks. This avoids having to manage complex state which can easily fall out of sync. However, I'd learn the fundamentals of react hooks first!
You're dealing with async function calls. Javascript doesn't wait for your async functions to complete before it continues with your program. This means your calls are probably still fetching, while you already set loadingData to false. You can fix this by using Promise.all to get a callback when the async functions resolve:
//API calls
async function getContactData() {
return axios
.get(config.apiURL + `/affiliations/name/${selectName}`)
.then((response) => {
return setContactData(response.data[0]);
});
}
async function getMeetingData() {
return axios
.get(config.apiURL + `/meetings_attendees/name/${selectName}`)
.then((response) => {
return setMeetingData(response.data);
});
}
useEffect(() => {
let mounted = true
return () => { mounted = false }
Promise.all([getContactData(), getMeetingData()]).then(() => {
if (mounted) setLoadingData(false)
})
}, []); // eslint-disable-line react-hooks/exhaustive-deps
Also note the let mounted = true I've added: you want to make sure this component still exists whenever your async calls complete. If the calls take a while, it's not unthinkable you might have navigated away, for instance.
Finally, it's not a wise idea to disable react-hooks/exhaustive-deps. With a few changes you can setup your code in such a way that this ignore is no longer needed.
React want you to provide getContactData, getMeetingData in the dependency array. You can fix that by moving the data fetching function outside of you component. This means they no longer have access to the selectName variable, but you can provide that variable as an argument:
function SummaryPageNew() {
let { selectName } = useParams();
const [contactData, setContactData] = useState();
const [meetingData, setMeetingData] = useState();
const [loadingData, setLoadingData] = useState(true);
//API calls
useEffect(() => {
let mounted = true
Promise.all([
getContactData({ selectName }),
getMeetingData({ selectName })
]).then(([contactData, meetingData]) => {
if (!mounted) return
setContactData(contactData)
setMeetingData(meetingData)
setLoadingData(false)
})
return () => { mounted = false }
}, [selectName]);
return () // Render your component
}
async function getContactData({ selectName }) {
return axios
.get(config.apiURL + `/affiliations/name/${selectName}`)
.then((response) => {
return setContactData(response.data[0]);
});
}
async function getMeetingData({ selectName }) {
return axios
.get(config.apiURL + `/meetings_attendees/name/${selectName}`)
.then((response) => {
return setMeetingData(response.data);
});
}
I have the following case:
export default function Names() {
const dispatch = useDispatch();
const [names, setNames] = useState([]);
const stateNames = useSelector(state => state.names);
const fetchNames = async () => {
try {
const response = await nameService.getNames();
dispatch(initNames(response.body));
setNames(response.body);
} catch (error) {
console.error('Fetch Names: ', error);
}
};
useEffect(() => {
fetchNames();
}, []);
return (
{ names.map((name, index) => (
<Tab label={ budget.label} key={index}/>
)) }
);
}
When my component is rendered in the browser console I get a warning: "React Hook useEffect has a missing dependency: 'fetchBudgets'. Either include it or remove the dependency array react-hooks / exhaustive-deps".
If I comment the line in which I write the names in Redux state, the warning does not appear.
I need the list of names in the state so that I can update the list when a new name is written to the list from the outside.
export default function AddNameComponent() {
const dispatch = useDispatch();
const [label, setLabel] = useState('');
const [description, setDescription] = useState('');
const onLabelChange = (event) => { setLabel(event.target.value); };
const onDescriptionChange = (event) => { setDescription(event.target.value); };
const handleSubmit = async (event) => {
try {
event.preventDefault();
const newName = {
label: label
description: description
};
const answer = await budgetService.postNewName(newName);
dispatch(add(answer.body)); // Adding new Name in to Redux state.names
} catch (error) {
setErrorMessage(error.message);
console.error('Create Name: ', error);
}
};
return (
<div>
// Create name form
</div>
)
}
This is how everything works, but I don't understand why I have a warning.
I tried to add a flag to the array with dependencies of usеЕffect.
I tried to pass the function 'fetchNames' through the parent component - in props and to add it as a dependency, but it is executed twice ...
Can you advise please!
It's just an eslint warning so you don't have to fix it. But basically any variables which are used in the useEffect function are expected to be included in the dependency array. Otherwise, the effect will never be re-run even if the function fetchBudgets were to change.
It is expecting your hook to look like
useEffect(() => {
fetchBudgets();
}, [fetchBudgets]);
Where the effect will run once when the component is mounted and run again any time that the fetchBudgets function changes (which is probably never).
If it's executing more than once, that means that fetchBudgets has changed and you should try to figure our where and why it has been redefined. Maybe it needs to be memoized?
Here are the docs on putting functions in the dependency array.
Thanks for your attention! I tried many options and finally found one solution.
useEffect(() => {
async function fetchNames() {
const response = await nameService.getNames();
dispatch(init(response.body));
setNames(response.body);
}
fetchNames();
}, [dispatch, props]);
I put 'props' in an array of dependencies for one useEffect execution.
I have a React stateless function component (SFC). I want a user to click a button and onclick a http call will be made from the SFC and when the response is received, I want to open a modal. Is it something achievable using SFC? Or do I need to keep a stateful component?
This is my code which makes the http call on load and then onClick opens the modal. But I want both the things to happen in sequence on the onClick event.
//HTTP CALL FUNCTION
function httpListCall(url) {
const [list, setData] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
axios
.get(url)
.then(function (response) {
setData(response.data.ResultList);
})
.catch(function (error) {
setError(error);
})
}, []);
return { list, error };
};
//SFC
const ListContainer = () => {
const { list, error } = httpListCall("/list.json"); //THIS IS ON LOAD NOW - I WANT TO CALL IT onClick
const [modalShow, setModalShow] = React.useState(false);
return(
<React.Fragment>
<div>
<button onClick={() => setModalShow(true)}/> //WANT TO MAKE API CALL HERE AND THEN OPEN THE MODAL
</div>
<ModalWidget show={modalShow} list={advisorList} error={error} onHide={() => setModalShow(false)}/>
</React.Fragment>
)
}
export default ListContainer;
ReactDOM.render(<ListContainer/>, document.getElementById("app"));
I have tried to make the http call from a function but it gives me an error:
"Invalid hook call. Hooks can only be called inside of the body of a function component."
SOLUTION (updated):
You have to follow custom hooks implementation requirement as I have mentioned in comment under your question - have to use name "use" in front of your custom hook function name and update useEffect dependencies.
function useHttpListCall(url) {
const [list, setData] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
axios
.get(url)
.then(function(response) {
setData(response.data);
})
.catch(function(error) {
setError(error);
});
}, [url]);
return { list, error };
}
And update your functional component to use your custom Hook like this.
const ListContainer = () => {
const [modalShow, setModalShow] = React.useState(false);
const [endpoint, setEndpoint] = React.useState(null);
const { list } = useHttpListCall(endpoint);
const handleClick = () => {
setModalShow(true);
setEndpoint("https://api.github.com/users?since=135");
};
return (
<React.Fragment>
<div>
<button onClick={handleClick}>Show modal of Github users</button>
</div>
{modalShow && list && (
<div>
{list.map(user => {
return <div key={user.id}>{user.login}</div>;
})}
</div>
)}
</React.Fragment>
);
};
I didn't implement modal as you did not provide the code for this component.
Fully working code is available here (for you to use as a reference): https://codesandbox.io/s/simple-custom-hook-n6ysw
You cant use the new react hooks (useState, useEffect etc.) in a "normal" function, it has to be a function component.
You can put the hooks inside the component scope and keep the axios request in a seperate function.