Pass state through Navigator - javascript

I'm using the React Native core Navigator component but having trouble figuring out how to pass data between components when pressing buttons in the Navigation Bar. Here is some example code of the setup that I have.
const NavigationBarRouteMapper = {
Title: (route, navigator) => {
let title;
switch (route.component.displayName) {
case 'FirstScreen':
title = 'First Screen';
break;
}
return (
<Text>
{title}
</Text>
)
},
LeftButton: (route, navigator) => {
let onButtonPress, buttonTitle;
switch (route.component.displayName) {
case 'SecondScreen':
buttonTitle = 'Close';
onButtonPress = () => navigator.pop();
break;
}
return (
<TouchableOpacity
onPress={onButtonPress}>
<Text>
{buttonTitle}
</Text>
</TouchableOpacity>
)
},
RightButton: (route, navigator) => {
let onButtonPress, buttonTitle;
switch (route.component.displayName) {
case 'SecondScreen':
buttonTitle = 'Save';
onButtonPress = () => {}; // #TODO Call onButtonPress in SecondScreen component
break;
}
return (
<TouchableOpacity
onPress={onButtonPress}>
<Text>
{buttonTitle}
</Text>
</TouchableOpacity>
);
}
};
const App = React.createClass({
renderScene(route, navigator) {
return <route.component navigator={navigator} {...route.props} />;
},
render() {
return (
<Navigator
style={styles.appContainer}
initialRoute={{component: SplashScreen}}
renderScene={this.renderScene}
navigationBar={
<Navigator.NavigationBar
routeMapper={NavigationBarRouteMapper}
/>
}
/>
);
}
});
const FirstScreen = React.createClass({
onButtonPress() {
this.props.navigator.push({component: SecondScreen});
},
render() {
return (
<View>
<TouchableHighlight onPress={this.onButtonPress}>
<Text>
Click Me
</Text>
</TouchableHighlight>
</View>
);
}
});
const SecondScreen = React.createClass({
getInitialState() {
return {
input: ''
}
},
onButtonPress() {
if (this.state.input.length) {
// Do something with this.state.input such as POST to remote API
this.props.navigator.pop();
}
},
render() {
return (
<View style={styles.container}>
<TextInput
onChangeText={(input) => this.setState({input})}
value={this.state.input}
/>
</View>
);
}
});
You can see from the comments that I have a value stored in the state of SecondScreen that I want to do something with when someone hits the Save button. Any ideas?

In your renderScene function, while returning 'route.component' you can pass props and read them in the component.

Related

Updating State of Parent component from Child Component (Tab Navigator)

