react-select update value when mutating api data - reactjs

i have a react-select component in which i am filing options from api data, whenever i am editing name of a person from api, the names in the options change but the name on the value remains same.
i want to change the value of select after i change the name of a person.
--here i am updating the name of a person using react-query
const deliveryPersonUpdate = (values) => {
const id = deliveryBoy && deliveryBoy.id;
const params = { deliveryBoyId: id, ...values };
updateDeliveryPerson.mutate(params, {
onSuccess: (data) => {
toggle();
setDeliveryBoy(data?.delivery_boy);
queryClient.invalidateQueries([GET_DELIVERY_BOY_LIST.name]);
toast.success("Category Updated");
},
onError: (error) => {
showErrorMessages({ error });
},
});
};
const handleSubmit = (values) => {
deliveryPersonUpdate(values);
};
--select component
<Select
onChange={(obj) => handleChange(obj)}
paintBg
noOptionsMessage={noOptionsMessage}
name="deliveryBoySelect"
className={cx(styles.addDeliveryBoySelect)}
defaultValue={
!isEmpty(data?.delivery_boys) && {
label: data?.delivery_boys?.[0].name,
value: data?.delivery_boys?.[0].id,
}
}
options={data?.delivery_boys.map((d) => ({
label: d.name,
value: d.id,
}))}
isSearchable
/>
--handle change for select
const handleChange = (e) => {
const deliveryBoyData = data.delivery_boys.find(
(obj) => obj.id === e.value
);
setDeliveryBoy(deliveryBoyData);
};
--here, after changing name of person, options are updating right away but the value is not updating

The problem you are facing is that you are using defaultValue for the select, which means the select is uncontrolled. Any time the defaultValue is changed, the select doesn't react to this change because it's not supposed to, it's just the default value.
You have two options:
make the select controlled (by using state)
make the select keyed (by re-mounting it when default value changes)
As for controlled select
You would have to replace defaultValue by value but also attach an onChange handler that changes the state. You kind of already have the onChange as it updates the state in RQ, but since you are using defaultValue, it doesn't propagate back. However, if you just used value I think there would be blinking because RQ is async by nature, so the user could see a frame where the value is still out of sync. So in order to fully do this, you would have to introduce a sync state as well.
const [value, setValue] = useState(props.value)
const onChange = (e) => {
setValue(e.target.value)
props.onChange(e.target.value)
}
useEffect(() => {
setValue(props.value)
}, [props.value])
<select value={value} onChange={onChange} />
What this does it that it keep a local state using setState, handles update from props using useEffect and uses value instead of defaultValue thanks to that. You could also lift the state up if necessary.
As for keyed component
In case the previous solution is not ergonomic or not good for any reason and the component is small-ish, you can also decide that instead of keeping the local state, you will just unmount the component and mount a fresh instance. What the result is, is that when the value changes, it mounts a new component so it can use defaultValue again. You keep the select unchanged except for adding a key prop.
<select key={props.value} defaultValue={props.value} onChange={onChange}>
Having the key the same as the value means that they will be in sync. When the local value changes, it doesn't blink, because we do not set value, the DOM updates on it's own and when the props.value finally changes is async manner by RQ, it create the component anew, making it with the current defaultValue.

Related

Make a query based on a state change

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.

Hook with abortable fetch

