Accessing navigator in Actions with React-Native and Redux - react-native

Using React-Native (0.19) and Redux, I'm able to navigate from scene to scene in React Components like so:
this.props.navigator.push({
title: "Registrations",
component: RegistrationContainer
});
Additionally I'd like to be able push components to the navigator from anywhere in the app (reducers and/or actions).
Example Flow:
User fills out form and presses Submit
We dispatch the form data to an action
The action sets state that it has started to send data across the wire
The action fetches the data
When complete, action dispatches that the submission has ended
Action navigates to the new data recently created
Problems I'm seeing with my approach:
The navigator is in the props, not the state. In the reducer, I do not have access to the props
I need to pass navigator into any action that needs it.
I feel like I'm missing something slightly simple on how to access Navigator from actions without sending in as a parameter.

In my opinion the best way to handle the navigation with Redux is with react-native-router-flux, because it can delegate all the navigation logic to Redux:
You can change the route from the reducer;
You can connect the router to the store and dispatch its own actions that will inform the store about route changes (BEFORE_ROUTE, AFTER_ROUTE, AFTER_POP, BEFORE_POP, AFTER_DISMISS, BEFORE_DISMISS);
An example
Here is an example on how easily you can save the currently focused route in the store and handle it in a component (that will be aware of being focused):
1. Connect a <Route> to Redux
Connecting a <Route> to Redux is easy, instead of:
<Route name="register" component={RegisterScreen} title="Register" />
you might write:
<Route name="register" component={connect(selectFnForRegister)(RegisterScreen)} title="Register" />
You can also simply connect the component itself in its own file like you usually do.
2. Connect a <Router> to Redux
If you need to inform Redux of the navigation status (i.e. when you pop a route) just override the <Router> component included in react-native-router-flux with a connected one:
import ReactNativeRouter, { Actions, Router } from 'react-native-router-flux'
const Router = connect()(ReactNativeRouter.Router)
Now when you use a <Router> it will be connected to the store and will trigger the following actions:
Actions.BEFORE_ROUTE
Actions.AFTER_ROUTE
Actions.AFTER_POP
Actions.BEFORE_POP
Actions.AFTER_DISMISS
Actions.BEFORE_DISMISS
Take a look at this for an example.
3. Catch the interested actions in your reducer
For this example I have a global reducer (where I keep the information needed by all my app) where I set the currentRoute:
case Actions.AFTER_ROUTE:
case Actions.AFTER_POP:
return state.set('currentRoute', action.name)
Now the reducer will catch every route change and update global.currentRoute with the currently focused route.
You also can do many other interesting things from here, like saving an history of the navigation itself in an array!
4. Update your component on focus
I'm doing it on componentDidUpdate of my component of the route named payment.
If global.currentRoute is payment and the previous global.currentRoute was different, than the component has just been focused.
componentDidUpdate(prevProps) {
const prevRoute = prevProps.global.currentRoute
const currentRoute = this.props.global.currentRoute
if (currentRoute === 'payment' && prevRoute !== currentRoute) {
this.props.actions.doSomething()
}
}
P.S.: Remember to check currentRoute === 'payment', otherwise you'll start doSomething() on every route change!
Also, take a look a the README.md for learning other stuff about the integration with Redux.
Hope it helps, long live Redux!

Maybe you could pass the information about title and component in an action and the component with the navigator can then push the right scene with the information of the state.

Related

Why does my state persist on screen change?

