In React Native, how can I access methods of one component from another component? - react-native

I'm trying to access a method of a React Native component from a different component. It is passed through props. Unfortunately, it seems like the components aren't providing their methods publicly. How can I get access to the method?
Have a look at the following, you'll see InsideView has this.props.myModal, which is a ShowMyModal component. However, it doesn't have access to the .openModal() method.
'use strict';
var React = require('react-native');
var {
AppRegistry,
ActionSheetIOS,
StyleSheet,
Text,
View,
} = React;
var InsideView = React.createClass({
makeItOpen: function() {
debugger;
this.props.myModal.openModal();
},
render: function() {
return (
<View>
<Text onPress={() => this.makeItOpen()}>Click me!</Text>
</View>
);
}
});
var ShowMyModal = React.createClass({
getInitialState: function() {
return {
isModalOpen: false,
}
},
openModal() {
this.setState({isModalOpen: true});
},
closeModal() {
this.setState({isModalOpen: false});
},
render: function() {
return (
<Text>isModalOpen = {String(this.state.isModalOpen)}</Text>
);
}
});
var AwesomeProject = React.createClass({
getInitialState: function() {
return {
myModal: <ShowMyModal />,
}
},
render: function() {
return (
<View style={{padding: 30}}>
<InsideView myModal={this.state.myModal}/>
{this.state.myModal}
</View>
);
},
});
AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject);

Something like this should work:
'use strict';
var React = require('react-native');
var {
AppRegistry,
ActionSheetIOS,
StyleSheet,
Text,
TouchableOpacity,
View,
} = React;
var InsideView = React.createClass({
render: function() {
return (
<View>
<TouchableOpacity onPress={() => this.props.openModal()}><Text>Open modal!</Text></TouchableOpacity>
<TouchableOpacity onPress={() => this.props.closeModal()}><Text>Close modal!</Text></TouchableOpacity>
</View>
);
}
});
var ShowMyModal = React.createClass({
render: function() {
return (
<Text>isModalOpen = {String(this.props.isVisible)}</Text>
);
}
});
var SampleApp = React.createClass({
getInitialState: function() {
return {
isModalOpen: false
}
},
_openModal: function() {
this.setState({
isModalOpen: true
});
},
_closeModal() {
this.setState({
isModalOpen: false
});
},
render: function() {
return (
<View style={{padding: 30}}>
<InsideView openModal={this._openModal} closeModal={this._closeModal}/>
<ShowMyModal isVisible={this.state.isModalOpen}/>
</View>
);
},
});
AppRegistry.registerComponent('SampleApp', () => SampleApp);

I don't think it's a good idea to store the components in state. State should really be used for component's data rather than sub-components. Dave's solution above is good approach but it could be done a bit better as it moves the state of modal to the application (which is not very good to separate concerns). It's good if modal can keep it's own state and know if it's visible or not. Then openModal() and closeModal() can do some extra stuff as needed (as opposed to somehow reacting to change in visibility of ShowModal). You can also avoid those extra _openModal and _closeModal which are boilerplate.
I think it's best to use refs. Refs is standard way to refer to other components. See here for more details about refs https://facebook.github.io/react/docs/more-about-refs.html You can use refs as strings and refer to the component by that strings but it's kind of ugly as introduces global names which contradict the component approach of react. But you can also use callbacks as refs to set your internal components as fields. There is a nice and simple example of this is react's documentation: http://facebook.github.io/react-native/docs/direct-manipulation.html#forward-setnativeprops-to-a-child. I copy it here in case the documentation gets updated:
var MyButton = React.createClass({
setNativeProps(nativeProps) {
this._root.setNativeProps(nativeProps);
},
render() {
return (
<View ref={component => this._root = component} {...this.props}>
<Text>{this.props.label}</Text>
</View>
)
},
});
What happens here - the view in question has callback ref which sets this._root as the view's backing component. Then in any other place in the component you can use this._root to refer to it.
So in your case it could look like below (note that you need those anonymous arrow functions rather than passing the openModal / closeModal methods because at the time of rendering _modal is not yet set, you can only refer to it later using the anonymous methods).
// ...
// InsideView render (same as in Dave's solution)
<View>
<TouchableOpacity onPress={() => this.props.openModal()}><Text>Open modal!</Text></TouchableOpacity>
<TouchableOpacity onPress={() => this.props.closeModal()}><Text>Close modal!</Text></TouchableOpacity>
</View>
// ...
// Sample App render ...
<View style={{padding: 30}}>
<InsideView openModal={ () => this._modal.openModal() } closeModal={ () => this._modal.closeModal() } />
<ShowMyModal ref={component => this._modal = component} />
</View>
Then your initial ShowModal implementation can stay as it is - with it's own state and own openModal and showModal functions.

