Toggle by index or id react native - reactjs

I am pulling categories and sub categories from an API which is working as expected. I have them in an accordion using expanded:
constructor(props) {
super(props);
this.state = {
...
expanded : false,
}
}
Then rendering categories and sub categories:
renderFilter=()=> {
const items = [];
for (let item of this.state.docCats) {
if( item.subCats) {
docItems = item.subCats.map(row => {
return<TouchableOpacity key={row.doc_sc_id} onPress={()=>this.FilterRequest(item.doc_cat_id, row.doc_sc_id)}><Text style={styles.filterCat}>{row.doc_sc_title}</Text></TouchableOpacity>
})
}
items.push(
<View key={item.doc_cat_id}>
<TouchableOpacity onPress={()=>this.FilterRequest(item.doc_cat_id, '')}>
<Text style={styles.filterCat}>{item.doc_cat_title}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={()=>this.toggleExpand()}>
<FontAwesome name={this.state.expanded ? 'chevron-up' : 'chevron-down'} size={20} color="#000" />
</TouchableOpacity>
{this.state.expanded &&
docItems
}
</View>
);
}
return items;
}
using a Toggle:
toggleExpand=()=>{
this.setState({expanded : !this.state.expanded})
}
My problem is when i use the toggleExpand it expands all the accordions at the same time. Is there a way i can open them individually by targetting the index or id?
I would prefer not to use a different class/component as i need access to the other functions and fetch requests that i have.

You should add a property to each docCats item that indicates whether its accordion should be expanded or not, then pass an id to the toggleExpand function to update this property for a specific item and the docCats array that's stored in your state. Use the expanded property of each item inside of the renderFilter function to indicate whether the accordion should be expanded or not.
Here's a simplified example, but should give you a good idea how to rewrite your code to make it work:
const items = Array.from({ length: 5 }).map((_, idx) => ({
id: idx,
title: `Accordion ${idx + 1}`
}));
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
items: items.map(item => ({
...item,
expanded: false
}))
};
}
toggleExpand = id => {
const nextItems = [...this.state.items].map(item => {
if (item.id === id) {
return {
...item,
expanded: !item.expanded
};
}
return item;
});
this.setState({
items: nextItems
});
};
renderFilter = () => {
return this.state.items.map(item => (
<div
style={{ border: `1px solid black`, width: `100`, height: `100` }}
onClick={() => this.toggleExpand(item.id)}
key={item.id}
>
<p>
{item.title} {item.expanded ? `is expanded` : `is not expanded`}
</p>
</div>
));
};
render() {
const renderedFilter = this.renderFilter();
return <div>{renderedFilter}</div>;
}
}
If you'd like to see it in action, simply click on each box to change the expanded state, here's the link to a working demo:
CodeSandbox

Related

React native VirtualizedList Re-render while scroll the list

