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);
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
I'm trying to avoid re-rendering unnecessary components
For example:
const[ValueState,SetValueState]=useState(5); //hook
<View>
<Text>{ValueState}</Text>
<View>
{
[...Array(3)].map((index,el)=>{
return (<View><Text>Hello there</Text></View>)
})
}
</View>
</View>
here every time I change the value of ValueState the entire map() segment also gets re-rendered
how do I avoid this and make the map() segment only render 1 time?
It depends on what the function you don't want to re-render depends on. In this case, your array and map function do not depend directly to ValueState.
One way to achieve 1 re-render is using React.memo
Example to render map function only once
import React, { useState } from "react";
import { View, Text, TouchableOpacity } from "react-native";
const ArrayMapSection = React.memo(()=> {
console.log("ArrayMapSection rendered")
return [...Array(3)].map((index,el)=>{
return (<View><Text>Hello there</Text></View>)
});
})
const App = () => {
const [ValueState,SetValueState]=useState(5); //hook
return(
<View>
<Text>{ValueState}</Text>
<TouchableOpacity onPress={()=>SetValueState(Math.random())}>Press to state update</TouchableOpacity>
<View>
<ArrayMapSection />
</View>
</View>
)
};
export default App;
If you run this program, you would see ArrayMapSection renderedonly once in the console. Now try to change the ValueState by pressing on Press to state update. The ArrayMapSection won't re-render because React.memo only re-renders if props changes
More info: https://reactjs.org/docs/react-api.html#reactmemo
Create a custom react component that takes the array as a prop and maps it to other components.
That way the component is only rerenderd if the array prop changes.
Code example:
const[ValueState,SetValueState]=useState(5);
<View>
<Text>{ValueState}</Text>
<CustomList array={[1,2,3]} />
</View>
export const CustomList = (array) => {
return (
<>
{
array.map((index,el)=>{
return (<View><Text>Hello there</Text></View>)
})
}
<\>
)
}
I'm trying to cascade a state change from a parent component down to a child.
export default class App extends React.Component {
constructor(props) {
super(props);
this.listUpdater = new_list_updater();
this.state = {
schoollist: this.listUpdater.update_list(),
}
}
listUpdateCallback = () => {
console.log("Callback triggered!");
console.log(this.state.schoollist.slice(1,3));
this.setState({schoollist: this.listUpdater.update_list()});
console.log(this.state.schoollist.slice(1,3));
}
render() {
return (
<SafeAreaView style={styles.container}>
<Header updateCallback={this.listUpdateCallback}/>
<SchoolList school_list={this.state.schoollist}/>
</SafeAreaView>
);
}
}
listUpdater.update_list() is a method in a class that implements getISchools and ShuffleArray and stores the list of schools that are being shuffled. It returns the shuffled list of schools.
import { shuffleArray } from './Shuffle'
import { getISchools } from './iSchoolData'
class ListUpdater{
constructor() {
console.log("Initiating class!");
this.currentSchoolList = [];
}
update_list() {
console.log("Updating list!");
if (this.currentSchoolList.length == 0){
this.currentSchoolList = getISchools();
}
else{
shuffleArray(this.currentSchoolList);
}
return(this.currentSchoolList);
}
}
export function new_list_updater(){
return new ListUpdater();
}
As far as I can tell everything works. When I press a refresh button in the Header component, it triggers the updateCallback, which updates the list stored in the state variable (verified by logging to console and ComponentDidUpdate()
This is the Component not refreshing:
import React from 'react';
import { StyleSheet, Text, View, SafeAreaView, FlatList } from 'react-native';
export default class SchoolList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<SafeAreaView style={styles.listArea}>
<FlatList
data = {this.props.school_list}
renderItem = {({item}) =>
<View style={styles.row}>
<View style={styles.num_area}>
<Text style={styles.num_text}>{item.key}</Text>
</View>
<View style={styles.text_area}>
<Text style={styles.univ_text}>{item.univ}</Text>
<Text style={styles.school_text}>{item.school}</Text>
</View>
</View>
}
/>
</SafeAreaView>
);
}
componentDidUpdate(){
console.log("SchooList Updated!");
}
}
The flow I'm expecting is:
Parent passes updateCallback reference to Header (child)
Refresh button in Header triggers updateCallback in Parent
updateCallback in Parent updates state with setState
Parent and relevant children that use state variable re-render, displaying new list
1-3 appear to be working, 4 is not!
Maybe your componenet is not re-rendering when you use setState for some reason. Try adding a warn in the render method to check this. I also noticed you are mutating the array this.currentSchoolList, winch is passade as reference for your state (all objects are passed as refence). Try replaceing this making a copy of the array beforing calling shuffleArray(this.currentSchoolList).
You can copy the array this way (this is ES6 sintax): newArray = [...oldArrray];
Or using other methods.
Suppose I have two components which aren't nested: a button and a panel. When the button is clicked, the panel will show or hide depending on the previous state (like an on/off switch). They aren't nested components, so the structure looks like this:
<div>
<Toolbar>
<Button />
</Toolbar>
<Content>
...
<ButtonPanel />
</Content>
</div>
I can't change the structure of the DOM. I also can't modify any other component other than the button and panel components.
The Button and ButtonPanel components are related, however, and will be used together throughout the solution. I need to pass a property to the panel to let it know when to show or when to hide. I was thinking about doing it with Context API, but I think there's something I'm doing wrong and the property never updates.
This is my code:
Context
import React from 'react';
export const ButtonContext = React.createContext({
showPanel: false,
});
Button
import React, { Component } from 'react';
import { ButtonContext } from './ButtonContext';
class Button extends Component {
constructor() {
super();
this.state = {
showPanel: false,
};
}
render() {
return (
<ButtonContext.Provider value={{ showPanel: this.state.showPanel }}>
<li>
<a
onClick={() => this.setState({ showPanel: !this.state.showPanel }, () => console.log('Changed'))}
>
<span>Button</span>
</a>
</li>
</ButtonContext.Provider>
);
}
}
export { Button };
Panel
import React, { Component } from 'react';
import { Panel, ListGroup, ListGroupItem } from 'react-bootstrap';
import { ButtonContext } from './ButtonContext';
class ButtonPanel extends Component {
static contextType = ButtonContext;
render() {
return (
<ButtonContext.Consumer>
{
({ showPanel }) => {
if (showPanel) {
return (
<Panel id="tasksPanel">
<Panel.Heading >Panel Heading</Panel.Heading>
<ListGroup>
<ListGroupItem>No Items.</ListGroupItem>
</ListGroup>
</Panel>
);
}
return null;
}
}
</ButtonContext.Consumer>
);
}
}
export { ButtonPanel };
I've also tried simply accessing the context in the ButtonPanel component like so:
render() {
const context = this.context;
return context.showPanel ?
(
<Panel id="tasksPanel">
<Panel.Heading >Tasks</Panel.Heading>
<ListGroup>
<ListGroupItem className="tasks-empty-state">No tasks available.</ListGroupItem>
</ListGroup>
</Panel>
)
:
null;
}
What am I doing wrong?
Thanks
From the React docs:
Accepts a value prop to be passed to consuming components that are descendants of this Provider.
So this means that <ButtonContext.Provider> has to wrap <ButtonContext.Consumer> or it has to be higher up in the component hierarchy.
So based on your use case, you could do:
// This app component is the div that wraps both Toolbar and Content. You can name it as you want
class App extends Component {
state = {
showPanel: false,
}
handleTogglePanel = () => this.setState(prevState => ({ togglePanel: !prevState.togglePanel }));
render() {
return (
<ButtonContext.Provider value={{ showPanel: this.state.showPanel, handleTogglePanel: this.handleTogglePanel }}>
<Toolbar>
<Button />
</Toolbar>
<Content>
<ButtonPanel />
</Content>
</ButtonContext.Provider>
);
}
}
class Button extends Component {
...
<ButtonContext.Consumer>
{({ handleTogglePanel }) => <a onClick={handleTogglePanel} />}
</ButtonContext.Consumer>
}
class ButtonPanel extends Component {
...
<ButtonContext.Consumer>
{({ showPanel }) => showPanel && <Panel>...</Panel>}
</ButtonContext.Consumer>
}
I am trying to capture all click events outside of my SearchBar component so that I can then tell the dropdown menu to close when one clicks out of it. I looked up examples of how to do this online and I need to use the global variable 'document' in javascript. However, it seems react native does not support this. Does anyone know a work around to use the 'document' variable or a react native equivalent?
class Products extends Component {
constructor(props) {
super(props);
this.setWrapperRef = this.setWrapperRef.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
setWrapperRef(node) {
this.wrapperRef = node;
}
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
alert('You clicked outside of me!');
}
}
componentWillMount() {
this.props.dispatch(getProductList());
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
}
render() {
const {isLoading, products} = this.props.products;
if (isLoading) {
return <Loader isVisible={true}/>;
}
return (
<View ref={this.setWrapperRef} style={styles.wrapper}>
<Header/>
<View style={styles.bodyWrapper}>
<ScrollView style={styles.scrollView}>
<ProductsContainer data={{productsList: { results: products }}}/>
</ScrollView>
<SearchBar style={styles.searchBar}/>
</View>
<Footer/>
</View>
);
}
}
function mapStateToProps(state) {
const {products} = state;
return {
products
};
}
export default connect(mapStateToProps)(Products);
You can't use document, it's an object on the window. The above answer is incorrect and hasn't taken into account this platform is React Native (answer has since been removed).
To handle click events, you you need to wrap everything in a TouchableWithoutFeedback.
<TouchableWithoutFeedback
onPress={this.hideSearchBar}
/>
I would add a zIndex style to the TouchableWithoutFeedback and one in styles.scrollView. Make sure the zIndex inside of styles.scrollView is more than the one you added to the TouchableWithoutFeedback.