useLazyQuery results into "Too many re-renders" - reactjs

I am trying to Execute queries manually and followed, therefore, the official Apollo documentation. I don't see any difference between the documentation and my code, however, I always get the error "Too many re-renders."
const FEED_QUERY = gql`
query contactAccessKey($accessKey: String!) {
contactAccessKey(accessKey: $accessKey) {
id
contactInformation
belongsTo {
id
username
email
}
}
}
`;
function AccessKeySeeEmergencyContact() {
const [contact, setContact] = useState("");
const [getContactInformation, { loading, data }] = useLazyQuery(FEED_QUERY);
if (loading) return <p>Loading ...</p>;
if (data && data.contactAccessKey) {
setContact(data.contactAccessKey);
}
const accessKeyCode = "34a60667-80e3-4c97-9ded-15875d6507b1";
return (
<div>
{contact && <div>{contact.contactInformation}</div>}
<button
onClick={() =>
getContactInformation({ variables: { accessKey: accessKeyCode } })
}
>
Click me!
</button>
</div>
);
}
export default AccessKeySeeEmergencyContact;

useApolloClient has functions that return a Promise and I have solved a similar issue by using this.
Please note I just copied and edited your code, there could be few syntax errors. All the best.
import { useApolloClient } from '#apollo/client';
function AccessKeySeeEmergencyContact() {
const [contact, setContact] = useState('');
const client = useApolloClient(); // this gives an ApolloClient
const accessKeyCode = '34a60667-80e3-4c97-9ded-15875d6507b1';
return (
<div>
{contact && <div>{contact.contactInformation}</div>}
<button
onClick={async () => {
const { data } = await client.query({
query: FEED_QUERY,
variables: { accessKey: accessKeyCode },
});
console.log(data);
}}>
Click me!
</button>
</div>
);
}
export default AccessKeySeeEmergencyContact;