I have Virtualized List initial render record up to 30 ,while render the data list automatically re render 2 to 4 times and also the new data added to the list
while rendering multi times we can't able to do any action like touch or navigate to another screen
My Code
class HomeDetails extends PureComponent {
constructor(props) {
super(props);
this.cellRefs = {};
this.flatListRef = React.createRef();
}
getItem = (data, index) => {
if (index in data) {
return {
key: `${data[index].id} - ${index}`,
id: data[index].id,
accountId: data[index].accountId,
displayName: data[index].displayName,
fullName: data[index].fullName,
};
}
};
keyExtractor(item, index) {
return `${item.id} - ${index}`;
}
getItemCount = data => {
return data.length;
};
_renderItem =({item,index}) => {
console.log(
'Rerendring',
item.accountId,
moment().format('MM/DD/YY hh:mm:ss a'),
);
return (
<View key={index} style={{height: 50, flexDirection: 'row'}}>
<Text>{`${item.accountId} ${moment().format(
'MM/DD/YY hh:mm:ss a',
)}`}</Text>
</View>
);
}
render(){
return (
<VirtualizedList
onScroll={this.onScrollHandler}
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
scrollEventThrottle={16}
ref={this.flatListRef}
horizontal={false}
decelerationRate="normal"
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
data={this.props.responseRecord}
pagingEnabled={true}
scrollToOverflowEnabled={false}
renderItem={this._renderItem}
keyExtractor={this.keyExtractor}
getItemCount={this.getItemCount}
getItem={this.getItem}
windowSize={21}
progressViewOffset={20}
initialNumToRender={15}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={100}
onEndReached={val => {
return this.props.getExtraData(2, 1);
}}
onEndReachedThreshold={0.1}
refreshing={this.props.postLoading}
extraData={this.props.refreshData}
disableIntervalMomentum={false}
removeClippedSubviews={true}
onRefresh={() => {
return this.props.getExtraData(1, 1);
}}
ItemSeparator={this.ItemSeparator}
ListFooterComponent={this.renderFooter}
/>
)
}
}
const mapStateToProps = ({post, auth, common}) => {
const {
responseRecord,
postLoading,
refreshData,
} = post;
return {
responseRecord,
postLoading,
refreshData,
};
};
const mapDispatchToProps = {
getExtraData,
};
export default connect(mapStateToProps, mapDispatchToProps)(HomeDetails);
..........................................................................
1.For initial 30 record rendering its re-render more that 2 times
2.when add more records its re-render more than 4 to 6 times
3.tried with purecomponent but no luck
code deployed in snack
https://snack.expo.dev/#pandianvpsm/cd5737
Internal, React's PureComponent implements the shouldComponentUpdate method and compares previous props values and new props or state values to avoid unnecessary re-renders.
This works well for primitive type values like numbers, strings, and booleans.
For referential types values (objects and arrays), this comparison is not always accurate. This is the behavior you have. this.props.responseRecord is an array of objects (referential types).
We can solve this problem by implementing our own componentShouldUpdate method as below:
/** Trigger component rerender only new elements added */
shouldComponentUpdate(nextProps, nextState) {
return this.props.responseRecord.length !== nextProps.responseRecord.length
}
Full code below
class HomeDetails extends React.Component {
constructor(props) {
super(props);
this.cellRefs = {};
this.flatListRef = React.createRef();
}
/** Trigger component rerender only new elements added */
shouldComponentUpdate(nextProps, nextState) {
return this.props.responseRecord.length !== nextProps.responseRecord;
}
getItem = (data, index) => {
if (index in data) {
return {
key: `${data[index].id} - ${index}`,
id: data[index].id,
accountId: data[index].accountId,
displayName: data[index].displayName,
fullName: data[index].fullName,
};
}
};
keyExtractor(item, index) {
return `${item.id} - ${index}`;
}
getItemCount = (data) => {
return data.length;
};
_renderItem = ({ item, index }) => {
console.log(
"Rerendring",
item.accountId,
moment().format("MM/DD/YY hh:mm:ss a")
);
return (
<View key={index} style={{ height: 50, flexDirection: "row" }}>
<Text>{`${item.accountId} ${moment().format(
"MM/DD/YY hh:mm:ss a"
)}`}</Text>
</View>
);
};
render() {
return (
<VirtualizedList
onScroll={this.onScrollHandler}
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
scrollEventThrottle={16}
ref={this.flatListRef}
horizontal={false}
decelerationRate="normal"
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
data={this.props.responseRecord}
pagingEnabled={true}
scrollToOverflowEnabled={false}
renderItem={this._renderItem}
keyExtractor={this.keyExtractor}
getItemCount={this.getItemCount}
getItem={this.getItem}
windowSize={21}
progressViewOffset={20}
initialNumToRender={15}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={100}
onEndReached={(val) => {
return this.props.getExtraData(2, 1);
}}
onEndReachedThreshold={0.1}
refreshing={this.props.postLoading}
extraData={this.props.refreshData}
disableIntervalMomentum={false}
removeClippedSubviews={true}
onRefresh={() => {
return this.props.getExtraData(1, 1);
}}
ItemSeparator={this.ItemSeparator}
ListFooterComponent={this.renderFooter}
/>
);
}
}
const mapStateToProps = ({ post, auth, common }) => {
const { responseRecord, postLoading, refreshData } = post;
return {
responseRecord,
postLoading,
refreshData,
};
};
const mapDispatchToProps = {
getExtraData,
};
export default connect(mapStateToProps, mapDispatchToProps)(HomeDetails);

