Currently, doing setSearchParams(/* an object */) will replace the existing search params. If I would like to extend the search params instead of replacing them, e.g. adding a new param. What will be the most elegant way to achieve that?
I know I can modify the searchParams directly, then do something like setSearchParams(newSearchParamsToObject). Yet, since searchParams is not an object but a URLSearchParams. The code will be long and ugly.
Edit: Since some one appears don't understand what I have posted
Currently if we do something like this:
const [searchParams, setSearchParams] = useSearchParams({ a: 1 });
setSearchParams({ b: 2 });
It will replace the searchParams with ?b=2.
What I would like to achieve is adding b=2 to existing search params, i.e. the final search params should be ?a=1&b=2.
I suppose it would/should be trivial to set the search param value you want to add.
const [searchParams, setSearchParams] = useSearchParams();
...
searchParams.set(newParamKey, newParamValue);
setSearchParams(searchParams);
const [searchParams, setSearchParams] = useSearchParams();
const clickHandler = () => {
searchParams.set("b", 2);
setSearchParams(searchParams);
};
...
<button type="button" onClick={clickHandler}>
Add "b=2" param
</button>
Introduced in react-router#6.4 the setSearchParams now also takes a function, similar to the useState hook, to access the previous query params object to update from.
setSearchParams(searchParams => {
searchParams.set("b", 3);
return searchParams;
});
This is handy if you are using the setSearchParams in a useEffect hook as you won't need to list the current searchParams object as an external dependency.
Router v6 solution here (don't know if works with previous version too): setSearchParams accept a function that should return new search params, actually giving you the previous value:
For example:
setSearchParams(prev => ([...prev.entries(), ['foo', 'bar']]);
This is basically like #Drew Reese answer, but using the previous "state", like you would do with useState from React.
You could also use the URLSearchParams.append(name, value) method, in case you want several 'b'.
setSearchParams(searchParams.append('b', 2));
It's not like your modifying the state, here. You're just manipulating an URLSearchParams object. You don't have to worry about mutating it.
This version ->
setSearchParams(prev => ([...prev.entries(), ['foo', 'bar']]);
will add the same param without replacing, like this ->
?s=5&p=0&p=1
param p was added the second time, instead of ?s=5&p=1
React Router v6 have the useSearchParams() method that return an array that has, as a first value, a way to get the actual search params, and as a second, a method to set new values, the return value type definition is this: [URLSearchParams, SetURLSearchParams] and you can find more on the official documentation.
You can see a full working example to solve your problem on this codesandbox.
And this is a wrap up:
import React, { useCallback, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
function getRandomNumber() {
return Math.floor(Math.random() * 1000) + 1;
}
function getAllParamsFromEntries(searchParamsEntity) {
const finalValues = {};
const entries = searchParamsEntity.entries();
let isDone = false;
while (!isDone) {
const iteratorElement = entries.next();
if (iteratorElement.value) {
finalValues[iteratorElement.value[0]] = iteratorElement.value[1];
}
isDone = iteratorElement.done;
}
return finalValues;
}
const MainScreen = () => {
const [searchParamsEntity, setSearchParams] = useSearchParams();
const allSearchParams = useMemo(() => {
return getAllParamsFromEntries(searchParamsEntity);
}, [searchParamsEntity]);
console.log(allSearchParams);
const appendRandomSearchParam = useCallback(() => {
setSearchParams((previousParams) => {
const searchParams = getAllParamsFromEntries(previousParams);
return {
...searchParams,
[`randomParam${getRandomNumber()}`]: getRandomNumber()
};
});
}, [setSearchParams]);
return (
<div>
<p>Search params now: {JSON.stringify(allSearchParams, undefined, 2)}</p>
<button onClick={appendRandomSearchParam}>Append</button>
</div>
);
};
Related
I am trying to find an item from a collection, from the code below, in order to update my react component, the propertState object isnt empty, it contains a list which i have console logged, however I seem to get an underfined object when i console log the value returned from my findProperty function... I am trying update my localState with that value so that my component can render the right data.
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState()
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
return (<>{property.propertyName}</>)
}
export default PropertyComponent
There are a few things that you shall consider when you are finding data and updating states based on external sources of data --useParams--
I will try to explain the solution by dividing your code in small pieces
const PropertyComponent = () => {
const { propertyId } = useParams();
Piece A: Consider that useParams is a hook connected to the router, that means that you component might be reactive and will change every time that a param changes in the URL. Your param might be undefined or an string depending if the param is present in your URL
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
Piece B: useSelector is other property that will make your component reactive to changes related to that selector. Your selector might return undefined or something based on your selection logic.
const[property, setProperty] = useState()
Piece C: Your state that starts as undefined in the first render.
So far we have just discovered 3 pieces of code that might start as undefined or not.
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
Piece D: Here is where more problems start appearing, you are telling your code that in every render a function findProperty will be created and inside of it you are calling the setter of your state --setProperty--, generating an internal dependency.
I would suggest to think about the actions that you want to do in simple steps and then you can understand where each piece of code belongs to where.
Let's subdivide this last piece of code --Piece D-- but in steps, you want to:
Find something.
The find should happen if you have an array where to find and a property.
With the result I want to notify my component that something was found.
Step 1 and 2 can happen in a function defined outside of your component:
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
NOTE: I took the liberty of modify your code by simplifying a little
bit your find function.
Now we need to do the most important step, make your component react at the right time
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState({ propertyName: '' }); // I suggest to add default values to have more predictable returns in your component
/**
* Here is where the magic begins and we try to mix all of our values in a consistent way (thinking on the previous pieces and the potential "undefined" values) We need to tell react "do something when the data is ready", for that reason we will use an effect
*/
useEffect(() => {
// This effect will run every time that the dependencies --second argument-- changes, then you react afterwards.
if(propertyId, propertyState.properties) {
const propertyFound = findProperty(propertyId, propertyState.properties);
if(propertyFound){ // Only if we have a result we will update our state.
setProperty(propertyFound);
}
}
}, [propertyId, propertyState.properties])
return (<>{property.propertyName}</>)
}
export default PropertyComponent
I think that in this way your intention might be more direct, but for sure there are other ways to do this. Depending of your intentions your code should be different, for instance I have a question:
What is it the purpose of this component? If its just for getting the property you could do a derived state, a little bit more complex selector. E.G.
function propertySelectorById(id) {
return function(store) {
const allProperties = propertiesStateSelector(store);
const foundProperty = findProperty(id, allProperties);
if( foundProperty ) {
return foundProperty;
} else {
return null; // Or empty object, up to you
}
}
}
Then you can use it in any component that uses the useParam, or just create a simple hook. E.G.
function usePropertySelectorHook() {
const { propertyId } = useParams();
const property = useSelector(propertySelectorById(propertyId));
return property;
}
And afterwards you can use this in any component
functon AnyComponent() {
const property = usePropertySelectorHook();
return <div> Magic {property}</div>
}
NOTE: I didn't test all the code, I wrote it directly in the comment but I think that should work.
Like this I think that there are even more ways to solve this, but its enough for now, hope that this helped you.
do you try this:
const found = propertyState.properties.find(element => element.propertyId === propertyId);
setProperty(found);
instead of all function findProperty
I've encountered interesting problem with custom react hooks, rule of hooks, and recursion. I would like to know if I'm doing something wrong. Thanks for you time.
I have a custom hook useServerData(entityIds). This hook internaly calls (depends on) other custom hook useFilters(entityIds). This hook cyclickly depends on useServerData(shorterEntityIds). Maybe it seems like a logical defect, but it is just a recurion because shorterEntityIds are strictly shorter array. Without rule of hooks, I would wrote recursion break like:
let filters = [];
if (shorterEntityIds.length > 0) {
filters = useFilters(shorterEntityIds)
}
But it is prohibited by rule of hooks (and I understand why). Without the break JavaScript throws Maximum call stack size exceeded.
How would you solve this? How to break the cycle? What is a React idiomatic way?
Thank you.
EDIT: As Bergi requested I'm adding more specific code for this.
// Returns dictionary where key is given filterId and value is QueryResult<FilterRange>
// Where returned FilterRange depends on Filter and prescending Filters for each Filter
export const useFilterRanges = (filterIds) => {
const filtersById = useFilters(filterIds);
const prescendingQueryFiltersById = usePrescendingQueryFilters(filterIds);
const filterRanges = useQueries(filterIds.map((filterId) => {
const filter = filtersById[filterId];
const prescendingFilters = prescendingQueryFiltersById[filterId];
return fetchFilterRange(filter, prescendingFilters);
}));
return zipObject(filterIds, filterRanges);
};
// Returns dictionary where key is given filterId and value is Array<QueryFilter>
// Where returned Array<QueryFilter> depends on internal state (order of Filters) and QueryFilters
export const usePrescendingQueryFilters = (filterIds) => {
const allFilterIds = useAllFilterIds();
const index = findLastIndex(allFilterIds, (filterId) => includes(filterIds, filterId));
// allPrecendingIds are always at least one item shorter than filterIds
const allPrecendingIds = take(allFilterIds, index);
const queryFiltersById = useQueryFilters(allPrecendingIds);
return chain(filterIds)
.keyBy()
.mapValues((filterId) => {
const index = indexOf(allFilterIds, filterId);
const precendingIds = take(allFilterIds, index);
const queryFilters = precendingIds.map((id) => queryFiltersById[id]);
return queryFilters.flatMap((queryFilter) => queryFilter ?? []);
})
.value();
};
// Returns dictionary where key is given filterId and value is QueryFilter
// Where returned QueryFilter depends on QueryResult<FilterRange> and Filter
export const useQueryFilters = (filterIds) => {
const filtersById = useFilters(filterIds);
const rangesById = useFilterRanges(filterIds);
return chain(filterIds)
.keyBy()
.mapValues((filterId) => {
const filter = filtersById[filterId];
const range = rangesById[filterId];
return constructQueryFilters(filter, range);
})
.value();
};
Note the code is simplified (I've doublechecked but it can contain some typos) and the real project cycle is even bigger, but I believe this is kind of minimal meaningful example. :)
I am trying to convert this demo into a function component. I am following these steps and I am stuck with the following:
Class version:
this.appointmentForm = connectProps(AppointmentFormContainer, () => {
const {
editingFormVisible,
editingAppointment,
data,
addedAppointment,
isNewAppointment,
previousAppointment,
} = this.state;
Function conversion attempt:
const [appointmentForm, setappointmentForm] = useState({});
setappointmentForm(connectProps(AppointmentFormContainer, () => {
const {
editingFormVisible,
editingAppointment,
data,
addedAppointment,
isNewAppointment,
previousAppointment,
};
The error with this version (tried several) is : "Parsing error: 'Const declarations' require an initialization value." it refers to the const in the line under the setappointmentForm but getting rid of it is incorrect as well. If the whole code is needed I will put it but it is quite long. Any ideas?
There's no right hand side to your const declaration. It's not legal javascript to do just this:
const foo;
You need to also give it a value, as in
const foo = "bar";
It looks like you're trying to do destructuring of a state object. In function components, it's common to split up your state instead of having it be in a single object, so you may not want to do this destructuring statement at all, but instead do independent states. For example:
const [editingFormVisible, setEditingFormVisible] = useState();
const [editingAppointment, setEditingAppointment] = useState();
const [data, setData] = useState();
// ... etc
If you don't want to split up the state and want to keep doing the destructuring assignment, then put the object to destructure on the right hand side.
const {
editingFormVisible,
editingAppointment,
data,
addedAppointment,
isNewAppointment,
previousAppointment,
} = someObject; // <------- added
There's a commonly used utility hook "useLatest", which returns a ref containing the latest value of the input. There are 2 common implementations:
const useLatest = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
ref.current = value;
return ref;
};
From https://github.com/streamich/react-use/blob/master/src/useLatest.ts
const useLatest = <T extends any>(current: T) => {
const storedValue = React.useRef(current)
React.useEffect(() => {
storedValue.current = current
})
return storedValue
}
From https://github.com/jaredLunde/react-hook/blob/master/packages/latest/src/index.tsx
The first version isn't suitable for React 18's concurrent mode, the second version will return the old value if used before useEffect runs (e.g. during render).
Is there a way to implement this that's both concurrent-safe and consistently returns the correct value?
Here's my attempt:
function useLatest<T>(val: T): React.MutableRefObject<T> {
const ref = useRef({
tempVal: val,
committedVal: val,
updateCount: 0,
});
ref.current.tempVal = val;
const startingUpdateCount = ref.current.updateCount;
useLayoutEffect(() => {
ref.current.committedVal = ref.current.tempVal;
ref.current.updateCount++;
});
return {
get current() {
// tempVal is from new render, committedVal is from old render.
return ref.current.updateCount === startingUpdateCount
? ref.current.tempVal
: ref.current.committedVal;
},
set current(newVal: T) {
ref.current.tempVal = newVal;
},
};
}
This hasn't been thoroughly tested, just wrote it while writing this question, but it seems to work most of the time. It should be better than both versions above, but it has 2 issues: it returns a different object every time and it's still possible to be inconsistent in this scenario:
Render 1:
ref1 = useLatest(val1)
Create function1, which references ref1
Commit (useLayoutEffect runs)
Render 2:
useLatest(val2)
Call function1
function1 will use val1, but it should use val2.
Here is what I think is correct:
const useLatest = <T extends any>(current: T) => {
const storedValue = React.useRef(current)
React.useLayoutEffect(() => {
storedValue.current = current
})
return storedValue.current
}
Is there a way to implement this that's both concurrent-safe and consistently returns the correct value?
The question doesn't actually explain what "this" means, i.e. how is useLatest called, and what purpose it fulfills in the application. So I'll have to guess for that ;) A somewhat realistic example would be very helpful.
In any case, it's probably useful to take a step back and ask if useLatest is the most suitable solution. If you find you don't need it, you also won't have to fix it.
With the way it works (depending on an effect to capture the value), it indeed won't play well with concurrent features. But even without them, it's an unreliable approach as the ref theoretically can change at any point, making renders unpredictable.
My guess of the use case is something similar to the proposed (and partially accepted) useEvent hook (GitHub PR).
function Chat() {
const [text, setText] = useState('');
const onClick = useEvent(() => {
sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
Its purpose is to capture the latest render's scope, like useCallback, but without the need for dependencies. It does this by using an unchanging callback that internally calls the latest created callback, in a ref that is re-assigned on every render. That way passing that callback as a prop won't cause any renders by itself.
You can implement this yourself already, but the RFC mentions some open questions about this approach.
export function useEvent(handler) {
const latestHandlerRef = useRef();
useLayoutEffect(() => {
latestHandlerRef.current = handler;
});
// Never changing callback.
return useCallback((...args) => {
latestHandlerRef.current(...args)
}, []);
}
I also tested setting latestHandlerRef.current = handler directly in render instead of the layout effect. For now this seems to work as expected but that's just my use case. In the PR some doubt is expressed over assigning to a ref during render, though possibly these concerns don't really apply here, as the ref is only ever accessed in the callback.
My code so far looks something like this:
const { ID } = useParams();
const [getObjects, {loading, data}] = useLazyQuery(GET_OBJECTS_BY_ID);
const objectWithID = props.data.find(datum => datum._id == ID);
if (objectWithID.conditional) {
getObjects({variables: {objectIds: objectWithID.subObjects}});
//Do a bunch of other stuff including a separate render
}
else {...}
What I'm essentially doing is finding an object with the specified ID first, and then querying for its subObjects. I want to first find the objectWithID variable before querying, and then based on one of its parameters, conditionally use its value, hence I think useLazyQuery helps to achieve this. However, this causes an infinite loop: for some reason it's being called an infinite number of times, crashing my webpage. Am I using useLazyQuery incorrectly? How could I prevent this infinite loop?
In this case, you're executing the query inside the render method, meaning it'll just keep firing. Instead, consider using a useEffect hook:
const { ID } = useParams();
const [getObjects, { loading, data }] = useLazyQuery(GET_OBJECTS_BY_ID);
const objectWithID = useMemo(() => {
return props.data.find((datum) => datum._id == ID);
}, [props.data, ID]);
useEffect(() => {
if (objectWithID.conditional) {
getObjects({ variables: { objectIds: objectWithID.subObjects } });
}
}, [getObjects, objectWithID]);
return objectWithID.conditional ? (
<>Render one thing</>
) : (
<>Render the other thing</>
);
Note that I also threw in a useMemo hook to memoize the objectWithID value so it only changes when props.data or ID changes.