React native accessing refs in a custom component - javascript

I have a custom TextInput. When I edit the first TextInput and hit the "Next" in the keyboard, I want it to focus the second TextInput. I have searched before in Stack Overflow and it seems I can do it using ref. However I'm not sure how to do that with custom TextInput.
Here is my basic CustomTextInput code:
let CustomTextInput = React.createClass({
propTypes: {
refName: React.PropTypes.string,
returnKeyType: React.PropTypes.string,
onSubmitEditing: React.PropTypes.func
},
getDefaultProps: function(){
return {
refName: "",
returnKeyType: "default",
onSubmitEditing: () => {}
}
},
render: function(){
return(
<View>
<TextInput
ref={this.props.refName}
returnKeyType={this.props.returnKeyType}
onSubmitEditing={this.props.onSubmitEditing}
/>
</View>
)
}
});
module.exports = CustomTextInput
And here is my Parent class that calls it:
let MyParent = React.createClass({
render: function(){
return(
<View>
<CustomTextInput
refName={'firstNameInput'},
returnKeyType={'next'}
onSubmitEditing={(event) => {
this.refs.lastNameInput.focus();
}}
/>
<CustomTextInput
refName={'lastNameInput'}
/>
</View>
)
}
});
Right now, when I press the Next in the keyboard, after selecting the firstName, I got an exception:
undefined is not an object (evaluating '_this2.refs.lastNameInput.focus')
I'm not sure what I did wrong there.. Any help is appreciated. :)

Let's start from the CustomTextInput component.
export default class CustomTextInput extends Component {
componentDidMount() {
if (this.props.onRef != null) {
this.props.onRef(this)
}
}
onSubmitEditing() {
this.props.onSubmitEditing();
}
focus() {
this.textInput.focus()
}
render() {
return (
<View>
<View style={this.state.isFocused ? styles.onFocusedStyle : styles.onBlurStyle}>
<TextInput
ref={input => this.textInput = input}
onSubmitEditing={this.onSubmitEditing.bind(this)}
/>
</View>
<Text style={styles.errorMessageField}>{this.state.errorStatus && this.props.errorMessage}</Text>
</View>
);
}}
Here i have a sample customTextInput. Important things to note here is the componentDidMount(), focus() method and ref property in the TextInput view in render method.
componentDidMount() method passes the ref of the whole CustomTextInput component to it's parent component. Through that reference we will call the focus method of CustomTextInput component from the parent component.
focus() method here focuses the textInput inside the CustomTextInput component by using the ref of TextInput component inside the CustomTextInput component.
The ref property of TextInput stores the reference of the TextInput. This reference is used by the focus() method.
Now let's see the parent component
export default class ParentComponent extends Component {
constructor(props) {
super(props);
this.focusNextField = this.focusNextField.bind(this);
this.inputs = {};
}
focusNextField(id) {
this.inputs[id].focus();
}
render() {
return (
<ScrollView
contentContainerStyle={{paddingBottom:100}}
keyboardDismissMode={'on-drag'}
scrollToTop={true}>
<View>
<View style={{marginTop: 10}}>
<CustomTextInput
onRef={(ref) => {
this.inputs['projectName'] = ref;
}}
onSubmitEditing={() => {
this.focusNextField('projectDescription');
}}
/>
</View>
<View style={{marginTop: 10}}>
<CustomTextInput
onRef={(ref) => {
this.inputs['projectDescription'] = ref;
}}
onSubmitEditing={() => {
this.focusNextField('subDivision');
}}
/>
</View>
<View style={{marginTop: 10}}>
<CustomTextInput
onRef={(ref) => {
this.inputs['subDivision'] = ref;
}}
onSubmitEditing={() => {
this.focusNextField('plan');
}}
/>
</View>
<View style={{marginTop: 10}}>
<CustomTextInput
onRef={(ref) => {
this.inputs['plan'] = ref;
}}
</View>
</View>
</ScrollView>
);
}}
Here in the parent component we store the ref of each CustomTextInput with onRef property and when the submit button from keyboard is pressed we call the focus method of the next CustomTextInput and the focus method of CustomTextInput focuses the TextInput inside the child component.

