How to wrap each Tab.Screen in the same component - react-navigation

when using react-navigation's BottomTabNavigator, how can I wrap each screen with a component? For example, say I have some tabs like so:
<NavigationContainer>
<Tab.Navigator tabBar={(props) => <MyTabBar {...props} />}>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
</NavigationContainer>
I want to wrap those two screens with the same component, BUT I need the same props that tabBar is getting because I want to render something different in the wrapped component based on the active route.
I could manually add the component in each screen's render, but I'd like to keep it in just one place and have it dynamically figure out what to render.
Just for reference, in react-navigation v4 I could do it like this:
// CustomTabView component
const { routes, index } = navigation.state;
const route = routes[index];
const descriptor = descriptors[route.key];
const ActiveScreen = descriptor.getComponent();
const currentKey = descriptor.key;
<View style={Styles.container}>
<ShellHeader
icon={route.params.actionIcon}
currentRoute={route}
currentTab={currentKey}
/>
<ActiveScreen navigation={descriptor.navigation} />
<CustomTabBar navigation={navigation} />
</View>
const AppContainer = createAppContainer(createNavigator(CustomTabView, CustomTabRouter, {}));
and CustomTabRouter would look something like:
const CustomTabRouter = TabRouter(
{
Home: {
screen: HomeScreen,
params: {
key: 'history',
actionIcon: {
navigation: () => NavigationService.navigateToHomeScreen()
}
}
},
Settings: {
screen: SettingsScreen,
params: {
key: 'contacts',
actionIcon: {
navigation: () => NavigationService.navigateToSettingsScreen()
}
}
}
{
initialRouteName: 'Home'
}
);
But that does not work in v6 as there is no createAppContainer anymore

Related

Setting the state from a callback in the parent component doesn't work

I'm trying to set the state from the parent component using a callback. This callback gets passed down to the child component that renders a material ui datatable. The callback responds onClick and passes some values to the callback. The problem is that setting the state with the values from the callback arguments doesn't work.
My assumption is that when the user clicks the button from the child component, it should invoke the callback function and pass the values I needed to set the state.
Parent Component:
export default function ViewJobs() {
const [type, setType] = useState('');
const [params, setParams] = useState({});
const callback = ({ cellValues, componentType, path }) => {
setType(componentType);
setParams(cellValues) // Sets the params with an object.
console.log(cellValues) // Displays the data I need in the console
history.push(path);
};
console.log(params) // Displays undefine in the console.
return(
<React.Fragment>
<TabPanel value={value} index={0} dir={theme.direction} >
<DataTable
jobs={job}
title='All'
parentCallback={callback}
/>
</TabPanel>
</React.Fragment>
);
}
Child Component
import React, { useEffect } from 'react';
export default function DataTable(props) {
const { jobs, parentCallback } = props;
const rows = jobs.payload;
const handleDiaryClick = (event, cellValues) => {
const params = {
cellValues,
componentType: 'diary',
path: "/view/jobs/diary"
};
parentCallback(params);
};
const renderDiaryElement = params => {
return (
<Button
variant="contained"
color="primary"
style={{ backgroundColor: "#000000" }}
onClick={(event) => {
handleDiaryClick(event, params);
}}
>
<MenuBookIcon />
</Button>
);
}
return (
<div
className={classes.root}
style={{ height: 400, width: '100%' }}
>
<DataGrid
rows={rows}
columns={columns}
pageSize={5}
//checkboxSelection
disableSelectionOnClick
/>
</div>
);
}
Since the state has been lifted up to the parent component, I'm under the impression that the code above should be working.
I tried to reproduce the issue but I couldn't replicate it.
Any advice or inputs are appreciated. Thanks.
After further checking on my codebase, I found that the history.push(path) located in my callback is causing the issue. I had to remove this line of code for it to work.

React Navigation header button that controls rendering of the screen

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() {
...
}
}

How to refresh a screen in react native inside a function?

I want to be able to delete elements from my FlatList. I couldn't do it with the onPress of the TouchableOpacity in ItemView so I decided to create a Button with the onPress={botClick} so when I fill the TextInput above that Button it erases that element from the AsyncStorage and then the element is also removed from proddata. My problem is that to see that element removal from the FlatList I have to change my navigation screen to another one and return to see the changes reflected. Can I put something inside of botClick() that refreshes or recharges the screen when the function is called to see the changes automatically without changing screens?
export default function TestScreen () {
const [proddata, setProddata] = useState([]);
const [deletepar, setDeletepar] = useState('');
const whenClick = () => {
console.log("hello");
}
async function botClick(){
try {
await AsyncStorage.removeItem(deletepar);
console.log("Removed");
//Add something here that refreshes or recharges the screen
}
catch(exception) {
}
};
const ItemView = ({item}) => {
return (
<TouchableOpacity onPress={whenClick}>
<View>
<Text>
{item[0]+ ' ' + item[1]}
</Text>
</View>
</TouchableOpacity>
);
};
async function carInState() {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
setProddata([...proddata, ...result]);
}
useFocusEffect(
React.useCallback(() => {
carInState();
}, [])
);
return (
<View>
<View>
<TextInput placeholder="..." onChangeText={(val) => setDeletepar(val)}/>
<View>
<Button title="Delete" onPress={botClick}/>
</View>
</View>
<FlatList
data={proddata}
renderItem={ItemView}
keyExtractor={(item, index) => index.toString()}
/>
</View>
);
};
It isn't a problem you can manipulate the state directly or get all data againg
async function botClick() {
try {
await AsyncStorage.removeItem(deletepar);
// 1) Variant load data again & invoke setProddata
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
setProddata([...result]);
// 2) Or you can remove it from you list
setProddata(prevProddata => prevProddata.filter(value => value.x.x.x !== deletepar))
} catch (exception) {}
}

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?