note: I am aware of the useAbortableFetch hook. Trying to recreate a simple version of it.
I am trying to create a hook that returns a function that can make an abortable fetch request.
Idea being I want this hook to hold the state and update it when needed.
The update part is controlled by another competent on input change
What I am working on currently is
function useApiData(baseUrl){
const [state, setState] = use state({
data: null,
error: null,
loading: false
})
const controller = useRef(new AbortController)
const fetchData = searchTerm => {
if(state.loading){
controller.current.abort()
controller.current = new AbortController;
}
const signal = controller.signal;
setState(state => ({...state, loading: true})
fetch(url + searchTerm, {signal})
.then(res => res.json())
.then(data => {
setState(state => ({...state, data}))
return data
})
.catch(error => {
setState(state => ({...state, error}))
})
.finally(() => setState({...state, loading: false}))
}
const fetchCallback = useCallback(debounce(fetchData, 500), [])
return {...state, search: fetchCallback}
}
Usage
function App(){
const dataState = useApiData(url);
return ComponentWithInputElement {...dataState} />
}
function ComponentWithInputElement(props){
const [value, setValue] = useState('')
const onInput = ev => {
setValue(ev.target.value)
props.search(ev.tagert.value)
}
return (
<>
<input value={value} on input={onInput}>
{props.data?.length && <render datacomp>}
</>
)
}
This seems to fail to even send the first request.
Any way to make this pattern work?
Doing this in a useEffect would be very simple but I won't have access to the input value to have it as a dep
useEffect(()=>{
const controller = new AbortController();
const signal = controller.signal
fetch(url + value, {signal})
return () => controller.abort()
},[value])
Part of what you are trying to do does not feel "right". One of the things you are trying to do is have the state of the input value (like the form state) stored in the same hook. But those are not the same bits of state, as when the user types, it is (temporarily until its saved back to the server) different to the state fetched from the server. If you reuse the same state item for both, in the process of typing in the field, you lose the state fetched from the server.
You may think, "but I don't need it any more" -- but that often turns out to be a false abstraction later when new requirements come about that require it (like you need to display some static info as well as an editable form). In that sense, in the long term it would likely be less reusable.
It's a classic case of modelling an abstraction around a single use case -- which is a common pitfall.
You could add a new state item to the core hook to manage this form state, but then you have made it so you can only ever have the form state at the same level as the fetched data -- which may work in some cases, but be "overscoping" in others.
This is basically how all state-fetch libs like react query work -- Your fetched data is separate to the form data. And the form data is just initialised from the former as its initial value. But the input is bound to that "copy".
What you want is possible if you just returned setState from an additional state item in the core hook then passed down that setState to the child to be used as a change handler. You would then pass down the actual form string value from this new state from the parent to the child and bind that to the value prop of the input.
However, I'd encourage against it, as its an architectural flaw. You want to keep your local state, and just initialise it from the fetched state. What I suggested might be OK if you intend to use it only in this case, but your answer implies reuse. I guess I would need more info about how common this pattern is in your app.
As for abort -- you just need to return the controller from the hook so the consumer can access it (assuming you want to abort in the consumers?)

Unable to update the `setState` inside `useEffect` conditionally

Component For Either Creating Or Editing The User
A part of the code below where I am setting the isActive state conditionally inside useEffect
I am getting the other fields of user_data and successfully updating the state
but only setIsActive is not updating the state
function CreateUser() {
const [isActive, setIsActive] = useState<boolean | undefined>();
useEffect(() => {
if (params_user_id?.id != null) {
const SINGLE_USER_URL = `users/${params_user_id.id}/edit`;
const getSingleUser = async () => {
const {data} = await axios.get(SINGLE_USER_URL)
console.log(data)
setIsActive(data.isactive)
console.log('isactive', isActive)
}
getSingleUser();
}
}, [params_user_id])
return (
<>
<Form.Check
defaultChecked={isActive}
className='mb-3'
type='checkbox'
id='active'
onChange={e => setIsActive(!(isActive))}
label='Active: Unselect for deleting accounts.'/>
</>
)
}
Form.Check From Bootstrap-React
When I hit the Edit page
I did try many things like ternary operator etc
checkBox is not checked bcoz it's undefined
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, it will likely run before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
In your case you're already using useEffect to update the state, but if you want to act upon that state change, or simply to log it's value, then you can use another separate useEffect for that purpose.
Example:
useEffect(() => {
console.log(isActive)
// Whatever else we want to do after the state has been updated.
}, [isActive])
This console.log will run only after the state has finished changing and a render has occurred.
Note: "isActive" in the example is interchangeable with whatever other state piece you're dealing with.
Check the documentation for more info about this.
Additional comments:
Best to avoid using the loose != to check for equality/inequality and opt for strict inequality check instead, i.e. - !==
Loose inequality is seldom used, if ever, and could be a source for potential bugs.
Perhaps it would better to avoid typing this as boolean | undefined.
Unless justified, this sounds like another potential source for bugs.
I would suggest relying on just boolean instead.
First, you can't get the state's updated value immediately after setting it because State Updates May Be Asynchronous
useEffect(() => {
if (params_user_id?.id != null) {
const SINGLE_USER_URL = `users/${params_user_id.id}/edit`;
const getSingleUser = async () => {
const {data} = await axios.get(SINGLE_USER_URL)
console.log(data)
setIsActive(data.isactive)
console.log('isactive', isActive) // <-- You can't get updated value immediately
}
getSingleUser();
}
}, [params_user_id])
Change the type of useState to boolean only and set the default value to false.
const [isActive, setIsActive] = useState<boolean>(false);
Then, in Form.Check, update onChange to this:
<Form.Check
defaultChecked={isActive}
className='mb-3'
type='checkbox'
id='active'
onChange={() => setIsActive((preVal) => !preVal)}
label='Active: Unselect for deleting accounts.'
/>

React js set State not getting Updated values in state object

I am new to react and hooks and I am trying to set the disabled state in the below code on the condition if state.items.length is greater than 3 but I am not getting the updated values in my state object.
So I tried to set the disabled state in the useEffect hook where I get the latest values of the state.
But if I setDisabled state in useEffect it goes into an infinite loop.
Can anyone tell me what is wrong with the code?
//This is how my state object and input fields looks.
const [state, setState] = useState({
items: [],
value: "",
error: null
});
<input
className={"input " + (state.error && " has-error")}
value={state.value}
placeholder="Type or paste email addresses and press `Enter`..."
onKeyDown={handleKeyDown}
onChange={handleChange}
onPaste={handlePaste}
/>
const handleKeyDown = evt => {
if (["Enter", "Tab", ","].includes(evt.key)) {
evt.preventDefault();
var value = state.value.trim();
if (value && isValid(value)) {
setState(prev => ({
...prev,
items: [...prev.items, prev.value],
value: ""
}));
}
//if my items array which is a count of emails i.e arrays of strings is greater than 3 I want to disable the input field.
if(state.items.length > 3){
setDisabled(true);
}
}
};
useEffect(()=>{
// if I set the disabled state which is an object inside the state param it goes into an infinite loop.
passStateToParent(state);
}[state])
You should start by declaring a new variable to hold and keep track of the disabled state. (use another useState)
Then next you should use useEffect to constantly check on the length of items in current state.
I have taken code from above mentioned codesandbox as a refernce.
// use this useState hook to keep track disabled state.
const [inputDisable, setInputDisabled] = useState(false);
//use effect to check, if state item length
useEffect(() => {
const items = [...state.items];
if (items.length === 3) {
setInputDisabled(true);
}
}, [state]);
Followed by this add a new attribute named disable in your input tag and assign the value of inputDisable to it.
Refer to this codesandbox link to see the live example.
https://codesandbox.io/s/vigorous-stallman-vck52?file=/src/App.js:490-523

if the event is recurring, i do not want to want to re render the data

I have a drop down which is consisting of one value.If the same value is selected for the second time in the drop down i do not want to re render the page again.
form = (
onChange={(e) => this.onP(e)}/>
);
onP = (event) => {
this.setState({
selectedItem: event.value
});
If I select the same drop down again then whatever the data already written(in the form) should remains the same
You could check the value of your selectedItem before updating the state of your component :
if (this.state.selectedItem !== event.value) {
this.setState({selectedItem: event.value});
}
If I understood your question well, you want to update state based on the previous value and React recommends using the callback API of setState if you attempting to do something of that kind.
onP = (event) => {
this.setState(prevState => {
if(prevState.selectedItem !== event.value){
return {selectedItem: event.value}
}
});

Resources