Here is a solution is you are using functional component:
Your custom component need to use React.forwardRef
const CustomTextInput = React.forwardRef((props, ref) => {
return (
<TextInput
{...props}
ref={ref}
/>
);
});
export default CustomTextInput;
The parent component that imports your custom component:
import React, { createRef } from 'react';
const ParentComponent = () => {
let customTextInputRef = createRef();
return (
<View>
<CustomTextInput
onSubmitEditing={() => customTextInputRef.current.focus()}}
/>
<CustomTextInput
ref={customTextInputRef}
/>
</View>
);
};

Here is a solution that worked for me — basically you make a reference within your custom component, which you can access from your reference in your parent component:
let CustomTextInput = React.createClass({
propTypes: {
refName: React.PropTypes.string,
returnKeyType: React.PropTypes.string,
onSubmitEditing: React.PropTypes.func
},
getDefaultProps: function(){
return {
refName: "",
returnKeyType: "default",
onSubmitEditing: () => {}
}
},
render: function(){
return(
<View>
<TextInput
ref="input"
returnKeyType={this.props.returnKeyType}
onSubmitEditing={this.props.onSubmitEditing}
/>
</View>
)
}
});
module.exports = CustomTextInput
And in the parent component:
let MyParent = React.createClass({
render: function(){
return(
<View>
<CustomTextInput
refName={'firstNameInput'},
returnKeyType={'next'}
onSubmitEditing={(event) => {
this.lastNameInput.refs.input.focus();
}}
/>
<CustomTextInput
refName={ref => this.lastNameInput = ref}
/>
</View>
)
}
});

let CustomTextInput = React.createClass({
componentDidMount() {
// this is to check if a refName prop is FUNCTION;
if (typeof this.props.rex === "function") {
this.props.refName(this.refs.inp);
}
}
render: function() {
return(
<View>
<TextInput ref={"inp"}/>
</View>
)
}
});
let MyParent = React.createClass({
render: function() {
return (
<View>
<CustomTextInput
refName={ (firstNameInput) => this.firstNameInput = firstNameInput }
/>
</View>
)
}
});

try this:
let AwesomeProject = React.createClass({
onSubmitEditing:function(event){
if (this.myTextInput !== null) {
this.myTextInput.focus();
}
},
render(){
return(
<View>
<CustomTextInput
returnKeyType={'next'}
onSubmitEditing={this.onSubmitEditing}
/>
<CustomTextInput
refName={(ref) => this.myTextInput = ref}
/>
</View>
)
}
});

Related

Both the parent and the child components get rendered simultaneously in react-native

I have a parent component that maps through an array of chapters and renders (an exercise) a child component for every item found and passes an array of exercises to it.
class ExercisesScreen extends Component {
showSelectedItemList = (screenName, text) => {
Navigation.push("ExercisesStack", {
component: {
name: screenName,
options: navOptionsCreator(text)
}
});
};
get chapters() {
return this.props.chapters.map(chapter => (
<TouchableOpacity key={chapter.id}>
<ExercisesList
onPress={() =>
this.showSelectedItemList(chapter.screenName, chapter.name)
}
exercises={chapter.exercises}
/>
</TouchableOpacity>
));
}
render() {
return <View>{this.chapters}</View>;
}
}
const mapStateToProps = state => ({
chapters: chaptersSelector(state)
});
When this child component receives the array of exercises, it maps through it and renders a list of exercises.
class ExercisesList extends Component {
render() {
return this.props.exercises.map(exercise => (
<View key={exercise.id}>
<TouchableOpacity
style={styles.button}
onPress={() =>
this.props.showSelectedItemList(exercise.screenName, exercise.name)
}
>
<Image source={exercise.icon}/>
<View>
<Text>{exercise.name}</Text>
</View>
<Image source={arrow} />
</TouchableOpacity>
<View />
</View>
));
}
}
ExercisesList.propTypes = {
onPress: PropTypes.func,
exercises: PropTypes.arrayOf(PropTypes.object)
};
The result I get from both components rendered simultaneously:
The question is, what should I do in order for them to render themselves separately and show the corresponding ExercisesList for every chapter in ExercisesScreen?
Make your child component ExercisesList as functional component that only show the corresponding ExercisesList for every chapter not perform any rendering.
Like below:
const ExercisesList = (props) => {
const { exercises } = props;
return({
exercises.map(exercise, index) => renderExcercise(exercise, index)
})
}
const renderExcercise = (exercise, index) => {
return(
<View key={exercise.id}>
<TouchableOpacity
style={styles.button}
onPress={() =>
this.props.showSelectedItemList(exercise.screenName, exercise.name)
}
>
<Image source={exercise.icon}/>
<View>
<Text>{exercise.name}</Text>
</View>
<Image source={arrow} />
</TouchableOpacity>
<View />
</View>
)
}
export default ExercisesList;
ExercisesList.propTypes = {
onPress: PropTypes.func,
exercises: PropTypes.arrayOf(PropTypes.object)
};

