Asynchronous State affecting conditional display of Element - javascript

I have a screen (parent) where a FlatList resides in, and the renderItem shows a Child element.
In this Child element, I have a Pressable, and when the user clicks on it, it shows a Checked Icon and changes its background colour.
This happens dynamically based off a state Array in the Parent where in each renderItem of the Child I pass the state Array.
And in the Child component I check if the ID of this Child element is present, if it is, a Checked Icon is shown and the background changes colour.
I know that states in React is asynchronous but I'm always having problems working through such scenarios.
I have tried checking in the Parent screen where the FlatList resides at, to instead pass a Boolean prop to the Child on whether to show the Checked Icon.
E.g. (Sorry always having trouble formatting code in SO)
<FlatList
data={displayData}
renderItem={({item}) => (
<Child
key={item}
userData={item}
id={item}
isSelected={selectedIds?.includes(item)}
// selectedIds={selectedIds}
selectedHandler={id => selectedHandler(id)}
/>
)}
keyExtractor={item => item}
/>
instead of
// In Parent Screen
<FlatList
data={displayData}
renderItem={({item}) => (
<Child
key={item}
userData={item}
id={item}
selectedIds={selectedIds} // here
selectedHandler={id => selectedHandler(id)}
/>
)}
keyExtractor={item => item}
/>
// In Child element
const Child = ({
id,
selectedIds,
selectedHandler
}) => {
return (
<Pressable
style={[
styles.checkContainer,
selectedIds?.includes(id) && { backgroundColor: '#3D9A12' }
]}
onPress={onPressHandler}
>
{selectedIds?.includes(id) && <CheckIcon />} {/* Problem lies here. Not showing Checked Icon */}
</Pressable>
);
};
I won't dump any code here as I have made a snack of the reproduction of my problem.
I appreciate any help please. Thank you so much
Unchecked:
Checked:

The problem is in the selectedHandler function.
You are storing the reference of your state in this variable.
let selectedArr = selectedIds;
and later directly modifying the state itself by doing so:
selectedArr.push(id);
This is why the state updation is not firing the re-render of your component.
Instead, what you need to do is:
let selectedArr = [...selectedIds];
By spreading it, you will be storing a copy of your array and not a reference to it. Now if you modify selectedArr, you won't modifying your state.
I made the changes in the snack provided by you and it now works fine.
The updated selectedHandler function:
const selectedHandler = id => {
let selectedArr = [...selectedIds];
console.log('before selectedArr', selectedArr);
if (selectedArr.includes(id)) {
selectedArr = selectedArr.filter(userId => userId !== id);
setSelectedIds(selectedArr);
console.log('after selectedArr', selectedArr);
return;
}
if (selectedArr.length > 2) {
selectedArr.shift();
}
selectedArr.push(id);
console.log('after selectedArr', selectedArr);
setSelectedIds(selectedArr);
};

Related

In React, how can I cause a called component to delete itself from a page when a state changes inside of it?

