React Navigation header button that controls rendering of the screen - react-navigation

PLEASE NOTE that 'this' is not accessible from a static function: React Native : Access Component state inside a static function
I am trying to define a button in the screen's header that, when clicked, will affect rendering, and will be replaced with another icon.
It is an old app, still using react navigation 3.
I didn't know how to do the following things:
modify the component's state from a function that is activated when the button is pressed
modify the screen parameter from this function
What I managed to implement is the following lame and embarrassing way to do it:
When the button is clicked, a static function is executed, which modfies a static variable
periodic code is fired in componentDidMount that checks whether the static detailedDisplay variable has been modified. If it has been modified, this code sets a state variable that affects rendering. This periodic code also modifies the screen parameter which changes the icon in the header (because, as I wrote above, I also failed to set the parameter from the static function).
How can this be done in not-so-lame way?
Here is my code:
import React, { Component } from 'react';
import Icon from 'react-native-vector-icons/Octicons';
...
export default class Messages extends Component {
...
var detailedDisplay = false;
...
static navigationOptions = ({ navigation }) => {
return {
headerRight: //navigation.getParam('detailedDisplay', false) ?
detailedDisplay ?
<TouchableOpacity onPress={() => this.toggleDisplay(navigation)}>
<Icon name={"check-circle"} />
</TouchableOpacity> :
<TouchableOpacity onPress={() => this.toggleDisplay()}>
<Icon name={"comment"} />
</TouchableOpacity>,
};
};
static toggleDisplay(navigation) {
detailedDisplay = !detailedDisplay;
// the following statement gave the error "cannot read property
// 'setParams' of undefined", so I am setting it below.
// navigation.setParams({ detailedDisplay });
}
constructor(props) {
super(props);
this.state = {
detailedDisplay: false,
};
}
componentDidMount() {
setInterval(() => {
if (detailedDisplay !== this.state.detailedDisplay) {
this.props.navigation.setParams({ detailedDisplay });
this.setState({ detailedDisplay });
}
}, 500);
}
...
return (
<View>
{ this.state.detailedDisplay ?
{this.renderConcise()} :
{this.renderDetailed()}
}
</View>
);
}
renderDetailed() {
...
}
renderConcise() {
...
}
}

I think you can try to use your state inside your navigationOption method
static navigationOptions = ({ navigation }) => {
return {
headerRight: //navigation.getParam('detailedDisplay', false) ?
this.state.detailedDisplay ?
<TouchableOpacity onPress={() => this.toggleDisplay(navigation)}>
<Icon name={"check-circle"} />
</TouchableOpacity> :
<TouchableOpacity onPress={() => this.toggleDisplay()}>
<Icon name={"comment"} />
</TouchableOpacity>,
};
};
And then change your state in the toggleDisplay method
static toggleDisplay(navigation) {
this.setState(state=> detailedDisplay:!state.detailedDisplay)
// the following statement gave the error "cannot read property
// 'setParams' of undefined", so I am setting it below.
// navigation.setParams({ this.state.detailedDisplay });
}

You can try and keep the param and a state variable in sync.
An example POC is here
Here, I have tried to keep the state toggleVariable and the param value in sync but updating them through a common setter, and on Component Mount it will just be the same as the parameter.

You cannot read properties of this because this in JS is dynamic, and when you pass function as callback this is lost. There is 2 solutions that i know:
One of them bind this using bind method:
constructor(props) {
super(props);
this.state = {
detailedDisplay: false,
};
this.toggleDisplay = this.toggleDisplay.bind(this);
this.navigationOptions = this.navigationOptions.bind(this)
}
Now you can use non-static methods and you can access react state and methods in them:
navigationOptions({ navigation }) {
// somewhere in code <button onPress={this.toggleDisplay}><button>
// this.props this.state this.setState is available here
};
toggleDisplay(navigation) {
// this.props this.state this.setState is available here
// this.props.navigation.setParams() is available too
}
Finally, you can change the detailedDisplay state and screen params in toggleDisplay function. So when you'll press button state'll be changed and your commponent'll be re-rendered.
Additional
Second way to do it is to call method in a function:
onPress={() => this.toggleDiaplay()}
Here you must also use non-static methods, and this will work the same way as the first solution with bind.

