Make a query based on a state change - reactjs

I am making a Pokemon team builder, and am trying to use react-query as I am trying to use the cache features to not make so many requests to the open and public PokeAPI.
I have a dropdown that allows a user to select the name of a Pokemon. When a Pokemon is selected, I want the 'Moves' dropdown to populate with a list of that Pokemons moves for selection.
My initial approach was to use a useEffect hook that depended on the Pokemon Dex Number.
useEffect(() => {
if (pokeDexNum) {
// fetch `https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`
}
}, [pokeDexNum])
I have no clue however how to use this with React-Query. What I want to do is when a user selects a Pokemon, a fetch is made to the PokeAPI to fetch that pokemons available moves, and puts them into the options of the 'Moves' dropdown so that they can select moves.
From the code below you can see that when a Pokemon from the 'Pokemon' dropdown is selected, it updates the const [pokeDexNum, setPokeDexNum] = useState(null); state
<Form>
<Form.Group>
<Form.Dropdown
label="Pokemon"
search
selection
clearable
selectOnBlur={false}
value={pokeDexNum}
onChange={(e, data) => setPokeDexNum(data.value)}
options={[
{ text: "Tepig", value: 498 },
{ text: "Oshawott", value: 501 },
{ text: "Snivy", value: 495 },
]}
/>
<Form.Dropdown label="Moves" search selection options={[]} />
</Form.Group>
</Form>
How would I be able to use react query to fetch depending on whether pokeDexNum is updated
Example of query
const { isLoading, error, data } = useQuery('getPokeMoves', () =>
fetch(`https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`).then(res =>
res.json()
)
)
The above query sometimes fetches even when pokeDexNum is null.

As stated in #Arjun's answer, you need to add the pokeDexNum state to the queryKey array. Think of it as a useEffect's dependency. Any time the pokeDexNum gets updated/changes, a refetch will be triggered for that query.
Since pokeDexNum's initial value is null, you don't want the query to fire before the state is updated. To avoid this, you can use the enabled option in useQuery:
const fetchPokemon = (pokeDexNum) => {
return fetch(`https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`).then((res) =>
res.json()
)
}
const { isLoading, error, data } = useQuery(
['getPokeMoves', pokeDexNum],
() => fetchPokemon(pokeDexNum),
{
enabled: Boolean(pokeDexNum)
}
)
Also, it would be a good idea to add some error handling, I imagine you omitted this for brevity.

Solution: You need to add your pokeDexNum to the queryKey of useQuery.
Here is my suggestion,
define a function to call useQuery. adding your pokeDexNum to the queryKey.
const useGetPokeMoves = (pokeDexNum) => {
return useQuery(
["getPokeMoves", pokeDexNum],
() => {
return fetch(`https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`).then(
(res) => res.json()
);
},
{ keepPreviousData: true }
);
};
Then use it in your component,
const { data } = useGetPokeMoves(pokeDexNum);
Whenever your state changes, the queryKey will also change and the query will be fetched.

Related

Get Firestore Data based on User Input

I am creating a React App which will be used to display Dynamic Maps for DnD (vid files). Based on the User Input in a select field, a video player will get the specific video file from the firebase firestore, where I store the video files.
const Streamplayer = ({vidSelection}) => { //vidSelection is the event.target.value from the Select Input Field (basically a String)
const [links, setLinks] = useState([])
console.log(vidSelection);
useEffect(() => {
getLinks()
}, [])
useEffect(() => {
console.log(links)
}, [links])
function getLinks(){
const vidRef = collection(db, 'links');
getDocs(vidRef).then(response => {
console.log(response.docs)
const lnks = response.docs.map(doc => ({
data: doc.data(),
id: doc.id,
}))
setLinks(lnks)
}).catch(error => console.log(error.message));
}
function getVid(){
const toReturn = links.map((link) => link.data.vidSelection);//I want to change whatever gets returned based on input
console.log(toReturn)
return toReturn;
}
return (
<video
controls={false}
autoPlay
muted
loop
className="Video"
key={getVid()}
>
<source src={getVid()} type="video/mp4" />
</video>
)
}
export default Streamplayer
So in the method getVid() I request the data stored in the Firebase Firestore "Links" Collection and I want to change whichever I get based on the userInput. So if the User chooses the map "Grotto", I want to make the call "link.data.grotto". But since vidSelection is a String, I can't simply insert it in this call. Any Ideas how to solve this?
You can make useEffect get re-called when props change, by providing the prop in the dependency list for useEffect. Simply change your first useEffect call to:
useEffect(() => {
getLinks()
}, [vidSelection])
When the selection changes, useEffect will fire and call getLinks, which will update your state.
(Also, minor suggestion: remove the getVid() function, and just set a const videoSource = links.map((link) => link.data.vidSelection) in the function body. Right now you're calling the function twice which will cause the mapping to happen twice, and I think it's generally clearer to have consts flow directly from state, makes it easier to reason about state.)

