How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};
Related
I have some files that builds a cart in a dropdown for my shop website.
One file adds the selected item to an array which will be my cart. The other file is the CartDropdown component itself. My cart only show the items when I close and open it (remounting), but I want it to remount every time I add a new item.
Adding item function:
const ProductContainer = ({ productInfo }) => {
const { cartProducts, setCartProducts } = useContext(CartContext);
const cartArray = cartProducts;
const addProduct = () => {
productInfo.quantity = 1;
if (cartArray.includes(productInfo)) {
const index = cartArray.findIndex((object) => {
return object === productInfo;
});
cartProducts[index].quantity++;
setCartProducts(cartArray);
} else {
cartArray.push(productInfo);
setCartProducts(cartArray);
}
// setCartProducts(cartArray)
console.log(cartProducts);
// console.log(cartArray)
};
};
dropdown component
const CartDropdown = () => {
const { setCartProducts, cartProducts } = useContext(CartContext);
const { setProducts, currentProducts } = useContext(ProductsContext);
// useEffect(() => {}, [cartProducts])
const cleanCart = () => {
const cleanProducts = currentProducts;
console.log(cleanProducts);
for (let i in cleanProducts) {
if (cleanProducts[i].hasOwnProperty("quantity")) {
cleanProducts[i].quantity = 0;
}
}
setProducts(cleanProducts);
setCartProducts([]);
};
return (
<div className="cart-dropdown-container">
<div className="cart-items">
{cartProducts.map((product) => (
<div key={product.id}>
<img src={product.imageUrl}></img>
</div>
))}
</div>
<button onClick={cleanCart}>CLEAN CART</button>
<Button children={"FINALIZE PURCHASE"} />
</div>
);
};
How can I force the dropdown to remount every time cartProducts changes?
CART CONTEXT:
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => { },
cartProducts: [],
setCartProducts: () => { }
})
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false)
const [cartProducts, setCartProducts] = useState([])
const value = { isCartOpen, setIsCartOpen, cartProducts, setCartProducts };
return (
<CartContext.Provider value={value}>{children}</CartContext.Provider>
)
}
product context
export const ProductsContext = createContext({
currentProducts: null,
setProducts: () => {}
})
export const ProductsProvider = ({children}) => {
const [currentProducts, setProducts] = useState(shop_data)
const value = {currentProducts, setProducts}
return(
<ProductsContext.Provider value={value}>{children}</ProductsContext.Provider>
)
}
You can change the key prop of the component every time you want to remount. Every time cartProduct changes, update the value of key. You can do that using a useEffect with cartProduct as a dependency.
<CartDropdown key={1} />
to
<CartDropdown key={2} />
Edit for more clarification:
const [keyCount, setKeyCount] = useState(0);
useEffect(() => {
setKeyCount(keyCount+1);
}, [cartProducts]);
<CartDropdown {...otherProps} key={keyCount} />
The first issue I see is that you are not using the callback to set the state inside the context but you are doing cartProducts[index].quantity++ and react docs specify
Do Not Modify State Directly
Also after cartProducts[index].quantity++, you call setCartProducts(cartArray); not with cartProducts which you actually updated (this is also the reason why "if I do usestate(console.log('A'), [cartProducts]) its not triggering everytime i add my cart product". But anyway there is an issue even if you would use cartArray for both:
You shouldn't directly do const cartArray = cartProducts since by doing so cartArray will be a reference to cartProducts (not a copy of it) which also shouldn't be modified (because it would mean that you are modifying state directly).
So first 2 things I recommend you to improve would be:
Initialize cartArray as a cartProducts deep copy (if your cartProducts is an array of objects, spread syntax won't do it). So I would reccomand you to check this question answers for creating a deep copy.
After you make sure that cartArray is a deep copy of cartProducts, doublecheck you use cartArray to create a local newValue then set the state of the context with the same value (so basically:
cartArray[index].quantity++;
setCartProducts(cartArray);
)
The deep copy part also apply for const cleanProducts = currentProducts; (you should also create a deep copy here for cleanProducts, instead of saving the object ref).
If you are not using deep copies, your code might still work in some cases, but you might encounter weird behaviors in some other instances (and thoose are really hard to debug). Therefore is a bad practice in general not using deep copies.
I am completing a technical challenge and came across a scenario I never had to deal with before.
I am asked to code up a shopping cart that has a UI that represents basic checkout data like order total, current items in cart, etc.
One of the requirements states I need to implement a Checkout class that can be instantiated:
const checkout = new Checkout();
And I should be able to obtain basic info from it like:
const total = checkout.total();
And add items to the cart through it:
checkout.add(product.id);
What makes this a tricky thing to solve is that I can't think of a clean "DRY" way of implementing it into the UI. This is mainly because any updates in the checkout class will not trigger any re-renders since it's not part of the state. I would usually use state variables for this.
I tried binding state variables to parameters in the checkout class like:
const [total, setTotal] = useState();
useEffect(()=>{
setTotal(checkout.total)
}, [checkout.total])
But checkout.total is only the reference to the method, so it never changes so I do not get the binding I want.
Trying out other stuff I managed to put together a "solution" but I question whether it is a good pattern.
I basically pass a callback to the checkout class which is called whenever the cart is updated. The callback is a state variable's setter, so:
const [cart, setCart] = useState<string[]>(checkout.cart);
checkout.callback = setCart;
Then inside the add method:
add(productId) {
// Some code...
this.callback([...this.cart]);
}
What this grants is that the cart state variable is updated whenever the checkout class has changes in its parameters. So it fires a rerender on the Cart component and all of its children that have props being passed down. Therefore I get a synced UI.
The thing is I kind of don't need the cart variable other than for forcing re-renders. I can get the cart info directly from the checkout class which is what I do. But for it to be reflected in the UI I need some state variable to be updated. It could even be a counter, I only went for cart instead of a counter to make it more coherent I guess.
Am I overcomplicating things here? Is there a pattern I am missing that is used for this scenarios? How does one usually interact with an instantiated class and ensures the UI is somehow updated from changes to the class?
EDIT (adding missing info):
The Checkout class needs to implement the following interface:
interface Checkout {
// ...
// Some non relevant properties methods
// ...
add(id: number): this;
}
So it is explicitly asked that the add method returns this (in order to allow function chaining).
mixing of patterns
Using OOP instances with methods that mutate internal state will prevent observation of a state change -
const a = new Checkout()
const b = a // b is *same* state
console.log(a.count) // 0
a.add(item)
console.log(a.count) // 1
console.log(a == b) // true
console.log(a.count == b.count) // true
React is a functional-oriented pattern and uses complimentary ideas like immutability. Immutable object methods will create new data instead of mutating existing state -
const a = new Checkout()
const b = a.add(item) // b is *new* state
console.log(a.count) // 0
console.log(b.count) // 1
console.log(a == b) // false
console.log(a.count == b.count) // false
In this way, a == b is false which effectively sends the signal to redraw this component. So we need a immutable Checkout class, where methods return new state instead of mutating existing state -
// Checkout.js
class Checkout {
constructor(items = []) {
this.items = items
}
add(item) {
return new Checkout([...this.items, item]) // new state, no mutation
}
get count() {
return this.items.length // computed state, no mutation
}
get total() {
return this.items.reduce((t, i) => t + i.price, 0) // computed, no mutation
}
}
export default Checkout
demo app
Let's make a quick app. You can click the 🍐 and 🥨 buttons to add items to the cart. The app will show the correct count and total as well as the individual items -
App component preview
Now "syncing" the class to the component is just using ordinary React pattern. Use your class and methods directly in your componenets -
import Checkout from "./Checkout.js"
import Cart from "./Cart.js"
function App({ products = [] }) {
const [checkout, setCheckout] = React.useState(new Checkout)
const addItem = item => event =>
setCheckout(checkout.add(item))
return <div>
{products.map(p =>
<button key={p.name} onClick={addItem(p)}>{p.name}</button>
)}
<b>{checkout.count} items for {money(checkout.total)}</b>
<Cart checkout={checkout} />
</div>
}
const data =
[{name: "🍐", price: 5}, {name: "🥨", price: 3}]
const money = f =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(f)
A simple Cart component uses JSON.stringify to quickly visualize each item -
// Cart.js
function Cart({ checkout }) {
return <pre>{JSON.stringify(checkout, null, 2)}</pre>
}
export default Cart
Run the demo below to verify the result in your browser -
class Checkout {
constructor(items = []) {
this.items = items
}
add(item) {
return new Checkout([...this.items, item])
}
get count() {
return this.items.length
}
get total() {
return this.items.reduce((t, i) => t + i.price, 0)
}
}
function App({ products = [] }) {
const [checkout, setCheckout] = React.useState(new Checkout)
const addItem = item => event =>
setCheckout(checkout.add(item))
return <div>
{products.map(p =>
<button key={p.name} onClick={addItem(p)}>{p.name}</button>
)}
<b>{checkout.count} items for {money(checkout.total)}</b>
<Cart checkout={checkout} />
</div>
}
const money = f =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(f)
function Cart({ checkout }) {
return <pre>{JSON.stringify(checkout, null, 2)}</pre>
}
const data = [{name: "🍐", price: 5}, {name: "🥨", price: 3}]
ReactDOM.render(<App products={data} />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
Hmm, looks like you need to share the state. The first solution that came to my mind is just to use the Class component. You can use force rerender while you need and write more custom logic without useEffect hacks.
The second solution is more clear IMO. It uses an Observer pattern. You need to add a subscription to your Checkout class. So basically.
useEffect(() => {
const subscription = (newState) => setState(newState)
const instance = new Checkout()
instance.subcribe(subscription)
return instance.unsubcribe(subscription)
}, [setState])
Since setState is immutable, this hook will be run only once.
Your idea is correct, you need somehow to start re-render to sync state of checkout object and state of a component.
E.g. you may do it by context and force update (in case if you do not want to duplicate data in object and state):
const CheckoutContext = React.createContext();
const checkout = new Checkout();
const CheckoutProvider = ({ children }) => {
// init force update, just to start re-render
const [ignored, forceUpdate] = React.useReducer((x) => x + 1, 0);
const add = (a) => {
checkout.add(a);
forceUpdate();
};
const total = checkout.total();
const value = { add, total };
return (
<CheckoutContext.Provider value={value}>
{children}
</CheckoutContext.Provider>
);
};
const Child = () => {
const v = React.useContext(CheckoutContext);
console.log(v.total);
return <button onClick={() => v.add(100)}>Click</button>;
};
export default function App() {
return (
<div className="App">
<CheckoutProvider>
<Child />
</CheckoutProvider>
</div>
);
}
You can make a Cart class that allows for observers to be notified when something important happens. To make it available for the react components, provide an instance of it with a context, and use a stateful hook to notify components by setting the state through the observer function.
Here we go:
First, we need a Cart class that notifies observers when something happens
export class Cart {
constructor() {
this.products = [];
this.subscribers = new Set();
}
subscribe = (notifyMe) => {
this.subscribers.add(notifyMe);
};
unSubscribe = (notifyMe) => {
this.subscribers.delete(notifyMe);
};
addToCart = (product) => {
this.products = [...this.products, product];
this.notify();
};
removeFromCart = (product) => {
this.products = this.products.filter(product);
this.notify();
};
notify = () => {
this.subscribers.forEach((n) => n(this.products));
};
}
We will expose this through the react tree with a context, so lets make one
const CartContext = React.createContext();
export const CartProvider = ({ children, cart }) => {
return <CartContext.Provider value={cart}>{children}</CartContext.Provider>;
};
Now for the trick! A hook that will update its state using the carts observer pattern, thereby notifying the component that uses it.
export const useCart = () => {
const cart = React.useContext(CartContext);
const [content, r] = React.useState();
React.useEffect(() => {
const notify = (productsInCart) => r(productsInCart);
cart.subscribe(notify);
cart.notify();
return () => cart.unSubscribe(notify);
}, [cart, r]);
return {
addToCart: cart.addToCart,
removeFromCart: cart.removeFromCart,
content
};
};
Note that it can be worth to update after subscribing.
Now we have our library set up, we can make some components. So here's where we instantiate the Cart class. We make a new Cart, and let the provider provide that instance
const cart = new Cart();
export default function App() {
return (
<div className="App">
<CartProvider cart={cart}>
<CartCounter />
<h1>Welcome to the shop</h1>
<h2>start putting stuff in the cart!</h2>
<Catalog />
<button
onClick={() => {
// this will still notify components
cart.addToCart({ foo: "bar" });
}}
>
add product by directly manipulating class instance
</button>
</CartProvider>
</div>
);
}
Here are the other components
const Catalog = () => {
const getProducts = async () =>
await fetch(
"https://random-data-api.com/api/commerce/random_commerce?size=6"
).then((r) => r.json());
const [products, setProducts] = React.useState();
React.useEffect(() => {
getProducts().then(setProducts);
}, []);
if (!products) {
return null;
}
return (
<ul
style={{
listStyle: "none",
display: "grid",
gridTemplateColumns: "50% 50%"
}}
>
{products.map((product) => (
<Item key={product.uid} product={product} />
))}
</ul>
);
};
const Item = ({ product }) => {
const { addToCart } = useCart();
const addProductToCart = () => addToCart(product);
return (
<li>
<article
style={{
maxWidth: 200,
border: "1px solid black",
margin: 10,
padding: 10
}}
>
<h4>{product.product_name}</h4>
<div>
<div>$ {product.price}</div>
<button onClick={addProductToCart}>add to cart</button>
</div>
</article>
</li>
);
};
const CartCounter = () => {
const { content } = useCart();
return <div>items in cart: {content?.length || 0}</div>;
};
This can be a pretty handy pattern, and can be taken pretty far (e.g. React Query works like this).
CodeSandbox link
I read with interest most of the answers, and I found them pretty explicative and comprehensive, especially the extensive example of the observer pattern.
I used the same approach to handle a similar need, without having to implement all the pattern and it makes use of the 'EventEmitter' class.
This way you can subscribe your React UI to several different type of events, olle's example would become something like this:
export class Cart extends EventEmitter {
constructor() {
super();
this.products = [];
}
addToCart = (product) => {
this.products = [...this.products, product];
this.emit("CART_UPDATE", this.products)
};
removeFromCart = (product) => {
this.products = this.products.filter(product);
this.emit("CART_UPDATE", this.products)
};
}
And in React you'd just need a custom hook or just an effect placed on top where you can place your event listeners:
export default function App() {
const cartRef = useRef(new Cart())
const [items, setItems] = useState([])
useEffect(()=>{
const cart = cartRef.current
cart.on("CART_UPDATE", setItems)
return () => cart.removeListener("CART_UPDATE", setItems)
}, []) //
return (
<div className="App">
<div>{items.map(it => item.id)}</div>
<button
onClick={() => {
// this will still notify components
cart.addToCart({ id: "bar" });
}}
>
add product by directly manipulating class instance
</button>
</div>
);
}
I think it is perfectly reasonable to send a callback to the object and then call that callback when it is needed. If you don't want to add any unnecessary data, then don't:
add(productId) {
// Some code...
this.callback();
}
checkout.callback = () => {
setTotal(checkout.total);
}
I'm building a chat app, I have 3 components from parent to child in this hierarchical order: Chat, ChatLine, EditMessage.
I'm looping through messages state in Chat to display multiple ChatLine components as a list, and I pass some state to ChatLine and then to EditMessage.
I need the state :
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
to remain in the parent component Chat so I can have access to it later there.
Anyway, now when I click on the Edit button, the EditMessage component shows a textarea, and I'm setting state onChange in it, but everytime I click the Edit button or type a letter in the textarea all the components rerender as I see in React DevTool Profiler, even the children that didn't get affected, I only need the Chat and affected ChatLine to rerender at most.
The whole code is available in CodeSandbox, and deployed in Netlify.
And here it is here also :
(Chat.js)
import { useEffect, useState } from "react";
import ChatLine from "./ChatLine";
const Chat = () => {
const [messages, setMessages] = useState([]);
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
useEffect(() => {
setMessages([
{ id: 1, message: "Hello" },
{ id: 2, message: "Hi" },
{ id: 3, message: "Bye" },
{ id: 4, message: "Wait" },
{ id: 5, message: "No" },
{ id: 6, message: "Ok" },
]);
}, []);
return (
<div>
<p>MESSAGES :</p>
{messages.map((line) => (
<ChatLine
key={line.id}
line={line}
editValue={editValue}
setEditValue={setEditValue}
editingId={editingId}
setEditingId={setEditingId}
/>
))}
</div>
);
};
export default Chat;
(ChatLine.js)
import EditMessage from "./EditMessage";
import { memo } from "react";
const ChatLine = ({
line,
editValue,
setEditValue,
editingId,
setEditingId,
}) => {
return (
<div>
{editingId !== line.id ? (
<>
<span>{line.id}: </span>
<span>{line.message}</span>
<button
onClick={() => {
setEditingId(line.id);
setEditValue(line.message);
}}
>
EDIT
</button>
</>
) : (
<EditMessage
editValue={editValue}
setEditValue={setEditValue}
setEditingId={setEditingId}
editingId={editingId}
/>
)}
</div>
);
};
export default memo(ChatLine);
(EditMessage.js)
import { memo } from "react";
const EditMessage = ({ editValue, setEditValue, editingId, setEditingId }) => {
return (
<div>
<textarea
onKeyPress={(e) => {
if (e.key === "Enter") {
// prevent textarea default behaviour (line break on Enter)
e.preventDefault();
// updating message in DB
updateMessage(editValue, setEditValue, editingId, setEditingId);
}
}}
onChange={(e) => setEditValue(e.target.value)}
value={editValue}
autoFocus
/>
<button
onClick={() => {
setEditingId(null);
setEditValue("");
}}
>
CANCEL
</button>
</div>
);
};
export default memo(EditMessage);
const updateMessage = (editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
};
The problem is that all of the child components see their props change any time any of them is in the process of being edited, because you're passing the current editing information to all of the children. Instead, only pass the current editing text (editValue) to the component being edited, not to all the others.
ChatLine doesn't use editValue when it's not the instance being edited. So I'd do one of two things:
Use a different component for display (ChatLine) vs. edit (ChatLineEdit). Almost the entire body of ChatLine is different depending on whether that line is being edited or not anyway. Then only pass editValue to ChatLineEdit.
Pass "" (or similar) as editValue to the one not being edited. In the map in Chat: editValue={line.id === editingId ? editValue : ""}.
Pass an "are equal" function into memo for ChatLine that doesn't care what the value of editValue is if line.id !== editingId. By default, memo does a shallow check of all props, but you can take control of that process by providing a function as the second argument. For instance:
export default memo(ChatLine, (prevProps, nextProps) => {
// "Equal" for rendering purposes?
return (
// Same chat line
prevProps.line === nextProps.line &&
// Same edit value setter (you can leave this out, setters from `useState` never change)
prevProps.setEditValue === prevProps.setEditValue && // ***
// Same editingId
prevProps.editingId === prevProps.editingId &&
// Same editingId setter (you can leave this out too)
prevProps.setEditingId === prevProps.setEditingId && // ***
(
// Same edit value...
prevProps.editValue === prevProps.editValue ||
// OR, we don't care because we're not being edited
nextProps.line.id !== nextProps.editingId
)
);
});
This is fragile, because it's easy to get the check wrong, but it's another option.
I would go for #1. Not even passing props to components that they don't need is (IMHO) the cleanest approach.
I've tried to find a solution to this, but nothing seems to be working. What I'm trying to do is create a TreeView with a checkbox. When you select an item in the checkbox it appends a list, when you uncheck it, remove it from the list. This all works, but the problem I have when I collapse and expand a TreeItem, I lose the checked state. I tried solving this by checking my selected list but whenever the useEffect function runs, the child component doesn't have the correct parent state list.
I have the following parent component. This is for a form similar to this (https://www.youtube.com/watch?v=HuJDKp-9HHc)
export const Parent = () => {
const [data,setData] = useState({
name: "",
dataList : [],
// some other states
})
const handleListChange = (newObj) => {
//newObj : { field1 :"somestring",field2:"someotherString" }
setDataList(data => ({
...data,
dataList: data.actionData.concat(newObj)
}));
return (
{steps.current === 0 && <FirstPage //setting props}
....
{step.current == 3 && <TreeForm dataList={data.dataList} updateList={handleListChange}/>
)
}
The Tree component is a Material UI TreeView but customized to include a checkbox
Each Node is dynamically loaded from an API call due to the size of the data that is being passed back and forth. (The roots are loaded, then depending on which node you select, the child nodes are loaded at that time) .
My Tree class is
export default function Tree(props) {
useEffect(() => {
// call backend server to get roots
setRoots(resp)
})
return (
<TreeView >
Object.keys(root).map(key => (
<CustomTreeNode key={root.key} dataList={props.dataList} updateList={props.updateList}
)))}
</TreeView>
)
CustomTreeNode is defined as
export const CustomTreeNode = (props) => {
const [checked,setChecked] = useState(false)
const [childNodes,setChildNodes] = useState([])
async function handleExpand() {
//get children of current node from backend server
childList = []
for( var item in resp) {
childList.push(<CustomTreeNode dataList={props.dataList} updateList={props.updateList} />)
}
setChildNodes(childList)
}
const handleCheckboxClick () => {
if(!checked){
props.updateList(obj)
}
else{
//remove from list
}
setChecked(!checked)
}
// THIS IS THE ISSUE, props.dataList is NOT the updated list. This will work fine
// if I go to the next page/previous page and return here, because then it has the correct dataList.
useEffect(() => {
console.log("Tree Node Updating")
var isInList = props.dataList.find(function (el) {
return el.field === label
}) !== undefined;
if (isInList) {
setChecked(true);
} else {
setChecked(false)
}
}, [props.dataList])
return ( <TreeItem > {label} </TreeItem> )
}
You put props.data in the useEffect dependency array and not props.dataList so it does not update when props.dataList changes.
Edit: Your checked state is a state variable of the CustomTreeNode class. When a Tree is destroyed, that state variable is destroyed. You need to store your checked state in a higher component that is not destroyed, perhaps as a list of checked booleans.
Edit: I don't understand the reason for downvotes, this was a good question and no other questions on this site solved my issue. I simply preloaded the data to solve my issue but that still doesn't solve the problem without using functional components.
I'm trying to pass users last message into the ListItem subtitle prop but I can't seem to find a way to return the value from the promise/then call. It's returning a promise instead of the value which gives me a "failed prop type". I thought about using a state but then I don't think I could call the function inside the ListItem component anymore.
getMsg = id => {
const m = fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(msg => {
return msg;
});
return m;
};
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={this.getMsg(item.id)} // failed prop type
bottomDivider
chevron
/>
);
You could only do it that way if ListItem expected to see a promise for its subtitle property, which I'm guessing it doesn't. ;-) (Guessing because I haven't played with React Native yet. React, but not React Native.)
Instead, the component will need to have two states:
The subtitle isn't loaded yet
The subtitle is loaded
...and render each of those states. If you don't want the component to have state, then you need to handle the async query in the parent component and only render this component when you have the information it needs.
If the 'last message' is something specific to only the ListItem component and not something you have on hand already, you might want to let the list item make the network request on its own. I would move the function inside ListItem. You'll need to set up some state to hold this value and possibly do some conditional rendering. Then you'll need to call this function when the component is mounted. I'm assuming you're using functional components, so useEffect() should help you out here:
//put this is a library of custom hooks you may want to use
// this in other places
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
const ListItem = ({
title,
bottomDivider,
chevron,
onPress,
id, //hae to pass id to ListItem
}) => {
const [lastMessage, setLastMessage] = useState(null);
const isMounted = useIsMounted();
React.useEffect(() => {
async function get() {
const m = await fireStoreDB.getUserLastMessage(
fireStoreDB.getUID,
id
);
//before setting state check if component is still mounted
if (isMounted.current) {
setLastMessage(m);
}
}
get();
}, [id, isMounted]);
return lastMessage ? <Text>DO SOMETHING</Text> : null;
};
I fixed the issue by using that promise method inside another promise method that I had on componentDidMount and added user's last message as an extra field for all users. That way I have all users info in one state to populate the ListItem.
componentDidMount() {
fireStoreDB
.getAllUsersExceptCurrent()
.then(users =>
Promise.all(
users.map(({ id, username }) =>
fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(message => ({ id, username, message }))
)
)
)
.then(usersInfo => {
this.setState({ usersInfo });
});
}
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={item.message}
bottomDivider
chevron
/>
);