FlatList renderItem not being highlighted when clicked the first time

Basically, I'm trying to setup a Flatlist in which multiple values can be selected.
My problem is with the styling of elements, when clicked the first time they don't get highlighted but when clicked the 2nd time they get highlighted.
FlatList Code
renderRow = ({item}) => (
<RowItem data={item} />
)
data = [
{
value: 'element1'
},
{
value: 'element2'
}
]
render(){
return (
<FlatList
data={this.data}
renderItem={this.renderRow}
keyExtractor={(item, index) => item + index}/>
)
}
RowItem Code
export default class RowItem extends React.Component {
state = {
isElementActive: false,
}
highlightElement = () => {
this.setState(prevState => ({
isElementActive: !prevState.isElementActive
}))
}
render() {
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={this.highlightElement}
style={[styles.container, this.state.isElementActive ? styles.activeContainer : styles.inactiveContainer]}>
<Text>{this.props.data.value}</Text>
</TouchableOpacity>
)
}
}
const styles = Stylesheet.create({
container: {
height: 100,
width: 300,
backgroundColor: 'red',
},
activeContainer: {
opacity: 0.7,
},
inactiveContainer: {
opacity: 1,
}
});
When clicking on the element once, the value of the isElementActive returns true (when I console.log it) but the styling "activeContainer" does not apply. However, when I click it again, the styling applies even though the value of isElementActive then becomes false.
By default the value starts off as false, and they are not highlighted (i.e. have opacity of 1) and for this reason I'm kind of confused when clicked the first time, the value of isElementActive changes but the styling does not apply.
I was able to make it work with setOpacityTo after the setState.
Working example: https://snack.expo.io/SJNSKQPIB
import React from 'react';
import {TouchableOpacity, FlatList, StyleSheet, Text} from 'react-native';
type State = {
active: boolean;
};
type Props = {
value: string;
};
class RowItem extends React.Component<Props, State> {
state = {
active: null,
};
ref = null;
highlightElement = () => {
this.setState(
prevState => ({
active: !prevState.active,
}),
() => {
this.ref.setOpacityTo(this.state.active ? 0.7 : 1);
},
);
};
render() {
return (
<TouchableOpacity
ref={ref => (this.ref = ref)}
onPress={this.highlightElement}
style={[styles.container]}>
<Text>{this.props.value}</Text>
</TouchableOpacity>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
height: 100,
backgroundColor: 'red',
},
});
export default class App extends React.Component {
data = [
{
value: 'element1',
},
{
value: 'element2',
},
];
render() {
return (
<FlatList
keyExtractor={(_, index) => index.toString()}
data={this.data}
renderItem={({item}) => <RowItem value={item.value} />}
/>
);
}
}

Delete an element by key from Array

