How to access cache to display data with React Query? - reactjs

My application has a user input an id to send as a request and the response data that matches that id is rendered and also cached.
I'd like to access the cache so that I can also display its data so that a user can see their previous queries. However, I receive this error with my attempts. TypeError: Cannot read property 'getQueryData' of undefined.
The following are my attempts to access the cache.
console.log(queryCache.getQueryData("rickandmorty"))
console.log(queryCache.getQueryData(["rickandmorty", idQuery]))
console.log(queryClient.getQueryData("rickandmorty"))
console.log(queryClient.getQueryData(["rickandmorty", idQuery]))
Please let me know what I'm doing wrong. I'm using version "react-query": "^3.13.0" https://codesandbox.io/s/rick-and-morty-2gy8r?file=/src/App.js
const [idQuery, setIdQuery] = useState(0);
const [rickAndMortyCharacter, setRickAndMortyCharacter] = useState({});
const queryRef = useRef(null);
const { register, handleSubmit, errors, setValue, watch } = useForm({
resolver: yupResolver(searchSchema)
});
const formId = watch("rickAndMortyId");
const handleRickAndMortyFetch = () => {
return delay()
.then(() => axios(`https://rickandmortyapi.com/api/character/${idQuery}`))
.then((res) => setRickAndMortyCharacter(res.data));
};
const { isLoading, error } = useQuery(
["rickandmorty", idQuery],
handleRickAndMortyFetch,
{
refetchOnWindowFocus: false,
enabled: idQuery !== 0,
useErrorBoundary: true
}
);
const disable = isLoading || parseFloat(formId) === idQuery;
const onSubmit = (formData) => {
setIdQuery(formData.rickAndMortyId);
};
return (
<div>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
queryRef.current.focus();
}}
resetKeys={[idQuery]}
>
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<h3>Rick and Morty</h3>
<input
type="number"
name="rickAndMortyId"
placeholder="Between 1 and 671"
ref={register}
disabled={isLoading}
/>
{errors.rickAndMortyId && <span>This field is required</span>}
{error && <p>Error occurred: {error.message}</p>}
<button type="submit" disabled={disable}>
Search Character
</button>
</form>
<button
onClick={() => {
const randomId = getRandomRickAndMortyCharacterId();
setValue("rickAndMortyId", randomId);
setIdQuery(randomId);
}}
>
Random
</button>
</div>
<div>
{isLoading && <p>Loading...</p>}
<RickAndMortyInfo rickAndMortyCharacter={rickAndMortyCharacter} />
</div>
<ReactQueryDevtools initialIsOpen={true} />
</ErrorBoundary>
</div>
);

In this code:
const [rickAndMortyCharacter, setRickAndMortyCharacter] = useState({});
const handleRickAndMortyFetch = () => {
return delay()
.then(() => axio('...'))
.then((res) => setRickAndMortyCharacter(res.data));
};
const { isLoading, error } = useQuery(
["rickandmorty", idQuery],
handleRickAndMortyFetch
);
After a successful fetch, you assign the result to a local state instead of handing to react-query, so it doesn't know what data should be cached. You need to return the result like this:
const handleRickAndMortyFetch = () => {
return delay()
.then(() => axio('...'))
.then((res) => res.data); // return instead of calling setState()
};
So react-query can 'catch' it and put it into cache. The next step is to remove your local state rickAndMortyCharacter and reference data instead. It is the resolved result after a successful fetch.
const {
isLoading,
error,
data // remove rickAndMortyCharacter and reference this instead
} = useQuery(
["rickandmorty", idQuery],
handleRickAndMortyFetch
);
Only then can you log the cache as normal:
queryClient.getQueryData(["rickandmorty", 518]);
Live Demo

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

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 - I have to click a button twice to display api data

I would like to display API data on a button click. My state is set to an empty array initially and I guess that's why it shows an empty array at first click, but what could I write differently to show data on first click ? After the initial click it works.
My code:
const Search = () => {
const [textInput, setTextInput] = useState('');
const [tickers, setTickers] = useState([]);
const [prices, setPrices] = useState([]);
const [isClicked, setIsClicked] = useState(false);
const inputHandler = (e) => {
setTextInput(e.target.value);
}
const showData = async (e) => {
e.preventDefault();
const url = `https://www.alphavantage.co/query?function=TIME_SERIES_MONTHLY&symbol=${textInput}&apikey=${process.env.REACT_APP_ALPHA_VANTAGE_API_KEY}`
try {
const data = await axios.get(url);
if(data) {
setPrices([data.data['Monthly Time Series']['2021-11-30']]);
}
} catch(err) {
console.log(err)
}
console.log(prices);
setIsClicked(true);
setTextInput('');
}
return (
<StyledSearch>
<h1>Security Price Monitor App </h1>
<form onSubmit={submitSearch}>
<input type="text" value={textInput} onChange={inputHandler} placeholder='Enter Ticker'/>
<button type="submit" onClick={showData}>Search</button>
</form>
{isClicked &&
<Chart tickers = {tickers} setTickers={setTickers} prices={prices} setPrices={setPrices}/>
}
</StyledSearch>
)
}
Try to change your conditional rendering condition to:
{prices.length > 0 && (
<Chart
tickers={tickers}
setTickers={setTickers}
prices={prices}
setPrices={setPrices}
/>
)}
I think that you can remove the isClicked state. It is redundant.

Fetch data before component render, Reactjs

