Passing function as a param in react-navigation 5 - reactjs

NOTE: This query is for react-navigation 5.
In react navigation 4 we could pass a function as a param while navigating but in react navigation 5, it throws a warning about serializing params.
Basically, what I am trying to do is, navigate to a child screen from parent screen, get a new value and update the state of the parent screen.
Following is the way I am currently implementing:
Parent Screen
_onSelectCountry = (data) => {
this.setState(data);
};
.
.
.
<TouchableOpacity
style={ styles.countrySelector }
activeOpacity={ 0.7 }
onPress={ () => Navigation.navigate("CountrySelect",
{
onSelect: this._onSelectCountry,
countryCode: this.state.country_code,
})
}
>
.
.
.
</TouchableOpacity>
Child Screen
_onPress = (country, country_code, calling_code) => {
const { navigation, route } = this.props;
navigation.goBack();
route.params.onSelect({
country_name: country,
country_code: country_code,
calling_code: calling_code
});
};

Passing a callback through react native navigation params is not recommended, this may cause the state to freeze (to not to update correctly). The better solution here would be using an EventEmitter, so the callback stays in the Screen1 and is called whenever the Screen2 emits an event.
Screen 1 code :
import {DeviceEventEmitter} from "react-native"
DeviceEventEmitter.addListener("event.testEvent", (eventData) =>
callbackYouWantedToPass(eventData)));
Screen 2 code:
import {DeviceEventEmitter} from "react-native"
DeviceEventEmitter.emit("event.testEvent", {eventData});
useEffect(() => {
return () => {
DeviceEventEmitter.removeAllListeners("event. testEvent")
};
}, []);

Instead of passing the onSelect function in params, you can use navigate to pass data back to the previous screen:
// `CountrySelect` screen
_onPress = (country, country_code, calling_code) => {
const { navigation, route } = this.props;
navigation.navigate('NameOfThePreviousScreen', {
selection: {
country_name: country,
country_code: country_code,
calling_code: calling_code
}
});
};
Then, you can handle this in your first screen (in componentDidUpdate or useEffect):
componentDidUpdate(prevProps) {
if (prevProps.route.params?.selection !== this.props.route.params?.selection) {
const result = this.props.route.params?.selection;
this._onSelectCountry(result);
}
}

There is a case when you have to pass a function as a param to a screen.
For example, you have a second (independent) NavigationContainer that is rendered inside a Modal, and you have to hide (dismiss) the Modal component when you press Done inside a certain screen.
The only solution I see for the moment is to put everything inside a Context.Provider then use Context.Consumer in the screen to call the instance method hide() of Modal.

Related

How to keep on adding the component on react state change.?

I have a simple requirement where there is a successBar component
<successBar msg={"message"}/>
this one I want to insert to the dom on some state change. lets say we have a state,
successMessage, if we set a message in this state, then we need to how the success bar
like:-
{
this.state.successMessage && <SuccessBar msg={this.state.successMessage} />
}
the above code works,
here the main requirement is when the successMessage is updated it should add a new <Successbar /> and not replace the previous, bar.
and I don't want to track the previous success msg.
the <SuccessBar /> component will dismiss itself after a few seconds.
how to achieve this, in react.
You can have an Array with the SuccessBar components you want to render, and push to that array on every successMessage change.
Like this:
import { useState, useEffect } from "react";
function SuccessBar({ msg }) {
return <div>{msg}</div>;
}
export default function App() {
const [successMessage, setSuccessMessage] = useState("");
const [successComponents, setSuccessComponents] = useState([]);
useEffect(() => {
if (successMessage) {
setSuccessComponents((prevArray) => [
...prevArray,
<SuccessBar msg={successMessage} />
]);
}
}, [successMessage]);
return (
successComponents.map((Component) => Component)
);
}

State updating only after second onPress

