React Router Dom and object in in search parameters - javascript

I am using React Router Dom v6. I would like to store some object search parameters in the URL. Right now, I am basically doing something like this:
const [searchParams, setSearchParams] = useSearchParams();
const allSearchParams = useMemo(() => {
const params: Record<string, string> = {};
for (const [key, value] of searchParams.entries()) {
if (value.startsWith('{') || value.startsWith('[')) {
try {
params[key] = JSON.parse(value);
} catch {
params[key] = value;
}
} else {
params[key] = value;
}
}
return params;
}, [searchParams]);
And when writing in the URL, I do:
const newF: Record<string, string> = { ...nextFilter };
Object.keys(newF).forEach((key) => {
if (typeof newF[key] === 'object' && newF[key]) {
newF[key] = JSON.stringify(newF[key]);
}
});
setSearchParams(createSearchParams(newF), {
replace: replaceSearch,
});
But this feels pretty hacky. Is there a proper way to store objects in the URL properly and safely? For example:
const filters = {
name: "user1",
region: {
city: "Sydney",
country: "Australia"
}
}

You can simplify your encoding and decoding process. You can use the below functions; you can add them in their own file and import them:
import {createSearchParams} from "react-router-dom"
export const encodeSearchParams = (params) => createSearchParams(params);
export const decodeSearchParams = (searchParams) => {
return [...searchParams.entries()].reduce((acc, [key, val]) => {
try {
return {
...acc,
[key]: JSON.parse(val)
};
} catch {
return {
...acc,
[key]: val
};
}
}, {});
};
This way, you don't have to memoize them. And you can use them like below, for example:
function HomePage() {
const [searchParams, setSearchParams] = useSearchParams();
function handleQueryParamsChange() {
const filters = {
name: "user1",
region: {
city: "Sydney",
country: "Australia"
}
};
const params = {
filters: JSON.stringify(filters),
anotherField: "Simple String"
};
setSearchParams(encodeSearchParams(params));
}
console.log(decodeSearchParams(searchParams).filters);
console.log(decodeSearchParams(searchParams).anotherField);
return (
<div>
<button onClick={handleQueryParamsChange} className="page">
Update query params
</button>
</div>
);
}

Related

creating a redux like stores using custom hooks

Here I implemented redux like store using custom hooks. everything goes well and code executed correctly but problem is that in reducer under switch statement "TOGGLE" there I return a updated state which is finally stored in globalstate but if I returned empty object {} instead of {products: updated} still globalstate updating correctly with a change that has been done in reducer...since i am not passing globalstate reference then how it is updated correctly
and what listeners exactly do in dispatch method in code
import MarkFavMulti from "./MarkFavMulti";
import classes from "./MarkFav.module.css";
import useStore from "../HookStore/Store";
import {reducer2} from "../SampleReducer";
const MarkFav = props => {
const [outfit, dispatch] = useStore(reducer2);
const onClicked = (id) => {
dispatch({type: "TOGGLE", id: id});
}
const element = outfit.products.map((item) => {
return <MarkFavMulti cloth={item.name}
favorite={item.favorite}
price={item.price}
key={item.id}
clicked={onClicked.bind(this, item.id)} />
});
return (
<main className={classes.Markfav}>
{element}
</main>
);
};
export default MarkFav;
import {useState, useEffect} from "react";
let globalState = {};
let listeners = [];
const useStore = (reducer) => {
const setState = useState(globalState)[1];
const dispatch = (action) => {
let curr = Object.assign({},globalState);
const newState = reducer({...curr}, action)
globalState = {...globalState,...newState};
for(let listener of listeners) {
listener(globalState);
}
};
useEffect(()=>{
listeners.push(setState);
return () => {
listeners.filter(item => item !==setState);
}
},[setState]);
return [globalState, dispatch];
};
export const initStore = (initialState) => {
if(initialState) {
globalState = {...globalState, ...initialState};
}
}
export default useStore;
let initialState = {
products: [
{ id: 1, name: "shirt", price: "$12", favorite: false },
{ id: 2, name: "jeans", price: "$42", favorite: false },
{ id: 3, name: "coat", price: "$55", favorite: false },
{ id: 4, name: "shoes", price: "$8", favorite: false },
]
}
const configureStore = () => {
initStore(initialState);
};
export default configureStore;
export const reducer2 = (state=initialState, action) => {
switch (action.type) {
case "TOGGLE":
let update = {...state};
let updated = [...update.products];
updated = updated.map(item => {
if(item.id === action.id) {
item.favorite = !item.favorite;
return item;
}
return item;
});
return {products: updated};
//if we return {} ...it will updated correctly in globalstate
default:
throw new Error("not reachable");
}
}
The behavior that you are describing is due to this object assignment right here:
item.favorite = !item.favorite;
Here you are directly mutating the properties of the item object. You probably thought that it would be fine since you are using a copy of the products array.
let update = {...state};
let updated = [...update.products];
What actually happens is that updated is a "shallow copy" of the original array. The array itself is a new array, but the items in that array are the same items as in the state. You can read more about that here.
You need to return a new item object instead of mutating it. Here's a concise way to write it using the ternary operator.
case "TOGGLE":
return {
...state, // not actually necessary since products is the only property
products: state.products.map((item) =>
item.id === action.id
? {
...item,
favorite: !item.favorite
}
: item
)
};