How to pass the props from parent to child to child of child?

I want to pass the props from App.js(parent) to CommentItem.js(child) to Button.js(child of child),
but the props is empty in Button,js(child of child)'s component.
App.js(parent) to CommentItem.js(child)
I pushed mainuser: this.state.head to FlatList, and the props(mainuser) passed by renderItem={({ item }) => <CommentItem {...item}
And, CommentItem.js(child) received mainuser by the code below.
const {
head,
text,
created,
mainuser,
subuser,
} =
CommentItem.js(child) to Button.js(child of child)
I thought props was pased to Button by ,
and Button(child of child) received the props by the code below.
const {
mainuser,
} = this.props;
However, props id empty in Button.js(child of child).
#
Are there any problems of my code?
Could you give some advice please?
App.js(parent)
export default class App extends Component<{}> {
constructor(props) {
super(props);
this.state = {
head: [],
list: [],
};
}
_onPress = (text) => {
const list = [].concat(this.state.list);
list.push({
key: Date.now(),
text: text,
done: false,
mainuser: this.state.head,
});
this.setState({
list,
});
}
render() {
const {
head,
list,
} = this.state;
var data = [["User1", "User2", "User3"],];
return (
<View style={styles.container}>
<View style={styles.dropdownHeader}>
<View style={styles.dropdown}>
<DropdownMenu
style={{flex: 1}}
bgColor={'white'}
tintColor={'#666666'}
activityTintColor={'green'}
handler={(selection, row) => this.setState({head: data[selection][row]})}
data={data}
>
</DropdownMenu>
</View>
</View>
<Text>To Do</Text>
<View style={styles.postinput}>
<CommentInput onPress={this._onPress} />
</View>
</View>
<View style={styles.CommentListContainer}>
<FlatList
/*inverted*/
style={styles.CommentList}
data={list}
renderItem={({ item }) => <CommentItem {...item} /> }
/>
</View>
);
}
}
CommentItem.js(child)
const CommentItem = (props) => {
const {
head,
text,
created,
mainuser,
subuser,
} = props;
return (
<View style={styles.container}>
<View style={styles.left}>
<Text style={styles.text}>{text}</Text>
</View>
<View style={styles.function}>
<Button {...mainuser} />
</View>
</View>
);
}
Button.js(child of child)
export default class ApplauseButton extends Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
const {
mainuser,
} = this.props;
return (
<View style={styles.container}>
<Text>{mainuser}</Text>
</View>
);
}
}
If you want to access mainuser as a prop, then you've to pass it as <Button mainuser={mainuser} />.
As it stands, you're spreading the contents of mainuser.
as per your code it seems that main user is array,
this.state = {
head: [],//array
list: [],
};
....
list.push({
key: Date.now(),
text: text,
done: false,
mainuser: this.state.head,// main user should be array
});
so you need to give prop name also in <Button />
like this
<Button mainuser={mainuser} />

"undefined is not an object" when trying to access TextInput of the sibling component using references