I'm building a React Native app which renders a flatlist of groups. By pressing on one group, you get to see all the individual items nested within.
I'm using the following this navigation library and I'm passing props as such:
const navigateToDetails = useCallback(() => {
Navigation.push(props.componentId, {
component: {
name: pagesNames.DETAILS,
passProps: {
groupId: selectedGroup,
},
},
});
});
All of this is within a functional component which has a couple more relevant things, to begin with we have the state and function that gets us selectedGroup:
const [selectedGroup, setSelectedGroup] = useState(Number);
function handleTouch(value) {
setSelectedGroup(value);
console.log('value: ' + value);
navigateToDetails(value);
}
value is taken from an item in a Flatlist which gets its data from this fake API.
Value is retrieved from the list in this fashion:
<FlatList
data={data}
renderItem={({ item }) => (
<TouchableOpacity
style={[styles.card, { width: windowWidth - 30 }]}
onPress={() => handleTouch(item.id)}>
{misc components}
</TouchableOpacity>
)}
/>
Expected Behaviour
I'm expecting that when I tap an item from the list, it'd navigate to the Details page and populate it with its data. However this does not happen. Upon logging out value and selectedGroup, I find that value is always logged out correctly but selectedGroup is always first logged out as 0.
Only after a second press does it show the expected id and populate the Details page.
You are redirected before state is set. initialize selectedGroup with null/undefined/0
try navigating in useEffect
function handleTouch(value) {
setSelectedGroup(value);
console.log('value: ' + value);
}
useEffect(() => {
if (selectedGroup) {
navigateToDetails(selectedGroup)
}
}, [selectedGroup, navigateToDetails])

Programmatic navigation in Ionic React using ExploreContainer template

I'm using the side menu template from Ionic React, so most of my logic is in ExploreContainer.tsx .
//imports
//some logic
const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
switch( name ) { //switch to show content on requested item from side menu
case 'Home':
return (
//content for home
);
case 'Info':
return (
//content for info
);
}
};
export default ExploreContainer;
Now, since I'm building this for mobile, I'm using Capacitor. This enables me to listen for hardware back button events. When the user presses their back button, I want them to back taken back to 'Home' in this example.
All answers I have found kind of always refer to buttons for navigation, where it's possible to declare a custom state and maybe use the <Redirect /> component.
Now, to extend the former example, lets say I want something like this:
//imports
import { Plugins } from '#capacitor/core';
const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
Plugins.App.addListener("backButton", () => {
name = "Home";
}
switch( name ) { //switch to show content on requested item from side menu
case 'Home':
return (
//content for home
);
case 'Info':
return (
//content for info
);
}
};
export default ExploreContainer;
Sadly, this does not work. Previously I've tried the following:
import { useHistory } from 'react-router-dom';
Plugins.App.addListener("backButton", () => {
history.push("/page/Home");
});
This however leads to some really ugly behavior. The app completely reloads, all variables are reset, etc.
If I understand correctly, the name property of the ExploreContainer is a state that gets changed when the user selects an item from the menu, so normally the app doesn't need to reload.
How would I go about this?
import { useHistory } from 'react-router-dom';
const history = useHistory();
Plugins.App.addListener("backButton", () => {
history.push("/page/Home");
});

How can I display a Persona component in a CommandBar in React Fabric-UI?

I am trying to display a Persona component on the far right of my CommandBar component, which I use as a header for my application.
Here's a code snippet
const getFarItems = () => {
return [
{
key: 'profile',
text: <Persona text="Kat Larrson" />,
onClick: () => console.log('Sort')
}
]
}
const FabricHeader: React.SFC<props> = () => {
return (
<div>
<CommandBar
items={getItems()}
farItems={getFarItems()}
ariaLabel={'Use left and right arrow keys to navigate between commands'}
/>
</div>
);
}
This throws a type error because the text prop expects a string and not a component. Any help would be appreciated!
Under the ICommandBarItemProps there is a property called commandBarButtonAs that the docs state:
Method to override the render of the individual command bar button.
Note, is not used when rendered in overflow
And its default component is CommandBarButton which is basically a Button
Basically there are two ways to do this.
Keep using Button, and apply your own renderer. Basically the IButtonProps you can add onRenderChildren which would allow you to add any Component such as Persona to render. This example would show you how it is done https://codepen.io/micahgodbolt/pen/qMoYQo
const farItems = [{
// Set to null if you have submenus that want to hide the down arrow.
onRenderMenuIcon: () => null,
// Any renderer
onRenderChildren: () => ,
key: 'persona',
name: 'Persona',
iconOnly: true,
}]
Or add your own crazy component not dependent on CommandBarButton but that means you need to handle everything like focus, accessibility yourself. This example would show you how it is done https://codepen.io/mohamedhmansour/pen/GPNKwM
const farItems = [
{
key: 'persona',
name: 'Persona',
commandBarButtonAs: PersonaCommandBarComponent
}
];
function PersonaCommandBarComponent() {
return (
);
}

