I'm trying to implement a list of users with searching by username possibility.
I've faced with the issue with re-rendering SearchListOfUsers after I change a prop usernameFilter in a parent Component SearchPeopleScreen and pass it to a child SearchListOfUsers.
I know that a component should re-render itself when its state is changed but in my case even state of child component doesn't change. How to update my child comp. SearchListOfUsers after I pass a prop usernameFilter?
Here is my parent comp. SearchPeopleScreen:
export default class SearchPeopleScreen extends Component {
constructor(props) {
super(props);
this.state = {
...
usernameFilter: ''
}
}
render() {
return(
<Container>
<Header style = {searchPeopleScreenStyle.header} searchBar>
<Title style = {searchPeopleScreenStyle.title}>
Search
</Title>
<Right/>
<Item style = {searchPeopleScreenStyle.searchFieldWrapper}>
<IconSimpleLine name = 'magnifier' color = {placeholder} size = {20} style = {{padding: 10}}/>
<TextInput
underlineColorAndroid = 'transparent'
onChangeText = {(text) => {
this.setState({usernameFilter: text});
}}
placeholder = 'Type username'
style = {searchPeopleScreenStyle.searchInput}
maxLength = {15}
/>
</Item>
</Header>
<Content>
<ScrollView contentContainerStyle = {searchPeopleScreenStyle.container}>
...
{/* Search screen's body */}
<SearchListOfUsers searchOption = {this.state.searchOption}
usernameFilter = {this.state.usernameFilter}/>
</ScrollView>
</Content>
</Container>
)
}
}
And here is my child comp. SearchListOfUsers:
export default class SearchListOfUsers extends Component {
constructor(props) {
super(props);
this.state = {
usersDataArray: [],
usernameFilter: this.props.usernameFilter
};
this.arrayHolder = [];
console.warn('1 - ' + this.state.usernameFilter)
}
componentDidMount() {
this.getAllUsersData()
console.warn(this.state.usernameFilter)
if(this.state.usernameFilter) {
this.filterUsers();
}
}
getAllUsersData = () => {
return new Promise((resolve, reject) => {
// getting users data and creating an array
...
allUsersDataArray.push({...});
this.setState({
usersDataArray: allUsersDataArray
});
resolve();
})
}
filterUsers = () => {
const newUsersDataArray = this.arrayHolder.filter((user) => {
const usernameInTheList = user.userUsername.toUpperCase();
const inputtedUsername = this.state.usernameFilter.toUpperCase();
return usernameInTheList.includes(inputtedUsername);
});
this.setState({
usersDataArray: newUsersDataArray
})
}
render() {
return(
<Content contentContainerStyle = {searchPeopleScreenStyle.listOfUsersWrapperGlobal}>
<FlatList
data = {this.state.usersDataArray}
keyExtractor = {(item) => (item.userId)}
renderItem = {({item}) => (
<UserListItem
country = {item.userCountry}
username = {item.userUsername}
...
/>
)}
/>
</Content>
)
}
}
}
If you need to filter your data based on selections from a parent component, you should also be filtering your collection there as well. Once you have filtered your collection, that should be passed to the child component.
The child component in this case should be purely presentational and static. It shouldn't care about filtering data or updating it's component state etc, it just wants to render out whatever props it is passed. searchOption, usernameFilter, dataCollection
You filter users in componentDidMount lifecyle method which means it will run only once on mounting process of child component.
You can filter in render method like
filterUsers = () => {
if(!this.props.usernameFilter.length) return this.state.usersDataArray
return this.state.usersDataArray.map((user) => {
const usernameInTheList = user.userUsername.toUpperCase();
const inputtedUsername = this.props.usernameFilter.toUpperCase();
return usernameInTheList.includes(inputtedUsername);
});
}
render() {
return(
<Content contentContainerStyle = {searchPeopleScreenStyle.listOfUsersWrapperGlobal}>
<FlatList
data = {this.filterUsers()}
keyExtractor = {(item) => (item.userId)}
renderItem = {({item}) => (
<UserListItem
country = {item.userCountry}
username = {item.userUsername}
...
/>
)}
/>
</Content>
)
}
}
Related
I'm having issues getting one of my fields to pre-populate with info and be editable. I've tried moving around the code where it sets the field with the data and either it's blank and editable or shows the pre-populated data but the UI prevents me from editing it.
The issue I'm having is with the bar field. Putting it in the constructor pre-populates the field with info but the UI is preventing me from editing it. Why? Where should this field be set or how can I fix it? Do I need to call where this object gets populated before navigating to this page, so it gets populated during constructor initialization or..?
Here's the class component snippet:
export class FooBarBazComponent extends Component{
constructor(props){
super(props);
this.state = {
foo: "",
bar: ""
};
const fooDetails = this.props.navigation.state.params.fooDetails;
this.state.foo = fooDetails.foo;
}
render(){
const disabled = this.state.foo.length !== 5 || this.state.bar.length < 5;
//I didn't put this code in the constructor because this object is undefined in the constructor
if(this.props.objResponse) {
this.state.bar = this.props.objResponse.bar;
}
return(
<View style={Styles.inputRow}>
<View style={Styles.inlineInput}>
<FormLabel labelStyle={Styles.label}>FOO</FormLabel>
<TextInputMask
onChangeText={foo => this.setState({ foo })}
value={this.state.foo}
/>
</View>
<View style={Styles.inlineInput}>
<FormLabel labelStyle={Styles.label}>BAR</FormLabel>
<TextInputMask
onChangeText={bar => this.setState({ bar })}
value={this.state.bar}
/>
</View>
</View>
);
}
}
I think best approach here is to make it a functional component. You can use React Hooks for stateful logic and keep your code way more cleaner.
I'd destructure the props and set them directly in the initial state. Then I'd add some conditional logic for rendering the input fields only when the initial state is set. Done!
When you want to change the state, just use the set function!
import React, { useState } from 'react';
export default function FooBarBazComponent({ navigation, objResponse }) {
// Initiate the state directly with the props
const [foo, setFoo] = useState(navigation.state.params.fooDetails);
const [bar, setBar] = useState(objResponse.bar);
const disabled = foo.length !== 5 || bar.length < 5;
return (
<View style={styles.inputRow} >
{/* Only render next block if foo is not null */}
{foo && (
<View style={styles.inlineInput}>
<FormLabel labelStyle={Styles.label}>FOO</FormLabel>
<TextInputMask
onChangeText={foo => setFoo(foo)}
value={foo}
/>
</View>
)}
{/* Only render next block if objResponse.bar is not null */}
{objResponse.bar && (
<View style={styles.inlineInput}>
<FormLabel labelStyle={Styles.label}>BAR</FormLabel>
<TextInputMask
onChangeText={bar => setBar(bar)}
value={bar}
/>
</View>
)}
</View>
);
}
I see few problems in the code.
state = {
foo: "",
bar: ""
};
The above need to be changed like this
this.state = {
foo: "",
bar: ""
};
Or else put your code outside the constructor.
Then from this,
const fooDetails = this.props.navigation.state.params.fooDetails;
this.state.foo = fooDetails.foo;
to
this.state = {
foo: props.navigation.state.params.fooDetails,
bar: ""
};
Because you should not mutate the state directly. and you have your props in the constructor already.
Then from this,
if(this.props.objResponse) {
this.state.bar = this.props.objResponse.bar;
}
}
move this to componentDidMount or where you do your API call. You should not mutate state and you shouldn't update the state in render method which will create a loop.
And also update the state using this.setState method.
If you still face any problem then you need to check your TextInputMask Component after doing the above.
You should never assign props to the state directly. It is an absolute no-no. Also if possible try moving to react hooks, it is much simpler and cleaner than this approach.
export class FooBarBazComponent extends Component {
constructor(props)
{
state = {
foo: "",
bar: ""
};
const fooDetails = this.props.navigation.state.params.fooDetails;
this.state.foo = fooDetails.foo;
}
static getDerivedStateFromProps(props, state) {
if (props.objResponse && props.objResponse.bar !== state.bar) {
return {
...state,
bar: props.objResponse.bar
}
}
return null;
}
render() {
const disabled =
this.state.foo.length !== 5 || this.state.bar.length < 5;
return (
<View style={styles.inputRow}>
<View style={styles.inlineInput}>
<FormLabel labelStyle={Styles.label}>FOO</FormLabel>
<TextInputMask
onChangeText={foo => this.setState({ foo })}
value={this.state.foo}
/>
</View>
<View style={styles.inlineInput}>
<FormLabel labelStyle={Styles.label}>BAR</FormLabel>
<TextInputMask
onChangeText={bar => this.setState({ bar })}
value={this.state.bar}
/>
</View>
</View>
);
}
}
First we will save the current props as prevProps in your component state. Then we will use a static component class method getDerivedStateFromProps to update your state based on your props reactively. It is called just like componentDidUpdate and the returned value will be your new component state.
Based on your code, I assume that your this.props.objResponse.bar is coming from an API response as seen in your comment
I didn't put this code in the constructor because this object is undefined in the constructor
If possible, it is better to use functional component with React hooks instead of using class in the future.
Here are some clean sample codes for your reference.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class FooBarBazComponent extends React.Component {
constructor(props) {
super(props);
const { foo, bar } = props;
this.state = {
// save previous props value into state for comparison later
prevProps: { foo, bar },
foo,
bar,
}
}
static getDerivedStateFromProps(props, state) {
const { prevProps } = state;
// Compare the incoming prop to previous prop
const { foo, bar } = props;
return {
// Store the previous props in state
prevProps: { foo, bar },
foo: prevProps.foo !== foo ? foo : state.foo,
bar: prevProps.bar !== bar ? bar : state.bar,
};
}
handleOnChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
}
renderInput = (name) => (
<div>
<label>
{`${name}:`}
<input onChange={this.handleOnChange} type="text" name={name} value={this.state[name]} />
</label>
</div>
)
render() {
const { prevProps, ...rest } = this.state;
return (
<section>
{this.renderInput('foo')}
{this.renderInput('bar')}
<div>
<pre>FooBarBazComponent State :</pre>
<pre>
{JSON.stringify(rest, 4, '')}
</pre>
</div>
</section>
);
}
}
class App extends React.Component {
// This will mock an api call
mockAPICall = () => new Promise((res) => setTimeout(() => res('bar'), 1000));
state = { bar: '' }
async componentDidMount() {
const bar = await this.mockAPICall();
this.setState({ bar });
}
render() {
const { bar } = this.state;
return (
<FooBarBazComponent foo="foo" bar={bar} />
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hopefully this gives you a general idea on how to do it.
Working example : https://codesandbox.io/s/react-reactive-state-demo-2j31u?fontsize=14
Try to setState() in componentDidMount() as below
componentDidMount() {
if (this.props.isFromHome) {
this.setState({ isFromHome: true });
} else {
this.setState({ isFromHome: false });
}
}
when you called setState() it will re-render the component.
I have a component where when I click on an icon, I execute a function that modify a state and then i can check the state and modify the icon. In that comonent, I am mapping datas and it renders several items.
But when I click on one icon all the icons of the components change too.
Here is the code for the component
export default class DiscoveryComponent extends Component {
constructor(props) {
super(props)
this.state = {
starSelected: false
};
}
static propTypes = {
discoveries: PropTypes.array.isRequired
};
onPressStar() {
this.setState({ starSelected: !this.state.starSelected })
}
render() {
return (
this.props.discoveries.map((discovery, index) => {
return (
<Card key={index} style={{flex: 0}}>
<CardItem>
<TouchableOpacity style={[styles.star]}>
<Icon style={[styles.iconStar]} name={(this.state.starSelected == true)?'star':'star-outline'} onPress={this.onPressStar.bind(this)}/>
</TouchableOpacity>
</CardItem>
</Card>
)
})
);
}
}
And here is the code for my screen that uses the component
export default class DiscoveryItem extends Component {
constructor(props) {
super(props);
this.state = {
discoveries: [],
loading: true
};
}
componentDidMount() {
firebase.database().ref("discoveries/").on('value', (snapshot) => {
let data = snapshot.val();
let discoveries = Object.values(data);
this.setState({discoveries: discoveries, loading: false});
});
}
render() {
return (
<Container>
<Content>
<DiscoveryComponent discoveries={this.state.discoveries} />
</Content>
</Container>
)
}
}
Your initiation is correct but you are missing INDEX of each item. Inside this.onPressStar() method check if item's index = currentItem. Also don't forget to set item id = index onpress.
I hope this has given you idea how to handle it.
You have to turn your stars into an Array and index them:
change your constructor:
constructor(props) {
super(props)
this.state = {
starSelected: []
};
}
change your onPressStar function to :
onPressStar(index) {
this.setState({ starSelected[index]: !this.state.starSelected })
}
and your icon to
<Icon style={[styles.iconStar]} name={(this.state.starSelected[index] == true)?'star':'star-outline'} onPress={()=>this.onPressStar(index)}/>
Well, the problem is that you have a single 'starSelected' value that all of your rendered items in your map function are listening to. So when it becomes true for one, it becomes true for all.
You should probably maintain selected state in the top level component, and pass down the discovery, whether its selected, and how to toggle being selected as props to a render function for each discovery.
export default class DiscoveryItem extends Component {
constructor(props) {
super(props);
this.state = {
discoveries: [],
selectedDiscoveries: [] // NEW
loading: true
};
}
toggleDiscovery = (discoveryId) => {
this.setState(prevState => {
const {selectedDiscoveries} = prevstate
const discoveryIndex = selectedDiscoveries.findIndex(id => id === discoveryId)
if (discoveryIndex === -1) { //not found
selectedDiscoveries.push(discoveryId) // add id to selected list
} else {
selectedDiscoveries.splice(discoveryIndex, 1) // remove from selected list
}
return {selectedDiscoveries}
}
}
componentDidMount() {
firebase.database().ref("discoveries/").on('value', (snapshot) => {
let data = snapshot.val();
let discoveries = Object.values(data);
this.setState({discoveries: discoveries, loading: false});
});
}
render() {
return (
<Container>
<Content>
{
this.state.discoveries.map(d => {
return <DiscoveryComponent key={d.id} discovery={d} selected={selectedDiscoveries.includes(d.id)} toggleSelected={this.toggleDiscovery} />
//<DiscoveryComponent discoveries={this.state.discoveries} />
</Content>
</Container>
)
}
}
You can then use your DiscoveryComponent to render for each one, and you're now maintaining state at the top level, and passing down the discovery, if it is selected, and the toggle function as props.
Also, I think you may be able to get snapshot.docs() from firebase (I'm not sure as I use firestore) which then makes sure that the document Id is included in the value. If snapshot.val() doesn't include the id, then you should figure out how to include that to make sure that you use the id as both key in the map function as well as for the selectedDiscoveries array.
Hope that helps
It works now, thanks.
I've made a mix between Malik and Rodrigo's answer.
Here is the code of my component now
export default class DiscoveryComponent extends Component {
constructor(props) {
super(props)
this.state = {
tabStarSelected: []
};
}
static propTypes = {
discoveries: PropTypes.array.isRequired
};
onPressStar(index) {
let tab = this.state.tabStarSelected;
if (tabStar.includes(index)) {
tabStar.splice( tabStar.indexOf(index), 1 );
}
else {
tabStar.push(index);
}
this.setState({ tabStarSelected: tab })
}
render() {
return (
this.props.discoveries.map((discovery, index) => {
return (
<Card key={index} style={{flex: 0}}>
<CardItem>
<Left>
<Body>
<Text note>{discovery.category}</Text>
<Text style={[styles.title]}>{discovery.title}</Text>
</Body>
</Left>
<TouchableOpacity style={[styles.star]}>
<Icon style={[styles.iconStar]} name={(this.state.tabStarSelected[index] == index)?'star':'star-outline'} onPress={()=>this.onPressStar(index)}/>
</TouchableOpacity>
</CardItem>
</Card>
)
})
);
}
}
I've been using React native for a month now but it's my first time to use a CheckBox in my application. So, lately I've been struggling to check a specific checkbox inside a Flatlist but now I can.
But upon testing my checkboxs I did notice that once I check a specific a CheckBox(or more than 1 checkbox) it doesn't UNCHECK.
So, my goal is to make a CheckBox that can check(ofcourse) and also uncheck, if ever a user mischeck or mistap a CheckBox.
Here's my code
export default class tables extends Component {
constructor(props){
super(props)
this.state = {
...
check: false
}
}
checkBox_Test = (item, index) => {
let { check } = this.state;
check[index] = !check[index];
this.setState({ check: check })
alert("now the value is " + !this.state.check);
alert("now the value is " + item.tbl_id);
console.log(item.tbl_id)
}
render() {
return(
<View>
....
<Flatlist
....
<CheckBox
value = { this.state.check }
onChange = {() => this.checkBox_Test(item) }
/>
....
/>
<View/>
)
}
}
Method 1: Make check an object
export default class tables extends Component {
constructor(props){
super(props)
this.state = {
...
check: {}
}
}
checkBox_Test = (id) => {
const checkCopy = {...this.state.check}
if (checkCopy[id]) checkCopy[id] = false;
else checkCopy[id] = true;
this.setState({ check: checkCopy });
}
render() {
return(
<View>
....
<Flatlist
....
<CheckBox
value = { this.state.check[item.tbl_id] }
onChange = {() => this.checkBox_Test(item.tbl_id) }
/>
....
/>
<View/>
)
}
}
Method 2: Make a separate item for each FlatList item
class ListItem extends Component {
constructor(props){
super(props)
this.state = {
...
check: false
}
}
checkBox_Test = (id) => {
this.setState((prevState) => ({ check: !prevState.check }));
}
render() {
return(
<View>
<CheckBox
value = { this.state.check }
onChange = { this.checkBox_Test }
/>
</View>
)
}
}
Let me know if it works for you
I'm trying to display/store a list of items in my flatlist, but the problem is when I save an item and load that item in a different screen it is in a kind of repetition(look for the screen shot). And when I try to add different item, this new item will replace the previous item with the same kind of repetition. What I'm targeting is to have a list.
List_ScreenShot
Here is my code
AddModal.js
export default class AddModal extends React.Component {
constructor(props) {
super(props);
this.state = {
modalVisible: props.modalVisible,
id: null,
count: 0,
price: null
};
}
state = {
text: '',
}
save = () => {
const { text } = this.state;
let myArray = {
text, text
}
AsyncStorage.setItem('myArray', JSON.stringify(myArray));
alert(text + 'saved');
}
onChange = (text) => {
this.setState({ text });
}
componentWillReceiveProps(nextProps) {
this.setState({
modalVisible: nextProps.modalVisible,
id: nextProps.id,
price: nextProps.price
})
}
render() {
console.log('inside AppModal', this.state.modalVisible);
return (
<View>
<TextInput style = { styles.input }
keyboardType = "numeric"
onChangeText = { this.onChange }
value = { this.state.text } //Item **
>
</TextInput>
</View>
<View}>
<TouchableOpacity
onPress = {() => { this.props.setModalVisible(false) }}
>
<Text style = { styles.buttonText }>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
onPress = { this.save }>
<Text style = { styles.buttonText }>Send</Text>
</TouchableOpacity>
</View>
)
}
}
Settlment.js
import Details from '../Menus/Details';
const key = '#MyApp:key';
export default class Settlement extends React.Component {
state = {
text: '',
storedValue: '',
myArray: ''
}
componentWillMount() {
//this.onLoad();
AsyncStorage.getItem('myArray')
.then(text => this.setState({ text }));
}
showData = async() => {
let myArray = await AsyncStorage.getItem('myArray');
let d = JSON.parse(myArray);
this.setState({ myArray : myArray });
}
render() {
const { myArray, text } = this.state;
return (
<View>
<TouchableOpacity onPress = {this.showData}>
<Text>Load Data</Text>
</TouchableOpacity>
<FlatList data = { this.state.myArray }
renderItem = {({ item }) =>
<Text>{myArray}</Text>
}
keyExtractor={(item, index) => index.toString()}
>
</FlatList>
</View>
);
}
}
What I see here:
const { text } = this.state;
let myArray = {
text, text
}
AsyncStorage.setItem('myArray', JSON.stringify(myArray));
alert(text + 'saved');
is an object called myArray, and nothing is being added to it. It's being defined and then assigned a value.
Maybe you could declare your array elsewhere like in the constructor (as an array, not an object, using myArray = []) and then use myArray.push(text) or if you want an array containing objects you can push object using myArray.push({ yourKeyName: text }). Also, it seems like the object you're storing in AsyncStorage is being replaced and not added to. But I'm not sure why you're getting multiple list items instead of just one.
PS - Where you're declaring state looks a bit off. I usually see it like this:
constructor() {
super();
this.state = {
text: '',
storedValue: '',
myArray: '',
};
}
Trying to get FlatList to display data from Firebase.
Setup is correct and I can see the date in my console, but don't know how to visualise it.
I'd like to see 'recipeOne' 'recipeTwo' 'recipeThree' in the list.
I am sure I am missing something basic.
Here is the code
...
import {DataConfig} from '../data/DataConfig';
const firebaseApp = firebase.initializeApp(DataConfig);
globalTexts = require('../styles/Texts.js');
globalColors = require('../styles/Colors.js');
export default class ListSort extends Component {
constructor(props) {
super(props);
this.dataItem = firebaseApp.database().ref('recipes');
this.state = {
item: []
}
};
componentWillMount() {
this._createItemList();
};
_createItemList = (dataItem) => {
this.dataItem.on('value', (snapshot) => {
var itemList = [];
snapshot.forEach((doc) => {
itemList.push({
key:doc.key,
itemType: doc.toJSON()
});
this.setState({item: itemList});
console.log(this.state.item);
})
})
};
render() {
return (
<View style={styles.container}>
<FlatList
data={this.state.item}
renderItem={({item, index}) => (
<View style={styles.cell}>
<Text style={globalText.btnFlatPrimary}>{item.recipes}</Text>
</View>
)}
/>
</View>
)
}
}
and here is the data. The rules in Firebase are setup as read:true only.
{
"recipes": {
"recipeOne": {...
"recipeTwo": {...
"recipeThree": {...
}
}