multiple custom react hooks in the same component

Is it an acceptable practice to have two custom react hooks in the same component, one after another?
The issue I am dealing with is as follows:
The first custom hook useBudgetItems will load, but the subsequent one will be undefined. I think I understand why it's happening (my budgetSettings property inside my useBudgetSettings loads after the console.log() statement), but I am not sure how to get around this and whether this is the right approach.
const BudgetCost ({ projectId }) => {
const { budgetCost, loaded } = useBudgetCost({ key: projectId });
const { budgetSettings } = useBudgetSettings({ key: projectId });
const [totalBudget, setTotalBudget] = useState(budgetCost.totalBudget);
const [budgetCosts, setbudgetCosts] = useState(budgetCost.items);
// This will be undefined
console.log(budgetSettings)
if(!loaded) return <div/>
return (
...
...
)
});
My useBudgetCost custom hook is as follow (the useBudgetSettings isn't much different in the mechanics.
const useBudgetCost = ({ key, notifyOnChange }) => {
const [loaded, setIsLoaded] = useState(false)
const { budgetCost, setBudgetCost } = useContext(ProjectContext)
useEffect(() => {
if(key)
return getBudgetCost(key);
},[key]);
const getBudgetCost = (key) => {
let { budgetCosts, loaded } = UseBudgetCostsQuery(key);
setBudgetCost(budgetCosts);
setIsLoaded(loaded);
}
let onBudgetCostChange = (update) => {
let tempBudgetCostItems = copyArrayReference(budgetCost);
tempBudgetCostItems = {
...tempBudgetCostItems,
...update
}
setBudgetCost(tempBudgetCostItems)
if (notifyOnChange)
notifyOnChange(update)
}
return {
loaded,
budgetCost,
onBudgetCostChange
}
}
useBudgetSettings component:
const useBudgetSetting = ({ key }) => {
const [loaded, setIsLoaded] = useState(false)
const { budgetSettings, setBudgetSettings } = useContext(ProjectCondext)
const globalContext = useContext(GlobalReferenceContext);
useEffect(() => {
if(key)
return getBudgetSetting(key);
},[key]);
const getBudgetSetting = (key) => {
let { budgetSettings, loaded } = UseBudgetSettingsQuery(key);
console.log(budgetSettings);
setBudgetSettings(budgetSettings);
setIsLoaded(loaded);
}
const getBudgetReferences = (overrideWithGlobal = false) => {
if(overrideWithGlobal)
return globalContext.getBudgetReferences();
return budgetSettings.map((item) => { return { value: item.key, label: item.costCode } });
}
const getCategoryText = (key) => _.get(_.find(getBudgetReferences(), (bc) => bc.value === key), 'label');
return {
loaded,
budgetSettings,
getCategoryText,
getBudgetReferences
}
}

How to change Redux state from a path string?

I have initial state like this:
const initialState = {
array: [
{
key: "value",
obj: {
key1: "value",
key2: "value",
},
array: [
{
key: "value",
obj: {
key1: "value",
key2: "value",
},
}
]
},
{
key: "value",
obj: {
key1: "value",
key2: "value",
},
},
{
key: "value",
obj: {
key1: "value",
key2: "value",
},
},
],
path: "",
value: ""
};
Reducer:
export const reducer = (state = initialState, action) => {
switch (action.type) {
case "SET_PATH":
return {
...state,
path: action.path
};
case "SET_NEW_VALUE":
return {
...state,
newValue: action.value
};
case "SET_NEW_BUILD":
//What next?
default:
return state
}
};
Action creators:
const setPath = (path) => ({type: "SET_PATH", path});
const setNewValue = (value) => ({type: "SET_NEW_VALUE", value});
const setNewBuild = (path, value) => ({type: "SET_NEW_BUILD", path, value});
And i need to change this state after this dispatch using a path string and new value.
dispatch(setNewBuild("array[0].obj.key1", "newValue");
Also the value can have form like this "obj: {key1: "newValue", key2: "newValue"}" hence will be created a new object.
How can i do this?
Here is an example using the set helper:
const REMOVE = () => REMOVE;
//helper to get state values
const get = (object, path, defaultValue) => {
const recur = (current, path, defaultValue) => {
if (current === undefined) {
return defaultValue;
}
if (path.length === 0) {
return current;
}
return recur(
current[path[0]],
path.slice(1),
defaultValue
);
};
return recur(object, path, defaultValue);
};
//helper to set state values
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path, newValue) => {
if (path.length === 1) {
return setKey(current, path[0], newValue);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1), newValue)
);
};
const oldValue = get(object, path);
const newValue = callback(oldValue);
if (oldValue === newValue) {
return object;
}
return recur(object, path, newValue);
};
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore } = Redux;
//action
const setNewBuild = (path, value) => ({
type: 'SET_NEW_BUILD',
path,
value,
});
const initialState = {
array: [
{
key: 'value',
obj: {
key1: 'value',
key2: 'value',
},
},
],
path: '',
value: '',
};
const reducer = (state = initialState, action) => {
const { type } = action;
if (type === 'SET_NEW_BUILD') {
const { path, value } = action;
return set(state, path, () => value);
}
return state;
};
const store = createStore(
reducer,
initialState,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
const App = () => {
const state = useSelector((x) => x);
const dispatch = useDispatch();
return (
<div>
<button
onClick={() =>
dispatch(
setNewBuild(
['array', 0, 'obj', 'key1'],
'new value key 1'
)
)
}
>
change array 0 obj key1
</button>
<button
onClick={() =>
dispatch(
setNewBuild(['array', 0, 'obj'], {
key1: 'change both key1',
key2: 'change both key2',
})
)
}
>
change array 0 obj
</button>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
The important bits are:
<button
onClick={() =>
dispatch(
// path is an array
setNewBuild(['array', 0, 'obj'], {
key1: 'change both key1',
key2: 'change both key2',
})
)
}
>
change array 0 obj
</button>
And in the reducer:
const { type } = action;
if (type === 'SET_NEW_BUILD') {
const { path, value } = action;
return set(state, path, () => value);
}
To do that with only the path there's 2 approaches, remember that in Redux everything has to be immutable, so no direct mutations should be done.
Easier approach: immer library that allows you to do mutating operations like push or using dot operator, but being immutable underneath.
Difficult approach: use spreads native JS spread operators for object and arrays, but you will understand better how things work.
I will leave the vanilla example, but if you prefer going with something straight forward you can use immer library.
function reducer(state = initialState, action) {
switch (action.type) {
case 'setNewData': {
const { array } = state;
const { path, value } = action.payload;
const index = path.match(/[\d]+/g);
const objectPath = path.split('.').slice(1);
const [elementToReplace] = array.slice(index, index + 1);
_.set(elementToReplace, objectPath, value); // using lodash set helper here
const newArray = [...array.slice(0, index), elementToReplace, ...array.slice()];
return {
...state,
array: newArray,
};
}
default: {
return state;
}
}
}
getState gives you current state at action dispatch time, also, do notice that you dispatch actions as higher-order functions ( UI-dispatch(action(payload => dispatch => dispatch({type, payload}))
//assuming { value } comes from UI and { path } refer to current stored data at state
const action = value => (dispatch, getState) =>
dispatch({type: "netNewData", value, path: getState().array[0].obj.key1}) ;
const reducer = ( state = initialState, action) => {
switch(action.type){
case "setNewData":
const { value, path } = action;
return {...state, value, path}
default: return state
}
}
This approach updates the reference as it iterates through the path keys. Those `slices are because the setter must stop at the penultimate key to set the value on the parent.
E.g. ({d: 4}).d = 6 not 4 = 6
path = ['a','b','c','d'];
state = {a: {b: {c: {d: 4}}}};
value = 6;
// jsonpath setter:
path.slice(0, -1).reduce((ref, key)=>ref[key], state)[path.slice(-1)] = value;
// jsonpath getter:
console.log(path.reduce((ref, key)=>ref[key], state), state.a.b.c); // 6 {d:6}

Update nested object in array in ReactJs

I am having trouble updating a nested object in an array in react state.
I am trying to compare whether or not an item already exists in the state based off of the name of that item.
Here's what I am trying to have happen:
Add item to cart. If the product name is the same, then increase qty by one. (this works)
If the name is not the same then add that new item to the state. (this also works)
If I go back to a previous item that I have already added, and want to add it again, then I want to find that item in state, compare it to the current object that is being passed in, and update the quantity by one. (this doesn't work)
I decided to implement the immutability-helper to simplify updating state.
I should note, that I am using NextJs and that this problem occurs only after I reload a new dynamic page.
Hope this is enough information... please let me know if more info is needed to be of assistance.
Thanks in advance!
Update 2: This is another problem that I am running into... maybe the answer is obvious, but I thought I'd just put it out there to get a second set of eyes on it (if this should be a new question, please let me know).
I have some optional parameters that go along with each item. The parameters are a nested object inside of the array. My solution was to use the following code to compare the data.
import React, { createContext, useState, useEffect } from 'react';
import update from 'immutability-helper';
export const CartContext = createContext();
export function CartProvider(props) {
const [ cart, setCart ] = useState([]);
//**********Start of the code to check the parameters of each item********
const checkArr = (obj1, obj2) => {
let newArr = [];
function onlyInFirst(first, second) {
for (let i = 0; i < first.length; i++) {
if (second.indexOf(first[i]) === -1) {
newArr.push(first[i]);
}
}
}
onlyInFirst(obj1, obj2);
onlyInFirst(obj2, obj1);
if (newArr.length === 0) {
return false;
} else {
return true;
}
};
//*******End of the code to check the parameters of each item**********
const addItem = (obj) => {
let dataCheck = true;
if (cart.length != 0) {
cart.map((e, i) => {
if (e.productName === obj.productName) {
const prevVal = Object.values(e.productParams);
const currentVal = Object.values(obj.productParams);
dataCheck = checkArr(prevVal, currentVal);
}
if (dataCheck === false) {
const object = e;
const cartCopy = cart;
const newObj = update(object, { quantity: { $set: object.quantity + 1 } });
const newState = update(cartCopy, { [i]: { $set: newObj } });
setCart(newState);
}
});
} else {
setCart([ ...cart, obj ]);
}
if (dataCheck === true) {
setCart([ ...cart, obj ]);
}
};
return <CartContext.Provider value={{ cart, addItem }}>{props.children}</CartContext.Provider>;
}
However, I'm still getting the same sample output as shown below, regardless of what parameters I add to productParams.
Does anyone see anything wrong with my logic here? I am at a loss of what to do...
UPDATE 1: I'm adding the object structure and sample output.
Object structure:
const obj = {
productName: product.name, // product.name comes from api
productParams: {}, // this is dynamically added elsewhere
quantity: 1,
productPrice: price
}
Sample Output From Chrome Dev Tools:
3) [{…}, {…}, {…}]
0: {productName: "Item 1", productParams: {…}, quantity: 4, productPrice: 60}
1: {productName: "Item 2", productParams: {…}, quantity: 3, productPrice: 3000}
2: {productName: "Item 1", productParams: {…}, quantity: 3, productPrice: 60}
length: 3
__proto__: Array(0)
import React, { createContext, useState, useEffect } from 'react';
import update from 'immutability-helper';
export const CartContext = createContext();
export function CartProvider(props) {
const [ cart, setCart ] = useState([]);
const addItem = (obj) => {
if (cart.length != 0) {
cart.map((e, i) => {
if (e.productName === obj.productName) {
const object = e;
const cartCopy = cart;
const newObj = update(object, { quantity: { $set: object.quantity + 1 } });
const newState = update(cartCopy, { [i]: { $set: newObj } });
setCart(newState);
} else {
setCart([ ...cart, obj ]);
}
});
} else {
setCart([ ...cart, obj ]);
}
};
return <CartContext.Provider value={{ cart, addItem }}>{props.children}</CartContext.Provider>;
}
There is a small mistake in your code. You should update cart outside of the map function instead of inside it when there are no matched object in the array.
import React, { createContext, useState, useEffect } from 'react';
import update from 'immutability-helper';
export const CartContext = createContext();
export function CartProvider(props) {
const [ cart, setCart ] = useState([]);
const addItem = (obj) => {
if (cart.length != 0) {
let dataExist = false;
cart.map((e, i) => {
if (e.productName === obj.productName) {
const object = e;
const cartCopy = cart;
const newObj = update(object, { quantity: { $set: object.quantity + 1 } });
const newState = update(cartCopy, { [i]: { $set: newObj } });
setCart(newState);
dataExist=true
}
});
if(dataExist) {
setCart([ ...cart, obj ]);
}
} else {
setCart([ ...cart, obj ]);
}
};
return <CartContext.Provider value={{ cart, addItem }}>{props.children} </CartContext.Provider>;
}
What your code doing was this, if the current item(e) from cart array doesn't match with obj, it was adding that obj in the array. Which should be done only after you have iterate the array and confirmed that there are no data exist in the array which is same as obj.
If that update doesn't solve your problem I might need some sample data(i.e. object structure, sample output etc...) from you to test this properly.
Please update your code with this one and it would be better if you can share obj data and cart data:
const addItem = (obj) => {
if (cart.length !== 0) {
for (let i = 0; i <= cart.length; i += 1) {
if (undefined !== cart[i]) {
if (obj.productName === cart[i].productName) {
const tempArr = [...cart];
tempArr.quantity += 1;
setCart(tempArr);
} else {
setCart([...cart, obj]);
}
}
}
} else {
setCart([...cart, obj]);
}
};
I solved it! InsomniacSabbir had the right idea. I just had to modify the code a bit to get the result I wanted.
Here's the solution
import React, { createContext, useState, useEffect } from 'react';
import update from 'immutability-helper';
export const CartContext = createContext();
export function CartProvider(props) {
const [ cart, setCart ] = useState([]);
const addItem = (obj) => {
let dataCheck = true;
if (cart.length != 0) {
cart.map((e, i) => {
if (e.productName === obj.productName) {
const object = e;
const cartCopy = cart;
const newObj = update(object, { quantity: { $set: object.quantity + 1 } });
const newState = update(cartCopy, { [i]: { $set: newObj } });
setCart(newState);
dataCheck = false;
}
});
} else {
setCart([ ...cart, obj ]);
}
if (dataCheck === true) {
setCart([ ...cart, obj ]);
}
};
return <CartContext.Provider value={{ cart, addItem }}>{props.children}</CartContext.Provider>;
}
I had an if/else statement in map which was causing the problem. I took out the else statement out of map and added another if statement into the function that checks if dataCheck is true/false. dataCheck would be set to false only if the if statement in map was executed.
Hope this answer helps!
Thanks for the help everyone!

Way to prevent TypeError: values is not iterable

I am developing a react application but encountered an error when I try to enter a value in a search bar, delete it using blur, and then enter another value. How to prevent this error?
TypeError: values is not iterable
handleBlur [as onBlur]
setValues(removeDuplicates([...values, createOption(inputValue)], 'value'));
Thank you.
const createOption = (label) => ({
label,
value: label,
});
const propTypes = {
initialValues: PropTypes.array,
onSearch: PropTypes.func,
debug: PropTypes.bool
};
const removeDuplicates = (myArr, prop) => {
return myArr.filter((obj, pos, arr) => {
return arr.map(mapObj => mapObj[prop]).indexOf(obj[prop]) === pos;
});
}
export const useSearchBar = (props) => {
// Validate props
PropTypes.checkPropTypes(propTypes, props, "property", "useSearchBar");
// Destructure props
let {
initialValues = [],
onSearch,
loading
} = props;
// But use the users state if provided
const [inputValue, setInputValue] = useState('');
const [values, setValues] = useState(initialValues);
const handleBlur = (event) => {
if(!inputValue || !inputValue.trim()) {
return;
}
setValues(removeDuplicates([...values, createOption(inputValue)], 'value'));
setInputValue('');
};
const handleValueChange = (val, actionMeta) => {
setValues(val);
};
const handleInputValueChange = (inputValue) => {
if(!inputValue) {
return;
}
const re = /^[\d,\s]+$/;
if (re.test(inputValue)) {
setInputValue(inputValue.trim());
}
};
const handleKeyDown = (event) => {
if (!inputValue) return;
switch (event.key) {
case 'Enter':
case 'Tab':
setInputValue('');
setValues(removeDuplicates([...values, createOption(inputValue)], 'value'));
event.preventDefault();
break;
default:
}
};

Categories

Resources