So for example:
{!loading ? (data.map((v, i) => {
return <Card key={i} title={v.name} image={v.pictures.sizes[4].link}} /> })
Now these cards show up as a stack of components on the main paing. In each card, there is a button. when I click that button it changes a state in the Card. How can I get that button to cause the Card I click to leave the main screen immediately without needing to reload everything?
I've tried quite a bit and nothing has worked--> Ive tried passed a parent state function, and dealing with the state inside the child
You need to move the state out of the Card and into the parent component. Then you can pass in a function as a prop to each Card component which can then be called on the button onClick function
export const Table = () => {
const [cards, setCards] = useState<Array<{id:string; name: string; pictures: any;}>>([]); // put your card data in here
const removeCard = (id: string) => setCards(prev => prev.filter(card => card.id !== id));
return (
<>
{cards.map(card => <Card key={card.id} id={card.id} title={card.name} image={card.pictures.sizes[4].link} remove={removeCard} />)}
</>
)
}

How to get state from two child components in React

I have a tabbed modal dialog and in one of my tabs I am rendering two image list components. Each component manages an array of image objects based on some props that are defined in the parent. In my dialog I have a single save button and I need to call the backend api to update or delete any image state from either of the image list components. For example
function MyItem() {
function handleSave() {
// How would I get the state from my ImageList components?
}
//other handlers
return (
<TabPanel index={0} title="Detail">
<HeaderWithSaveButton onSaveClick={handleSave} />
<SomeTextContent/>
</TabPanel>
<TabPanel index={1} title="Images">
<ImageList key="banner" />
<ImageList key="icon" />
</TabPanel>
)
}
The ImageList components maintain their own state in an array about the images that are added or removed.
function ImageList({key}) {
const [images, setImages] = useState([{imageKey: key, url:`/images/${key}`, fileData: null}])
function handleImageSelected(image){
setImages() // add image
}
// other handlers
return (
<ImageUploader/>
<SortedImageList images={images} />
)
}
I have the image list working but obviously the state is local to each one, so I have no way to access it in the parent's save button in Item component.
Is this something I can use Context for? Would there have to be two contexts that will be merged? If I lifted the state up to the Item component, how would I keep track of both of those arrays? Also, the item component is getting bloated already.
But the basic question is an approach to manage the state in the two image lists yet access it in the parent so I can figure out what needs to be sent to the api when they save.
You could pass a state update function to each component to allow them to update the state of their parent. I'm not sure if this is a particularly 'React-y' way to do it but something like:
function MyItem() {
const [imageState, setImageState] = useState({});
function handleSave() {
// Use imageState to access the images
}
//other handlers
return (
<TabPanel index={0} title="Detail">
<HeaderWithSaveButton onSaveClick={handleSave} />
<SomeTextContent/>
</TabPanel>
<TabPanel index={1} title="Images">
<ImageList key="banner" setImageState={setImageState} />
<ImageList key="icon" setImageState={setImageState} />
</TabPanel>
)
}
And then your ImageList component can use the passed state setter to inform the parent component:
function ImageList({key, setImageState}) {
const [images, setImages] = useState([{imageKey: key, url:`/images/${key}`, fileData: null}])
function handleImageSelected(image){
setImages() // add image
setImageState((current) => {
return {
...current,
[key]: image,
}
})
}
// other handlers
return (
<ImageUploader/>
<SortedImageList images={images} />
)
}
The other solution is to 'raise' the state to the parent component

React: Edit and Delete from a pop-up menu on a list item

I have a list container component, which is the parent. It maps out the list rows. On the list row component, which is the child, every item has a button to toggle a pop-up menu, which has a button for edit, and a button for delete. The menu itself is a sibling to the list rows because if I include it in the list rows component, each row will render a menu and when toggled, they would all stack up on top of each other. The edit and delete buttons toggle either a form for the edit, or directly remove the item.
What I currently have is:
// Parent / Container
const [itemID, setItemID] = useState(null);
const handleMenuOpen = (id) => (e) => {
setAnchorEl(e.currentTarget); // for menu placement
setItemID(id);
};
const handleItemDelete = () => {
dispatch(deleteItem(itemID));
};
<List>
<ListRow handleMenuOpen={handleMenuOpen} />
<Menu handleItemDelete={handleItemDelete} itemID={itemID} />
</List>;
// List Row
<Button onClick={handleMenuOpen(item.id)} />;
// Menu
<MenuItem onClick={() => handleModalOpen(itemID)} />;
<MenuItem onClick={() => handleItemDelete()} />;
The edit button works fine but no matter how I try, I cannot get setItemID to work from the onClick on the list item. It always come out as the initial value of null. I console logged that the ID in the function parameter came out properly but the setState hook did not work.
I tried putting the useState on the list item and pass the ID through useContext but came out undefined when handledItemDelete was called.
I tried using ref on the child to get the ID from the parent, which also came out as undefined.
I cannot think of how to use useEffect to check for a change in the handleMenuOpen parameter.
I am out of ideas. Anyone know what the issue is and how to fix it?
You should probably just pass the handleMenuOpen function and rely on the selected element and then store it's id in itemID variable.
const handleMenuOpen = (e) => {
setAnchorEl(e.currentTarget); // for menu placement
setItemID(e.currentTarget.id);
};
<MenuItem onClick={handleMenuOpen} />;
i had the same problem before. I think you should handle the popup toggling in the child component, so something like this.
function Parent() {
function handleDelete(item) {
deleteFunction(item.id)
}
return (
<div>
{[].map((item, index) => {
return (
<ListRowItem key={index} handleDelete={handleDelete} item={item} />
)
})}
</div>
)
}
function ListRowItem({handleDelete, item}) {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isModelVisible, setIsModalVisible] = useState(false)
return (
<div>
<Button onClick={isMenuOpen === true ? () => setIsMenuOpen(true) : () => setIsMenuOpen(false)} />
{isModelVisible === true ? <ModalItem /> :null}
{isMenuOpen === true ?
<div>
<MenuItem onClick={() => setIsModalVisible(true)} />
<MenuItem onClick={() => handleDelete(item.id)} />
</div>
: null}
</div>
)
}
I assume you are doing a certain loop to render each List Row inside the List component
let's say all items are in an items array which you loop:
{items.map(item => (
<ListRow handleMenuOpen={handleMenuOpen}/>
<Menu handleItemDelete={handleItemDelete} item={item} />
)}
now in the Menu container or component, you would have the item and pass it to the Menu item

FlatList update is slow

I have a FlatList that holds 5000 items. There is an option to switch on the "Select Mode", which renders a checkbox next to each item. Whenever I press the corresponding button to switch to that mode, there is a noticeable delay before checkboxes are actually rendered.
This is my state:
{
list: data, // data is taken from an external file
selectedItems: [] // holds ids of the selected items,
isSelectMode: false
}
My list:
<FlatList
data={list}
extraData={this.state}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
removeClippedSubviews
/>
My list item:
<ListItem
title={item.title}
isCheckboxVisible={isSelectMode}
isChecked={isChecked}
onPress={() => this.toggleItem(item)}
/>
Each list item implements shouldComponentUpdate:
...
shouldComponentUpdate(nextProps) {
const { isCheckboxVisible, isChecked } = this.props;
return isChecked !== nextProps.isChecked || isCheckboxVisible !== nextProps.isCheckboxVisible;
}
...
I always thought that FlatList only renders items that a currently visible within the viewport, but here it feels like the entire list of 5000 items is re-rendered. Is there anything I can improve here? Or perhaps I am doing something totally wrong?
Full code: https://snack.expo.io/#pavermakov/a-perfect-flatlist

How to avoid re-render in React?

I am making a simple accordion which has text editor inside it.
If we click expand text then the text editor gets opened and if we enter some text inside the editor and click shrink, then the accordion gets closed.
Again if click on the expand text of accordion where we made the changes, then the text already entered is missing inside it.
I can understand that this re render every time we click on the expand text. Also this code,
<Text> {toggleValue === index && item.content && <EditorContainer />} </Text>
check for the item clicked then it gets opened so re render happens here and hence I am losing the entered text.
Complete working example:
https://codesandbox.io/s/react-accordion-forked-dcqbo
Could you please kindly help me to retain the value entered inside the text editor despite of the clicks over the text Expand/Shrink?
Put the editor's state into a persistent parent component. Since the NormalAccordion encompasses all editors, and you want persistent state just one editor, use another component, so that the state doesn't get lost when the editor unmounts, then pass it down for the editor to use:
const OuterEditorContainer = ({ toggleValue, setToggleValue, item, index }) => {
const [editorState, setEditorState] = useState(EditorState.createEmpty());
const toggleHandler = (index) => {
index === toggleValue ? setToggleValue(-1) : setToggleValue(index);
};
return (
<Accordion>
<Heading>
<div
style={{ padding: "10px", cursor: "pointer" }}
className="heading"
onClick={() => toggleHandler(index)}
>
{toggleValue !== index ? `Expand` : `Shrink`}
</div>
</Heading>
<Text>
{toggleValue === index && item.content && (
<EditorContainer {...{ editorState, setEditorState }} />
)}
</Text>
</Accordion>
);
};
const NormalAccordion = () => {
const [toggleValue, setToggleValue] = useState(-1);
return (
<div className="wrapper">
{accordionData.map((item, index) => (
<OuterEditorContainer
{...{ toggleValue, setToggleValue, item, index }}
/>
))}
</div>
);
};
// text_editor.js
export default ({ editorState, setEditorState }) => (
<div className="editor">
<Editor
editorState={editorState}
onEditorStateChange={setEditorState}
toolbar={{
inline: { inDropdown: true },
list: { inDropdown: true },
textAlign: { inDropdown: true },
link: { inDropdown: true },
history: { inDropdown: true }
}}
/>
</div>
);
You could also put the state into the text_editor itself, and always render that container, but only conditionally render the <Editor.
You need to save the entered text and pass it as props from the parent component to EditorContainer.
Right now everytime you render it (e.g. when we click expand)
It looks like you set an empty state.
Something like:
EditorContainer
editorState: this.props.editorState || EditorState.createEmpty()
onEditorStateChange = (editorState) => {
// console.log(editorState)
this.props.setEditorState(editorState);
};
And in Accordion:
{toggleValue === index &&
item.content &&
<EditorContainer
editorState={this.state.editorState[index]}
setEditorState={newText => this.setState({...this.state, newText}) />}
Didn't try to execute it, but I think that's the way to achieve it.
Ps: Class components are almost not used anymore. Try to use function components and learn about useState hook, looks so much cleaner in my opinion

Categories

Resources