React Native Screen refresh from nested component screen - react-native

I have a screen, PDP, that screen contains a component, TopNews. I want TopNews onclick to redraw PDP (it passes in an Article ID which the PDP uses to retrieve the article). The diagram below shows the flow
The code I have to support this inside TopNews is;
<TouchableOpacity onPress={() => navigation.navigate('Pdp', {articleid: item.id})}>
The challenge is the TouchableOpacity event triggers, but the page doesn't refresh the PDP. I don't want to refresh only the PDP as I may include the TopNews component in other screens outside of the PDP, its just in this case its inside of the screen it needs to call.

There are more then one way to do it.
You can create a state for the article id and allow the TopNews to set this state:
const PDP = () => {
const [ id, setId ] = React.useState(0);
return (
<div>
<ArticleComponent articleId={id} />
<TopNews idSetter={setId} />
</div>
);
}
const ArticleComponent = ({ articleId }) => {
return /* something that renders the article by the id */ ;
}
const TopNews = ({ idSetter }) => {
return (
<TouchableOpacity
onPress={() => {
idSetter(item.id);
navigation.navigate('Pdp')
}}>
);
}
Another more elegant way, is to use react contexts. You can create a context provider in the PDP component and change the context data in any PDP's children component.

I found the answer, instead of using navigation.navigate, use navigation.push instead;
<TouchableOpacity onPress={() => navigation.push('Pdp', {articleid: item.id})}>
This works perfectly :)

Related

Child component not rerendered on prop change

In the following code, I expect OfferList to rerender when I add an offer item to the store. OfferList itself is not an observable, but the offer array is passed as a prop.
export const MerchantScreen: FC = observer(() => {
const { merchantStore } = useStores()
return (
<View>
<OfferList data={merchantStore.offers} />
<View>
<Button title={"New Offer"} onPress={() => merchantStore.addOffer()}/>
</View>
</View>
)
})
export const OfferList: FC<OfferListProps> = ({ data }: OfferListProps) => {
const renderItem = (offer: ListRenderItemInfo<any>) => {
return (
<Text>{offer.name}</Text>
)
}
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
)
}
I use Mobx State Tree. All merchantStore.addOffer() does for now is push another offer item into the array.
What I tried / findings:
When I read from the store in my MerchantScreen, e.g. by adding
<Text>{ merchantStore.offers.toString() }</Text>
, the OfferList will also update. I suspect that reading from the store directly in the parent component will force a rerender of the child component as well.
I stumbled upon some answers here that would indicate that a missing key attribute within the FlatList renderItems could be the issue. Tried using key={item.id} to no avail. Also, as you can see I use the keyExtractor prop of FlatList.
Another answers suggested introducing local state to the component like this:
export const OfferList: FC<OfferListProps> = ({ data }: OfferListProps) => {
const [offers, setOfferss] = useState()
useEffect(() => {
setOffers(data)
}, [data])
const renderItem = (offer: ListRenderItemInfo<any>) => {
return (
<Text>{offer.name}</Text>
)
}
return (
<FlatList
data={offers}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
)
}
This does not work and my gutfeeling is that this is not how it's done.
As you see my MerchantScreen parent component is an observable while my child component OfferList is not. From my expectation, the observer on my parent component should be enough. The parent component should already detect the change in the store and rerender. The child component in itself does not even use stores.
Overall, the problem at hand seems quite trivial so I guess I am just missing out on an important detail.
MobX only tracks data accessed for observer components if they are directly accessed by render, so if you want to react to each of the offers you need to access them somewhere. You sort of did when you tried merchantStore.offers.toString(), that's why it worked.
So first of all you need to make OfferList an observer.
But then you have FlatList which is native component and you can't make it an observer. What you can do is to access each offers item inside OfferList (just to subscribe for updates basically) like that data={offers.slice()} or even better with MobX helper method toJS data={toJS(offers)}
Depending on your use case you might also want to use <Observer> inside renderItem callback:
const renderItem = (offer: ListRenderItemInfo<any>) => {
return (
<Observer>{() => <Text>{offer.name}</Text>}</Observer>
)
}

