Interact with StackNavigator's navigationOptions from view inside of TabNavigator - javascript

How can I make navigationOptions interact with view inside of TabNavigator?
for example, from this question how do I access a component's state from static navigationOptions, I can make navigationOptions responsive to component's state.
But what if I want to make it responsive a step further, responsive to component's state inside of TabNavigator?
As image shows below, I want to make Next button enabled only if more than one checkbox get checked.
But there's no effect to provide headerRight in my component.
export class CreateGroupCamera extends Component {
static navigationOptions = ({ navigation }) => {
const { state, setParams, navigate } = navigation;
const params = state.params || {};
return {
headerTitleStyle: { alignSelf: 'center' },
headerRight: <Button title='Next' disabled={params.validator ? !params.validator() : true} />
}
}
}

I found an answer myself after try error 3 hours...
follow these steps you can have a workable version.
1. Instead of expose TabNavigator directly, wrap it.
const Tabs = TabNavigator({
...
});
export class TabView extends Component {
render() {
return (<Tabs />);
}
}
2. Pass callback function down with screenProps. When called, setParams() into navigationOptions.
export class TabView extends Component {
static navigationOptions = ({navigation}) => {
const { state, setParams, navigate } = navigation;
const params = state.params || {};
return {
headerRight = <Button title='Next' disabled={params.validator ? !params.validator() : true} />
};
}
_validator(validator) {
this.props.navigation.setParams({validator});
}
render() {
return (<Tabs screenProps={{ callback: this._validator } />);
}
}
3. Inside main component, pass function with screenProps.callback.
_validator() {
return this.selectedDevices().length > 0;
}
render() {
return (
<Checkbox onChecked={ () => {
this.props.screenProps.callback(this._validator);
}} />
);
}
result:

Related

How to override methods in function based component React?

Overriding is working the parent component is class based component
// Parent Button Class
class Button extends React.Component {
createLabel = () => {
return <span>{this.props.label}</span>;
};
render() {
return <button>{this.createLabel()}</button>;
}
}
//child Button class
class CustomButton extends Button {
// Over Riding createLabel Function
createLabel = () => {
const { label, icon } = this.props;
return <span>{icon ? icon : "" + label}</span>;
};
}
function Test(props) {
return (
<div>
<CustomButton label={"Save"} icon={"pi pi-loading"} />
</div>
);
}
export default Test;
I need help how to implement my CustomButton Component when the parent Button is function based component Like Below.
const Button = (props) => {
const createLabel = () => {
if (props.label) {
return <span>{props.label}</span>;
}
};
return <button>{createLabel()}</button>;
};
is that possible to override methods of function based component?
Use composition over inheritence. The original button can take a label as a callback:
// Basic button
function Button({createLabel}) {
return <button>{createLabel()}</button>;
}
Then you can easily create wrappers for specific situations:
function CustomButton({label, icon}) {
// Use a specific createLabel function
return <Button
createLabel = {() => {
// this function has direct access to props
return <span>{icon ? icon : "" + label}</span>;
}}
/>;
}
This is the version closest to what you have. However, you can also just pass the <span> directly as a prop, or use children to place it inside the component. Or use useCallback to make the callback more efficient. Many more improvements possible.

How to get access to navigate/goBack function in the class that rendered AppContainer in ReactNative

I've created an app with several pages to navigate through and it works fine when I send navigation requests from child pages (goBack or navigate('...')). The problem is that I need to switch pages from a class that created the AppContainer. It looks more complicated. I tried in several ways with refs, but all the time navigate function is not accessible. I want to push a home page in my callback function of the BasicFooter.
I tried to use dispatch as well, but the same problem. I am not sure if it is even possible to make it work by reference to AppContainer or do I need to build a structure with
<NavigationContainer>
<Stack.Navigator>{/* ... */}</Stack.Navigator>
</NavigationContainer>
That's my code:
export default class App extends React.Component {
constructor(props) {
super(props);
this.refNavi = React.createRef();
this.goToMain_audio = this.goToMain_audio.bind(this);
}
goToMain_audio() {
console.log("Object ", this.refNavi.current.navigation.navigate('Home')); //<==Undefined!
}
render() {
return ( <
View style = {
styles.container
} >
<AppContainer ref = {
this.refNavi
}
style = {
styles.container
} </AppContainer>
<View style = {
styles.footer1
}>
<BasicFooter style = {
styles.footer1
}
clickedAudio = {
this.goToMain_audio
}
clickedMood = {
this.refNavi.current.navigate('Home')
} //<== Undefined!
>
<
/BasicFooter> </View>
}
}
const AppNavigator = createStackNavigator({
Home: {
screen: MainScreen,
},
SettingsPage: {
screen: Settings,
},
SettingsMain: {
screen: SettingsMain,
},
ConnectionPage: {
screen: Connection,
}
}, {
initialRouteName: "Home",
transitionConfig,
header: null,
headerMode: 'none'
});
const AppContainer = createAppContainer(AppNavigator);

Navigate if my var is not set in my redux state

I'm making somme connexion/inscription screens. I have a profil bottom tab button. The goal is if the user click on it, It will either show his profil page if he is connected (I'm setting an user in my redux store), or show a Landing screen with the login button and subscribe button if he is not.
How can i navigate to this landing page every time the user click on "Profil" on my bottom tab bar if the redux store is empty ?
Here is my profil code :
// Screens / Profil.js
import React from "react";
import { StyleSheet, View, Text, Button } from "react-native";
import { color } from "../constants/color";
import { connect } from "react-redux";
/*
* Class Profil
* Appears when the users click on the profil button in the bottom tab bar
* Return JSX component
*/
class Profil extends React.Component {
constructor(props) {
super(props);
}
// This is what I have tried so far
componentWillMount() {
if (this.props.currentUser == null) {
this.props.navigation.navigate("Landing");
}
}
/*
* Render the page
* #param null
* #return JSX component
*/
render() {
const { currentUser } = this.props;
console.log(currentUser);
if (currentUser == null) {
return (
<View style={styles.mainContainer}>
<Text>You are not connected</Text>
</View>
);
} else {
return (
<View style={styles.mainContainer}>
<Text>Welcome {currentUser.user.username}</Text>
<Button title="Déconnexion" onPress={this._disconnection} />
</View>
);
}
return null;
}
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
backgroundColor: color.whisper
}
});
const mapStateToProps = state => {
return {
currentUser: state.connection.currentUser
};
};
export default connect(mapStateToProps)(Profil);
So when I first click on my Profil it goes well on my landing page:
But if I click again on Profil, It does not show my landing page :
I guess it's because the component did not rerender himself . How could i Handle a constant redirection according to my redux state var ?
UPDATE : My stack Navigator
const ProfilStackNavigator = createStackNavigator({
Profil: {
screen: Profil,
navigationOptions: {
title: "Profil"
}
},
Landing: {
screen: Landing,
headerMode: "none",
navigationOptions: {
header: null
}
},
SignUp: {
screen: SignUp,
navigationOptions: {
title: "Inscription"
}
},
SignIn: {
screen: SignIn,
navigationOptions: {
title: "Connection"
}
}
});
I think you have two options to solve this, the first one is using NavigationEvents and the second one is navigationOptions
First option, add didFocus listener to your profile component, which will get fired when profile tab is focused.
class Profil extends React.Component {
subscribe = null;
constructor(props){}
focused = () => {
if (this.props.currentUser == null) {
this.props.navigation.navigate('Landing');
}
};
componentDidMount = () => {
this.subscribe = this.props.navigation.addListener('didFocus', this.focused);
};
componentWillUnmount() {
this.subscribe && this.subscribe.remove();
this.subscribe = null;
}
render() {
...
}
}
Second option, with navigationOptions add tabBarOnPress which gets fired when you click the tabbar.
Profil: {
screen: ProfilStackNavigator,
navigationOptions: {
tabBarLabel: 'Profil',
tabBarOnPress: ({ navigation, defaultHandler }) => {
// console.log('Pressed');
defaultHandler();
},
},
},
I have successfully done this.
You have the correct idea.
Perhaps your navigation object is the issue?
Here is an example I coded for a 3D Earthquake Visualizer with React/Redux/Three. I passed the history object as a prop to my components and used that to dynamically change the routing. (not included in example)
I would suggest ensuring your router API is correct and then logging the state on will mount lifecycle.
I hope this has been helpful. Good Luck! :)
const mapStateToProps = state => {
return {
...
quakes: state.viz.quakes,
...
};
};
componentDidMount(){
// if data hasn't download redirect to splash
if (this.props.quakes[this.props.feedIndex].length === 0) {
this.props.history.push("/");
}
...
}