I used library react-sortable-hoc for drag and drop element, but the library documentation does not have any actions for delete items. I want to delete, drag and drop item when click on close button. Which method is right for removing elements by key from object?
React
const SortableItem = SortableElement(({ value }: { value: string }, onRemove: any) =>
<div className="dragItems" style={{ background: 'gray' }}>
<img src={value} alt="" />
<button className="dragCloseBtn" onClick={() => onRemove(any)} />
</div>
);
const SortableList = SortableContainer(({ items }: { items: string[] }) => {
return (
<div className="dragAndDrop">
{items.map((value, index) => (
<SortableItem key={'item-${index}'} index={index} value={value} />
))}
</div>
);
});
constructor(props: any) {
super(props);
this.state = {
items: [
{
"id": 0,
"link": "https://via.placeholder.com/150"
},
{
"id": 1,
"link": "https://via.placeholder.com/150"
}
],
};
}
public onSortEnd = ({ oldIndex, newIndex }: { oldIndex: number, newIndex: number }) => {
this.setState({
items: arrayMove(this.state.items, oldIndex, newIndex),
});
};
public onRemove(e: { target: { value: any; }; }) {
const array = [...this.state.items];
const index = array.indexOf(e.target.value)
if (index !== -1) {
array.splice(index, 1);
this.setState({items: array});
}
}
<SortableList items={this.state.items}
onSortEnd={this.onSortEnd}
lockAxis="xy"
axis="xy" />
UPDATED:
Hi there, I figured out what went wrong and made a successful remove event on your application.
Everything is illustrated with comments at this codesandbox.
=========
I modified this one, it should do the required using Array's filter method.
public onRemove(e: { target: { value: any; }; }) {
let array = [...this.state.items];
const intId = parseInt(e.target.value, 10);
array = array.filter(item => item.id !== intId);
this.setState({items: array});
}
So there were few problems in your code! You seemed to be confuse how react works with passing down props. You have to pass down the method required for remove. And you should bind it inside the class that you will be calling it.
onRemove should be bound to current context
onRemove should be passed down across the component tree
Check my //[NOTE]====> comments for additional explanation
Working code sandbox is here
import * as React from "react";
import * as ReactDOM from "react-dom";
import {
arrayMove,
SortableContainer,
SortableElement
} from "react-sortable-hoc";
//[NOTE]====>value contains the relevent object containing the callback. Onclick call it with the relevant id
const SortableItem = SortableElement(
({ value }: { value: any }, onRemove: any) => (
<div className="dragItems" style={{ background: "gray", margin: "20px" }}>
<img src={value.link} alt="" />
<button className="dragCloseBtn" onClick={() => value.onRemove(value.id)}>
{" "}
X{" "}
</button>
</div>
)
);
const SortableList = SortableContainer(({ items }: { items: string[] }) => {
return (
<div className="dragAndDrop">
{items.map((value, index) => (
<SortableItem key={`item-${index}`} index={index} value={value} />
))}
</div>
);
});
class SortableComponent extends React.Component<{}, { items: string[] }> {
constructor(props: {}) {
super(props);
//[NOTE]====>Send the callback on each element bound to the current context
//This is like telling the function from where exactly the function will be called
this.state = {
items: [
{
id: 0,
link: "https://via.placeholder.com/150",
onRemove: this.onRemove.bind(this)
},
{
id: 1,
link: "https://via.placeholder.com/150",
onRemove: this.onRemove.bind(this)
}
]
};
}
public render() {
return <SortableList items={this.state.items} onSortEnd={this.onSortEnd} />;
}
public onSortEnd = ({
oldIndex,
newIndex
}: {
oldIndex: number;
newIndex: number;
}) => {
this.setState({
items: arrayMove(this.state.items, oldIndex, newIndex)
});
};
//[NOTE]====>Use the id to filter out and set the new set of items
public onRemove(id) {
console.log(id);
let array = [...this.state.items];
const intId = parseInt(id, 10);
array = array.filter((item: any) => item.id !== intId);
this.setState({ items: array });
}
}
ReactDOM.render(<SortableComponent />, document.getElementById("root"));

Issue with rendering component