I am having a slight issue with my react code. I want the data to be completely fetched before rendering. However, I have tried various ways such as 'setGroupLoaded to true' after the async call, but it is still not working. When the component first mounts, 'groupLoaded == false and myGroup == [],', then it goes to the next conditional statement where 'groupLoaded == true' but the myGroup [] is still empty. I was expecting the myGroup [] to be filled with data since groupLoaded is true. Please I need help with it.
function CreateGroup({ currentUser }) {
const [name, setName] = useState("");
const [myGroup, setMyGroup] = useState([]);
const [groupAdded, setGroupAdded] = useState(false);
const [groupLoaded, setGroupLoaded] = useState(false);
const handleChange = (e) => {
const { value, name } = e.target;
setName({
[name]: value,
});
};
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
useEffect(() => {
let groupId = myGroup.length ? myGroup[0].id : "";
let groupName = myGroup.length ? myGroup[0].groupName : "";
if (myGroup.length) {
JoinGroup(currentUser, groupId, groupName);
setTimeout(() => fetchGroupMembers(), 3000);
}
}, [myGroup]);
let itemsToRender;
if (groupLoaded && myGroup.length) {
itemsToRender = myGroup.map((item) => {
return <Group key={item.id} item={item} deleteGroup={deleteGroup} />;
});
} else if (groupLoaded && myGroup.length === 0) {
itemsToRender = (
<div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="exampleInputTitle">Group Name</label>
<input
type="text"
className="form-control"
name="name"
id="name"
aria-describedby="TitleHelp"
onChange={handleChange}
/>
</div>
<button type="submit" className="btn btn-primary">
Add group{" "}
</button>
</form>
</div>
);
}
return (
<div>
<h3>{currentUser ? currentUser.displayName : ""}</h3>
{itemsToRender}
</div>
);
}
The problem is here:
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
When you call setGroupLoaded(true), the first call to setMyGroup(groupArr) hasn't happened yet because fetchMyGroup(currentUser) is asynchronous. If you've never done this before, I highly recommend putting in some logging statements, to see the order in which is executes:
useEffect(() => {
const fetchGroupCreated = async () => {
console.log("Got data")
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
console.log("Before starting to load")
fetchGroupCreated();
setGroupLoaded(true);
console.log("After starting to load")
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
The output will be:
Before starting to load
After starting to load
Got data
This is probably not the order you expected, but explains exactly why you get groupLoaded == true, but the myGroup is still empty.
The solution (as Nick commented) is to move the setGroupLoaded(true) into the callback, so that it runs after the data is retrieved:
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
setGroupLoaded(true);
};
fetchGroupCreated();
An alternative approach may be to await the call to fetchGroupCreated(). I'm not sure if it'll work, but if it does it'll be simpler:
await fetchGroupCreated();

How can I update component whenever typing in input tag in React?

What the below code does is to get data from API, and then render it on the page. searchChange function takes a value from the input tag, and setValue for query state. My api endpoint takes argument to filter the API such as http://127.0.0.1:8000/api/deals/?q=${query}.
I'm very confused how I can update the DealList component with the API updated with query state whenever typing something in the input tag. I'm thinking of that I need to something in searchChange function, but not sure what to do there.
index.js
const useFetch = (url, query, defaultResponse) => {
const [result, setResult] = useState(defaultResponse);
const getDataFromAPI = async url => {
try {
const data = await axios.get(url);
setResult({
isLoading: false,
data
});
} catch (e) {}
};
useEffect(() => {
if (query.length > 0) {
getDataFromAPI(`${url}?q=${query}`);
} else {
getDataFromAPI(url);
}
}, []);
return result;
};
const Index = ({ data }) => {
const query = useInput("");
const apiEndpoint = "http://127.0.0.1:8000/api/deals/";
const dealFetchResponse = useFetch(apiEndpoint, query, {
isLoading: true,
data: null
});
const searchChange = e => {
query.onChange(e);
query.setValue(e.target.value);
};
return (
<Layout>
<Head title="Home" />
<Navigation />
<Container>
<Headline>
<h1>The best lease deal finder</h1>
<h4>See all the lease deals here</h4>
</Headline>
<InputContainer>
<input value={query.value} onChange={searchChange} />
</InputContainer>
{!dealFetchResponse.data || dealFetchResponse.isLoading ? (
<Spinner />
) : (
<DealList dealList={dealFetchResponse.data.data.results} />
)}
</Container>
</Layout>
);
};
export default Index;
The biggest challenge in something like this is detecting when a user has stopped typing.. If someone is searching for 'Milk' - when do you actually fire off the API request? How do you know they aren't searching for 'Milk Duds'? (This is hypothetical, and to demonstrate the 'hard' part in search bars/APIs due to their async nature)..
This is typically solved by debouncing, which has been proven to work, but is not very solid.
In this example, you can search Github repos...but even in this example, there are unnecessary requests being sent - this is simply to be used as a demonstration. This example will need some fine tuning..
const GithubSearcher = () => {
const [repos, setRepos] = React.useState();
const getGithubRepo = q => {
fetch("https://api.github.com/search/repositories?q=" + q)
.then(res => {
return res.json();
})
.then(json => {
let formattedJson = json.items.map(itm => {
return itm.name;
})
setRepos(formattedJson);
});
}
const handleOnChange = event => {
let qry = event.target.value;
if(qry) {
setTimeout(() => {
getGithubRepo(qry);
}, 500);
} else {
setRepos("");
}
};
return (
<div>
<p>Search Github</p>
<input onChange={event => handleOnChange(event)} type="text" />
<pre>
{repos ? "Repo Names:" + JSON.stringify(repos, null, 2) : ""}
</pre>
</div>
);
};
ReactDOM.render(<GithubSearcher />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

Resources