I have a ToDo component and an Item component in my react app. I noticed that when I trigger my onDelete function from my Item component, it only has the tdList state variable in the state it was in when I created the item component. Why does this happen and how can I fix this issue.
function ToDo() {
const [tdList, setTD] = useState([]);
const [item, setItem] = useState("");
const onDelete = (id) => {
// console.log(id);
console.log(tdList);
for(let i=0; i<tdList.length; i++){
if (tdList[i].props.id == id){
// setTD(tdList.splice(i, 1))
}
}
// setTD(tdList.splice())
};
const onHandleSubmit = (event) => {
event.preventDefault();
setTD([...tdList, (<Item id={itemsAdded} item={item} delete={onDelete} />)]);
setItem('');
// console.log(tdList);
itemsAdded++;
};
...more code...
Don't put React components into state. It breaks the natural order of how they're supposed to work and can make the control flow that's been written very difficult to understand. Instead, into state, put only the values needed to create React components from later - and when returning from the component, create the components from that state.
For your code, you could do something like:
const [lastIdUsed, setLastIdUsed] = useState(-1); // use this instead of reassigning a non-React itemsAdded variable
const [tdData, setTdData] = useState([]);
const onDelete = (id) => {
// use `.filter`, not `.splice` in React - don't mutate state
setTdData(tdData.filter(tdItem => tdItem.id !== id));
};
const onHandleSubmit = (event) => {
event.preventDefault();
setTdData([...tdData, { id: lastIdUsed + 1, item }]);
setItem('');
setLastIdUsed(lastIdUsed + 1);
};
const tds = tdData.map(
tdItem => <Item id={tdItem.id} item={tdItem.item} delete={onDelete} />
);
And then with the tds, return them or interpolate them into the JSX at the end.
Only create components right before you're going to return them.
Related
For learning purposes, I'm creating an e-shop, but I got stuck with localStorage, useEffect, and React context. Basically, I have a product catalog with a button for every item there that should add a product to the cart.
It also creates an object in localStorage with that item's id and amount, which you select when adding the product to the cart.
My context file:
import * as React from 'react';
const CartContext = React.createContext();
export const CartProvider = ({ children }) => {
const [cartProducts, setCartProducts] = React.useState([]);
const handleAddtoCart = React.useCallback((product) => {
setCartProducts([...cartProducts, product]);
localStorage.setItem('cartProductsObj', JSON.stringify([...cartProducts, product]));
}, [cartProducts]);
const cartContextValue = React.useMemo(() => ({
cartProducts,
addToCart: handleAddtoCart, // addToCart is added to the button which adds the product to the cart
}), [cartProducts, handleAddtoCart]);
return (
<CartContext.Provider value={cartContextValue}>{children}</CartContext.Provider>
);
};
export default CartContext;
When multiple products are added, then they're correctly displayed in localStorage. I tried to log the cartProducts in the console after adding multiple, but then only the most recent one is logged, even though there are multiple in localStorage.
My component where I'm facing the issue:
const CartProduct = () => {
const { cartProducts: cartProductsData } = React.useContext(CartContext);
const [cartProducts, setCartProducts] = React.useState([]);
React.useEffect(() => {
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
}
}
)();
}, []);
console.log('cartProducts', cartProducts);
return (
<>
<pre>
{JSON.stringify(cartProductsData, null, 4)}
</pre>
</>
);
};
export default CartProduct;
My service file with fetchProductById function:
const domain = 'http://localhost:8000';
const databaseCollection = 'api/products';
const relationsParams = 'joinBy=categoryId&joinBy=typeId';
const fetchProductById = async (id) => {
const response = await fetch(`${domain}/${databaseCollection}/${id}?${relationsParams}`);
const product = await response.json();
return product;
};
const ProductService = {
fetchProductById,
};
export default ProductService;
As of now I just want to see all the products that I added to the cart in the console, but I can only see the most recent one. Can anyone see my mistake? Or maybe there's something that I missed?
This looks bad:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
You run a loop, but cartProducts has the same value in every iteration
Either do this:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...cartProducts, fetchedProduct]);
});
Or this:
const values = Promise.all(Object.values(retrievedProducts).map(x => ProductService.fetchProductById(x.id)));
setCartProducts(values)
The last is better because it makes less state updates
Print the cartProducts inside useEffect to see if you see all the data
useEffect(() => {
console.log('cartProducts', cartProducts);
}, [cartProducts]);
if this line its returning corrects values
const productsObj = localStorage.getItem('cartProductsObj');
then the wrong will be in the if conditional: replace with
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetched = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...fetched, fetchedProduct]);
});
}
}
Issue
When you call a state setter multiple times in a loop for example like in your case, React uses what's called Automatic Batching, and hence only the last call of a given state setter called multiple times apply.
Solution
In your useEffect in CartProduct component, call setCartProducts giving it a function updater, like so:
setCartProducts(prevCartProducts => [...prevCartProducts, fetchedProduct]);
The function updater gets always the recent state even though React has not re-rendered. React documentation says:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
What I am trying to achieve is after fitlerRequest function called filterListValue should manipulate.
But what happening is on radioHandleChange it's start manipulating the filterListValue.
I think they have the same memory reference but how to make a copy of it?
export default function CustomFilter(props) {
const { filterListValue, setFilterListValue } = props;
const [radioValue, setRadioValue] = useState(filterListValue)
const radioHandleChange = (e, list) => {
setRadioValue(radioValue => {
let copy = [...radioValue]
copy[indexChange].value = e.target.value
copy[indexChange].id = list.id
return copy
});
}
const filterRequest = () => {
setFilterListValue(radioValue)
handleClose()
};
}
Just make copies using .... Although not mentioned, looks like props.filterListValue is an array. So create a copy when starting with useState hook.
Also, when setting radio value, make a copy of the item (since it is an object) so you are not mutating the state by mistake.
const { filterListValue, setFilterListValue } = props;
const [radioValue, setRadioValue] = useState([...props.filterListValue])
const radioHandleChange = (e, list) => {
setRadioValue(radioValue => {
let copy = [...radioValue]
let copyItem = {...copy[indexChange]};
copyItem[indexChange].value = e.target.value
copyItem[indexChange].id = list.id
return copyItem
});
}
PS:
You can destructure inside the function argument parenthesis. It is common practice:
export default function CustomFilter({filterListValue, setFilterListValue}) {
I am trying to implement a simple search algorithm for my products CRUD.
The way I thought to do it was entering the input in a search bar, and the products that matched the search would appear instantly every time the user changes the input, without needing to hit a search button.
However, the way I tried to do it was like this:
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
const queryProducts = filterProducts(queryString, productList);
setProductList(queryProducts);
}, [queryString, productList]);
I know that productList changes every render, and that's probably why it isn't working. But I didn't figure out how can I solve the problem. I've seen other problems here and solutions with useReducer, but I none of them seemed to help me.
The error is this one below:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
what you are doing here is fetching a product list and filtering it based on the query string and using that filtered list to render the UI. So ideally your filteredList is just a derived state based on your queryString and productList. So you can remove the filterProducts from your useEffect and move it outside. So that it runs when ever there is a change in the state.
function filterProducts (productName = '', productList = []) {
return productName.trim().length > 0 ? productList.filter((prod)=> {
return prod.title === productName;
}); : productList
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [queryString, setQueryString] = useState('');
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
}
}, [queryString]);
// query products is the derived state
const queryProducts = filterProducts(queryString, productList);
// Now instead of using productList to render something use the queryProducts
return (
{queryProducts.map(() => {
.....
})}
)
If you want the filterProducts to run only on change in queryString or productList then you can wrap it in useMemo
const queryProducts = React.useMemo(() => filterProducts(queryString, productList), [queryString, productList]);
When you use a setState function in a useEffect hook while having the state for that setState function as one of the useEffect hook's dependencies, you'll get this recursive effect where you end up infinitely re-rendering your component.
So, first of all we have to remove productList from the useEffect. Then, we can use a function to update your state instead of a stale update (like what you're doing in your example).
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
setProductList(prevProductList => {
return filterProducts(queryString, prevProductList)
});
}, [queryString]);
Now, you still get access to productList for your filter, but you won't have to include it in your dependencies, which should take care of the infinite re-rendering.
I recommend several code changes.
I would separate the state that immediately reflects the user input at all times from the state that represents the query that is send to the backend. And I would add a debounce between the two states. Something like this:
const [query, setQuery] = useState('');
const [userInput, setUserInput] = useState('');
useDebounce(userInput, setQuery, 750);
I would split up the raw data that was returned from the backend and the filtered data which is just derived from it
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
I would split up the useEffect and not mix different concerns all into one (there is no rule that you cannot have multiple useEffect)
useEffect(() => {
if (query.trim() === '') {
Axios
.get("http://localhost:3001/api/product/get-all")
.then((data) => { setProducts(data.data) });
}
}, [query]);
useEffect(
() => setFilteredProducts(filterProducts(userInput, products)),
[userInput, products]
);
I have a list of warehouses that I pull from an API call. I then render a list of components that render checkboxes for each warehouse. I keep the state of the checkbox in an object (using the useState hook). when I check/uncheck the checkbox, I update the object accordingly.
My task is to display a message above the checkbox when it is unchecked. I tried simply using the object, however, the component was not re-rendering when the object changed.
I found a solution to my problem by simply adding another useState hook (boolean value) that serves as a toggle. Since adding it, the component re-renders and my object's value is read and acted on appropriately.
My question is: why did I have to add the toggle to get React to re-render the component? Am I not updating my object in a manner that allows React to see the change in state? Can someone explain to me what is going on here?
I've created a sandbox to demonstrate the issue: https://codesandbox.io/s/intelligent-bhabha-lk61n
function App() {
const warehouses = [
{
warehouseId: "CHI"
},
{
warehouseId: "DAL"
},
{
warehouseId: "MIA"
}
];
const [warehouseStatus, setWarehouseStatus] = useState({});
const [toggle, setToggle] = useState(true);
useEffect(() => {
if (warehouses.length > 0) {
const warehouseStates = warehouses.reduce((acc, item) => {
acc[item.warehouseId] = true;
return acc;
}, {});
setWarehouseStatus(warehouseStates);
}
}, [warehouses.length]);
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
setToggle(!toggle);
};
return warehouses.map((wh, idx) => {
return (
<div key={idx}>
{!warehouseStatus[wh.warehouseId] && <span>This is whack</span>}
<MyCheckbox
initialState
id={wh.warehouseId}
onCheckChanged={handleChange}
label={wh.warehouseId}
/>
</div>
);
});
}
Thanks in advance.
You are mutating state (don't mutate state)
this:
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
};
should be:
const handleChange = ({name,value}) => {
setWarehouseStatus({...warehouseStatus,[name]:value});
};
See the problem?
const newState = warehouseStatus; <- this isn't "newState", it's a reference to the existing state
const { name, value } = obj;
newState[name] = value; <- and now you've gone and mutated the existing state
You then call setState with the same state reference (directly mutated). React says, "hey, that's the same reference to the state I previously had, I don't need to do anything".
I am using react-redux with hooks, and I need a selector that takes a parameter that is not a prop. The documentation states
The selector function does not receive an ownProps argument. However,
props can be used through closure (see the examples below) or by using
a curried selector.
However, they don't provide an example. What is the proper way to curry as described in the docs?
This is what I've done and it seems to work, but is this right? Are there implications from returning a function from the useSelector function (it seems like it would never re-render?)
// selectors
export const getTodoById = state => id => {
let t = state.todo.byId[id];
// add display name to todo object
return { ...t, display: getFancyDisplayName(t) };
};
const getFancyDisplayName = t => `${t.id}: ${t.title}`;
// example component
const TodoComponent = () => {
// get id from react-router in URL
const id = match.params.id && decodeURIComponent(match.params.id);
const todo = useSelector(getTodoById)(id);
return <span>todo.display</span>;
}
When the return value of a selector is a new function, the component will always re-render on each store change.
useSelector() uses strict === reference equality checks by default, not shallow equality
You can verify this with a super simple selector:
const curriedSelector = state => () => 0;
let renders = 0;
const Component = () => {
// Returns a new function each time
// triggers a new render each time
const value = useSelector(curriedSelector)();
return `Value ${value} (render: ${++renders})`;
}
Even if the value is always 0, the component will re-render on each store action since useSelector is unaware that we're calling the function to get the real value.
But if we make sure that useSelector receives the final value instead of the function, then the component only gets rendered on real value change.
const curriedSelector = state => () => 0;
let renders = 0;
const Component = () => {
// Returns a computed value
// triggers a new render only if the value changed
const value = useSelector(state => curriedSelector(state)());
return `Value ${value} (render: ${++renders})`;
}
Conclusion is that it works, but it's super inefficient to return a new function (or any new non-primitives) from a selector used with useSelector each time it is called.
props can be used through closure (see the examples below) or by using a curried selector.
The documentation meant either:
closure useSelector(state => state.todos[props.id])
curried useSelector(state => curriedSelector(state)(props.id))
connect is always available, and if you changed your selector a little, it could work with both.
export const getTodoById = (state, { id }) => /* */
const Component = props => {
const todo = useSelector(state => getTodoById(state, props));
}
// or
connect(getTodoById)(Component)
Note that since you're returning an Object from your selector, you might want to change the default equality check of useSelector to a shallow equality check.
import { shallowEqual } from 'react-redux'
export function useShallowEqualSelector(selector) {
return useSelector(selector, shallowEqual)
}
or just
const todo = useSelector(state => getTodoById(state, id), shallowEqual);
If you're performing costly computations in the selector or the data is deeply nested and performance becomes a problem, take a look at Olivier's answer which uses memoization.
Here is a solution, it uses memoïzation to not re-render the component on each store change :
First I create a function to make selectors, because the selector depends on the component property id, so I want to have a new selector per component instances.
The selector will prevent the component to re-render when the todo or the id prop hasn't changed.
Lastly I use useMemo because I don't want to have more than one selector per component instance.
You can see the last example of the documentation to have more information
// selectors
const makeGetTodoByIdSelector = () => createSelector(
state => state.todo.byId,
(_, id) => id,
(todoById, id) => ({
...todoById[id],
display: getFancyDisplayName(todoById[id])
})
);
const getFancyDisplayName = t => `${t.id}: ${t.title}`;
// example component
const TodoComponent = () => {
// get id from react-router in URL
const id = match.params.id && decodeURIComponent(match.params.id);
const getTodoByIdSelector = useMemo(makeGetTodoByIdSelector, []);
const todo = useSelector(state => getTodoByIdSelector(state, id));
return <span>todo.display</span>;
}
Yes, it is how it's done, simplified example:
// Curried functions
const getStateById = state => id => state.todo.byId[id];
const getIdByState = id => state => state.todo.byId[id];
const SOME_ID = 42;
const TodoComponent = () => {
// id from API
const id = SOME_ID;
// Curried
const todoCurried = useSelector(getStateById)(id);
const todoCurried2 = useSelector(getIdByState(id));
// Closure
const todoClosure = useSelector(state => state.todo.byId[id]);
// Curried + Closure
const todoNormal = useSelector(state => getStateById(state)(id));
return (
<>
<span>{todoCurried.display}</span>
<span>{todoCurried2.display}</span>
<span>{todoClosure.display}</span>
<span>{todoNormal.display}</span>
</>
);
};
Full example:
This is helper-hook useParamSelector for TypeScript, which implements the official approach of Redux Toolkit.
Hook implementation:
// Define types and create new hook
export type ParametrizedSelector<A, R> = (state: AppState, arg: A) => R;
export const proxyParam: <T>(_: AppState, param: T) => T = (_, param) => param;
export function useParamSelector<A, R>(
selectorCreator: () => ParametrizedSelector<A, R>,
argument: A,
equalityFn: (left: R, right: R) => boolean = shallowEqual
): R {
const memoizedSelector = useMemo(() => {
const parametrizedSelector = selectorCreator();
return (state: AppState) => parametrizedSelector(state, argument);
}, [typeof argument === 'object' ? JSON.stringify(argument) : argument]);
return useSelector(memoizedSelector, equalityFn);
}
Create parametrized selector:
export const selectUserById = (): ParametrizedSelector<string, User> =>
createSelector(proxyParam, selectAllUsers, (id, users) => users.find((it) => it.id === id));
And use it:
const user = useParamSelector(selectUserById, 1001); // in components
const user = selectUserById()(getState(), 1001); // in thunks
You can also use it hook with selectors created with reselect's createSelector.