in Parent component, I'm trying to focus the TextInput of second Child component when submit button is pressed in the TextInput of the first Child component but this error comes up: error message
Child.js
import React from "react";
import { View, TextInput} from "react-native";
export default class Child extends React.Component {
focus = ref => {
ref.input.focus();
};
render() {
return (
<View>
<TextInput
placeholder='enter text'
onSubmitEditing={() => {
this.focus(this.props.destinationRef);
}}
ref={input => {
this.input = input;
}}
/>
</View>
);
}
}
Parent.js
import React from "react";
import Child from "./Child";
import { View, StyleSheet } from "react-native";
export default class Parent extends React.Component {
// componentDidMount() {
// setTimeout(() => {
// this.two.input.focus();
// }, 3000);
// }
render() {
return (
<View style={styles.container}>
<Child
destinationRef={() => {
if (!this.two) return this.two;
}}
ref={input => {
this.one = input;
}}
/>
<Child ref={input => (this.two = input)} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
}
});
Note that when I uncomment the componendDidMount, second TextInput successfully focuses three seconds after mounting.
I think the problem in render update. In first render time destinationRef is undefined, parent state not updated or force updated so props not updated too.
I try little bit optimize your code. You can use arrow function to bind this:
import React from "react";
import Child from "./Child";
import { View, StyleSheet } from "react-native";
export default class Parent extends React.Component {
_makeFocus(){
this.two.input.focus();
}
handleOnSubmit(){
this._makeFocus();
}
render() {
return (
<View style={styles.container}>
<Child
onSubmit={this.handleOnSubmit.bind(this)}
ref={input => {
this.one = input;
}}
/>
<Child ref={input => (this.two = input)} />
</View>
);
}
}
I would change the logic a little bit since I think the ref is giving you problems.
Parent:
<View style={styles.container}>
<Child next={this.two} ref={comp => this.one = comp}/>
<Child next={this.one} ref={comp => this.two = comp}/>
</View>
Child:
<TextInput
placeholder='enter text'
onSubmitEditing={() => {
if (this.props.next)
this.props.next.focus();
}}
/>
EDIT:
Your approach was correct I believe, I would just update the parent into this:
export default class Parent extends React.Component {
constructor(props){
super(props);
this.references = {};
}
onFocus = (ref) => {
this.references[ref].focus();
}
render() {
return (
<View style={styles.container}>
<Child
destinationRef={'two'}
onFocus={this.onFocus}
ref={input => this.references['one'] = input}
/>
<Child ref={input => this.references['two'] = input} />
</View>
);
}
}
and your child to:
export default class Child extends React.Component {
render() {
return (
<View>
<TextInput
placeholder='enter text'
onSubmitEditing={() => {
this.props.onFocus(this.props.destinationRef);
}}
/>
</View>
);
}
}

When changing the parent's state from a child, why does the parent not render again?