How to properly implement "subscription"-like fetches with React useEffect

I have a question about the "proper" (or most idiomatic) way to implement network fetch behavior in React based on a single changing property.
A simplified example of the functionality I'm building is below: I am looking to build a multi-page form that "auto-saves" a draft of form inputs as the user navigates back/forth between pages.
TL;DR - I thought useEffect hooks would be the right way to save a draft to the backend every time a url slug prop changes, but I'm running into issues, and wondering about suggestions for the "right" tool for this type of behavior.
Here is my attempt so far. My code is technically working how I want it to, but violates React's recommended hook dependency pattern (and breaks the exhaustive-deps ESLint rule).
import React from 'react';
const ALL_SLUGS = [
'apple',
'banana',
'coconut',
];
function randomUrlSlug() {
return ALL_SLUGS[Math.floor((Math.random() * ALL_SLUGS.length))];
}
// just resovles the same object passed in
const dummySaveDraftToBackend = (input) => {
return new Promise((resolve, _reject) => {
setTimeout(() => {
resolve(input);
}, 1000);
});
};
export function App() {
const [urlSlug, setUrlSlug] = React.useState(randomUrlSlug());
return (
<MyComponent urlSlug={urlSlug} setUrlSlug={setUrlSlug} />
);
}
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
// useCallback memoization is technically unnecessary as written here,
// but if i follow the linter's advice (listing handleSave as a dependency of the useEffect below), it also suggests memoizing here.
// However, complexState is also technically a dependency of this callback memo, which causes the fetch to trigger every time state changes.
//
// Similarly, moving all of this inside the effect hook, makes the hook dependent on `complexState`, which means the call to the backend happens every time a user changes input data.
const handleSave = React.useCallback(() => {
console.log('*** : start fetch');
setLoading(true);
dummySaveDraftToBackend(complexState).then((resp) => {
console.log('fetch response: ', resp);
// to keep this example simple, here we are just updating
// a dummy "responseCount", but in the actual implementation,
// I'm using a state reducer, and want to make some updates to form state based on error handling, backend validation, etc.
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState]);
// I know this triggers on mount and am aware of strategies to prevent that.
// Just leaving that behavior as-is for the simplified example.
React.useEffect(() => {
if (urlSlug) {
handleSave();
}
}, [urlSlug]); // <- React wants me to also include my memoized handleSave function here, whose reference changes every time state changes. If I include it, the fetch fires every time state changes.
return (
<div className="App">
<h2>the current slug is:</h2>
<h3>{urlSlug}</h3>
<div>the current state is:</div>
<pre>{JSON.stringify(complexState, null, 2)}</pre>
<div>
<h2>edit foo</h2>
<input value={complexState.foo} onChange={(e) => setComplexState((s) => ({ ...s, foo: e.target.value }))} disabled={loading} />
</div>
<div>
<h2>edit baz</h2>
<input value={complexState.baz} onChange={(e) => setComplexState((s) => ({ ...s, baz: e.target.value }))} disabled={loading} />
</div>
<div>
<button
type="button"
onClick={() => setUrlSlug(randomUrlSlug())}
disabled={loading}
>
click to change to a random URL slug
</button>
</div>
</div>
);
}
As written, this does what I want it to do, but I had to omit my handleSave function as a dependency of my useEffect to get it to work. If I list handleSave as a dependency, the hook then relies on complexState, which changes (and thus fires the effect) every time the user modifies input.
I'm concerned about violating React's guidance for not including dependencies. As-is, I would also need to manually prevent the effect from running on mount. But because of the warning, I'm wondering if I should not use a useEffect pattern for this, and if there's a better way.
I believe I could also manually read/write state to a ref to accomplish this, but haven't explored that in much depth yet. I have also explored using event listeners on browser popstate events, which is leading me down another rabbit hole of bugginess.
I know that useEffect hooks are typically intended to be used for side effects based on event behavior (e.g. trigger a fetch on a button click). In my use case however, I can't rely solely on user interactions with elements on the page, since I also want to trigger autosave behavior when the user navigates with their browser back/forward controls (I'm using react-router; current version of react-router has hooks for this behavior, but I'm unfortunately locked in to an old version for the project I'm working on).
Through this process, I realized my understanding might be a bit off on proper usage of hook dependencies, and would love some clarity on what the pitfalls of this current implementation could be. Specifically:
In my snippet above, could somebody clarify to me why ignoring the ESLint rule could be "bad"? Specifically, why might ignoring a dependency on some complex state can be problematic, especially since I dont want to trigger an effect when that state changes?
Is there a better pattern I could use here - instead of relying on a useEffect hook - that is more idiomatic? I basically want to implement a subscriber pattern, i.e. "do something every time a prop changes, and ONLY when that prop changes"
If all the "state" that is updated after saving it to backend is only a call count, declare this as a separate chunk of state. This eliminates creating a render loop on complexState.
Use a React ref to cache the current state value and reference the ref in the useEffect callback. This is to separate the concerns of updating the local form state from the action of saving it in the backend on a different schedule.
Ideally each useState hook's "state" should be closely related properties/values. The complexState appears to be your form data that is being saved in the backend while the responseCount is completely unrelated to the actual form data, but rather it is related to how many times the data has been synchronized.
Example:
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow' });
const [responseCount, setResponseCount] = React.useState(0);
const complexStateRef = React.useRef();
React.useEffect(() => {
complexStateRef.current = complexState;
}, [complexState]);
React.useEffect(() => {
const handleSave = async (complexState) => {
console.log('*** : start fetch');
setLoading(true);
try {
const resp = await dummySaveDraftToBackend(complexState);
console.log('fetch response: ', resp);
setResponseCount(count => count + 1);
} catch(error) {
// handle any rejected Promises, errors, etc...
} finally {
setLoading(false);
}
};
if (urlSlug) {
handleSave(complexStateRef.current);
}
}, [urlSlug]);
return (
...
);
}
This feels like a move in the wrong direction (towards more complexity), but introducing an additional state to determine if the urlSlug has changed seems to work.
export function MyComponent({ urlSlug, setUrlSlug }) {
const [slug, setSlug] = React.useState(urlSlug);
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
const handleSave = React.useCallback(() => {
if (urlSlug === slug) return // only when slug changes and not on mount
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState, urlSlug, slug]);
React.useEffect(() => {
if (urlSlug) {
handleSave();
setSlug(urlSlug)
}
}, [urlSlug, handleSave]);
Or move handleSave inside the useEffect (with additional slug check)
Updated with better semantics
export function MyComponent({ urlSlug, setUrlSlug }) {
const [autoSave, setAutoSave] = React.useState(false); // false for not on mount
React.useEffect(() => {
setAutoSave(true)
}, [urlSlug])
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
React.useEffect(() => {
const handleSave = () => {
if(!autoSave) return
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}
if (urlSlug) {
handleSave();
setAutoSave(false)
}
}, [autoSave, complexState]);

Where to calculate groups for fluentui detaillist in nextjs

I am learning react, and given this simple example of using SWR to fetch some items from an API and showing the items with groups using fluentui DetailedList - I am running into a problem with the groups.
Whenever I click a group in UI to collapse/uncollapse, that seems to trigger a rerender, and then the component will createGroups(data) again which resets the UI again back to original state as the groups object is recalculated.
Where am I supposed to actually store / calculate the groups information of my data? Initial it needs to be created, but from there it seems that it should only needs to be reevaluated whenvere the swr api returns new data - and then i still properly would want to merge in the current state from collapsed groups that the user might have changed in the UI.
Is it because i properly should not use SWR as it refreshes data live - and only do it on page refresh?
const SWR = ({ children, listid, onSuccess }: { children: ((args: SWRResponse<any, any>) => any), listid: string, onSuccess?: any }) => {
const url = `http://localhost:7071/api/Lists/${listid}`;
console.log(url);
const {data,error } = useSWR(url, { fetcher: fetcher, isPaused: () => listid === undefined, onSuccess });
const items = data.value;
const groups = createGroups(data)
return <... DetailsList group={groups} items={items} ... >; // ... left out a few details ...
};
What about adding a state for holding the groups and an useEffect for when data changes and insde the useEffect you should check if the content has changed before updating the groupState.
const hasChanged(data) => {
return data.notEquals(state.data)); // write your own logic for comparing the result
};
useEffect(() => { if (hasChanged(data)) {
setState(prev=> ({ ...prev, group: createGroup(data), data: data });
}}, [data]);
You dont actually need to store the group, you can just hold the data in your state, but the important part is to be able to check if any change actually took place before changing the state.
Another thing worth trying is the compare option in the useSWR hook. So instead of placing the "hasChanged" logic inside an useEffect hook, perhaps it could be in the compare function. Haven't had the chanse to test this myself though.
A third and final option would be to place the creation of groups inside your fetcher. Perhaps the most intuitive solution for this particular case, though I'm not completely sure it will prevent the unnecessary re-renders.
const fetcher = url => axios.get(url).then(res=> {
return {
items: res.data.value,
groups: createGroups(res.data),
};
});
const SWR = ({ children, listid, onSuccess }: { children: ((args: SWRResponse<any, any>) => any), listid: string, onSuccess?: any }) => {
const { data, error } = useSWR(url, fetcher, ...);
return <... DetailsList group={data.groups} items={data.items} ... >; // ... left out a few details ...
};

React Hooks, how to handle dependency If i want to run only when one of them changes?

I wanted to ask if there's a proper way of handling a situation when we have an useEffect that we only care it runs when one variable of the dependency array is changed.
We have a table that has pagination and inputs to filter the content
with a button.
When the user changes the action (inputs) we update this state and
when the user press search we fetch from the api.
When we have results paginated, we then hook on the page and if it
changes we then fetch the corresponding page.
I solved the issue by having the ref of action and checking if the previous value was different from the current value. Though I don't know if this is the best practice
I did something like this.
const FunctionView = () => {
const actionRef = useRef({})
// action object have query params for the api
const fetchData = useCallback((page) => {
// call and api and sets local values
}, [action])
// this hook handle page next or previous
useEffect(() => {
let didCancel = false
if (isEqual(Object.values(action), Object.values(actionRef.current))) {
if (!didCancel) fetchData(page + 1)
}
actionRef.current = action
return () => {
didCancel = true
}
}, [page, fetchData, action])
return (
<>
Components here that changes {action} object, dates and category
<Button onClick={() => fetchData(page)}>Search</Button>
<Table>...</Table>
</>
)
}
I know I can set the dependency array to only page but react lint plugin complains about this so I ended up with warnings.
A common mistake is trying to directly wire the state of forms up to the data they represent. In your example, you DO want to perform the search when action changes. What you're actually trying to avoid is updating action every time one of the inputs changes prior to submit.
Forms are, by nature, transient. The state of the form is simply the current state of the inputs. Unless you really want things to update on every single keystroke, the storage of these values should be distinct.
import { useCallback, useEffect, useState } from 'react'
const Search = () => {
const [action, setAction] = useState({})
const loadData = useCallback(() => {
// call api with values from `action`
}, [action])
useEffect(loadData, [loadData])
return (
<>
<SearchForm setRealData={setAction} />
<Table>...</Table>
</>
)
}
const SearchForm = ({ setRealData }) => {
// don't want to redo the search on every keystroke, so
// this just saves the value of the input
const [firstValue, setFirstValue] = useState('')
const [secondValue, setSecondValue] = useState('')
const handleFirstChange = (event) => setFirstValue(event.target.value)
const handleSecondChange = (event) => setSecondValue(event.target.value)
// now that we have finished updating the form, update the state
const handleSubmit = (event) => {
event.preventDefault()
setRealData({ first: firstValue, second: secondValue })
}
return (
<form onSubmit={handleSubmit}>
<input value={firstValue} onChange={handleFirstChange} />
<input value={secondValue} onChange={handleSecondChange} />
<button type="submit">Search</button>
</form>
)
}

Why is it requiring two clicks to execute state change in react?

I'm having a real hard time trying to wait for state change. I'm using react hooks and context.
Basically, I have a custom dropdown, no form elements used here. On click, it will run a function to change the state of two parameters:
// IdeaFilters.js
const organizationContext = useOrganization();
const { organization, filterIdeas, ideas, loading } = organizationContext;
const [filter, setFilter] = useState({
statusId: "",
orderBy: "voteCount"
});
// Build the parameters object to send to filterIdeas
const onClick = data => {
setFilter({ ...filter, ...data });
filterIdeas(filter);
};
So the onClick is attached to a dropdown in the render method:
First dropdown:
<Dropdown.Item onClick={() => onClick({ orderBy: "popularityScore" })}>Popularity</Dropdown.Item>
Second dropdown:
<Dropdown.Item key={status.id} onClick={() => onClick({ statusId: status.id })}>
{status.name}
</Dropdown.Item>
fetchIdeas() basically runs a function available in my context, that builds those parameters for my axios request. Every time I click, it requires two clicks for the idea to run. Sometimes it runs as expected, but most of the time it takes two clicks.
Any reason why I'm running into this issue?
Any help will be really appreciated!
Try this
const onClick = data => {
const newFilter = { ...filter, ...data }
setFilter(newFilter)
filterIdeas(newFilter)
}
Instead of
const onClick = data => {
setFilter({ ...filter, ...data })
filterIdeas(filter)
}
In React setState is asynchronous, I guess that was your problem. Try the code above, if it works and you don't fully understand, please comment below I'll edit the answer to explain more clearly. Read my other answer here (yet in the case your problem is asynchronous setState) about how to handle async setState.

Resources