access `headerRight` inside a screen that is hosted by stack navigator

I have created a stack navigator:
import {createStackNavigator} from '#react-navigation/stack';
const TheStack = createStackNavigator();
Then, This is my navigator, it claimed component={LandingScreen}:
<TheStack.Navigator ...>
<TheStack.Screen
name="LandingScreen"
component={LandingScreen}
options={{
title: '',
headerLeft: null,
headerRight: () => (
<MyHeaderRightComponent />
),
}}
/>
<TheStack.Navigator>
As you can see above in options of the screen, there is headerRight, I have declared using MyHeaderRightComponent as headerRight so that it is shown on the right side of the header on screen.
Here is my LandingScreen.js :
import React, {useState} from 'react';
import {View, StyleSheet} from 'react-native';
const LandingScreen = ({navigation}) => {
// How can I access the `headerRight` component I have set above from here?
...
}
My question is how can I access the headerRight inside my LandingScreen.js? I know I can update or reset the headerRight by:
navigation.setOptions({headerRight:() => <NewHeaderRightComponent/>})
But now what I need is to access the previous already set component, not setting a new one. How to do that?
Edits to the answer as per the request received in comments. The answer is the same. This is just further demonstration on how to use it.
// The screen component where you want to pass the state.
const Screen = (props) => {
const [color, setColor] = useState("#CCCCCC");
const { navigation } = props //This is important or else UseEffect will be called each time any of the props change
useEffect(() => {
navigation.setParams({ color: color }); // Where its being passed.
}, [color, navigation]);
return (
<>
<Button onPress={() => setColor("#800000")} /> // Change the color state to Maroon
<Button onPress={() => setColor("#FED700")} /> // Change the color state to Gold
</>
)
}
Your Header Component:
const MyHeaderComponent = (props) {
return(
<View style={{ backgroundColor: props.bgColor }} />
)
}
Then you can retrieve this bit in headerRight. Like this:
headerRight:() => <MyHeaderComponent bgColor={route.params.color} />
Note: This method is valid for React Navigation v5. Version 4 has a getParams() function to retrieve the params, but it was dropped in Version 5.
Original Answer
You can create a useState hook in the screen and pass its value into your header component. So, when the header component updates the state, it can be accessed from within the screen where you have defined the state.
you can use setParams() function to set the params you want to use in the header component. Then, use route.params.nameofyourprop to get them in the headerComponent, where you can consume it.
This is to pass params from outside the header to inside of it.
headerRight:() => <MyHeaderRightComponent propname={route.params.propvalue} />
This to to set the Params from outside your header which you can access inside the headerRight component.
const [values, setValue] = useState()
navigation.setParams({propname: value})
This way you can pass state between the header and the screen.
You can also pass the setValue function of the useState in this manner, but it will throw a warning because functions are objects in Javascript and thus its not possible to index them... or something on those lines.

React navigation doesn't refresh the page when parameter changed

