edit: passing data as parameter into onSuccess worked for me
const [location, setLocation] = useState(null)
const [address, setAddress] = useState('')
const [desc, setDesc] = useState('')
const { data, isLoading, isError } = useQuery(["order"], async () => getOrders(id), {
onSuccess: (data) => {
setAddress([data[0].location.address.streetName, " ", data[0].location.address.buildingNumber, data[0].apartmentNumber ? "/" : '', data[0].apartmentNumber])
setDesc(data[0].description)
setLocation(data[0].location)
}
})
I can't initialize state with data from react-query. States are updating only after refocusing the window. What is the proper way to fetch data from queries and put it into states?
import { useParams } from "react-router-dom"
import { useQuery } from '#tanstack/react-query'
import { getOrders } from '../../api/ordersApi'
import { useState } from 'react'
import { getLocations } from '../../api/locationsApi'
const Order = () => {
const { id } = useParams()
const [address, setAddress] = useState('')
const [desc, setDesc] = useState('')
const [location, setLocation] = useState(undefined)
const { data, isLoading, isError } = useQuery(["order"], async () => getOrders(id), {
onSuccess: () => {
if (data) {
setDesc(data[0].description)
setAddress([data[0].location.address.streetName, " ", data[0].location.address.buildingNumber, data[0].apartmentNumber ? "/" : '', data[0].apartmentNumber])
}
}
})
const { data: locationData, isLoading: isLocationLoading, isError: isLocationError } = useQuery(["locations"], getLocations, {
onSuccess: () => {
if (locationData) {
setLocation(data[0].location)
}
}
})
if (isLoading || isLocationLoading) return (
<div>Loading</div>
)
if (isError || isLocationError) return (
<div>Error</div>
)
return (
data[0] && locationData && (
<div>
<h2>Zlecenie nr {data && data[0].ticket}</h2>
<h1>{address}</h1>
<textarea
type="text"
value={desc}
onChange={(e) => setDesc(e.target.value)}
placeholder="Enter description"
/>
<select
id="locations"
name="locations"
value={JSON.stringify(location)}
onChange={(e) => setLocation(JSON.parse(e.target.value))}
>
{locationData?.map((locationElement) => <option key={locationElement._id} value={JSON.stringify(locationElement)}>{locationElement.abbrev}</option>)}
</select>
<h1>{location?.abbrev}</h1>
<pre>{data && JSON.stringify(data[0], null, "\t")}</pre>
</div>)
)
}
export default Order
I know I am doing something wrong - data is queried but states are set to values from useState(). Please check my code and help me get into what is wrong here.
Consider the query keys (first parameter that you passing) of useQuery like useEffect's dependency array. Whatever you passed there, if it changes query will refetch too, just like how it's done on useEffect hook.
In your example, it seems like you have a desc state, and when it changes you want to query re-fetched and set to address and location states.
In your code, there is a logical mistake in that you have a text area that sets desc state and you are trying to make a request with it and storing the response in the same state.
True approach is create a state to store your desired query (like location) use it as a key in useQuery and store the data to another state. Even better, you can use the useReducer hook to store your data in a form format to have more readable and stable approach
Related
I am currently building an e-commerce website with React, and I have question about query params.
In every e-commerce website, there are filters. I want to use useSearchParams for this filter (sort, conditions, min price, max price, categories, etc.). But I am not 100% sure if I am using searchParams correctly.
I am currently using react state for conditions and sort, and update searchParams using useEffect.
Code:
export default function Shop() {
const [searchParams, setSearchParams] = useSearchParams();
const [sort, setSort] = useState("newest");
const [conditions, setConditions] = useState([]);
const handleChangeSort = (e) => {
setSort(e.target.value);
searchParams.set("sort", sort);
setSearchParams(searchParams);
};
const handleChangeConditions = (e, checked, newValue) => {
if (checked) {
const newList = [...conditions, newValue];
setConditions(newList);
return;
}
const newList = conditions.filter((condition) => condition !== newValue);
setConditions(newList);
};
const handleReset = () => {
setSort("newest");
setConditions([]);
};
useEffect(() => {
if (conditions.length === 0) {
searchParams.delete("conditions");
setSearchParams(searchParams);
return;
}
searchParams.set("conditions", JSON.stringify(conditions));
setSearchParams(searchParams);
}, [conditions, searchParams, setSearchParams]);
return (
<>
<Sort sort={sort} onChange={handleChangeSort} />
<Conditions conditions={conditions} onChange={handleChangeConditions} />
<Button variant="contained" onClick={handleReset}>
Reset Filter
</Button>
</>
);
}
Link to CodeSandbox
Is this right approach? Or am I doing something wrong?
The "right" approach is the one that works for your use case. I don't see any overt issues with the way you are using the queryString parameters. Here I'd say if you want the React state to be the source of truth and the queryString as the cached version then you'll want to initialize the state from the query params and persist the state updates to the query params. This keeps the query params and local component state synchronized.
Example:
export default function Shop() {
const [searchParams, setSearchParams] = useSearchParams();
// Initialize states from query params, provide fallback initial values
const [sort, setSort] = useState(searchParams.get("sort") || "newest");
const [conditions, setConditions] = useState(
JSON.parse(searchParams.get("conditions")) || []
);
// Handle updating sort state
const handleChangeSort = (e) => {
setSort(e.target.value);
};
// Persist sort to query params
useEffect(() => {
searchParams.set("sort", sort);
setSearchParams(searchParams);
}, [searchParams, setSearchParams, sort]);
// Handle updating conditions state
const handleChangeConditions = (e, checked, newValue) => {
if (checked) {
setConditions((conditions) => conditions.concat(newValue));
} else {
setConditions((conditions) =>
conditions.filter((condition) => condition !== newValue)
);
}
};
// Persist conditions to query params
useEffect(() => {
if (conditions.length) {
searchParams.set("conditions", JSON.stringify(conditions));
} else {
searchParams.delete("conditions");
}
setSearchParams(searchParams);
}, [conditions, searchParams, setSearchParams]);
const handleReset = () => {
setSort("newest");
setConditions([]);
};
return (
<>
<Sort sort={sort} onChange={handleChangeSort} />
<Conditions conditions={conditions} onChange={handleChangeConditions} />
<Button variant="contained" onClick={handleReset}>
Reset Filter
</Button>
</>
);
}
What I would like to happen is when displayBtn() is clicked for the items in localStorage to display.
In useEffect() there is localStorage.setItem("localValue", JSON.stringify(myLeads)) MyLeads is an array which holds leads const const [myLeads, setMyLeads] = useState([]); myLeads state is changed when the saveBtn() is clicked setMyLeads((prev) => [...prev, leadValue.inputVal]);
In DevTools > Applications, localStorage is being updated but when the page is refreshed localStorage is empty []. How do you make localStorage persist state after refresh? I came across this article and have applied the logic but it hasn't solved the issue. I know it is something I have done incorrectly.
import List from './components/List'
import { SaveBtn } from './components/Buttons';
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
const [display, setDisplay] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const localStoredValue = JSON.parse(localStorage.getItem("localValue")) ;
const [localItems] = useState(localStoredValue || []);
useEffect(() => {
localStorage.setItem("localValue", JSON.stringify(myLeads));
}, [myLeads]);
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
// setLocalItems((prevItems) => [...prevItems, leadValue.inputVal]);
setDisplay(false);
};
const displayBtn = () => {
setDisplay(true);
};
const displayLocalItems = localItems.map((item) => {
return <List key={item} val={item} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
);
}
export default App;```
You've fallen into a classic React Hooks trap - because using useState() is so easy, you're actually overusing it.
If localStorage is your storage mechanism, then you don't need useState() for that AT ALL. You'll end up having a fight at some point between your two sources about what is "the right state".
All you need for your use-case is something to hold the text that feeds your controlled input component (I've called it leadText), and something to hold your display boolean:
const [leadText, setLeadText] = useState('')
const [display, setDisplay] = useState(false)
const localStoredValues = JSON.parse(window.localStorage.getItem('localValue') || '[]')
const handleChange = (event) => {
const { name, value } = event.target
setLeadText(value)
}
const saveBtn = () => {
const updatedArray = [...localStoredValues, leadText]
localStorage.setItem('localValue', JSON.stringify(updatedArray))
setDisplay(false)
}
const displayBtn = () => {
setDisplay(true)
}
const displayLocalItems = localStoredValues.map((item) => {
return <li key={item}>{item}</li>
})
return (
<main>
<input name="inputVal" value={leadText} type="text" onChange={handleChange} required />
<button onClick={saveBtn}> Save </button>
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
)
I'm putting navbar in my _app.js so I don't need to insert it in every component. My problem is that after I login it outputs an error Rendered more hooks than during the previous render. and its pointing it on useQuery(GETCARTDATA
Pls check my code here
const App = ({ Component, pageProps }) => {
const token = getToken()
const [isPopUpShow, setPopUpShow] = useState(false)
const [cartStateData, setCartStateData] = useState([])
const [isCartOpen, setCartOpen] = useState(false)
let cartDetailsData
if (token) {
// eslint-disable-next-line react-hooks/rules-of-hooks
cartDetailsData = useLazyQuery(GETCARTDATA, {
variables: {
page: 1
},
})
// eslint-disable-next-line react-hooks/rules-of-hooks
useMemo(() => {
const cartData = get(cartDetailsData.data, 'findCartDetails.orders') || []
const cartItems = []
if (cartData.length) {
cartData.map(
itm =>
itm.lineItems.length &&
itm.lineItems.map(item => cartItems.push(item))
)
}
setCartStateData(cartItems)
}, [cartDetailsData.data])
}
return (
<>
<div className="app-outer">
{token ? (
<ShowroomHeader
isPopUpShow={isPopUpShow}
setPopUpShow={setPopUpShow}
cartStateData={cartStateData}
cartDetailsData={cartDetailsData}
token={token}
/>
) : (
<Navbar />
)}
</div>
<div className="main">
<Component {...pageProps} />
</div>
</>
)
}
export default withApollo(App)
As #xadmn mentioned, you're rendering your hooks conditionally while React expects the same number of hook calls on every render, thus breaking the rules of Hooks.
You'll need to remove your if statement and move your condition inside a useEffect hook, using useLazyQuery's returned function to execute the query from there. You can also move your useMemo code to the onCompleted callback, since it depends on the results from the query.
const App = ({ Component, pageProps }) => {
const token = getToken()
const [isPopUpShow, setPopUpShow] = useState(false)
const [cartStateData, setCartStateData] = useState([])
const [isCartOpen, setCartOpen] = useState(false)
const [getCardData, cartDetailsData] = useLazyQuery(GETCARTDATA, {
onCompleted: (data) => {
const cartData = get(data, 'findCartDetails.orders') || []
const cartItems = []
if (cartData.length) {
cartData.map(
itm =>
itm.lineItems.length &&
itm.lineItems.map(item => cartItems.push(item))
)
}
setCartStateData(cartItems)
}
})
useEffect(() => {
if (token) {
getCardData({ variables: { page: 1 } })
}
}, [token])
return (
// Your JSX here
)
}
I'm building an app using react, redux, and redux-saga.
The situation is that I'm getting information from an API. In this case, I'm getting the information about a movie, and I will update this information using a basic form.
What I would like to have in my text fields is the value from the object of the movie that I'm calling form the DB.
This is a brief part of my code:
Im using 'name' as an example.
Parent component:
const MovieForm = (props) => {
const {
movie,
} = props;
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({
name,
});
};
const handleSetValues = () => {
console.log('hi');
console.log(movie, name);
setName(movie.name);
setValues(true);
};
useEffect(() => {
if (movie && values === false) {
handleSetValues();
}
});
return (
<Container>
<TextField
required
**defaultValue={() => {
console.log(movie, name);
return movie ? movie.name : name;
}}**
label='Movie Title'
onChange={(e) => setName(e.target.value)}
/>
</Container>
);
};
export default MovieForm;
....
child component
const MovieUpdate = (props) => {
const { history } = props;
const { id } = props.match.params;
const dispatch = useDispatch();
const loading = useSelector((state) => _.get(state, 'MovieUpdate.loading'));
const created = useSelector((state) => _.get(state, 'MovieUpdate.created'));
const loadingFetch = useSelector((state) =>
_.get(state, 'MovieById.loading')
);
const movie = useSelector((state) => _.get(state, 'MovieById.results'));
useEffect(() => {
if (loading === false && created === true) {
dispatch({
type: MOVIE_UPDATE_RESET,
});
}
if (loadingFetch === false && movie === null) {
dispatch({
type: MOVIE_GET_BY_ID_STARTED,
payload: id,
});
}
});
const updateMovie = (_movie) => {
const _id = id;
const obj = {
id: _id,
name: _movie.name,
}
console.log(obj);
dispatch({
type: MOVIE_UPDATE_STARTED,
payload: obj,
});
};
return (
<div>
<MovieForm
title='Update a movie'
buttonTitle='update'
movie={movie}
onCancel={() => history.push('/app/movies/list')}
onSubmit={updateMovie}
/>
</div>
);
};
export default MovieUpdate;
Then, the actual problem is that when I use the default prop on the text field the information appears without any problem, but if i use defaultValue it is empty.
Ok, I kind of got the answer, I read somewhere that the defaultValue can't be used int the rendering.
So I cheat in a way, I set the properties multiline and row={1} (according material-ui documentation) and I was able to edit this field an receive a value to display it in the textfield
Playing with React those days. I know that calling setState in async. But setting an initial value like that :
const [data, setData] = useState(mapData(props.data))
should'nt it be updated directly ?
Bellow a codesandbox to illustrate my current issue and here the code :
import React, { useState } from "react";
const data = [{ id: "LION", label: "Lion" }, { id: "MOUSE", label: "Mouse" }];
const mapData = updatedData => {
const mappedData = {};
updatedData.forEach(element => (mappedData[element.id] = element));
return mappedData;
};
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
export default function App() {
const [loadedData, setLoadedData] = useState(data);
const [filter, setFilter] = useState("");
const filterData = () => {
return loadedData.filter(element =>
filter ? element.id === filter : true
);
};
//loaded comes from a useEffect http call but for easier understanding I removed it
return (
<div className="App">
<button onClick={() => setFilter("LION")}>change filter state</button>
<ChildComponent dataProp={filterData()} />
</div>
);
}
So in my understanding, when I click on the button I call setFilter so App should rerender and so ChildComponent with the new filtered data.
I could see it is re-rendering and mapData(updatedData) returns the correct filtered data BUT ChildComponent keeps the old state data.
Why is that ? Also for some reason it's rerendering two times ?
I know that I could make use of useEffect(() => setMappedData(mapData(dataProp)), [dataProp]) but I would like to understand what's happening here.
EDIT: I simplified a lot the code, but mappedData in ChildComponent must be in the state because it is updated at some point by users actions in my real use case
https://codesandbox.io/s/beautiful-mestorf-kpe8c?file=/src/App.js
The useState hook gets its argument on the very first initialization. So when the function is called again, the hook yields always the original set.
By the way, you do not need a state there:
const ChildComponent = ({ dataProp }) => {
//const [mappedData, setMappedData] = useState(mapData(dataProp));
const mappedData = mapData(dataProp);
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
EDIT: this is a modified version in order to keep the useState you said to need. I don't like this code so much, though! :(
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
let actualMappedData = mappedData;
useMemo(() => {
actualMappedData =mapData(dataProp);
},
[dataProp]
)
console.log("** Render Child Component **");
return Object.values(actualMappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Your child component is storing the mappedData in state but it never get changed.
you could just use a regular variable instead of using state here:
const ChildComponent = ({ dataProp }) => {
const mappedData = mapData(dataProp);
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};