I have parent component and child(listView) component. My target is to send backend dispatched data from parent to child. I achieve that via button click. Problem is that child renders after second parent button click. Maybe my mistake is somewhere in componentWillReceiveProps?
Parent:
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: '',
noRepos: false
};
}
componentWillReceiveProps(newProps) {
this.setState({
dataSource: newProps.data
});
this.validateData(newProps)
}
submitUsername() {
if(this.validateInput()){
this.props.dispatch({type: 'DUMMY', state: this.state.username});
} else {
this.setState({emptyInput: true});
}
}
render() {
return (
...
<View>
<ListViewComponent dataRecieved={this.state.dataSource} />
</View>
...
);
}
}
export default connect(state => {
return {
data: state.data,
};
})(Parent);
Child:
export default class ListViewComponent extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
}),
};
}
propTypes: {
dataRecieved: PropTypes.func.string
};
componentWillReceiveProps(newProps) {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(this.props.dataRecieved),
});
}
renderRow(rowData) {
return (
<View>
<Text>{rowData.name}</Text>
</View>
);
}
render() {
return (
<View>
<ListView dataSource={this.state.dataSource}
enableEmptySections={true}
renderRow={this.renderRow} />
</View>
);
}
}
Alright, a general advice is to always keep a single source of truth:
do not copy the data that you already have in props to your internal component state. Use the data in props.
try to create your components as stateless as possible (see above...use props, or have the component listen to a 'store'. See Redux or AltJs).
Specifically to try to solve your issue:
In parent replace:
<ListViewComponent dataRecieved={this.state.dataSource} />
with
<ListViewComponent dataRecieved={this.props.data} />
And in ListViewComponent, don't do:
this.setState({
dataSource: this.state.dataSource.cloneWithRows(this.props.dataRecieved),
});
but do:
render() {
var ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
})
, dataSource = ds.cloneWithRows(this.props.dataRecieved);
return (
<View>
<ListView dataSource={dataSource}
enableEmptySections={true}
renderRow={this.renderRow} />
</View>
);
}
The above is untested code, but should serve as a guide to what approach to follow.
The way you originally implemented ListViewComponent is fine, and you SHOULD be using componentWillReceiveProps when refreshing your ListView. Every best practice out there says to do this (just Google react native redux listview). You just have a slight error in your implementation that I mentioned in a comment above. Also, you should not be recreating the ListView.DataSource inside the render function, that is not good for performance and defeats the purpose of rowHasChanged in ListView.DataSource.
The error that I'm talking about is here:
componentWillReceiveProps(newProps) {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(this.props.dataRecieved),
});
}
it should be:
// ListViewComponent
componentWillReceiveProps(newProps) {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(newProps.dataRecieved),
});
}
Also, your Parent should not be holding a dataSource in its state, just pass data straight down to ListViewComponent because Redux is passing it as a prop already:
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
// ...
};
}
componentWillReceiveProps(newProps) {
// ...
}
submitUsername() {
if (this.validateInput()) {
this.props.dispatch({ type: 'DUMMY', ... });
} else {
// ...
}
}
render() {
return (
...
<View>
{/* this.props.data automatically comes from the `connect` wrapper below */}
<ListViewComponent dataRecieved={this.props.data} />
</View>
...
);
}
}
export default connect(state => {
return {
data: state.data,
};
})(Parent);
You should also take a look at this gist. It is an example of how to use Redux alongside a ListView. His example uses cloneWithRowsAndSections, but because you don't have sections, you just adapt with cloneWithRows instead. This gist was written by a pretty active core React Native developer.
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 know there's many similar threads but since I have a hard time understanding the answers I figured I might try with my own code and see if I can make any sense of the answers.
I just set this up really simple to test it out. I have an Index file that is opened when I start the app. In the index I have testValue in this.state:
Update:
In SignIn:
constructor(props){
super(props);
this.state={
credentials: {
username: "",
password:"",
}
}
this.navigate = this.props.navigation.navigate;
{...}
this.navigate("main", {
credentials: this.state.username,
});
In main:
constructor(props) {
super(props);
this.params = this.props.navigation.state.params;
this.navigate = this.props.navigation.navigate;
{...}
render() {
console.log(this.params.username);
This would actually log the testValue. All you have to do is to pass the testValue state as a prop in TestIndex component. Like this:
Index.jsx
export default class Index {
constructor(props) {
super(props);
this.state = {
testValue: 'hello'
}
}
render() {
return <TestIndex testValue={this.state.testValue} />
}
}
TestIndex.jsx
export default class TestIndex extends React.Component {
index() {
this.props.navigation.navigate("index")
}
handleClickMain = () => {
this.props.navigation.navigate("index");
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity style={styles.btn} onPress={() => {
console.log(this.props.testValue) //here's where I want it to log testValue from the index file
this.handleClickIndex()
}
}>
</TouchableOpacity>
</View>
);
}
}
I am trying to populate a Listview in my react-native app using data from Firebase and am currently getting this error:
"Objects are not valid as a React child (found object with keys {title}). If you meant to render a collection of children, use an array instead or wrap the object using createFragment(object) from the React addons. Check the render method of 'Text.'"
This is occurring on my call to clonewithrows. Currently my input to this function (after parsing response to Firebase) is: [ { title: 'New York' }, { title: 'Boston' } ]
Here is my relevant code; please let me know if you see anything that may be causing the problem.
const huntsRef = new Firebase(`${ config.FIREBASE_ROOT }/hunts`)
class Home extends Component {
constructor(props) {
super(props);
var conDataSource = new ListView.DataSource(
{rowHasChanged: (r1, r2) => r1.guid != r2.guid});
this.state = {
dataSource: conDataSource
};
}
listenForItems(huntsRef) {
huntsRef.on('value', (snap) => {
var hunts = [];
snap.forEach((child) => {
hunts.push({
title: child.val().title
});
});
this.setState({
dataSource: this.state.dataSource.cloneWithRows(hunts)
});
console.log('datasource' + this.state.dataSource);
});
}
componentDidMount() {
this.listenForItems(huntsRef);
}
renderRow(rowData, sectionID, rowID) {
console.log("ROWDATA" + rowData);
}
render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => <Text>{rowData}</Text>}
/>
);
}
}
module.exports = Home;
As the message says, rowData is javascript object and cannot be rendered. You can stringify your rowData. Ex:
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => <Text>{JSON.stringify(rowData)}</Text>}
/>
I have a project in react-native (0.23) with Meteor 1.3 as back-end and want to display a list of contact items. When the user clicks a contact item, I would like to display a checkmark in front of the item.
For the connection to Meteor DDP I use the awesome library inProgress-team/react-native-meteor.
import Meteor, { connectMeteor, MeteorListView, MeteorComplexListView } from 'react-native-meteor';
class ContactsPicker extends React.Component {
constructor(props) {
super(props);
this.state = {
subscriptionIsReady: false
};
}
componentWillMount() {
const handle = db.subscribe("contacts");
this.setState({
subscriptionIsReady: handle.ready()
});
}
render() {
const {subscriptionIsReady} = this.state;
return (
<View style={gs.standardView}>
{!subscriptionIsReady && <Text>Not ready</Text>}
<MeteorComplexListView
elements={()=>{return Meteor.collection('contacts').find()}}
renderRow={this.renderItem.bind(this)}
/>
</View>
);
}
The first problem is, that subscriptionIsReady does not trigger a re-render once it returns true. How can I wait for the subscription to be ready and update the template then?
My second problem is that a click on a list item updates the state and should display a checkmark, but the MeteorListView only re-renders if the dataSource has changed. Is there any way to force a re-render without changing/ updating the dataSource?
EDIT 1 (SOLUTION 1):
Thank you #user1078150 for providing a working solution. Here the complete solution:
'use strict';
import Meteor, { connectMeteor, MeteorListView, MeteorComplexListView } from 'react-native-meteor';
class ContactsPicker extends React.Component {
getMeteorData() {
const handle = Meteor.subscribe("contacts");
return {
subscriptionIsReady: handle.ready()
};
}
constructor(props) {
super(props);
this.state = {
subscriptionIsReady: false
};
}
componentWillMount() {
// NO SUBSCRIPTION HERE
}
renderItem(contact) {
return (
<View key={contact._id}>
<TouchableHighlight onPress={() => this.toggleSelection(contact._id)}>
<View>
{this.state.selectedContacts.indexOf(contact._id) > -1 && <Icon />}
<Text>{contact.displayName}</Text>
</View>
</TouchableHighlight>
</View>
)
}
render() {
const {subscriptionIsReady} = this.data;
return (
<View>
{!subscriptionIsReady && <Text>Not ready</Text>}
<MeteorComplexListView
elements={()=>{return Meteor.collection('contacts').find()}}
renderRow={this.renderItem.bind(this)}
/>
</View>
);
}
}
connectMeteor(ContactsPicker);
export default ContactsPicker;
You have to do something like this :
getMeteorData() {
const handle = Meteor.subscribe("contacts");
return {
ready: handle.ready()
};
}
render() {
const { ready } = this.data;
}