I am developing a React Native app and I am currently using a component in 2 different places of my app. I navigate from one place to another using a Drawer Navigator (React Navigation v6). This component updates a state when receiving a response from an API. However, if I switch to the other place where this component gets rendered, the state remains (therefore the visual message from the API appears inside it).
Why does this happen? Shouldn't all states reset on unmount and get the initial value (the one passed to useState()) when the component gets mounted again? This not the behavior I want and from what I know, this was not supposed to happen.
What can I do to make my state not persist on screen change? I have implemented the code below to be executed when a menu screen is selected, but has no effect on this issue:
navigation.dispatch(
CommonActions.reset({
index: 0,
key: null,
routes: [{ name: targetScreen }]
})
This behavior in react-native differs from react. This is documented here.
If you are coming to react-navigation from a web background, you may assume that when user navigates from route A to route B, A will unmount (its componentWillUnmount is called) and A will mount again when user comes back to it. While these React lifecycle methods are still valid and are used in react-navigation, their usage differs from the web.
Consider a screen A and a screen B. Suppose we navigate from A to B.
When going back from B to A, componentWillUnmount of B is called, but componentDidMount of A is not because A remained mounted the whole time.
This is valid for all navigators in react-native-navigation. Thus, if we have a tab bar navigation with 5 tabs, and each tab nests a Stack, then we could navigate on one stack to a certain depth, then switch the tab, and then switch the tab back again, and all screens visited will remain mounted.
Edit: In response to the comments. The recommended standard pattern to resolve your issue is as follows.
// some state in my component
const [state, setState] = useState()
useFocusEffect(
React.useCallback(() => {
// set state to initial value
setState()
}, [])
);
This is described here.
Your component does not unmount when you navigate out of component. Consider manually reseting your state inside this hook useFocusEffect

React Native component is not triggering componentDidMount() after first mount

I have two react native components, One is profile(showing logged user details) and another one for login.
In login component I set the user details to AsyncStorage.
import {AsyncStorage} from 'react-native';
Adding user to AsyncStorage:
const user = {
fname:"Tenusha",
lname:"Guruge"
}
AsyncStorage.setItem("user",JSON.stringify(user))
In the profile component I read the AsyncStorage
componentDidMount() {
AsyncStorage.getItem("user").then(user => {
console.log(user)
this.setState({user})
})
}
When logout I clear the AsyncStorage:
AsyncStorage.removeItem("user")
Problem is Once I log in and set the user details to AsyncStorage, it store the data and I can view them in profile component. Once user logout, the data in storage are cleared, but when I navigate to profile component the previously loaded data is still there.
I need a way to read the current AsyncStorage data when every time user navigate to profile component.
To be sure a component is unmounted you can use createSwitchNavigator.
You can use a loginScreen as child while the other child is your Drawer.
When doing a logout you clear your asyncStorage and then navigate to the loginScreen and the SwitchNavigator will unmount your Drawer.
DrawerNavigation will keep the screens active while moving between his childs after every screens's first focus, making every componentDidMount getting triggered only at first render.
Track navigation changes and clear state here
Consider state-manager like redux it will be cleanest approach
Your issue is that your state is not getting cleared when using DrawerNavigator instead you can use stackNavigator if you want your component to unmount when navigating out and clearing the state automatically.
Another approach can be of using state management library such as redux or flux and clear out state whenever user is logging out.

Running api fetch on entering screen

I am creating an app in React Native (create react native) and I am attempting to fetch data from an API every time a user transitions to a screen.
For navigation, I am using the React Navigation library with a combination of Drawer and Stack Navigators.
Typically, I have seen fetch handled via the ComponentDidMount() lifecycle. However, when navigating to a screen in a stack navigator, the ComponentDidMount() lifecycle doesn't appear to trigger and the fetch doesn't run.
Ex. user is on post index, then navigates to add post screen, submits and is redirected to view screen (for that post), then clicks back to return to index. Returning to index does not trigger ComponentDidMount() and fetch isn't run again, so results are not updated.
Additionally, sometimes I need to access navigation params to alter a fetch request when navigation between screens.
I originally was attempting to determine screen transition (when navigation params were passed) via componentWillReceiveProps() method, however, this seemed a bit unreliable.
I did more reading and it sounds like I should be subscribing to listeners (via react navigation). I am having a hard time finding recent examples.
Current process (example)
On the desired screen I would subscribe to listener(s) in the componentDidMount() method:
async componentDidMount() {
this.subs = [this.props.navigation.addListener('willFocus', payload => this.setup(payload))];
}
Based of an example, it sounds like its good practice to remove all subs when unmounting the screen, so I also add:
componentWillUnmount() {
this.subs.forEach((sub) => {
sub.remove();
});
}
then I add a setup() callback method that calls whatever fetch methods I require:
setup = (payload) => {
this.getExampleDataFromApi();
};
Additionally, sometimes I need to access Navigation params that will be used in API queries.
I am setting these params via methods in other screens like so:
goToProfile = (id) => {
this.props.navigation.navigate('Profile', {
exampleParam: 'some value',
});
};
It seems like I cannot access the navigation prop via the provided getParam() method when within callback method from a navigation listener.
For example, this returns undefined.
setup = (payload) => {
console.log(navigation.getParam('exampleParam', []));
};
Instead I am having to do
componentDidFocus = (payload) => {
console.log(payload.action.params.exampleParam);
};
Question
I wanted to ask if my approach for handling fetch when navigating seems appropriate, and if not, what is a better way to tackle handling API requests when navigating between screens?
Thanks, I really appreciate the help!

Render different tabs on BottomTabNavigator based on params

I have an app that starts with a loader screen where I determine whether the user is an admin or not. This screen navigates to a BottomTabNavigator, but I want to show different tabs based on whether the user is admin or not. I looked at the documentation for custom navigators but this still requires the navigator to be created outside the class, so I can't use the params. I also tried this:
export default class BottomTabNavigator extends Component {
render() {
const BottomTabComponent = createBottomTabNavigator({
...
});
return (
<BottomTabComponent {...this.props} />
);
}
}
But that doesn't work either (seems like I shouldn't pass the navigation prop down). Removing {...this.props} is also no good because then I would have to wrap the component in an app container, but that is also not right, so I'm not sure how to proceed.