redirect dependent on ajax result using react

I would like to redirect to a component in case the data of the success has a certain value.
When ajax returns the data, depending on the value of the data redirected to the Contents class that I previously imported.
I've been looking for information about the push method
My error is: Error: Invariant failed: You should not use <Redirect> outside a <Router>
import React, { Component } from 'react';
import { Modal,Button } from 'react-bootstrap';
import $ from 'jquery';
import { Redirect } from 'react-router';
import Contents from './Contents';
class Login extends Component {
constructor(props, context) {
super(props, context);
this.handleShow = this.handleShow.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleloginClick = this.handleloginClick.bind(this);
this.handleUsernameChange = this.handleUsernameChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.state = {
show: true,
username: "",
password: "",
};
}
handleloginClick(event) {
var parametros = {
username: this.state.username,
password: this.state.password
}
const { history } = this.props;
$.ajax({
data: parametros,
url: "https://privada.mgsehijos.es/react/login.php",
type: "POST",
success: function (data) {
}
});
}
handleUsernameChange(event) {
this.setState({username: event.target.value});
}
handlePasswordChange(event) {
this.setState({password: event.target.value});
}
handleClose() {
this.setState({ show: false });
}
handleShow() {
this.setState({ show: true });
}
render() {
If(Condicion){
return (<Redirect to={'./Contents'} />);
}
return (
//Here my modal.
);
}
}
export default Login;
you can use Router dom to navigate.
My fiddle: https://jsfiddle.net/leolima/fLnh9z50/1/
const AboutUs = (props) => {
console.log(props.location.state)
console.log('Hi, you are in About page, redirecting with router dom in 3 seconds')
setTimeout(() => {
props.history.push('/')}, 3000);
return <h1>Now we're here at the about us page.</h1>;
};
Full Example:
// Select the node we wish to mount our React application to
const MOUNT_NODE = document.querySelector('#app');
// Grab components out of the ReactRouterDOM that we will be using
const { BrowserRouter, Route, Switch, NavLink, Link } = window.ReactRouterDOM;
// PropTypes is used for typechecking
const PropTypes = window.PropTypes;
// Home page component
const Home = () => {
return <h1>Here we are at the home page.</h1>;
};
// AboutUs page component
const AboutUs = (props) => {
console.log(props.location.state)
return <h1>Now we're here at the about us page.</h1>;
};
// NotFoundPage component
// props.match.url contains the current url route
const NotFoundPage = ({ match }) => {
const {url} = match;
return (
<div>
<h1>Whoops!</h1>
<p><strong>{url.replace('/','')}</strong> could not be located.</p>
</div>
);
};
// Header component is our page title and navigation menu
const Header = () => {
// This is just needed to set the Home route to active
// in jsFiddle based on the URI location. Ignore.
const checkActive = (match, location) => {
if(!location) return false;
const {pathname} = location;
return pathname.indexOf('/tophergates') !== -1 || pathname.indexOf('/_display/') !== -1;
}
return (
<header>
<h1>Basic React Routing</h1>
<nav>
<ul className='navLinks'>
{/* Your home route path would generally just be '/'' */}
<li><NavLink to="/tophergates" isActive={checkActive}>Home</NavLink></li>
<li><Link to={{
pathname: "/about",
state: { fromDashboard: true }
}}>About</Link></li>
</ul>
</nav>
</header>
);
};
// Out layout component which switches content based on the route
const Layout = ({children}) => {
return (
<div>
<Header />
<main>{children}</main>
</div>
);
};
// Ensure the 'children' prop has a value (required) and the value is an element.
Layout.propTypes = {
children: PropTypes.element.isRequired,
};
// The top level component is where our routing is taking place.
// We tell the Layout component which component to render based on the current route.
const App = () => {
return (
<BrowserRouter>
<Layout>
<Switch>
<Route path='/tophergates' component={Home} />
<Route path='/_display/' component={Home} />
<Route exact path='/' component={Home} />
<Route path='/about' component={AboutUs} />
<Route path='*' component={NotFoundPage} />
</Switch>
</Layout>
</BrowserRouter>
);
};
// Render the application
ReactDOM.render(
<App />,
MOUNT_NODE
);

Resources