Filtering an icon from an array of icon strings for re-render

I'm trying to take an e.target.value which is an icon and filter it out from an array in state, and re-render the new state minus the matching icons. I can't seem to stringify it to make a match. I tried pushing to an array and toString(). CodeSandbox
✈ ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
Here is the code snippet (Parent)
removeMatches(icon) {
const item = icon;
const iconsArray = this.props.cardTypes;
const newIconsArray =iconsArray.filter(function(item) {
item !== icon
})
this.setState({ cardTypes: newIconsArray });
}
This is a function in the parent component Cards, when the child component is clicked I pass a value into an onClick. Below is a click handler in the Child component
handleVis(e) {
const item = e.target.value
this.props.removeMatches(item)
}
First of all, there's nothing really different about filtering an "icon" string array from any other strings. Your example works like this:
const icons = ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
const icon = "✈";
const filteredIcons = icons.filter(i => i !== icon);
filteredIcons // ["♘", "♫", "♫", "☆", "♘", "☆"]
Your CodeSandbox example has some other issues, though:
Your Card.js component invokes this.props.removeMatches([item]) but the removeMatches function treats the argument like a single item, not an array.
Your Cards.js removeMatches() function filters this.props.cardTypes (with the previously mentioned error about treating the argument as a single item not an array) but does not assign the result to anything. Array.filter() returns a new array, it does not modify the original array.
Your Cards.js is rendering <Card> components from props.cardTypes, this means that Cards.js is only rendering the cards from the props it is given, so it cannot filter that prop from inside the component. You have a few options:
Pass the removeMatches higher up to where the cards are stored in state, in Game.js as this.state.currentCards, and filter it in Game.js which will pass the filtered currentCards back down to Cards.js.
// Game.js
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
// ...
<Cards cardTypes={this.state.currentCards} removeMatches={this.removeMatches} />
// Cards.js
<Card removeMatches={this.props.removeMatches}/>
// Card.js -- same as it is now
Move Cards.js props.cardTypes into state (ex state.currentCards) within Cards.js, then you can filter it out in Cards.js and render from state.currentCards instead of props.cardTypes. To do this you would also need to hook into componentWillReceiveProps() to make sure that when the currentCards are passed in as prop.cardTypes from Game.js that you update state.currentCards in Cards.js. That kind of keeping state in sync with props can get messy and hard to follow, so option 1 is probably better.
// Cards.js
state = { currentCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ currentCards: nextProps.cardTypes });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
render() {
return (
<div>
{ this.state.currentCards.map(card => {
// return rendered card
}) }
</div>
);
}
Store all the removed cards in state in Cards.js and filter cardTypes against removedCards before you render them (you will also need to reset removedCards from componentWillReceiveProps whenever the current cards are changed):
// Cards.js
state = { removedCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ removedCards: [] });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
removedCards: [...prevState.removedCards, ...items]
}));
}
render() {
const remainingCards = this.props.cardTypes.filter(card => {
return this.state.removedCards.indexOf(card) < 0;
});
return (
<div>
{ remainingCards.map(card => {
// return rendered card
})}
</div>
);
}
As you can see, keeping state in one place in Game.js is probably your cleanest solution.
You can see all 3 examples in this forked CodeSandbox (the second 2 solutions are commented out): https://codesandbox.io/s/6yo42623p3

Resources