I have a parent class that passes a function called editClassInfo to a child class. This function, is bound to the parent, and when called, immutably changes the state of the parent. However, the parent does not render again, instead having to set the state from within the parent.
Specifically, the problem is with a modal component that has a textInput that, when the text is changed, sets the state of the parent screen. However, when the modal is closed, the screen's state does not update immediately, even though the state is changed. Instead, another this.setState() has to be called in order for the render to happen again.
Here is a picture reference of the problem:
https://imgur.com/a/oCHRTIu
Here is the code:
This is the parent component.
export default class Classes extends Component {
static navigationOptions = {
title: 'Classes'
};
constructor() {
super();
this.state = {
numObjects: 3.,
classes: [
{
className: "Math",
teacherName: "Someone",
},
{
className: "Science",
teacherName: "Someone",
},
{
className: "Art",
teacherName: "Someone",
}
]
};
this.editClassInfo = this.editClassInfo.bind(this);
}
editClassInfo(index, infoType, value) {
let newClass = this.state.classes;
switch (infoType) {
case 'className':
newClass[index].className = value;
break;
case 'teacherName':
newClass[index].teacherName = value;
break;
}
this.setState({classes: newClass});
}
addClass(name, name2) {
let newClass = this.state.classes.concat({className: name, teacherName: name2});
this.setState({classes: newClass});
}
loadClasses = () => {
this.setState({
numObjects: this.state.numObjects * 2,
})
}
render(){
const classData = this.state.classes;
console.log(this.state.classes[0]);
return (
<View style={styles.container}>
<TopBar title='Classes'/>
<View style={styles.classHeader}>
<Text style={styles.currentClasses}> CURRENT CLASSES </Text>
<TouchableOpacity onPress={() => {this.addClass('World History', 'Someone')}}>
<Image
style = {styles.addClass}
source={require('../resources/addClass.png')}
/>
</TouchableOpacity>
</View>
<FlatList
data = { classData }
onEndReached = {this.loadClasses}
keyExtractor = {(item, index) => index.toString()}
initialNumtoRender = {3}
renderItem = {({item}) =>
<ClassBox
index={this.state.classes.indexOf(item)}
editClassInfo ={this.editClassInfo}
className={item.className}
teacherName={item.teacherName}
/>
}
/>
</View>
);
}
}
I pass editClassInfo onto a component called ClassBox:
export default class ClassBox extends Component {
static propTypes = {
className: PropTypes.string.isRequired,
teacherName: PropTypes.string.isRequired,
index: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.state = {
isVisible: false,
};
this.modalVisible = this.modalVisible.bind(this);
}
modalVisible(visible) {
this.setState({isVisible: visible});
}
render(){
return(
<View>
<ClassEdit
index={this.props.index}
editClassInfo={this.props.editClassInfo}
isVisible={this.state.isVisible}
modalClose={this.modalVisible}
className={this.props.className}
teacherName={this.props.teacherName}
/>
<TouchableOpacity onPress={() => {this.modalVisible(true)}}>
<View style={styles.container}>
<Image
source={{uri: 'http://via.placeholder.com/50x50'}}
style={styles.classImage}
/>
<View style={styles.classInfo}>
<Text style={styles.className}>
{this.props.className}
</Text>
<Text style={styles.teacherName}>
{this.props.teacherName}
</Text>
</View>
</View>
</TouchableOpacity>
</View>
)
}
}
This component contains the Child Modal ClassEdit:
export default class ClassEdit extends Component {
static propTypes = {
index: PropTypes.number.isRequired,
isVisible: PropTypes.bool.isRequired,
className: PropTypes.string.isRequired,
teacherName: PropTypes.string.isRequired
}
render() {
return(
<Modal
animationType="none"
transparent={false}
visible={this.props.isVisible}
>
<View style={styles.container}>
<View style={styles.closeTop}>
<TouchableOpacity onPress={() => {
this.props.modalClose(false);
}}>
<Image
style={styles.closeIcon}
source={require('../resources/close.png')}
/>
</TouchableOpacity>
</View>
<View style={styles.classInfo}>
<Image
source={{uri: 'http://via.placeholder.com/150x150'}}
style={styles.classImage}
/>
<TextInput
style={styles.className}
placeholder='Class Name'
value={this.props.className}
onChangeText = {(className) => {this.props.editClassInfo(this.props.index, 'className', className)}}
/>
<TextInput
style={styles.teacherName}
placeholder='Teacher Name'
value={this.props.teacherName}
onChangeText = {(teacherName) => {this.props.editClassInfo(this.props.index, 'teacherName', teacherName)}}
/>
</View>
</View>
</Modal>
);
}
}
It is in this last component called ClassEdit where the parent state gets changed, but when the modal is closed, the updated state is not seen, instead having to call addClass to trigger it.
I am a bit new to react-native so my code might not be the best, and the problem might be really simple, but any help would be appreciated.
let newClass = this.state.classes; creates a reference to the actual classes state, you're then mutating it.
To create a new array in an immutable way you can do this :
ES6 :
let newClass = [...this.state.classes];
ES5 :
let newClass = [].concat(this.state.classes);

Updating Parent Component via Child Component in React Native

