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

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.

Related

Is it possible to wait for a component to render? React Testing Library/Jest

I have a component. It has a button. Upon pressing the button, I am changing the style of the button text (color) using setState function. When I am testing the changed component, the test is failing because the change happens asynchronously. I want to do something as is given here (https://testing-library.com/docs/dom-testing-library/api-async/)
const button = screen.getByRole('button', { name: 'Click Me' })
fireEvent.click(button)
await screen.findByText('Clicked once')
fireEvent.click(button)
await screen.findByText('Clicked twice')
But rather than waiting for the text to change. I want to wait for the text color to change. Thanks
This is the code for my button
<Button onPress = {() => {this.setState({state : 1});}}>
<Text style = {style}>Button Text</Text>
</Button>
So when this button is pressed. state is set to 1. And in render :
if(this.state.state === 1) style = style1
else style = style2;
But it can be seen from logs that render is called after the test checks for the styles. So How can I wait for the render to complete before checking if the font color has been changed?
Here is the testing code
test('The button text style changes after press', () => {
const {getByText} = render(<Component/>);
fireEvent.press(getByText('button'));
expect(getByText('button')).toHaveStyle({
color : '#ffffff'
});
})
It looks like you have a custom button, not a native button. I'm guessing your component is something like this:
import React from "react";
import {Text, TouchableOpacity} from "react-native";
const Button = ({pressHandler, children}) => (
<TouchableOpacity onPress={pressHandler}>
{children}
</TouchableOpacity>
);
const ColorChangingButton = ({text}) => {
const [color, setColor] = React.useState("red");
const toggleColor = () => setTimeout(() =>
setColor(color === "green" ? "red" : "green"), 1000
);
return (
<Button pressHandler={toggleColor}>
<Text style={{color}}>{text}</Text>
</Button>
);
};
export default ColorChangingButton;
If so, you can test it with waitFor as described here:
import React from "react";
import {
fireEvent,
render,
waitFor,
} from "#testing-library/react-native";
import ColorChangingButton from "../src/components/ColorChangingButton";
it("should change the button's text color", async () => {
const text = "foobar";
const {getByText} = render(<ColorChangingButton text={text} />);
fireEvent.press(getByText(text));
await waitFor(() => {
expect(getByText(text)).toHaveStyle({color: "green"});
});
});
For a native button which has rigid semantics for changing colors and doesn't accept children, instead using title="foo", a call to debug() shows that it expands to a few nested elements. You can use
const text = within(getByRole("button")).getByText(/./);
expect(text).toHaveStyle({color: "green"});
inside the waitFor callback to dip into the button's text child and wait for it to have the desired color.
I used the same packages/versions for this post as shown in React Testing Library: Test if Elements have been mapped/rendered.
You can try
<Text style = {this.state.state === 1 ? style1 : style2}>Button Text</Text>
This will consequently lead to the style being defined all time. So you don't have to wait for the setState to complete.
Edit
You can use the callback provided by setState function to perform your tests for styles.
this.setState({
state : 1
} , () => {
//this is called only after the state is changed
//perform your test here
})

use useState hook, however the setter function is undefined at runtime

I am using useState hook in my react-native project. I have a screen which renders my custom component named MyComponent. The setter function of state is called in MyComponent 's onSelected callback.
import React, {useState} from 'react';
import MyComponent from './components/MyComponent'
const MyScreen=()=> {
...
const {parts, setParts} = useState(initialParts);
return (<View>
<MyComponent onSelected={()=> {
...
setParts(newParts)
}}/>
</View>)
}
...
MyComponent looks like this, in the onPress callback of TouchableOpacity, it calls the passed in onSelected function:
const MyComponent= ({onSelected})=> {
...
return (<TouchableOpacity onPress={()=>{
onSelected();
...
}}>
...
</TouchableOpacity>)
}
When I run my app on iOS emulator, the screen shows, when I tap on MyComponent, I get error TypeError: setParts is not a function. (In setParts(newParts)), 'setParts' is undefined.
Why I get this error?
Your destructuring here seems wrong:
const {parts, setParts} = useState(initialParts);
Shouldn't be this:
const [parts, setParts] = useState(initialParts);
?
Hmmm, it seems like you have to read the React official documentation to know more about UseState.
here is fix to your code:
const MyScreen = () => {
const [parts, setParts] = useState(initialParts) // see this fix.
return (
<View>
<MyComponent
onSelected={() => {
setParts(newParts)
}}
/>
</View>
)
}
basically, it is in the form of array de-structuring instead of object like you wrote above.
learn more: https://reactjs.org/docs/hooks-state.html

React Native Error: The action 'NAVIGATE' with payload {"name":"GameScreen"} was not handled by any navigator

I know that this issue has been brought up before but I have not found any answer that works for me.
I simply want to navigate from screen "Start" to screen "Game", using App.js as the router.
App.js:
const Stack = createStackNavigator();
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Start">
<Stack.Screen name="Start" component={StartingScreen} />
<Stack.Screen name="Game" component={GameScreen} />
</Stack.Navigator>
</NavigationContainer>
);
};
StartingScreen.js:
const StartingScreen = ({ navigation }) => {
//lots of code
return (
//more code
<Button title="Begin" onPress={() => {navigation.navigate('GameScreen')}}/>
)
This gives me the error in the title with ("Do you have a game named 'GameScreen'?") and nothing happens. I have tried following the React Navigation docs, but in their example they put everything in App.js, that does not work for me. Other things I have tried include exporting the navigation stack to StartingScreen.js, changing the arguments of navigation.navigate(), placing the navigator inside StartingScreen.js.
GameScreen is spelled exactly the same in all places.
Change your code like below
const StartingScreen = ({ navigation }) => {
//lots of code
return (
//more code
<Button title="Begin" onPress={() => {navigation.navigate('Game')}}/>
)
You are giving the component name but you should provide the name of the screen that you give in the stack which is 'Game'
As your screen name is given 'Game' in the stack navigator, you should navigate to 'Game' instead of 'GameScreen'.
onPress={() => {navigation.navigate('Game')}}

How to get props variable in detailed props React-Native

I have a custom component like this
const MyCustomComponent = ({ value, style }) => {
// I can access value & style with value and style
return <View style={style}>
<Text>{value}</Text>
</View>
}
I can called it with
<MyCustomComponent value="123" style={{ color: "blue" }} />
My question is how to get arguments or alyelse to get all props passed to my component?
In native function, i can use arguments to get allProps as an Array and set it in a new variable like const allProps = arguments[0]
What about in arrow function?
What you have here is a functional component, it is built as such that it only receives one object - that is props. When you did this: const MyComponent({value, style}) you destructured the prop object, extracting only the two variables.
You should instead do this:
const MyCustomComponent = (props) => {
//you can access the values like this
console.log(props.style, props.value)
//or you can access them like this which is the same thing you did
//earlier
const {style, value} = props;
console.log(style, value)
return (
...
)
}
Keep in mind that you need to have React in scope, so be sure to import it at the top:
import React from 'react';
Functional components are very well explained in React documentation: https://reactjs.org/docs/components-and-props.html
Do you want to get 'value' and 'style' props using a single variable?
You can use:
const MyCustomComponent = (props) => {
// I can access value & style with value and style
return <View style={props.style}>
<Text>{props.value}</Text>
</View>
}

set React Native state to the Button title prop

I can't figure out how to update the state in my basic React Native application to equal whatever is in the title prop of the Button.
I've tried just setting the state to be {title} and that hasn't worked. I am using the useState hook so I don't think I should need to use "this.".
import React, {useState} from 'react';
import { View, Text, Button } from 'react-native';
const StarterForm = () => {
const [formStage, setFormStage] = useState(1)
const [feelings, setFeelings] = useState('')
console.log(feelings)
const updateFormStage = () => {
setFormStage(formStage + 1)
setFeelings({title})
}
switch (formStage) {
case 1:
return (
<View>
<Text>How are you?</Text>
<Button title="Excellent" onPress={updateFormStage}/>
</View>
)
case 2:
return (
<Text>This is the case of two</Text>
)
}
};
In the example, I expect console.log(feelings) to equal "Excellent" once the button has been pressed.
You can use ref for that, but I think the best way to solve your problem is store "Excellent" in a variable, and use onPress={() => updateFormStage(mVariable)}
One way would be setting reference for your defined button and after click on it, retrieve data from reference like this:
<Button ref={ref => { this.button = ref; }}
title="Excellent"
onPress={this.updateFormStage} />
You can access your title via button reference using this.button.title:
updateFormStage = () => {
console.log(this.button.title);
}