I'm using react navigation to navigate between pages.
I have 2 pages. Let's call them A and B. I have some cards on page A and has touchable opacities on cards to navigate to page B with a different id (using this id for getting data from the server).
<TouchableOpacity style={styles.button} onPress={() => this.props.navigation.navigate('pageB',{id:this.props.id})} >
<Text>PageB</Text>
</TouchableOpacity>
It works well on first navigate. But when I navigate back to page A (by using side menu) and select another navigation button to navigate page B (different parameter) it navigates me to the same page with first navigate.
I have tried to use navigation key like that but I guess I could do that;
this.props.navigation.navigate('pageB',{id:this.props.id}, this.props.id)}
You have to set the new Parameter like this:
static navigationOptions = ({ navigation }) => {
let value = navigation.getParam("value");
return {
title: name,
headerRight: () => (<Button title="Edit" onPress={() => navigation.navigate("Edit", { val: value})} />),
};
And set somewhere always the new parameter like this:
let value = this.state.value;
this.props.navigation.setParams({value});

React - native StackNavigator refresh previous component after hitting back button

I'm stuck for weeks on this problem that I can't solve. I have been through a lot of different solution and none of them is working..
Let-s say I have component A and component B
Component A : contains a flatlist, when click on one item leads me to component B.
Component B: contains details of component A items, when clicking on a button update data, data that component A is using in state current, which make the clicked items turn to orange. I would like when hitting back button to update those data in component A and actually have the clicked items in orange, right now nothing is happening..
Component A :
async interventionList(){
const planningList = await getInterventionList(this.state.userToken, '2019-02-27', '2019-02-27');
this.setState({list: planningList});
}
renderItem = ({ item }) => (
<View>
<TouchableHighlight onPress={
() => this.props.navigation.navigate('InterventionStart', { getIntervention: () => this.interventionsList() })}>
<View style={[styles.block, this.state.current !== null && this.state.current.remoteInterventionId === item.num ? styles.began : styles.notbegan]}>
<Text> {SomeData} <Text>
</View>
</View>
</TouchableHighlight>
</View>
)
Component B :
componentWillUnmount() {
this.props.navigation.state.params.getIntervention();
}
But nothing happens. I tried to console log, to see if those functions where called after clickin on back button and they are... So I don't understand what I am doing wrong or is it not the way to do this at all ?
You can use willFocus method of react-navigation
in your componentDidMount or in your constructor
this.willFocus = this.props.navigation.addListener('willFocus', () => {
this.interventionList();
});
It will call your interventionList method whenever your component is about to be focused. Make sure you remove the listener on your componentWillUnmount
this.willFocus.remove();
For reference
https://reactnavigation.org/docs/en/navigation-events.html

Opening context menu on long press in React Native

I'd like to have a context menu triggered on long press different places using React Native.
I.e. in a dialer like the default dailer. You can long-click on any contact and get a 'copy number' menu. And also you can long-click on the name of the person once you've opened their 'contact card'.
The straight-forward way needs a lot of copy-pasted boilerplate, both components and handlers.
Is there a better pattern for doing this?
All Touchable components (TouchableWithoutFeedback, TouchableOpacity etc.) has a property called onLongPress. You can use this prop to listen for long presses and then show the context menu.
To eliminate code mess and doing lots of copy paste you can separate your context menu as a different component and call it when the long press happen. You can also use an ActionSheet library to show the desired options. React native has a native API for iOS called ActionSheetIOS. If you get a little bit more experience in react and react-native you can create a better logic for this but I'm going to try to give you an example below.
// file/that/contains/globally/used/functions.js
const openContextMenu = (event, user, callback) => {
ActionSheetIOS.showActionSheetWithOptions({
options: ['Copy Username', 'Call User', 'Add to favorites', 'Cancel'],
cancelButtonIndex: [3],
title: 'Hey',
message : 'What do you want to do now?'
}, (buttonIndexThatSelected) => {
// Do something with result
if(callback && typeof callback === 'function') callback();
});
};
export openContextMenu;
import { openContextMenu } from './file/that/contains/globally/used/functions';
export default class UserCard extends React.Component {
render() {
const { userObject } = this.props;
return(
<TouchableWithoutFeedback onLongPress={(event) => openContextMenu(event, userObject, () => console.log('Done')}>
<TouchableWithoutFeedback onLongPress={(event) => openContextMenu(event, userObject, () => console.log('Done'))}>
<Text>{userObject.name}</Text>
<Image source={{uri: userObject.profilePic }} />
</TouchableWithoutFeedback>
</TouchableWithoutFeedback>
);
}
}
Similarly as the previous answer combine onLongPress with imperative control for popup menu - something like
<TouchableWithoutFeedback onLongPress={()=>this.menu.open()}>
<View style={styles.card}>
<Text>My first contact name</Text>
<Menu ref={c => (this.menu = c)}>
<MenuTrigger text="..." />
<MenuOptions>
// ...
</MenuOptions>
</Menu>
</View>
</TouchableWithoutFeedback>
When it comes to a lot of boilerplate - in React you can do your own components that you can reuse everywhere thus reducing boilerplate (and copy&paste)
See full example on https://snack.expo.io/rJ5LBM-TZ