Declaratively update markup inside component

I have a component in my app that renders some data, most commonly a page title.
Markup:
<Toolbar>
<ToolbarRow>
<div id="title-bar">
{children}
</div>
</ToolbarRow>
</Toolbar>
How would I declaratively be able to change the data inside?
I've tried react-side-effects which allowed me to indeed change the title to be rendered but then I wanted to be able to add components as well.
Components aren't to be stored inside state so there's that…
Then I looked at Portals, which seem to exactly what I want but I get Target container is not a DOM element.
Markup for the portal component:
import React from "react";
import {createPortal} from 'react-dom'
const PageTitle = ({title, children}) => {
return createPortal(
<p>foo</p>,
document.getElementById('title-bar')
)
};
export default PageTitle;
I'm calling the portal component like so:
<PageTitle title="Document Overview"/>
As you can see from the above snippet, the other component adds a <div id="title-bar" />, so I guess it has to do with timing.
Anyone have a good idea?
I would just put components into the state here:
const bars = [];
export class TitleBar extends Component {
state = { children: [] };
componentDidMount() { bars.push(this); }
componentWillUnmount() { bars.splice(bars.indexOf(this), 1); }
render() { return this.state.children };
}
const RealPageTitle = ({ title }) => <div> { title } </div>;
export class PageTitle extends Component {
constructor(props) {
super(props);
this.real = RealPageTitle(props);
}
componentDidMount() {
for(const bar of bars)
bar.setState(({ children }) => ({ children: children.concat(this.real) }));
}
componentWillUnmount() {
for(const bar of bars)
bar.setState(({ children }) => ({ children: children.filter(child => child !== this.real) }));
}
render() { }
}
That way you can just add <PageTitle title={"Test"} /> somewhere on the page and it gets added to the title bar.
I know this does not follow "best practices", but it certainly works