I have a parent component and two child components. One of the child components is a form with an add button. I am able to capture the text when add button is pressed, but stuck when trying to pass that value to the parent component, update the array in parent component and re-render the view.
Parent Component:
var Tasks = ['Competitor Study','Content Plan','Write','Promote','Consumer Research']
//Build React Component
class MyTaskList extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.titleStyle}>
My Tasks
{"\n"}
({Moment().format("MMM Do YY")})
{"\n"}
</Text>
<AddTask />
{this.workitems()}
</View>
);
}
workitems() {
return (Tasks.map(function(workitem,i) {
return <ListItem key={i} workitem={workitem} />
}
)
);
}
}
Child component with the form
class AddTask extends Component {
constructor(props) {
super(props);
this.state = {
enterTask: 'Enter task'
};
}
onTaskTextChanged(event) {
this.setState({ enterTask: event.nativeEvent.text });
}
onAddPress() {
var newtask = this.state.enterTask;
console.log('new task - '+newtask);
}
render() {
return (
<View style={styles.addTask}>
<TextInput
style={styles.taskInput}
value={this.state.enterTask}
onChange={this.onTaskTextChanged.bind(this)}
placeholder='Enter task'/>
<TouchableHighlight style={styles.button}
onPress={this.onAddPress.bind(this)}
underlayColor='#99d9f4'>
<Text style={styles.buttonText}>Add</Text>
</TouchableHighlight>
</View>
);
}
}
I would instead use state for this as using forceUpdate() is not recommended
From React docs:
Calling forceUpdate() will cause render() to be called on the component, skipping shouldComponentUpdate(). This will trigger the normal lifecycle methods for child components, including the shouldComponentUpdate() method of each child. React will still only update the DOM if the markup changes.
Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render(). This makes your component "pure" and your application much simpler and more efficient.
(Based off of #1ven answer)
Parent Component:
//Build React Component
class MyTaskList extends Component {
constructor(props) {
super(props);
this.state = {
tasks: [
'Competitor Study',
'Content Plan',
'Write','Promote',
'Consumer Research'
]
}
}
handleAddTask(task) {
var newTasks = Object.assign([], this.state.tasks);
newTasks.push(task);
this.setState({tasks: newTasks});
}
render() {
return (
<View style={styles.container}>
<Text style={styles.titleStyle}>
My Tasks
{"\n"}
({Moment().format("MMM Do YY")})
{"\n"}
</Text>
<AddTask onAddTask={this.handleAddTask.bind(this)} />
{this.workitems()}
</View>
);
}
workitems() {
return this.state.tasks.map(function(workitem,i) {
return <ListItem key={i} workitem={workitem} />
});
}
}
Child component with the form
class AddTask extends Component {
constructor(props) {
super(props);
this.state = {
enterTask: 'Enter task'
};
}
onTaskTextChanged(event) {
this.setState({ enterTask: event.nativeEvent.text });
}
onAddPress() {
var newtask = this.state.enterTask;
console.log('new task - '+newtask);
// Providing `newtask` variable to callback.
this.props.onAddTask(newtask);
}
render() {
return (
<View style={styles.addTask}>
<TextInput
style={styles.taskInput}
value={this.state.enterTask}
onChange={this.onTaskTextChanged.bind(this)}
placeholder='Enter task'/>
<TouchableHighlight style={styles.button}
onPress={this.onAddPress.bind(this)}
underlayColor='#99d9f4'>
<Text style={styles.buttonText}>Add</Text>
</TouchableHighlight>
</View>
);
}
}
You should provide handleAddTask callback from parent to child component:
var Tasks = ['Competitor Study','Content Plan','Write','Promote','Consumer Research']
//Build React Component
class MyTaskList extends Component {
handleAddTask(task) {
// When task will be added, push it to array
Tasks.push(task);
}
render() {
return (
<View style={styles.container}>
<Text style={styles.titleStyle}>
My Tasks
{"\n"}
({Moment().format("MMM Do YY")})
{"\n"}
</Text>
<AddTask onAddTask={this.handleAddTask} />
{this.workitems()}
</View>
);
}
workitems() {
return (
Tasks.map(function(workitem,i) {
return <ListItem key={i} workitem={workitem} />
})
);
}
}
Next, you should pass task from child component to this callback:
class AddTask extends Component {
constructor(props) {
super(props);
this.state = {
enterTask: 'Enter task'
};
}
onTaskTextChanged(event) {
this.setState({ enterTask: event.nativeEvent.text });
}
onAddPress() {
var newtask = this.state.enterTask;
console.log('new task - '+newtask);
// Providing `newtask` variable to callback.
this.props.onAddTask(newtask);
}
render() {
return (
<View style={styles.addTask}>
<TextInput
style={styles.taskInput}
value={this.state.enterTask}
onChange={this.onTaskTextChanged.bind(this)}
placeholder='Enter task'/>
<TouchableHighlight style={styles.button}
onPress={this.onAddPress.bind(this)}
underlayColor='#99d9f4'>
<Text style={styles.buttonText}>Add</Text>
</TouchableHighlight>
</View>
);
}
}
That's it. Hope, it helps!

Categories