Try doing something like this:
UPDATED: removed static from the toggleDisplay and added Class reference in the static function.
import React, { Component } from 'react';
import Icon from 'react-native-vector-icons/Octicons';
...
export default class Messages extends Component {
...
state = {
detailedDisplay: false,
};
...
static navigationOptions = ({ navigation }) => {
return {
headerRight: navigation.getParam('detailedDisplay', false) ?
<TouchableOpacity onPress={() => Messages.toggleDisplay()}>
<Icon name={"check-circle"} />
</TouchableOpacity> :
<TouchableOpacity onPress={() => Messages.toggleDisplay()}>
<Icon name={"comment"} />
</TouchableOpacity>,
};
};
toggleDisplay() {
this.setState({detailedDisplay: !this.state.detailedDisplay})
this.props.navigation.setParams({detailedDisplay: true});
}
constructor(props) {
super(props);
}
componentDidMount() {
// setInterval(() => {
// if (detailedDisplay !== this.state.detailedDisplay) {
// this.props.navigation.setParams({ detailedDisplay });
// this.setState({ detailedDisplay });
// }
// }, 500);
}
...
return (
<View>
{ this.state.detailedDisplay ?
{this.renderConcise()} :
{this.renderDetailed()}
}
</View>
);
}
renderDetailed() {
...
}
renderConcise() {
...
}
}

Related

Formik's use with react-query onSubmit throwing "Invalid hook call"