I am new to React and React Native and I am trying to build an cooking recipe app. I am trying to update my dish state in DishTabNavigator.js from DishIngredients.js so that I can send the data to firebase from DishTabNavigator.js, however I do not know how I can update the state. I tried lifting the state up but I couldn't do it. Been stuck on this for a day now. Any help would be appreciated
DishTabNavigator.js
const Tab = createMaterialTopTabNavigator();
export default function MyTabs({ navigation }) {
const [dish, setDish] = useState([{ ingredients: [] }]);
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity style={styles.iconright}>
<Text>Send dish to Firebase</Text>
<FontAwesome name="save" size={24} color="black" />
</TouchableOpacity>
),
});
});
return (
<Tab.Navigator>
<Tab.Screen
name="Ingredient"
component={DishIngredients}
ingredients={dish}
/>
</Tab.Navigator>
);
}
DishIngredients.js
class DishIngredients extends Component {
constructor(props) {
super(props);
this.state = {
ingredients: [],
textInput: [],
};
//Add TextInput
addTextInput = (index) => {
let textInput = this.state.textInput;
textInput.push(
<TextInput
style={styles.textInput}
onChangeText={(text) => this.addValues(text, index)}
/>
);
this.setState({ textInput });
};
//Function to add values into the states
addValues = (text, index) => {
let dataArray = this.state.ingredients;
let checkBool = false;
if (dataArray.length !== 0) {
dataArray.forEach((element) => {
if (element.index === index) {
element.text = text;
checkBool = true;
}
});
}
if (checkBool) {
this.setState({
ingredients: dataArray,
});
} else {
dataArray.push({ text: text, index: index });
this.setState({
ingredients: dataArray,
});
}
};
//function to console the output
getValues = () => {
console.log("Data", this.state.ingredients);
this.props.ingredients = this.state.ingredients;
console.log(this.props.ingredients);
};
render() {
return (
<View>
<View style={styles.icon}>
<View style={{ margin: 10 }}>
<TouchableOpacity>
<Ionicons
name="add"
size={24}
color="black"
onPress={() => this.addTextInput(this.state.textInput.length)}
/>
</TouchableOpacity>
</View>
<View style={{ margin: 10 }}>
<Button title="Remove" onPress={() => this.removeTextInput()} />
</View>
</View>
{this.state.textInput.map((value) => {
return value;
})}
<Button title="Get Values" onPress={() => this.getValues()} />
</View>
);
}
}
export default DishIngredients;
You could use React Context
const DishContext = React.createContext({
dish: [],
setDish: () => {},
});
export const useDishContext = () => React.useContext(DishContext);
export const AppDishProvider = ({children}) => {
const [dish, setDish] = useState([{ingredients: []}]);
const defaultDish = {
dish,
setDish,
};
return (
<DishContext.Provider value={defaultDish}>{children}</DishContext.Provider>
);
};
Then
/** Wrap your TabNavigator with AppDishProvider */
<AppDishProvider>
<YourTabNavigator />
</AppDishProvider>
Then
/**
* In DishIngredients, or your TabNavigator...
* You get access to dish-state, and setDish using your
* custom-hook `useDishContext`
*/
const { dish, setDish } = useDishContext();
And if you're using class-based component
class DishIngredient extends React.Component {
static contextType = DishContext;
/**
* Current context-value could be accessed by
* `this.context`
*/
render() {
const {dish, setDish} = this.context;
/** .... */
}
}
Assuming you're not having redux wired in your app... ReactContext is your way to go...
Without changing a lot in your current code you can do like this
const addIngredient = (ingredient) => {
// TODO: ingredient
};
return (
<Tab.Navigator>
<Tab.Screen name="Ingredient">
{(props) => (
<DishIngredients
{...props}
ingredients={dish}
addIngredient={addIngredient}
/>
)}
</Tab.Screen>
</Tab.Navigator>
);
As you see there is a function addIngredient (You can rename as you wish) that will be introduced as prop in DishIngredients like this.props.addIngredient(<Your Ingredient text>).
ReactContext also a nice solution above but I think, this will force your app re-render every time your context changes. It might impact on your performance.

React Native Redux not updating Boolean value

I created a Boolean state for my shopping app in which when a user clicks the buy button, it should set that state to true and display the cart total in a container, but it's not working.
Below is my code:
reducer:
if (action.type === SHOW_CART) {
return {
...state,
show: action.showCart,
};
const initialstate = {
show: false,
}
That's my reducer with initial state :
export const showCart = () => {
return {
type: SHOW_CART,
showCart: true,
};
};
That's my action:
<View style={styles.buy}>
<Text
onPress={() => {
this.props.addToCart(item.id);
this.props.showCart();
}}
>
Buy Once
</Text>
</View>
const mapStateToProps = (state) => {
return {
items: state.clothes.jeans,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addToCart: (id) => dispatch(addToCart(id)),
showCart: () => dispatch(showCart()),
};
};
In this code I used a flat list, in the flat list I used onPress to show the cart container and to dispatch my action
App
export default function App() {
return (
<>
<Provider store={Store}>
<NavigationContainer>
<AppNavigation />
</NavigationContainer>
<ViewChart />
</Provider>
</>
);
}
ViewCart.js
<View>
{this.props.show ? (
<View style={styles.total}>
<Text style={styles.totaltext}>Total:</Text>
<Text style={styles.priceTotal}>{this.props.total}</Text>
<View style={styles.onPress}>
<Text
style={styles.pressText}
onPress={() => this.props.navigation.navigate("Cart")}
>
View Cart
</Text>
</View>
</View>
) : null}
</View>
const mapStateToProps = (state) => {
return {
total: state.clothes.total,
show: state.clothes.show,
};
};
export default connect(mapStateToProps)(ViewChart);
In App.js I added the conatiner of view cart, which should be displayed when user clicks buy
and in view cart I'm getting error undefined is not an object (evaluating '_this.props.navigation.navigate')
Change
this.props.showCart;
to
this.props.showChart();

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)
};