How to pass parameters to a deep react-navigation Stack Navigator

I have a react-native app using react-navigation. I want to pass some props all along the stack navigator, but not sure what is the right thing to do.
To give more details, my screens are:
ListThingsScreen: List all things.
ShowThingScreen: Show details of the thing.
EditThingScreen: Edit the thing.
They are organized in such way:
const HomeStack = createStackNavigator(
{
ListThings: ListThingsScreen,
ShowThing: ShowThingScreen,
EditThing: EditThingScreen,
},
{
initialRouteName: "ListThings"
}
);
export default createBottomTabNavigator({
HomeStack
});
An app user:
Log in.
See all things (in ListThingsScreen).
Click on one of them, jump to ShowThingScreen to see its details.
Click on a button of the details screen, jump to EditThingScreen to edit the thing.
Once finished editing, jump back to ShowThingScreen.
In ListThingsScreen componentDidMount(), I will make async call to fetch logged in user data such as userId, username, etc. I want all screens in the stack knows about them to behave correctly when show and when edit.
Solutions I can think of:
Pass parameters to routes, as suggested by react-navigation docs. This works, but as I have more screens in the stack, I write too much code like navigation.push("RouteName", {user}) and navigation.getParam("user", "No user").
This is pretty much the only solution I have found. An alternative is to use React context. I like this approach better, however I want to use react-navigation and the concept of Router and Route don't exist in the library.
// ListItemsScreen.js
import { BrowserRouter as Router, Route } from "react-router-dom";
const UserContext = React.createContext();
<UserContext.Provider value={{ user }}>
<Router>
<div className="app-container">
<Route path="/show/:thingId" component={ShowThing} />
<Route path="/edit/:thingId" component={EditThing} />
</div>
</Router>
</UserContext.Provider>
// ShowItemScreen.js
<UserContext.Consumer>
{({user}) => (...)}
</UserContext.Consumer>
Which approach is more "correct"? If it is the 2nd approach, how can I do it with react-navigation?