Bad Apollo example!
const [contact, setContact] = useState("");
const [getContactInformation, { loading, data }] = useLazyQuery(FEED_QUERY);
if (loading) return <p>Loading ...</p>;
// additional condition to avoid endless rerendering
if (!contact && data && data.contactAccessKey) {
setContact(data.contactAccessKey);
}
Using state is unnecessary (even with condition forces additional rerendering) - you can read data directly from data (it's just a private variable):
return (
<div>
{data && <div>{data.contactInformation}</div>}

Related

How to use useCollection react-firebase hook with firebase v9

I want to use react-firebase-hooks for firestore, but I cant seem to make it work properly, I have collection of users and it has subcollection of budgets. If user is logged in I want to show his budgets. My code:
The problem is mainly in useCollection hook, where I need to pass user.uid, but user is undefined in the beginning. How else should I do this?
const Homepage = () => {
const [user, loading, error] = useAuthState(auth);
const [showAddBudgetModal, setShowAddBudgetModal] = useState(false);
const [value, dataLoading, dataError] = useCollection(collection(db, `users/${user!.uid}/budgets`), {
snapshotListenOptions: { includeMetadataChanges: true },
});
const [showAddExpenseModal, setShowAddExpenseModal] = useState(false);
const [addExpenseModalBudgetId, setAddExpenseModalBudgetId] = useState('');
const openAddExpenseModal = (budgetId?: any) => {
setShowAddExpenseModal(true);
setAddExpenseModalBudgetId(budgetId);
};
if (loading) {
return (
<div>
<p>Initialising User...</p>
</div>
);
}
if (error) {
return (
<div>
<p>Error: {error.message}</p>
</div>
);
}
if (user) {
return (
<div>
<h1>Welcome {user.displayName}!</h1>
<button onClick={logOut}>Logout</button>
<div className="buttons">
<button onClick={() => setShowAddBudgetModal(true)}>Add budget</button>
</div>
<AddBudgetModal show={showAddBudgetModal} onClose={() => setShowAddBudgetModal(false)}></AddBudgetModal>
<div>
{dataError && <strong>Error: {JSON.stringify(error)}</strong>}
{dataLoading && <span>Collection: Loading...</span>}
{value && user && (
<div>
{value.docs.map((budget) => (
<BudgetCard
key={budget.id}
name={budget.name}
max={budget.max}
onAddExpenseClick={() => openAddExpenseModal(budget.id)}
onViewExpensesClick={() => openAddExpenseModal(budget.id)}
></BudgetCard>
))}
</div>
)}
</div>
</div>
);
}
};
export default Homepage;
There are 2 ways to solve this problem in my opinion
First one is to use optional chaining to check whether the user exist or not eg:
const [value, dataLoading, dataError] = useCollection(user && query(
collection(getFirestore(app), "users", user.uid, "budgets"), {
snapshotListenOptions: {
includeMetadataChanges: true
},
});
For more information about this there is a similar thread about this issue.
Second way is to use useEffect hook to implement the same like this:
useEffect(() => {
if (user) {
const [value, dataLoading, dataError] = useCollection(user && query(
collection(getFirestore(app), "users", user.uid, "budgets"), {
snapshotListenOptions: {
includeMetadataChanges: true
},
});
}
}, [user]);
In this way your user can only be rendered if and only if data related to the user is loaded already, but make sure in future to not set user in useEffect as it will create an infinite loop as useEffect has provided user in dependencies array.
For more about usEffect you can go through this documentations

Why I'm getting Rendered more hooks error?

I was wonder what I'm doing wrong here.
I'm getting this error: "Rendered more hooks than during the previous render."
export default function ProductDetails() {
//Use State
const {qty, increaseQty, decreaseQty, onAdd, setQty} = useStateContext();
//Reset Qty
useEffect(() => {
setQty(1);
}, []);
//Fetch Slug
const {query} = useRouter();
//Fetch Graphql data
const [results] = useQuery({
query: GET_PRODUCT_QUERY,
variables: {slug: query.slug}
})
const {data, fetching, error} = results;
//Check for data coming in
if(fetching) return <p>Loading...</p>;
if(error) return <p>Oh no....</p>;
//Extract Product Data
const {title,description, image, gallery } = data.products.data[0].attributes;
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
console.log(img);
//Create a toast
const notify = () => {
toast.success(`${title} added to your cart`, {duration: 1500});
}
return(
<DetailsStyle>
<Gallery>
<img src={gallery.data[0].attributes.formats.medium.url} alt={title} />
<Thumbnails>
{gallery.data.map((image, index) => (
<SingleThumb key={index} >
<img src={image.attributes.formats.thumbnail.url} alt={title} />
</SingleThumb>
)
)}
</Thumbnails>
</Gallery>
<ProductInfo>
<h3>{title}</h3>
<p>{description}</p>
<Quantity>
<span>Quantity</span>
<button><AiFillMinusCircle onClick={decreaseQty} /></button>
<p>{qty}</p>
<button><AiFillPlusCircle onClick={increaseQty}/></button>
</Quantity>
<Buy onClick={() => {
onAdd(data.products.data[0].attributes, qty)
notify();
}}>Add To Cart</Buy>
</ProductInfo>
</DetailsStyle>
)
}
Something wrong is in this line: const [img, setImg] = useState();
Why I can't use more hooks here.
Does anyone know why I'm getting this?
You are using early return
and this line of code won't execute every time:
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
This is only conditionally called:
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
Because the component has earlier conditional return statements. Move it to earlier in the function. (Generally I invoke useState operations right away.)
Hooks need to always be consistently called in the same order on every render.
You declare your state after some return statements. It means that if you had any errors or you were in loading state, the state is not defined. But maybe in the next render, the data is set and then your state will be defined with the inital value (gallery.data[0].attributes.formats.medium.url).
It's forbidden in react because all of the hooks should always be in the same order on every single render. In order to fix this, you should change the place of your useState for img.
Hope it helps:
export default function ProductDetails() {
const [img, setImg] = useState('');
//Use State
const {qty, increaseQty, decreaseQty, onAdd, setQty} = useStateContext();
//Reset Qty
useEffect(() => {
setQty(1);
}, []);
//Fetch Slug
const {query} = useRouter();
//Fetch Graphql data
const [results] = useQuery({
query: GET_PRODUCT_QUERY,
variables: {slug: query.slug}
})
const {data, fetching, error} = results;
//Check for data coming in
//Extract Product Data
useEffect(() => {
if(results && results.data) {
const {data} = results
const { gallery } = data.products.data[0].attributes;
setImg(gallery.data[0].attributes.formats.medium.url);
}
}, [results]);
useEffect(() => {
console.log(img);
}, [img]);
//Create a toast
const notify = (title) => {
toast.success(`${title} added to your cart`, {duration: 1500});
}
if(fetching) {
return <p>Loading...</p>;
} else if(error) {
return <p>Oh no....</p>;
} else if(data) {
const { title, description, image, gallery } = data.products.data[0].attributes;
return(
<DetailsStyle>
<Gallery>
<img src={gallery.data[0].attributes.formats.medium.url} alt={title} />
<Thumbnails>
{gallery.data.map((image, index) => (
<SingleThumb key={index} >
<img src={image.attributes.formats.thumbnail.url} alt={title} />
</SingleThumb>
)
)}
</Thumbnails>
</Gallery>
<ProductInfo>
<h3>{title}</h3>
<p>{description}</p>
<Quantity>
<span>Quantity</span>
<button><AiFillMinusCircle onClick={decreaseQty} /></button>
<p>{qty}</p>
<button><AiFillPlusCircle onClick={increaseQty}/></button>
</Quantity>
<Buy onClick={() => {
onAdd(data.products.data[0].attributes, qty)
notify(title);
}}>Add To Cart</Buy>
</ProductInfo>
</DetailsStyle>
)
} else {
return null;
}
}
There should be no return before hooks.
These lines
if(fetching) return <p>Loading...</p>;
if(error) return <p>Oh no....</p>;
should be after all hooks

Make an update request using Axios in React and Material UI

I am pretty new to React, and the only CRUD functionality I have done has been in MVC.net, so I am pretty lost here and none of the tutorials I am finding seem to be the situation I have going on...or maybe I am just misunderstanding them. The tips I got with my initial question helped, but I am still no getting it to work, so hopefully now I have supplies enough info to help other help me. I did no include all of the input fields because their are like 15 and it was just redundant.
I am pulling up the modal using this onClick event:
onClick={()=> {handleEditModal(item)}}
modalFunctions.js
// Modal Functionality
export function ModalFunctions() {
const [selectedRecord, setSelectedRecord] = useState([]);
const [openModal, setOpenModal] = useState(false);
const handleEditModal = item =>{
setOpenModal(true)
setSelectedRecord(item)
}
return {
selectedRecord,
setSelectedRecord,
openModal,
setOpenModal,
handleEditModal,
handleDetailModal
}
}
// Form Functionality
export function FormFunctions(validateOnChange = false, validate) {
const [values, setValues] = useState('');
const [errors, setErrors] = useState({});
const handleInputChange = e => {
const { name, value } = e.target
setValues({
...values,
[name]: value
})
if (validateOnChange)
validate({ [name]: value })
}
return {errors, handleInputChange, setErrors, setValues, values}
}
DataTable.js
// Imported Modal Functions
const {
selectedRecord,
openModal,
setOpenModal,
handleEditModal,
handleDetailModal
} = ModalFunctions();
const baseURL = 'http://localhost:8080/api/tanks';
// Fetch Data
useEffect(() => {
const fetchData = async () =>{
setLoading(true);
try {
const {data: response} = await axios.get(baseURL);
setTableData(response);
} catch (error) {
console.error(error.message);
}
setLoading(false);
};
fetchData();
}, [baseURL, setTableData, setLoading]);
// The Modal
return(
<Modal
title={ "Editing: " + (selectedRecord.tankName) }
openModal={openModal}
setOpenModal={setOpenModal}
>
<TankEdit
selectedRecord={selectedRecord}
setOpenModal={setOpenModal}
openModal={openModal}
/>
</Modal>
)
TankEdit.js
export function TankEdit(props) {
const { baseURL, openModal, selectedRecord, setOpenModal, setTableData } = props;
const validate = (fieldValues = item) => {
let temp = { ...errors }
if ('tankName' in fieldValues)
temp.tankName = fieldValues.tankName ? "" : "This field is required."
setErrors({
...temp
})
if (fieldValues === values)
return Object.values(temp).every(x => x === " ")
}
const {
values,
setValues,
errors,
setErrors,
handleInputChange,
} = FormFunctions(true, validate);
useEffect(() => {
if (selectedRecord !== null)
setValues({
...selectedRecord
})
}, [selectedRecord, setValues])
function editRecord() {
axios.put(`${baseURL}`, {
title: "Success",
body: "The record had been successfully updated"
}).then ((response) => {setTableData(response.data);})
}
const handleSubmit = e => {
e.preventDefault()
if (validate()) {
editRecord(values);
}
setOpenModal(false)
}
const item = values; // used for easier referencing (matches table item)
return (
<Form onSubmit={handleSubmit} open={openModal}>
<Grid>
<Controls.Input
name="tankName"
label="Tank Name"
value={item.tankName}
onChange={handleInputChange}
error={errors.tankName}
/>
</Grid>
<Grid>
<Controls.Button
type="submit"
text="Submit"
/>
<Controls.Button
text="Cancel"
color="default"
onClick={()=>{setOpenModal(false)}}
/>
</Grid>
</Form>
)
}
Input.js
export default function Input(props) {
const { error=null, label, name, onChange, value, ...other } = props;
return (
<TextField
variant="outlined"
label={label}
name={name}
value={value}
defaultValue=''
onChange={onChange}
{...other}
{...(error && {error:true,helperText:error})}
/>
)
}
My company is only wanting a Read and an Update function, since Creating and Deletion will be handled another ways, so this is my final hangup. I think I am close, but I am missing something.
Can anyone point me in the right direction?
THANKS!!!!
If you want to write an update request you would use axios.put to send the data to your back-end.
In your handleSubmit function you do:
let response = await axios.put('http://-------/api/tanks', { name: 'tank' });
(The second parameter is an object that needs to contain all the form data fields)
Also make sure you call e.preventDefault() in the handleSubmit function so you don't accidentally navigate elsewhere.
Then you will update your database or whatever using your back-end.
for update you should use put or patch method
and you should send id of item you want to update in request url.
I insert 2 example here.
this is for put:
const res = await axios.put('/api/article/123', {
title: 'Making PUT Requests with Axios',
status: 'published'
});
this is for patch:
const res = await axios.patch('/api/article/123', {
title: 'Making PUT Requests with Axios',
status: 'published'
});

React Apollo Client: Pagination and fetch data on button click

I am new to Apollo Client and want to implement pagination. My code looks like this:
I am using RickandMorty endpoint for this (https://rickandmortyapi.com/graphql)
useCharacters.tsx
import { useQuery, gql } from '#apollo/client';
const GET_ALL_CHARACTERS = gql`
query GetCharacters($page: Int) {
characters(page: $page) {
info {
count
pages
}
results {
id
name
}
}
}
`;
export const useCharacters = (page: number = 1) => {
const { data, loading, error } = useQuery(GET_ALL_CHARACTERS, { variables: { page } });
return { data, loading, error };
};
App.tsx
export const App = () => {
const { data, loading, error } = useCharacters(1);
const nextPage = () => {
const { data, loading, error } = useCharacters(2);
};
return (
<>
{loading ? (
<div> Loading... </div>
) : error ? (
<div>Error</div>
) : (
<>
<CharacterList data={data.characters.results} />
<div onClick={nextPage}> Next </div>
</>
);
};
It is fetching data properly the first time but I want to fetch new data when Next button is clicked on page 2.
I know I can't call useQuery() in a method like this as hooks cannot be called inside a block and also the data, error, and loading won't be accessible outside.
How can I fix this issue? I tried googling it but could not find any help related to this.
This might help other developers who are new to Apollo Client and will save them time.
fetchMore() can be used for pagination with Apollo Client.
useCharacters.tsx
export const useCharacters = (page: number = 1, name: string = '') => {
const { data, loading, error, fetchMore } = useQuery(GET_ALL_CHARACTERS, {
variables: { page, name },
notifyOnNetworkStatusChange: true, // to show loader
});
return { data, loading, error, fetchMore }; // returning fetchMore
};
App.tsx
export const App = () => {
const { data, loading, error, fetchMore } = useCharacters(1);
const nextPage = () => {
/* You can call the returned fetchMore() here and pass the next page number.
updateQuery() simply updates your data to the newly fetched records otherwise return previous records
*/
fetchMore({
variables: {
page: 2,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return fetchMoreResult;
},
});
};
return (
<>
{loading ? (
<div> Loading... </div>
) : error ? (
<div>Error</div>
) : (
<>
<CharacterList data={data.characters.results} />
<div onClick={nextPage}> Next </div>
</>
);
};

not able to render api results in react

This is code and I am trying to access the result from the api but I am not able to show it on my page.
The results are visible in console but not in page.
I have tried few things but it hasn't worked for me.
I am stuck on this only for the last few days.
import React, { useState } from 'react'
const SearchArea = () => {
const [input,setInput] = useState("");
const [results,setResults] = useState(null)
const onInputChange = (ev) => {
setInput(ev.target.value)
}
const onSearch = () => {
fetch(`https://api.shrtco.de/v2/shorten?url=${input}`)
.then(r=>r.json())
.then(result=>{
setResults(result) ;
console.log(result.result.full_short_link)})
}
const onKeyDown= (ev) =>{
if(ev.keyCode === 13){
onSearch();
}
}
const renderResult = () => {
if(results && results.ok === 0){
return <div>No link to convert.</div>
}
if(results && results.length > 0){
return <div>{results.map((item)=><div>{item.results.result.full_short_link}</div>)}</div>
}
return null
}
return (
<div>
<div className="search-bar">
<input type="text" onKeyDown={onKeyDown} onChange={onInputChange} value={input} className="searching" placeholder="Shorten a link here..."></input>
<button className="short-butt" onClick={onSearch}>Shorten It!</button>
<div>{renderResult()}</div>
</div>
</div>
)
}
export default SearchArea
From what I can see, it seems that you are trying to show MULTIPLE results from the API. So you must start with an array instead of null in the state.
const [results, setResults] = useState([]);
Then for every response from the API, you could either push into the results with results.push() (not recommended) or you could do spread operator like below (more recommended):
fetch(`https://api.shrtco.de/v2/shorten?url=${input}`)
.then((r) => {
return r.json();
})
.then((result) => {
console.log(result.result.full_short_link);
setResults([...results, result.result.full_short_link]); //spread operator
})
.catch((e) => {
console.error(e);
});
Later you can use map on showing the results.
if(results && results.length > 0){
return <div>{results.map((item)=><div>{item.results.result.full_short_link}</div>)}</div>
}
Result:
You can see the code in action: https://codesandbox.io/s/react-link-shorter-7ro9o?file=/index.js

Resources