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>
</>
);
}
Related
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
I have a banner in my react app which I can close:
I persist this state in localStorage:
const [bannerShown, setBannerShown] = useState(true);
useEffect(() => {
const data = localStorage.getItem('MY_APP_STATE');
if (data !== null) {
setBannerShown(JSON.parse(data));
}
}, []);
useEffect(() => {
localStorage.setItem('MY_APP_STATE', JSON.stringify(bannerShown));
}, [bannerShown]);
{bannerShown && (<MyBanner onClick={() => setBannerShown(false)} />)}
This is working fine. Now I want to add a condition:
I only want to show the banner when it contains a certain query param:
import queryString from 'query-string';
const queryParams = queryString.parse(location.search);
const hasQueryParam = queryString
.stringify(queryParams)
.includes('foo=bar');
How do I combine above state and boolean (hasQueryParam) in a clean way?
You can chain together several statements in the JSX {bannerShown && hasQueryParam && (<MyBanner onClick={() => setBannerShown(false)} />)}
Or you could put this into an if statement
let showItem;
if (bannerShown && hasQueryParam) {
showItem = <MyBanner onClick={() => setBannerShown(false)} />
}
Then in the JSX {showItem}
set state true if query-string has any value
useEffect(() => {
const foo = qs.parse(location.search);
console.log(foo)
if(!Object.keys(foo).length === 0){
setBanner(true)
}else{
setBanner(false)
}
}, [])
The object of this app is to allow input text and URLs to be saved to localStorage. It is working properly, however, there is a lot of repeat code.
For example, localStoredValues and URLStoredVAlues both getItem from localStorage. localStoredValues gets previous input values from localStorage whereas URLStoredVAlues gets previous URLs from localStorage.
updateLocalArray and updateURLArray use spread operator to iterate of previous values and store new values.
I would like to make the code more "DRY" and wanted suggestions.
/*global chrome*/
import {useState} from 'react';
import List from './components/List'
import { SaveBtn, DeleteBtn, DisplayBtn, TabBtn} from "./components/Buttons"
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
//these items are used for the state of localStorage
const [display, setDisplay] = useState(false);
const localStoredValues = JSON.parse(
localStorage.getItem("localValue") || "[]"
)
let updateLocalArray = [...localStoredValues, leadValue.inputVal]
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = JSON.parse(localStorage.getItem("URLValue") || "[]")
const tabBtn = () => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
const url = tabs[0].url;
setMyLeads((prev) => [...prev, url]);
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
localStorage.setItem("URLValue", JSON.stringify(updateURLArray));
});
setDisplay(false)
};
//handles change of input value
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
setDisplay(false);
// update state of localStorage
localStorage.setItem("localValue", JSON.stringify(updateLocalArray))
};
const displayBtn = () => {
setDisplay(true);
};
const deleteBtn = () => {
window.localStorage.clear();
setMyLeads([]);
};
const listItem = myLeads.map((led) => {
return <List key={led} val={led} />;
});
//interates through localStorage items returns each as undordered list item
const displayLocalItems = localStoredValues.map((item) => {
return <List key={item} val={item} />;
});
const displayTabUrls = URLStoredVAlues.map((url) => {
return <List key={url} val={url} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<TabBtn tabBtn={tabBtn} />
<DisplayBtn displayBtn={displayBtn} />
<DeleteBtn deleteBtn={deleteBtn} />
<ul>{listItem}</ul>
{/* displays === true show localstorage items in unordered list
else hide localstorage items */}
{display && (
<ul>
{displayLocalItems}
{displayTabUrls}
</ul>
)}
</main>
);
}
export default App
Those keys could be declared as const and reused, instead of passing strings around:
const LOCAL_VALUE = "localValue";
const URL_VALUE = "URLValue";
You could create a utility function that retrieves from local storage, returns the default array if missing, and parses the JSON:
function getLocalValue(key) {
return JSON.parse(localStorage.getItem(key) || "[]")
};
And then would use it instead of repeating the logic when retrieving "localValue" and "URLValue":
const localStoredValues = getLocalValue(LOCAL_VALUE)
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = getLocalValue(URL_VALUE)
Similarly, with the setter logic:
function setLocalValue(key, value) {
localStorage.setItem(key, JSON.stringify(value))
}
and then use it:
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
setLocalValue(URL_VALUE, updateURLArray);
// update state of localStorage
setLocalValue(LOCAL_VALUE, updateLocalArray)
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 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