I have a React Native form that I'm trying to use Formik with react-query.
The problem is using useQuery() in a function called from onSubmit I am getting hook errors:
Warning: An unhandled error was caught from submitForm() [Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I believe I understand what the error is but I don't understand how to do a workaround to get this working.
The example I threw together just to demonstrate the issue:
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { Button, StyleSheet, Text, TextInput, View } from 'react-native';
import { useQuery } from 'react-query';
import { Formik } from 'formik';
import axios from 'axios';
const getPokemonList = async () => {
const { data } = await axios.get("https://pokeapi.co/api/v2/pokemon");
return data;
};
function authenticate(username, password) {
const { isLoading, error, data } = useQuery('fetchLuke', getPokemonList);
if (data) {
{
return (
<Text>
{JSON.stringify(data, null, 2)}
</Text>
);
}
}
if (error) {
return (
<Text>{error}</Text>
);
}
if ( isLoading ) {
return (
<Text>Retrieving Luke Skywalker Information...</Text>
);
}
}
export default function App() {
return (
<View style={styles.container}>
<Formik
initialValues={{ email: '' }}
onSubmit={(values, actions) => {
authenticate(values.email);
actions.resetForm();
}}
>
{({
handleChange,
handleBlur,
handleSubmit, values }) => <View>
<TextInput
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
value={values.email}
/>
<Button onPress={handleSubmit} title="Submit" />
</View>}
</Formik>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
This is fairly straightforward.
Create a variable and assign useQuery to it (in App, because useQuery is a hook and hooks can only be used in functional components)
Call the variable in your onSubmit or pass it to authenticate...
react-query has a dependent query feature where the query only runs when a custom condition is satisfied.
Check the example below from https://react-query.tanstack.com/guides/dependent-queries
// Get the user
const { data: user } = useQuery(['user', email], getUserByEmail)
const userId = user?.id
// Then get the user's projects
const { isIdle, data: projects } = useQuery(
['projects', userId],
getProjectsByUser,
{
// The query will not execute until the userId exists
enabled: !!userId,
}
)
// isIdle will be `true` until `enabled` is true and the query begins to fetch.
// It will then go to the `isLoading` stage and hopefully the `isSuccess` stage :)

How do I render an image using map function?

I've tried do render the image from URL but I have no success. If write the URI ok, but if I write item.show.image.original.replace ('http:', 'https:') to get the image no success.
The problem is thereĀ“s no error but not rendering.
The others values I've had success with.
import React, { Component } from "react";
import { Text, View, Image } from "react-native";
let termo = 'batman';
const API = 'http://api.tvmaze.com/search/shows?q='+termo;
export default class mapFunction extends Component {
constructor(props) {
super(props);
this.state = {
array: [], };
}
componentDidMount() {
fetch(API)
.then(response => response.json())
.then(data => this.setState({ array: data}));
}
list = () => {
return this.state.array.map(item => {
return (
<View style={{marginLeft: 10}}>
<Text>{item.score}</Text>
<Text>{item.show.name}</Text>
<Text>{item.show.type}</Text>
<Text>{item.show.language}</Text>
<Text>{item.show.summary} </Text>
<Image source={{uri:'https://static.tvmaze.com/uploads/images/original_untouched/6/16463.jpg'}}
style={{width:90, height:150}} />
</View>
);
});
};
render() {
return <View>{this.list()}
</View>;
}
}
Just change render function as below:
render() {
return (
<View>{this.list()}</View>
);
}

React Navigation: setParams in Nested Navigator

In my React Native application, I use React Navigation.
It's an app that enables the user to search an underlying database, i.e. for names. The GIF below illustrates the navigation.
From the landing screen, Go to search button is pressed (Main Stack Navigator) --> The Header appears, which is alright.
On the second screen, there is a bottomTabNavigator, where names is chosen (in names, there is a second StackNavigator nested).
This leads to the third screen. Here, three cards are shown. With the help of the second StackNavigator, clicking on Mehr opens a details screen.
What I want to achieve is that the Header of the first StackNavigator (that one at the top) disappears as soon as the user opens the details screen.
You see a button there because in the first step, I wanted to let the Header disappear on button click.
The below code works if it is implemented in a screen that is derived from the first StackNavigator directly. But because I am inside a nested navigator, it does not work anymore.
Here is the code:
App.tsx:
imports ...
class RootComponent extends React.Component {
render() {
const image = require('./assets/images/corrieBackground3.png');
console.log('calling the store', this.props.resultValue); // undefined
return (
<View style={styles.container}>
<LandingPage />
</View>
);
}
}
const RootStack = createStackNavigator(
{
LandingPage: {
screen: RootComponent,
navigationOptions: {
header: null,
},
},
SearchScreen: {
screen: SearchScreen,
navigationOptions: {
title: 'I SHOULD DISAPPEAR',
},
},
},
{
initialRouteName: 'LandingPage',
},
);
const AppContainer = createAppContainer(RootStack);
export default class App extends React.Component {
render() {
return <AppContainer />;
}
}
TwoTabs.tsx (for the 2nd screen):
imports ...
const SearchBarStack = createStackNavigator(
{
SearchBar: {
screen: SearchBar,
navigationOptions: {
header: null,
},
},
Details: {
screen: Details,
navigationOptions: {
title: 'I am here, above header disapear',
},
},
},
{
initialRouteName: 'SearchBar',
},
);
const TabNavigator = createBottomTabNavigator(
{
One: {
screen: SearchCriteria,
navigationOptions: {
tabBarLabel: 'criteria',
},
},
Two: {
screen: SearchBarStack,
navigationOptions: {
tabBarLabel: 'names',
},
},
},
);
const TabLayout = createAppContainer(TabNavigator);
type Props = {};
const TwoTabsHorizontal: React.FC<Props> = ({}) => {
return (
<View>
<TabLayout />
</View>
);
};
export default TwoTabs;
SearchBar.tsx (3rd screens skeleton):
import ...
type Props = {};
const SearchBar: React.FC<Props> = () => {
// logic to perform database query
return (
<View>
<ScrollView>
... logic
<SearchResult></SearchResult> // component that renders 3 cards
</ScrollView>
</View>
);
};
export default SearchBar;
Card.tsx (card rendered by SearchResult):
imports ...
type Props = {
title: string;
navigation: any;
};
const Card: React.FC<Props> = ({title, navigation}) => {
return (
<Content>
<Card>
<CardItem>
<Right>
<Button
transparent
onPress={() => navigation.navigate('Details')}>
<Text>Mehr</Text>
</Button>
</Right>
</CardItem>
</Card>
</Content>
);
};
export default withNavigation(Card);
And finally, the Details screen together with its Content. Here, the Header from the first StackNavigator should be hidden.
imports ...
type Props = {};
const Details: React.FC<Props> = ({}) => {
return (
<View>
<Content></Content>
</View>
);
};
export default Details;
imports ...
type Props = {
navigation: any;
};
class Content extends React.Component {
state = {
showHeader: false,
};
static navigationOptions = ({navigation}) => {
const {params} = navigation.state;
return params;
};
hideHeader = (hide: boolean) => {
this.props.navigation.setParams({
headerShown: !hide,
});
console.log('props ', this.props.navigation);
};
render() {
return (
<View>
<View>
</View>
<Button
title={'Press me and the header will disappear!'}
onPress={() => {
this.setState({showHeader: !this.state.showHeader}, () =>
this.hideHeader(this.state.showHeader),
);
}}
/>
</View>
);
}
}
export default withNavigation(CardExtended);
Maybe someone has an idea?

How to save and display and image in react-native?

I have a question in react-native. Im using a module called "react-native-image-picker" to pick an image and display it on my app.
Now what i want is to store it somewhere (database, or local storage) and when i open again the app, the image that i choosed should be there. But i dont know what is the best option to do it.
I've already tryied to read some stuff like react-native-fs and fetch-blob but it doesnt help me, i guess.
What is the best option to do it?
Thank you.
First, renders view according to condition. For example if image is available then simply display the image else display TouchableOpacity which will help use to select pictures :
import React, { Component } from React;
import { View, TouchableOpacity, Text, Image } from 'react-native';
import ImagePicker from 'react-native-image-picker';
import AsyncStorage from '#react-native-community/async-storage';
class App extends Component {
constructor(props) {
super(props);
this.state = {
isImageAvailable: false,
profilePic: null
}
}
componentDidMount = () => {
this.getImage();
}
getImage = async () => {
const profilePic = await AsyncStorage.getItem("profilePic");
if (profilePic) {
this.setState({
isImageAvailable: true,
profilePic: JSON.parse(profilePic)
});
}
}
selectProfilePic = () => {
const options = {
title: 'Select Avatar',
storageOptions: {
skipBackup: true,
path: 'images',
},
};
ImagePicker.showImagePicker(options, (response) => {
console.log('Response = ', response);
if (response.didCancel) {
console.log('User cancelled image picker');
} else if (response.error) {
console.log('ImagePicker Error: ', response.error);
} else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
} else {
const source = { uri: response.uri };
// You can also display the image using data:
// const source = { uri: 'data:image/jpeg;base64,' + response.data };
AsyncStorage.setItem("profilePic", JSON.stringify(source));
this.setState({
profilePic: source,
isImageAvailable: true
});
}
});
}
render() {
return (
<View>
{
this.state.isImageAvailable && (
<Image source={this.state.profilePic} style={{ width: 200, height: 200 }} />
)
}
{
!this.state.isImageAvailable && (
<TouchableOpacity onPress={this.selectProfilePic}>
<Text>Choose Profile Pic</Text>
</TouchableOpacity>
)
}
</View>
)
}
}
Hope it will help you.
You can use realmdb as an aternative to Asyncstorage.

How to pass value to headerRight button

When use the react-navigation and add the headerRight button
...
class Screen extends React.Component {
static navigationOptions = {
headerRight: (
<TouchableOpacity>
<Icon .../>
</TouchableOpacity>
)
}
render() {
return (
<View />
)
}
}
Then I want to animate this button, but I encounter a problem when passing value
...
class Screen extends React.Component {
static navigationOptions = {
headerRight: (
<Animated.View style={animatedStyle}> <---- how to pass it
<TouchableOpacity>
<Icon .../>
</TouchableOpacity>
</Animated.View>
)
}
componentWillMount() {
this.animatedValue = new Animated.Value(0)
}
componentDidMount() {
Animated.timing(...}).start()
}
render() {
const interpolateRotation = ...
const animatedStyle = ... <---- the animatedStyle value
return (
<View />
)
}
}
I have tried the this.animatedStyle and animatedStyle for Animated.View, but it also can not effectiveness. It seems static navigationOptions is not inline within the class Screen
You can use this.props.navigation.setParams({ headerRightButtonStyle: this.animatedValue }) in constructor or componentWillMount to pass this value. Then read it like this:
static navigationOptions = ({ navigation }) => {
const params = navigation.state.params
return {
headerRight: (
<Animated.View style={params.headerRightButtonStyle}>
<TouchableOpacity>
<Icon .../>
</TouchableOpacity>
</Animated.View>
)
}
}
setParams will also trigger header rerender. You can use this method to pass header title/style/etc.
Related documentation: https://reactnavigation.org/docs/headers.html#using-params-in-the-title

Resources