Related

Iterate over values of Map to render all icon components but don't work however render one icon works

I am developing react-native project.
I have a function which set icon metadata into a Map :
export function getIconsMetadata() {
// a map of icons' metadata
const iconsMetadata = new Map();
...
// code to set icon metadata to the map
iconsMetadata.set("foo", "Foo");
iconsMetadata.set("bar", "Bar");
...
return iconsMetadata;
}
There is another function which returns the actual icon component based on the icon type (i.e. the value of iconsMetadata holds the icon type):
export function getMyIcon(iconType) {
switch (iconType) {
case 'Foo':
return <Foo />;
case 'Bar':
return <Bar />;
...
}
In my screen, I have a function to show icon component by iterating over the values of the above icons' metadata Map, and try to render each icon component:
export const MyScreen() => {
const showIcons = () => {
[...getIconsMetadata().values()].map((iconType, index) => {
const iconComponent = getMyIcon(iconType);
return <View key={index}>
{iconComponent}
</View>;
});
};
return (
<View style={styles.container}>
{/*I call the showIcons function here to render icons*/}
{showIcons()}
</View>
)
}
Problem is the icons are not shown on screen.
But if I directly return one icon component in my screen:
export const MyScreen = () => {
...
const showOneIcon = () => {
return <View>
<Foo />
</View>;
});
}
return (
<View style={styles.container}>
{/*I show one icon*/}
{showOneIcon()}
</View>
)
}
The <Foo /> icon component is rendered successfully on the screen.
So, why iterating the map to show all icons don't work?
The problem is that you’re not returning anything from showIcons. Either you remove { } from there
const showIcons = () =>
[...getIconsMetadata().values()].map((iconType, index) => {
const iconComponent = getMyIcon(iconType);
return <View key={index}>{iconComponent}</View>;
});
or add return before [...getIconsMetadata().values()].map
const showIcons = () => {
return [...getIconsMetadata().values()].map((iconType, index) => {
const iconComponent = getMyIcon(iconType);
return <View key={index}>{iconComponent}</View>;
});
};

How to access value calculated in `useEffect` hook from renderer