React does not render recursive reactive components

Given this component :
import React, { Component } from 'react';
import TrackerReact from 'meteor/ultimatejs:tracker-react';
export default class SubscriptionView extends TrackerReact(Component) {
constructor(props) {
super(props);
let params = props.params || [];
if (!Array.isArray(params)) {
params = [params];
}
this.state = {
subscription: {
collection: Meteor.subscribe(props.subscription, ...params)
}
};
}
componentWillUnmount() {
this.state.subscription.collection.stop();
}
render() {
let loaded = this.state.subscription.collection.ready();
if (!loaded) {
return (
<section className="subscription-view">
<h3>Loading...</h3>
</section>
);
}
return (
<section className="subscription-view">
{ this.props.children }
</section>
);
}
};
And another component :
import SubscriptionView from './SubscriptionView.jsx';
export const Foo = () => (
<SubscriptionView subscription="allFoo">
<SubscriptionView subscription="singleBar" params={ 123 }>
<div>Rendered!</div>
</SubscriptionView>
</SubscriptionView>
);
The first Subscription is re-rendered when the data is available, however the second one is rendered only once and nothing more. If I place a console.log(this.props.subscription, ready); inside the render function of SubscriptionView, I see
allFoo false
allFoo true
singleBar false
and that's it.
On the server side, both publish methods are
Meteor.publish('allFoo', function () {
console.log("Subscribing foos");
return Foos.find();
});
Meteor.publish('singleBar', function (id) {
console.log("Subscribing bar", id);
return Bars.find({ _id: id });
});
Both of the publish methods are being called.
Why isn't the second SubscriptionView reactive?
* Solution *
This is based on alexi2's comment :
import React, { Component } from 'react';
import TrackerReact from 'meteor/ultimatejs:tracker-react';
export default class SubscriptionLoader extends TrackerReact(Component) {
constructor(props) {
super(props);
let params = props.params || [];
if (!Array.isArray(params)) {
params = [params];
}
this.state = {
done: false,
subscription: {
collection: Meteor.subscribe(props.subscription, ...params)
}
};
}
componentWillUnmount() {
this.state.subscription.collection.stop();
}
componentDidUpdate() {
if (!this.state.done) {
this.setState({ done: true });
this.props.onReady && this.props.onReady();
}
}
render() {
let loaded = this.state.subscription.collection.ready();
if (!loaded) {
return (
<div>Loading...</div>
);
}
return null;
}
};
Then, inside the parent component's render method :
<section className="inventory-item-view">
<SubscriptionLoader subscription='singleBar' params={ this.props.id } onReady={ this.setReady.bind(this, 'barReady') } />
<SubscriptionLoader subscription='allFoos' onReady={ this.setReady.bind(this, 'foosReady') } />
{ content }
</section>
Where setReady merely sets the component's state, and content has a value only if this.state.barReady && this.state.foosReady is true.
It works!
Try separating out your SubscriptionView Components like this:
import SubscriptionView from './SubscriptionView.jsx';
export const Foo = () => (
<div>
<SubscriptionView subscription="singleBar" params={ 123 }>
<div>Rendered!</div>
</SubscriptionView>
<SubscriptionView subscription="allFoo">
<div>Rendered Again!</div>
</SubscriptionView>
</div>
);
Edit from comments conversation
Not sure if I am on the right track but you could build Foo as a 'smart' component that passes props to each SubscriptionView as required, and then use Foo as a reusable component.
Let's say that what I need to render is FooBarForm, which requires both Foos and Bars to be registered, in that specific use case. How would you do that?
You could create Components Foos and Bars that took props as required and create a parent component FooBarForm that contained those Components and passed the necessary data.
FooBarForm would handle the state and as that changed pass it to the props of its child components.
Now state is being centrally managed by the parent component, and the child components render using props passed from the parent.
The child components would re-render as their props changed depending on whether the state being passed from the parent component had changed.

Categories