const container = useCallback((node: any) => {
console.log(node);
}, []);
return (
<>
{reports.map((report, i) => (
<Resizer ref={container} key={i}>
{report}
</Resizer>
))}
</>
);
--> this works well but with no index
const container = useCallback((node: any, index: number) => {
console.log(index, node);
}, []);
return (
<>
{reports.map((report, i) => (
<Resizer ref={(node) => container(node, i)} key={i}>
{report}
</Resizer>
))}
</>
);
--> infinite loop occurs!!
Is there a reason why second code occurs infinite loop?
Adding global index variable won't be the method
In the first instance you are passing function directly as the ref, and so it is only created once.
But in second case a new instance of the function is created on every render, and so the function that is passed to the ref will have a different reference each time which will re-render every time.
You can try passing index as an dependency to the useCallback hook, so that it is only recreated when the index value changes.
const container = useCallback((node: any, index: number) => {
console.log(index, node);
}, [index]);
Related
I have two components, the parent(name Parent) and child(name Child), on the parent, i map an array and render the child, so the child appears like 4 times(number of times the child is displayed based on the mapping), i have an input field on the Child component (which will be 1 input field for the rendered child component), i am basically trying to get the values of the input field from all the rendered Child component (4 been rendered based on the mapping) and then send it to my parent component (store it in a state on the parent component).
mock code
parent component
const Items = [1,2,3,4]
export const Parent= () => {
return (<div>
{Items.map((Item, index) => {
return (
<div key={index}>
<Child />
</div>
);
})}
</div>
)
}
child component
export const Child = () => {
const [Amount, setAmount] = useState();
return (
<input
value={Amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
)
}
sorry for the bad code formatting.
This is a mock of what it looks like
this should give a somewhat clear understanding or image of the issue, i basically want to get all the Amount on the 4 render children, place it in an array and send it to the Parent component (so i can call a function that uses all the amount in an array as an argument)
i tried to set the values of the Child component to a state on context (it was wrong, it kept on pushing the latest field values that was edited, i am new to react so i didnt understand some of the things that were said about state lifting
Congratulations, you've discovered the need for the Lifting State Up React pattern. Lift the "amount" state from the child component up to the parent component. The parent component holds all the state and provides it and a callback function down to children components via props.
Example:
import { useState } from "react";
import { nanoid } from "nanoid";
const initialState = Array.from({ length: 4 }, (_, i) => ({
id: nanoid(),
value: i + 1
}));
const Child = ({ amount, setAmount }) => {
return (
<input
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
);
};
const Parent = () => {
const [items, setItems] = useState(initialState);
const setAmount = (id) => (amount) =>
setItems((items) =>
items.map((item) =>
item.id === id
? {
...item,
value: amount
}
: item
)
);
return (
<div>
{items.map((item) => (
<Child
key={items.id}
amount={item.value}
setAmount={setAmount(item.id)}
/>
))}
</div>
);
};
try following method:
const Items = [1, 2, 3, 4];
export default function Parent() {
return <Child items={Items} />;
}
export function Child(props) {
const [childItems, setChildItems] = useState(props.items);
const handleChange = (e, index) => {
let temp = childItems;
temp[index] = e.target.value;
setChildItems(temp);
};
return (
<div>
{childItems.map((item, index) => {
return (
<div key={index}>
<input
value={item}
onChange={(e) => handleChange(e, index)}
placeholder="Amount"
/>
</div>
);
})}
</div>
);
}```
I have code like this:
export default function App() {
const [items, setItems] = useState([]);
const handleClick = (number) => {
console.log("typeof", typeof number);
items.includes(number)
? setItems((prevState) =>
prevState.filter((prevSlot) => prevSlot != number)
)
: setItems((items) => [...items, number]);
};
useEffect(() => {
console.log("items", items);
}, [items]);
return (
<div className="App">
<ButtonDiv handleClick={handleClick} slot="5" />
<ButtonDiv handleClick={handleClick} slot="10" />
<ButtonDiv handleClick={handleClick} slot="15" />
<ButtonDiv handleClick={handleClick} slot="20" />
<ButtonDiv handleClick={handleClick} slot="25" />
</div>
);
}
ButtonDiv
const ButtonDiv = ({ handleClick, slot }) => {
return useMemo(() => {
console.log("renderButton");
return <button onClick={() => handleClick(slot)}>Click me</button>;
}, [handleClick, slot]);
};
Here, on clicking on button div, it is re-rendering for every button components, how could I prevent it from happening, and still keep maintaining the core functionality of toggling value when clicked on button.
codesandbox
Every time App renders, it generates a new handleClick function and passes it as a prop to <ButtonDiv>.
The useMemo hook lists handleClick in the dependencies.
Since handleClick has changed, the memoized function has to be called to regenerate the result.
You probably want to wrap the creation of handleClick in useCallback.
You are sending a new reference of handleClick every time that you are rendering the App
With some modifications and using useCallback you can make it work without re-rendering the divButtons, useCallback without deps will generate always the same instance so it won't be refreshing your buttons.
Had to remove the items state, since if you add it you will have always the instance of the state at the moment the handleClick is created, so using just the set state and the prevState we can manage to make it work
const handleClick = useCallback(
(number) => {
console.log("typeof", typeof number, number);
setItems((prevState) =>
prevState.includes(number) ?
prevState.filter((prevSlot) => prevSlot != number) :
[...prevState, number]
)
},
[]
);
Adding image of working example.
I am displaying the name of each skid by iterating the skidList array. I have also provided a button "copy" which calls insertSkid when clicked and "delete" which calls deleteSkid. In insertSkid I am updating the skidList array. However, the Modal.Body doesn't update the skid names on changes to the skidList array.
Here's my code.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
let newSkid = {};
newSkid[name] = "PEN";
let newSkidList = [];
newSkidList.push(newSkid);
setSkidList(newSkidList);
const insertSkid = skid => {
let newSkidList = skidList;
newSkidList.push(skid);
setSkidList(newSkidList);
console.log("Added New Skid" + skidList.length);
};
const deleteSkid = (index) => { setSkidList([...skidList.splice(index, 1)]); }
return (
<Modal
backdrop="static"
show={true}
centered
dialogClassName={"createnewskid-modal"}
>
<Modal.Body>
{skidList.flatMap((skid, index) => (
<div>
{skid[name]}
<Button onClick={insertSkid.bind(this,skid)}>copy</Button>
<Button onClick={()=> deleteSkid(index)}>delete</Button>
<Divider />
</div>
))}
</Modal.Body>
</Modal>
)
}
There are several issues with the code your presenting. As previously stated, the useEffect() hook would be useful here to handle the initialization of the skid array:
useEffect(()=>{
let newSkid = {name: "PEN"};
setSkidList([...skidList, newSkid])
},[])
// The empty array second param tells the component to only execute the above code once on mount
Note: The spread operator (...) is used here to generate a new list based on skidList to which we are appending newSkid.
Next, you should use the same method just described to update your list in the insertSkid function:
const insertSkid = (skid) =>{
setSkidList([...skidList, skid])
}
Finally, I would suggest instead of binding you function, use a anonymous function inside the onclick prop:
onClick = {() => insertSkid(skid)}
if you want to setSkidList(newSkidList), do it inside useEffect.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
useEffect(()=> {
let newSkid = {};
newSkid[name] = "PEN";
setSkidList([newSkid]);
}, [])
//...
}
Call setSkidList out of the useEffect block will cause multiple state updates
It because an array is available by link, and for React skidList after push was not change. Try:
const insertSkid = useCallback((skid) => {
setSkidList([...skidList, skird]);
}, [skidList]);
you can use useEffect for inital data load, and for update you can use prev to get last state value and update yours and do the return, its not needed you need to add more variable assignment.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
useEffect(() => {
const newSkid = {[name] : "PEN"};
setSkidList([newSkid]);
}, [])
const insertSkid = skid => {
setSkidList(prev => [...prev, skid]);
};
return (
<div>
{skidList.flatMap((skid, index) => (
<div>
{skid[name]}
<button onClick={insertSkid.bind(this,skid)}>copy</button>
<hr />
</div>
))}
</div>
)
}
I have a scenario here
const Parent = () => {
handleClick = (id) => {
console.log(id)
}
return <div>
users.map((user, index) =>
<child key={index} onClick={(user.id)=>handleClick(user.id)} />)
</div>
}
The child is using React.memo so it won’t re-render unless its props are changed. I don’t want the child to be re-render when the parent renders but in this scenario it will re-render because I am using an anonymous function. I can put handleClick inside of useCallback but how can I avoid the anonymous function here. I have to use an anonymous function here because I am expecting some arguments here.
You can try using bind.
const Parent = () => {
handleClick = (id) => {
console.log(id)
}
return <div>
users.map((user, index) =>
<Child key={index} onClick={handleClick.bind(this, user.id)} />)
</div>
}
Here's the relevant section of the React docs: Passing Arguments to Event Handlers
I have a list of components coming out of an array of objects. See the code:
const SitesList = () => {
const [loadingState, setLoadingState] = useState(false);
const copySite = (site) => {
setLoadingState(true);
copySiteAction(site)
.then(() => setLoadingState(false))
.catch(() => setLoadingState(false));
};
return filterSites.map((s, i) => (
<div>
{!loadingState ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => copySite(s)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
I want to show the component <LoadingStateComponent /> only on the index/component when I click the Copy Site button. As it is right now, every time I click on that button, all of the components disappear, hence what I see is the <LoadingStateComponent /> until loadingState is set to false.
Any ideas on how can I achieve this?
Rather than having a single "loading" state value, you need one for each item in the array. So, you basically need a second array that's the same size as filterSites to track the state of each index. Or, perhaps a better way would be a simple map linking indices to their corresponding loading state, so that you don't even need to worry about creating an array of X size.
const SitesList = () => {
const [loadingState, setLoadingState] = useState({});
const copySite = (site, index) => {
setLoadingState({...loadingState, [index]: true});
copySiteAction(site)
.then(() => setLoadingState(oldLoadingState => ({...oldLoadingState, [index]: false})))
.catch(() => setLoadingState(oldLoadingState => ({...oldLoadingState, [index]: false})));
};
return filterSites.map((s, i) => (
<div>
{!loadingState[i] ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => copySite(s, i)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
This may or may not solve your problem, but it will solve other problems and its too long for a comment.
This:
const SitesList = () => {
const [loadingState, setLoadingState] = useState(false);
const copySite = (site) => {
setLoadingState(true);
copySiteAction(site)
.then(() => setLoadingState(false))
.catch(() => setLoadingState(false));
};
return filterSites.map((s, i) => (
<div>
{!loadingState ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => copySite(s)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
Needs to look more like this:
const SitesList = () => {
const [loadingState, setLoadingState] = useState(false);
useEffect(() => {
if (loadingState) {
apiCall()
.then(whatever)
.catch(whatever)
.finally(() => setLoadingState(false));
}
}, [loadingState]);
return filterSites.map((s, i) => (
<div key={s.id}> // Note the key
{!loadingState ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => setLoadingState(true)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
React assumes that your pure functional component is a pure function. If you violate that assumption it is under no obligation to respect your side-effects or render them when you'd expect. You need to wrap any side-effects (like a xhr request) in a useEffect hook, and like all hooks it must be called unconditionally. The way I've written it the hook will be called every time loadingState changes and will make the call if it is true, you may need to tweak for your actual use case.
Additionally, items rendered from an array need a unique key prop, I'm assuming your sites have ids but you will need to figure out a way to generate one if not.