I am developing a React-Native project with functional component.
Here is a very simple screen which renders a calculated result list. Since I need to calculation to be called only once so I put it inside the useEffect hook.
import {doCalculation} from '../util/helper'
const MyScreen = ({navigation}) => {
useEffect(() => {
// I call a function from a helper module here.
// The result is a list of object.
const result = doCalculation();
// eslint-disable-next-line
}, []);
// renderer
return (
<View>
// Problem is 'result' is not accessible here, but I need to render it here
{result.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
As you can see I have called the doCalculation() to get the result inside useEffect hook. My question is how can I render the result in the return part? Since the result is calculated inside the hook, it is not accessible in the renderer.
P.S. Moving the const result = doCalculation() outside the useEffect hook is not an option since I need the calculation to be called only once.
Below is an example. According to the above comments it looks like you want it to be called once on component mount. All you really need to do is add a useState
import {doCalculation} from '../util/helper'
const MyScreen = ({navigation}) => {
const [calculatedData, setCalculatedData] = useState([])
useEffect(() => {
// I call a function from a helper module here.
// The result is a list of object.
const result = doCalculation();
setCalculatedData(result)
// eslint-disable-next-line
}, []);
// renderer
return (
<View>
// Problem is 'result' is not accessible here, but I need to render it here
{calculatedData.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
const [calculatedData, setCalculatedData] = useState([])
useState is a hook used to store variable state. When calling setCalculatedData inside the useEffect with empty dependency array it will act similar to a componentDidMount() and run only on first mount. If you add variables to the dependency array it will re-run every-time one of those dep. change.
You can change the data inside the calculatedData at anytime by calling setCalculatedData with input data to change to.
Make use of useState to save the calculation result and then use the variable inside return. See https://reactjs.org/docs/hooks-state.html.
Code snippet:
import {doCalculation} from '../util/helper'
const MyScreen = ({navigation}) => {
const [result, setResult] = useState([]);
useEffect(() => {
// I call a function from a helper module here.
// The result is a list of object.
const tempRes = doCalculation();
setResult(tempRes);
// eslint-disable-next-line
}, []);
// renderer
return (
<View>
// Problem is 'result' is not accessible here, but I need to render it here
{result.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
Is async function?
if the function is not async (not wating for respond like from api) - you don't need useEffect.
import React from 'react';
import { Text, View } from 'react-native';
import {doCalculation} from '../util/helper'
const results = doCalculation();
const MyScreen = () => {
return (
<View>
{results.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
else you should wait until the results come from the server..
import React, { useState, useEffect } from 'react';
import { Text, View } from 'react-native';
import { doCalculation } from '../util/helper';
const MyScreen = () => {
const [results, setResults] = useState(null) // or empty array
useEffect(() => {
(async () => {
setResults(await doCalculation());
})();
}, []);
return (
<View>
{results?.map(item => <Text key={item.id}> {item.value} </Text>) || "Loading..."}
</View>
)
}
export default MyScreen;
and I can use more readable code:
if (!results) {
return <View>Loading...</View>
}
return (
<View>
{results.map(item => <Text key={item.id}> {item.value} </Text>)}
</View>
)
the async function can be like:
const doCalculation = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ id: 1, value: 1 }]);
}, 2000);
});
};

loading on navigating between screens

I'm new in RN. When I want to navigate between screens I create this function:
displayScreen2 = () => {
this.props.navigation.navigate("screen2")
}
and I call it in onPress={this.displayScreen2}
with TouchableOpacity or any Touchable when the user clicks he has to wait 1 second or 2 before displaying the screen. So what I want is to change the Touchable icon to an loader.
It's simple if I use a conditional rendering but I don't know how to do it now, when I have to change my state? Any suggestions?
this is my approach:
<TouchableOpacity
style={Styles.topButton}
onPress= {() => {
this.setState({loading: 'load'},
() => {
displayScoreListView()
// this.setState({loading: 'icone'})
}
)
}}
>
<Text style={Styles.scoreListButtonTextRed}>{this.state.loading}</Text>
that not work, tha state change but visualy not because if I return to the first screen I have 'load' in the text component
You could create a custom component wrapping whatever Touchable you prefer, I've used this technique in my production apps before. The button has it's own state which allows you to automatically display a loading indicator when necessary.
export class ButtonWorker extends Component {
state = {
working: false
}
onButtonPress = () => {
this.setState(
{ working: true },
() => {
this.props.onPress(this.onWorkFinished);
}
);
}
onWorkFinished = () => {
this.setState({ working: false });
}
render() {
return (
<TouchableOpacity>
{this.state.working ? (
<ActivityIndicator />
) : (
this.props.children
)}
</TouchableOpacity>
);
}
}
And then use it like a normal button with additional logic!
export class NavigationScreen extends Component {
navigate = (done) => {
// ... Asynchronous logic goes here
done();
this.props.navigation.navigate("Screen2");
}
render() {
return (
<Fragment>
{/* ... */}
<ButtonWorker onPress={this.navigate} />
</Frament>
);
}
}

React Native Pass data to another component

I am new to React Native and trying to build a Messenger app and I have 2 components Search and Messenger​. I am struggling to pass the data I got from Search to Messenger.
Search component finds user (receiver) and me being sender I want to communicate but after finding user in Search I want to pass that user to Messenger so that I can chat with that specific user that found in <Search> component.
In addition, Search component has Views that will display user calendar etc.. so ideally I don't want to use <Messenger> in render() method of Search as it will include Messenger component features inside the Search component which destroys the purpose of <Search> component.
So my code is :
'use strict';
var Search = React.cerateClasss({
getDefaultProps: function () {
return {
date: new Date(),
singerName:''
};
},
getInitialState: function () {
return {
date: this.props.date,
artistName: '',
artistUserId: 1,
maxNoArtist: 0,
imagePath: '../common/images/1.png',
user: null
}
},
getArtistName: function () {
var artist = [];
var query = new Parse.Query(Parse.User);
query.equalTo('userId', this.state.artistUserId);
return query.first({
success: (result) => {
this.setState({artistName: result.get('name')});
this.props.singerName= result.get('name');
this.setState({imagePath: result.get('image').url()});
},
error: (data, error) => {
console.log('Error occured : ' + error.message())
}
});
},
render: function () {
if (!this.state.user) {
return <View style={styles.container}>
<Text style={styles.label}> Loading.... </Text>
</View>
}
var username = this.state.user.get('username');
return (
<View style={styles.container}>
<ResponsiveImage source={{uri:this.state.imagePath}} initHeight="200" initWidth="400"/>
<Text style={styles.label}>
{this.state.artistName}
</Text>
<View style={styles.innerButtonView}>
<Button text={'Onki'} onPress={this.getPreviousArtistName}/>
<Button text={'Indiki'} onPress={this.getNextArtistName}/>
</View>
<CalendarPicker
selectedDate={this.state.date}
onDateChange={this.onDateChange}
/>
<View style={styles.innerButtonView}>
<Button text={'Cyk'} onPress={this.onLogoutPress}/>
<Button text={'Habarlas'} onPress={this.onPress}/>
</View>
<Messenger singerName={this.props.singerName}></Messenger> // BREAKS SEARCH COMPONENT PURPOSE - INCLUDES MESSENGER FEATURES IN TO SEARCH COMPONENT
</View>
);
},
})
var Messenger = React.createClass({
getInitialState: function () {
return {
greeting: 'Salam',
date: new Date(),
errorMessage: '',
user: null,
olderMessageTextFrom: [],
olderMessageTextTo: [],
olderMessageDateFrom: [],
olderMessageDateTo: [],
earlierMessages: []
}
},
componentWillMount: function () {
Parse.User.currentAsync().then((user) => {
this.setState({user: user})
}
)
},
getMessages() {
return [
{
text: this.state.greeting,
name: this.props.singerName,
image: require('../common/images/1.png'),
position: 'left',
date: new Date()
},
I am late to answer but I did in different way using props.
I have two components.
Splash.js
Home.js
I am passing the data (Let's take String) from Splash.js to Home.js.
First component (Sender)
this.props.navigation.navigate('Home', {user_name: userName})
Second component (Receiver)
this.props.navigation.state.params.user_name
Hope this would help you.
OK, so based on your infos, I think the issue is that you don't get the singerName in the Messenger component.
First, I'd change your getArtistName method to this :
getArtistName: function () {
var artist = [];
var query = new Parse.Query(Parse.User);
query.equalTo('userId', this.state.artistUserId);
return query.first({
success: (result) => {
this.setState({artistName: result.get('name')});
// Removed the this.props.singerName = ...
this.setState({imagePath: result.get('image').url()});
},
error: (data, error) => {
console.log('Error occured : ' + error.message())
}
});
}
then in your render method :
<Messenger singerName={this.state.artistName} />
Inside a component you need to use setState and not change props :
that is to say that this.props.singerName = 'singer' is a wrong way of doing things, you should do this.setState({singerName: 'singer'}); then access it with this.state.singerName
Inside your messenger component, you access it with this.props.singerName

React Native Pass properties on navigator pop

I'm using NavigatorIOS on my react native app. I want to pass some properties when navigating back to previous route.
An example case:
I'm in a form page. After submitting data, I want to go back to the previous route and do something based on the submitted data
How should I do that ?
Could you pass a callback func on the navigator props when you push the new route and call that with the form data before you pop to the previous route?
Code sample showing how to use a callback before pop. This is specifically for Navigator and not NavigatorIOS but similar code can be applied for that as well.
You have Page1 and Page2. You are pushing from Page1 to Page2 and then popping back to Page1. You need to pass a callback function from Page2 which triggers some code in Page1 and only after that you will pop back to Page1.
In Page1 -
_goToPage2: function() {
this.props.navigator.push({
component: Page2,
sceneConfig: Navigator.SceneConfigs.FloatFromBottom,
title: 'hey',
callback: this.callbackFunction,
})
},
callbackFunction: function(args) {
//do something
console.log(args)
},
In Page2 -
_backToPage1: function() {
this.props.route.callback(args);
this.props.navigator.pop();
},
The function "callbackFunction" will be called before "pop". For NavigatorIOS you should do the same callback in "passProps". You can also pass args to this callback. Hope it helps.
You can use AsyncStorage, save some value on child Component and then call navigator.pop():
AsyncStorage.setItem('postsReload','true');
this.props.navigator.pop();
In parent Component you can read it from AsyncStorage:
async componentWillReceiveProps(nextProps) {
const reload = await AsyncStorage.getItem('postsReload');
if (reload && reload=='true')
{
AsyncStorage.setItem('postsReload','false');
//do something
}
}
For NavigatorIOS you can also use replacePreviousAndPop().
Code:
'use strict';
var React = require('react-native');
var {
StyleSheet,
Text,
TouchableOpacity,
View,
AppRegistry,
NavigatorIOS
} = React;
var MainApp = React.createClass({
render: function() {
return (
<NavigatorIOS
style={styles.mainContainer}
initialRoute={{
component: FirstScreen,
title: 'First Screen',
passProps: { text: ' ...' },
}}
/>
);
},
});
var FirstScreen = React.createClass({
render: function() {
return (
<View style={styles.container}>
<Text style={styles.helloText}>
Hello {this.props.text}
</Text>
<TouchableOpacity
style={styles.changeButton} onPress={this.gotoSecondScreen}>
<Text>Click to change</Text>
</TouchableOpacity>
</View>
);
},
gotoSecondScreen: function() {
console.log("button pressed");
this.props.navigator.push({
title: "Second Screen",
component: SecondScreen
});
},
});
var SecondScreen = React.createClass({
render: function() {
return (
<View style={styles.container}>
<Text style={styles.helloText}>
Select a greeting
</Text>
<TouchableOpacity
style={styles.changeButton} onPress={() => this.sayHello("World!")}>
<Text>...World!</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.changeButton} onPress={() => this.sayHello("my Friend!")}>
<Text>...my Friend!</Text>
</TouchableOpacity>
</View>
);
},
sayHello: function(greeting) {
console.log("world button pressed");
this.props.navigator.replacePreviousAndPop({
title: "First Screen",
component: FirstScreen,
passProps: {text: greeting}
});
}
});
var styles = StyleSheet.create({
mainContainer: {
flex: 1,
backgroundColor: "#eee"
},
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
marginTop: 50,
},
helloText: {
fontSize: 16,
},
changeButton: {
padding: 5,
borderWidth: 1,
borderColor: "blue",
borderRadius: 4,
marginTop: 20
}
});
AppRegistry.registerComponent("TestApp", () => MainApp);
You can find the working example here: https://rnplay.org/apps/JPWaPQ
I hope that helps!
I had the same issue with React Native's navigator which I managed to solve using EventEmitters and Subscribables. This example here was really helpful: https://colinramsay.co.uk/2015/07/04/react-native-eventemitters.html
All I needed to do was update for ES6 and the latest version of React Native.
Top level of the app:
import React, { Component } from 'react';
import {AppRegistry} from 'react-native';
import {MyNavigator} from './components/MyNavigator';
import EventEmitter from 'EventEmitter';
import Subscribable from 'Subscribable';
class MyApp extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.eventEmitter = new EventEmitter();
}
render() {
return (<MyNavigator events={this.eventEmitter}/>);
}
}
AppRegistry.registerComponent('MyApp', () => MyApp);
In the _renderScene function of your navigator, make sure you include the "events" prop:
_renderScene(route, navigator) {
var Component = route.component;
return (
<Component {...route.props} navigator={navigator} route={route} events={this.props.events} />
);
}
And here is the code for the FooScreen Component which renders a listview.
(Note that react-mixin was used here in order to subscribe to the event. In most cases mixins should be eschewed in favor of higher order components but I couldn't find a way around it in this case):
import React, { Component } from 'react';
import {
StyleSheet,
View,
ListView,
Text
} from 'react-native';
import {ListItemForFoo} from './ListItemForFoo';
import reactMixin from 'react-mixin'
import Subscribable from 'Subscribable';
export class FooScreen extends Component {
constructor(props) {
super(props);
this._refreshData = this._refreshData.bind(this);
this._renderRow = this._renderRow.bind(this);
var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
dataSource: ds.cloneWithRows([])
}
}
componentDidMount(){
//This is the code that listens for a "FooSaved" event.
this.addListenerOn(this.props.events, 'FooSaved', this._refreshData);
this._refreshData();
}
_refreshData(){
this.setState({
dataSource: this.state.dataSource.cloneWithRows(//YOUR DATASOURCE GOES HERE)
})
}
_renderRow(rowData){
return <ListItemForFoo
foo={rowData}
navigator={this.props.navigator} />;
}
render(){
return(
<ListView
dataSource={this.state.dataSource}
renderRow={this._renderRow}
/>
)
}
}
reactMixin(FooScreen.prototype, Subscribable.Mixin);
Finally. We need to actually emit that event after saving a Foo:
In your NewFooForm.js Component you should have a method like this:
_onPressButton(){
//Some code that saves your Foo
this.props.events.emit('FooSaved'); //emit the event
this.props.navigator.pop(); //Pop back to your ListView component
}
This is an old question, but currently React Navigation's documentation for Passing params to a previous screen suggests that we use navigation.navigate() and pass whatever parameters we want the previous screen to have.