React Native - render update every time when childs make setstate (I want not)

I have a page.js with different components that inside them make setState in componentDidMount(). And the render of parent execute every time (10 in total). But is a problem because I want execute a request on api rest on componentDidUpdate and this function execute very times.
There are any way for the setstate in a child not affecting the render of parent?
Update with code
page.js
import Header from 'Pages/userprofile/general/HeaderMenuUser';
import UserHeader from 'Pages/userprofile/basic/header/UserHeader';
import UserContent from 'Pages/userprofile/basic/content/UserContent';
...
class UserProfile extends Component {
static navigationOptions = ({navigation}) => {
const user = navigation.getParam('user', '');
return {
title: 'User Profile',
header: (navigation) => <Header {...navigation} title="My profile" user={user} origin="UserProfile" edit={false} />,
headerTintColor: '#000',
headerTitleStyle: {
fontWeight: 'bold',
},
};
};
getProfile(){
//const { user } = this.props.navigation.state.params;
//Profile.get(user.id).then( response => {
ProfileApi.get().then( response => {
this.setState({ user: response.user });
this.props.navigation.setParams({
user: response.user,
});
});
}
render() {
//console.warn(this.props.isFocused ? 'Focused' : 'Not focused');
return (
<ScrollView style={[styles.scroll, globalStyles.generalScrollContainer]}>
<View style={{backgroundColor:'#fff', minHeight: 50}}>
<GradientBackground />
<UserHeader user={this.state.user} edit={false}/>
</View>
{ typeof this.state.user === "object" && (
<View style={styles.container}>
<UserContent user={this.state.user} />
</View>
)}
</ScrollView>
);
}
}
export default withNavigationFocus(UserProfile);
UserContent.js
export default class UserContent extends Component {
updateState = (update) => {
if (update['simple']){
this.setState({ [update['key']]: update['value'] });
} else {
projects = this.state.user.projects;
projects.push(update['value']);
this.setState( prevState => ({
user: {
...prevState.user,
projects: projects
}
}));
this.setState( prevState => ({
user: {
...prevState.user,
can_create_projects: update['value'].can_create_projects
}
}));
Functions.storeItem('user', this.state.user);
}
};
componentDidMount() {
Functions.getApiToken().then((response) => {
this.setState({ url: url + response });
});
}
render() {
return (
<View>
<View style={styles.content}>
<UserBiography user={this.state.user} />
<View style={[globalStyles.rowBetween, globalStyles.rowVerticalCenter]}>
<Text style={globalStyles.titleText}>Projects</Text>
{ this.state.user.can_create_projects && (
<View>
<TouchableOpacity
style={styles.button}
onPress={() => { this.setState({collapsed: !this.state.collapsed}) }}>
<Text style={[globalStyles.textTag, this.state.collapsed ? globalStyles.backgroundBlue : globalStyles.backgroundGary ]}>{this.state.collapsed ? "Add" : "Close add"} project</Text>
</TouchableOpacity>
</View>
) }
</View>
<View style={[globalStyles.borderBottomView, {marginBottom: 30}]}></View>
<UserCreateProject collapsed={this.state.collapsed} updateState={this.updateState}/>
{typeof this.state.user === 'object' && this.state.user.projects.length > 0 && (this.state.user.projects.map((project, index) => {
return (<UserProject project={project} key={project.id} updateState={this.updateState} />)
}))}
</View>
</View>
)
}
}
You can use shouldComponentUpdate() method to control your render:
shouldComponentUpdate(nextProps, nextState) {
if (nextState.render_screen) {
return true;
} else {
return false;
}
}

React native accessing refs in a custom component

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>
)
}
});

Categories