I have component where i want to search for a list of users. The issue that i am having is that my this.state.employees is not called before render. I am using the componentDidMount to get my list of employees, see below:
componentDidMount() {
if (!!this.props.employees && this.props.employees.length == 0) {
this.props.listEmployees();
}
}
So at the moment by component is returning no employees. Does anyone know the best way of doing this.
class ShareForUsers extends Component {
constructor(props){
super(props);
this.state = {
props,
menuOpen: false,
value: "",
values: []
};
}
componentDidMount() {
if (!!this.props.employees && this.props.employees.length == 0) {
this.props.listEmployees();
}
}
componentWillReceiveProps(nextProps) {
this.setState({ ...nextProps })
}
render() {
if (!!this.state.employees) return <p>no employees</p>;
console.log(this.state.employees)
return (
<div>
{persons}
{this.renderUsers}
<TextField
fullWidth
value={this.state.value}
InputProps={{
startAdornment: this.state.values
.concat()
.sort(({ label: aLabel }, { label: bLabel }) => {
if (aLabel < bLabel) return -1;
else if (aLabel > bLabel) return 1;
return 0;
})
.map(chip => (
<InputAdornment
component={Chip}
label={chip.label}
onDelete={() => {
const value = chip;
this.setState(({ values: prevValues }) => {
const values = prevValues;
const idx = values.indexOf(value);
if (idx === -1) {
values.push(value);
} else {
values.splice(idx, 1);
}
return {
values
};
});
}}
/>
))
}}
onChange={evt => {
const value = evt.target.value;
this.setState({
value,
menuOpen: value.length > 0
});
}}
onFocus={() =>
this.setState(({ value }) => ({
menuOpen: value.length > 0
}))
}
onBlur={() => this.setState({})}
/>
<div>
{this.state.menuOpen ? (
<Paper
style={{
position: "absolute",
zIndex: 100,
width: "100%"
}}
>
{this.state.employees
.filter(
employee =>
employee.user.email.toLowerCase().indexOf(this.state.value) > -1
)
.map(employee => (
<MenuItem
key={employee.value}
onClick={() => {
this.setState(({ values: prevValues }) => {
const values = prevValues.concat();
const idx = values.indexOf(employee);
if (idx === -1) {
values.push(employee);
} else {
values.splice(idx, 1);
}
return {
values,
value: "",
menuOpen: false
};
});
}}
>
{employee.label}
</MenuItem>
))}
</Paper>
) : (
""
)}
</div>
</div>
)
}
}
const shareForUsers = withStyles(styles)(ShareForUsers)
export default connect(
state => state.user,
dispatch => bindActionCreators(actionCreators, dispatch)
)(shareForUsers);
You don't need to keep the employees in your state. You can still use the this.props.employees in your render method.
Be especially wary of the componentWillReceiveProps lifecycle method, it has been marked for removal in future release of React. It was enabling many anti-patterns in eyes of React community, so they provided a replacement lifecycle getDerivedStateFromProps. I highly recommend reading the official blog post on this lifecycle method.
Some notes on your code:
constructor(props){
super(props);
this.state = {
props,
menuOpen: false,
value: "",
values: []
};
}
Note 1:
You are putting the "props" in your state.
This is absolutely not necessary. Wherever you need to access state, you will also have access to props. So, no benefit of putting it inside your state and potential downsides of unwanted/unexpected state changes because an unrelated prop is changing.
Note 2:
Bad initial state. If you really need (for some reason), to have employees in your state. Then you must initialize them properly in the constructor.
Note 3:
You're missing a feedback for the user that you are "loading/fetching" the data. As of now, when you render, first a user will see: No Employees Found and then when data has been fetched, they will magically see the screen.
If your flow was "synchronous" in nature, it might have been ok, but you are having an async behaviour, so you must also prepare for following states of your component:
Loading|Error|Loaded(No Data)|Loaded(data found)

Remove one item from flatlist

I'm using FlatList to show a list of data.
I was trying dozens of example how to remove one row from data, but couldn't find the right solution.
Right now I'm removing all data from state, but I want to remove just one item.
Here is my HomeScreen which displays list of data:
class HomeScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
data: data.products
};
}
static navigationOptions = {
title: "Products"
};
keyExtractor = (item, index) => item.id;
openDetails = data => {
this.props.navigation.navigate("Details", {
data
});
};
deleteItem = data => {
this.setState({ data: ''})
}
renderProduct = ({ item, index }) => {
return (
<Item
itemTitle={item.title}
openDetails={() => this.openDetails(item)}
itemUrl={item.imageUrl}
data={this.state.data}
deleteItem={() => this.deleteItem(item)}
/>
);
};
render() {
return (
<FlatList
data={this.state.data}
renderItem={this.renderProduct}
keyExtractor={this.keyExtractor}
/>
);
}
}
export default HomeScreen;
Here is my Item component which is showing one item and receiving deleteRow function as prop:
const Item = props => {
return (
<View>
<TouchableOpacity onPress={props.deleteItem}>
<Image
source={{ uri: props.itemUrl }}
style={{ width: "100%", height: 220 }}
/>
<Text style={styles.productTitle}>{props.itemTitle}</Text>
</TouchableOpacity>
</View>
);
};
export default Item;
Use below deleteItem function.
deleteItem = data => {
let allItems = [...this.state.data];
let filteredItems = allItems.filter(item => item.id != data.id);
this.setState({ data: filteredItems })
}
This should filter out the deleted item.

Resources