What is the best 'React' way of creating a custom component that can be accessible from any screen?
Think of a custom Alert component. I need it in every screen of my app, so when an error occurs I can show it.
Currently I am doing it this way:
// AlertModal.js
import ... from ...;
const AlertModal = (props) => {
const {isSuccess, headerText, bodyText, onClosed, isOpen, onBtnPress} = props;
return (
<Modal
...
isOpen={isOpen}
onClosed={() => onClosed()}
>
<View ...>
<Text>{headerText}</Text>
<Text>{bodyText}</Text>
<Button
...
onPress={() => onBtnPress()}
>
...
</Button>
</View>
</Modal>
)
};
export default AlertModal;
//Foo.js
import ...from ...;
const Foo = (props) => {
const { alertIsSuccess, alertHeaderText, alertBodyText, alertIsOpen, alertOnClosed, alertOnBtnPress, onBtnPress, ...rest } = props;
return (
<View style={}>
...
<View style={}>
...
</View>
<Button
onPress={() => onBtnPress()}
/>
<AlertModal
isSuccess={alertIsSuccess}
headerText={alertHeaderText}
bodyText={alertBodyText}
isOpen={alertIsOpen}
onClosed={alertOnClosed}
onBtnPress={alertOnBtnPress}
/>
</View>
)
};
export default QrCodeReader;
//FooContainer.js
import ... from ...;
class FooContainer extends Component {
constructor(props) {
super(props);
this.state = {
bar: false,
baz: true,
bac: '',
bax: false,
bav: '',
// Alert
alertIsOpen: false,
alertIsSuccess: false,
alertHeaderText: '',
alertBodyText: '',
alertOnClosed() {},
alertOnBtnPress() {},
};
this.onBtnPress = this.onBtnPress.bind(this);
}
alert(isSuccess, headerText, bodyText, onBtnPress, onClosed) {
const self = this;
this.setState({
alertIsOpen: true,
alertIsSuccess: isSuccess,
alertHeaderText: headerText,
alertBodyText: bodyText,
alertOnBtnPress: onBtnPress || function () { self.alertClose() },
alertOnClosed: onClosed || function () {},
});
}
alertClose() {
this.setState({
alertIsOpen: false,
});
}
onBtnPress() {
this.alert(
true,
'header text',
'body text',
)
}
render() {
return (
<Foo
onBtnPress={this.onBtnPress}
{...this.state}
/>
);
}
}
export default FooContainer;
As you can see it is a pain (and I think an incorrect way). Doing this way I would need to include AlertModal component in every component of my app where I need to display alerts = duplicate props and making new unnecessary <AlertModal /> components.
What is the correct way?
p.s. I use react-native-router-flux as a router in my app.
p.s.s. I am coming to React-native from a Meteor.js + Cordova. There I can just create one modal and include it in the main layout and show/hide it when necessary with the appropriate dynamic text inside it.
This is how I navigate in my app:
//Main.js
class Main extends Component {
render() {
return (
<Router backAndroidHandler={() => true}>
<Scene key="root">
<Scene key="login" type='reset' component={SignInContainer} initial={true} hideNavBar={true}/>
<Scene key="qrCode" type='reset' component={FooContainer} hideNavBar={true} />
</Scene>
</Router>
);
}
}
Based on the fact that you use react-native-router-flux I can suggest:
Rework AlertModal into scene component on is own, do not use Modal. You can control what to display in it by passing properties.
Include AlertModal component in your router as 'modal' schema like this:
import AlertModal from '../components/AlertModal';
//Main.js
class Main extends Component {
render() {
return (
<Router backAndroidHandler={() => true}>
<Scene key="root">
<Scene key="login" type='reset' component={SignInContainer} initial={true} hideNavBar={true}/>
<Scene key="qrCode" type='reset' component={FooContainer} hideNavBar={true} />
<Scene key="alertModal" duration={250} component={AlertModal} schema="modal" direction="vertical" hideNavBar={true} />
</Scene>
</Router>
);
}
}
And then you can just call it from any scene with:
Actions.alertModal({ >optional props you want to pass< });
Not sure if is desired behaviour but this kind of modals may be dismissed in Android with back button. If not, there is a way around it.
This might not be the best solution, but what you can do is use the context functionality of React. Essentially, pass a function reference that triggers your component to show/hide (maybe by changing its state, for instance) into the context. Then every child in the hierarchy should be able to call that function.
However, Facebook discourages using the context functionality, and it might give you some problems when debugging, since it is not as easy to track as your props/state.
Another solution that comes to mind might be playing with Redux, creating an action that changes a property to which the component is subscribed. Then all your other components can just dispatch that action, changing the value of the property that makes the modal component to display.
Related
I am developing an android app using react-native and i have a but of a struggle of figuring out why my component re-renders on each page navigation.
In my case i have created a wrapper component called Container.android.js
const Container = props => {
useEffect(() => {
// fetching some data from async storage
},[])
// Using my hook here
const {code, error} = usePdaScan({
onEvent: (code) => {
// setting state via useState
},
onError: (error) => {
Alert.alert('Error', error);
},
trigger: 'always'
});
return <View>
{props.children}
</View>
}
Then i declare routes with Stack.Screen where each stack uses a component wrapped with the Container component.
<Stack.Screen name="Overview" component={Overview} />
And my Overview compnent is this
const Overview = props => {
return <Container>
<Text>Overview page</Text>
</Container>
}
My problem is that inside the Container component, there is a hook called usePdaScan. Each time i navigate to another page and the hook gets called, the Container component re-renders twice... I cant get a lead on this... Halp!
UPDATE: My homepage is a class component where it seems to work ok (render only once)
class Home extends Component {
state = {
productBarcodes: null
}
componentDidMount(){
// get data fetches data from async storage
getData('#productBarcodes').then(res => {
this.setState({
...this.state,
productBarcodes: JSON.parse(res)
})
})
}
render() {
const { productBarcodes } = this.state;
return (
<Container {...this.props}>
<View style={{marginBottom: 20, flex:1}}>
{productBarcodes.length} Products
</View>
</Container>
)
}
}
The usePdaScan hook in the Container component gets called once on my Homepage but twice on every other page like Overview etc... Both Homepage and Overview are wrapped with my Container component
How do I properly type navigation passed as props to another component? As per the docs
Each screen component in your app is provided with the navigation prop automatically.
And also,
To type check our screens, we need to annotate the navigation prop and the route prop received by a screen.
type Props = NativeStackScreenProps<RootStackParamList, 'Profile'>;
I have a navigation component with router:
const App: React.FC = () => {
const [userMetrics, setUserMetrics] = useState<UserMetrics>(null);
const Stack = createNativeStackNavigator<RootStackParamList>();
return (
<UserMetricsContext.Provider value={{ userMetrics, setUserMetrics }}>
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Tests" component={Tests} />
</Stack.Navigator>
</NavigationContainer>
</UserMetricsContext.Provider>
);
};
And in Home screen I want to receive navigation prop to pass it down further to form which have a button that will navigate to Tests component and pass form data as params:
interface Props {
navigation: NativeStackScreenProps<RootStackParamList, "Home">;
}
export const Home: React.FC<Props> = ({ navigation }) => {
const { setUserMetrics } =
useContext<IUserMetricsContextType>(UserMetricsContext);
return (
<View style={styles.container}>
<StatusBar style="auto" />
<HealthCheckForm onSubmit={setUserMetrics} navigation={navigation} />
</View>
);
};
and now the problem starts to be visible in form component because typescript is assuming one level too much, I have typed the props like I did in the parent Home component like so:
interface Props {
onSubmit: React.Dispatch<React.SetStateAction<UserMetrics>>;
navigation: NativeStackScreenProps<RootStackParamList, "Home">;
}
and typescript wants me to use it like so:
const submitHandler = (data: UserMetrics) => {
onSubmit(data);
navigation.navigation.navigate("Tests", { userMetrics: data });
console.log(data);
};
This is not working however, the correct - working and navigating usage is
navigation.navigate("Tests", { userMetrics: data });
and when I navigate to the Tests component and pass the params along, I don't know how to receive them in Test component. I am trying to do it analogically like so:
interface Props {
navigation: NativeStackScreenProps<RootStackParamList, "Tests">;
}
export const Tests: React.FC<Props> = ({ navigation }) => {
const { userMetrics } =
useContext<IUserMetricsContextType>(UserMetricsContext);
console.log({ params: navigation.route.params });
return (
<View>
<DisplayList />
</View>
);
};
And I get yet another error about reading properties of undefined.
Thanks
There are solutions for nested Navigators (starting point here: https://reactnavigation.org/docs/nesting-navigators/). However, in this case I would suggest not making your HealthCheckForm have any awareness of the navigation state. Just pass it a standard onSubmit() prop and handle all the navigation within the Home component.
Also as a tip make sure to set up your RootStackParamList correctly so that the "tests" route is expecting {userMetrics: YourDataType}. Here is a random example of setting that up.
export type RootStackParamList = {
myRouteName: undefined;
tests: { userMetrics: MyDataType }; // TS now expects MyDataType on the props for tests route
terminalAndMate: {
departingTerminal: WSFTerminal;
arrivingTerminal: WSFTerminal;
};
...
I would also suggest typing your screen props this way instead of as an interface. NativeStackScreenProps can carry params as defined on the rootStackParamList:
type TestsScreenProps = NativeStackScreenProps<RootStackParamList, "tests">;
With those two changes, the tests screen should have props.route.params, which will contain MyDataType.
try: https://reactnavigation.org/docs/5.x/typescript/
-addition in regards to Seven's comment-
Take a look at the type declaration for NativeStackScreenProps.
export declare type NativeStackScreenProps<ParamList extends ParamListBase, RouteName extends keyof ParamList = string> = {
navigation: NativeStackNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
By making the interface as you did, you are saying the type of props for that component is
{ navigation: { navigation: NativeStackNavigation..etc , route: RouteProp }}
You can see that it is double nested and not necessary as the type provided by the library supports all of the functionality you need.
onsubmit
Your onSubmit function would look something like this:
//home.tsx
const onSubmit = (data: YourDataType) => {
props.navigation.navigate("tests", { userMetrics: data});
}
return (
//...stuff
<YourFormComponent onSubmit={onSubmit} />
This way all your navigation is handled by home, which is on the same 'level' as tests, and you keep your navigation a little cleaner.
Shouldn't need any useEffect calls.
I'm a bit new to React, and have been practicing by creating an application using the enums rendering method specified in this article.
However, I'm trying to apply it in a slightly different way than the article talks about, more specifically using it to conditionally render all of my website except for the <Nav /> based on the lastLinkClicked state. I've got different page classes for each condition as listed in the WEB_PAGES object.
Maybe I'm misunderstanding this method, since I don't have much experience with enums, but my pages aren't rendering correctly. Here's my code:
class App extends Component {
constructor(props){
super(props);
this.state = {
x: ...
y: ...
z: ...
lastClickedLink: 'home' //changes to 'new', 'settings', etc. using another function not listed here
}
}
render() {
function onLinkClick(link) {
const WEB_PAGES = {
home: <Home
x={this.state.x}
/>,
new: <NewPost />,
settings: <Settings />,
signup: <SignUp />,
login: <Login />
};
return (
<div>
{WEB_PAGES.link}
</div>
);
}
return (
<div>
<Nav
y={this.state.y}
z={this.state.z}
/>
{onLinkClick(this.state.lastClickedLink)}
</div>
);
}
}
export default App;
I removed some code for brevity's sake. The error I'm getting with this setup is TypeError: Cannot read property 'state' of undefined for home under the WEB_PAGES object.
I initially thought that this was pointing to the WEB_PAGES object, but changing this to App showed that state was undefined as well. I'm not really sure what to do at this point.
Is the enums conditional rendering method even doable on this scale? And if not, what other method would be the most ideal for this situation? Many thanks!
In javascript, When you create a function using function keyword it creates his own new scope and also creates default object this. So while you were trying to access this.state.x then it will not state property inside the function. It becomes this.undefined.x. so it is giving the error.
Whereas arrow function {(() => {})} does not create this object but create internal scope.
try following render method in your code:
render() {
return (
<div>
<Nav
y={this.state.y}
z={this.state.z}
/>
{((link) => {
const WEB_PAGES = {
home: <Home
x={this.state.x}
/>,
new: <NewPost />,
settings: <Settings />,
signup: <SignUp />,
login: <Login />
};
return (
<div>
{WEB_PAGES[link]}
</div>
);
})(this.state.lastClickedLink)}
</div>
);
}
use {WEB_PAGES[link]} when you try to use . it will not work
const Link = ({ lastClickedLink }) => {
const WEB_PAGES = {
home: <Home x={lastClickedLink} />,
new: <NewPost />,
settings: <Settings />,
signup: <SignUp />,
login: <Login />
};
return (
<div>
{WEB_PAGES[link]}
</div>
);
}
render() {
return (
<div>
<Nav
y={this.state.y}
z={this.state.z}
/>
<Link lastClickedLink={lastClickedLink} />
</div>
);
}
This variant has more readability and extensibility. Based on Shubham Batra example.
According to the docs on react-navigation you can call navigate from the top level component using the following:
import { NavigationActions } from 'react-navigation';
const AppNavigator = StackNavigator(SomeAppRouteConfigs);
class App extends React.Component {
someEvent() {
// call navigate for AppNavigator here:
this.navigator && this.navigator.dispatch(
NavigationActions.navigate({ routeName: someRouteName })
);
}
render() {
return (
<AppNavigator ref={nav => { this.navigator = nav; }} />
);
}
}
However I'm trying to figure out how can this be done if the logic that does the dispatch is in another component that is rendered on the same level as the navigator? In my case I create my navigators (A drawer with a stack navigator and other nested navigators) and then I render them using the <Drawer>. On the same level I'm loading my <PushController> component to handle push notifications. The pushcontroller actually gets the event that I want to dispatch on.
I can't figure out how to pass(?) the ref to the pushcontroller component so I can use it, currently the following isn't working. I get the console log telling me that the fcm.ACTION.OPEN_NOTIFICATION triggered but no dispatch occurs. I suppose it could be because the ref is created during a render and it isn't available to pass yet when the render occurs? But I'm also not sure you would do things this way in order to give another component access to a ref declared at the same level. Thanks for your help in advance!
Drawer + PushController rendering
render(){
return(
<View style={{flex: 1}}>
<Drawer ref={nav => { this.navigator = nav; }}/>
<PushController user={this.props.user} navigator={this.navigator}/>
</View>
)
}
PushController snippet:
import { NavigationActions } from 'react-navigation';
async doFCM() {
FCM.getInitialNotification().then(notif => {
console.log('Initial Notification', notif);
if(notif.fcm.action === "fcm.ACTION.OPEN_NOTIFICATION"){
console.log('fcm.ACTION.OPEN_NOTIFICATION triggered', notif);
this.props.navigator && this.props.navigator.dispatch(NavigationActions.navigate({routename: 'Chat'}))
}
});
}
Answer was to move the navigate call to a function defined at render and pass it to the component.
import { NavigationActions } from 'react-navigation';
...
//class definition omitted for brevity
render(){
const callNavigate = (routeName, params) => {
console.log('Params', params);
this.navigator.dispatch({
type: NavigationActions.NAVIGATE,
routeName: routeName,
params: params
})
}
return(
<View style={{flex: 1}}>
<Drawer ref={nav => this.navigator = nav }/>
<PushController callNavigate={callNavigate}/>
</View>
)
}
The function is called within PushController like this:
this.props.callNavigate('RouteName', params);
I’m using react native drawer of https://github.com/root-two/react-native-drawer
I am trying to call the method openDrawer() by passing in variable into NavigationBarRouteMapper. I tried logging inside NavigationBarRouteMapper, and it logs the variable passed in correctly. But when it used inside the NavigationBarRouteMapper, by clicking the Left Navigation button of ‘Open Drawer’, it does not do anything:
class drawerPractice extends Component {
...
openDrawer(){
this._drawer.open()
}
render() {
return (
<Drawer
content={<DrawerPanel/>}
openDrawerOffset={100}
ref={(ref) => this._drawer = ref}
type='static'
tweenHandler={Drawer.tweenPresets.parallax}
>
<Navigator
configureScene={this.configureScene}
initialRoute={{name: 'Start', component: Start}}
renderScene={this.renderScene}
style={styles.container}
navigationBar={
<Navigator.NavigationBar
style={styles.navBar}
routeMapper={NavigationBarRouteMapper(this.openDrawer)}
/>
}
/>
</Drawer>
);
}
}
var NavigationBarRouteMapper = openDrawer => ({
LeftButton(route, navigator, index, navState){
return(
<TouchableHighlight onPress={()=>{openDrawer}}>
<Text>Open Menu</Text>
</TouchableHighlight>
)
}
},...
Why may be the issue?
On your constructor() method add this: this.openDrawer = this.openDrawer.bind(this);
You're likely using this on the wrong scope.
There is a common confusion when working with React components and the new ES6 extends syntax. If you use React.createClass, it will bind this to all of your functions, but when using the ES6 approach of extends React.Component you have to bind your functions manually.
You can do it either inline using
<TouchableHighlight onPress={()=>{this.openDrawer.bind(this)}}>
Alternatively, you can add to your constructor, after super():
this.openDrawer = this.openDrawer.bind(this);
Personally I like this approach as I find this code a bit easier to read.
For more information about the ES6 way, check this link.