connect a react-navigation directly to redux? - reactjs

Is it possible to connect a TabNavigator or a StackNavigator directly to redux?
This is very useful for conditionally navigating to a screen, based on whether or not some condition exists in the Redux store (i.e. if the user is signed in or signed out).
Example:
const Tabs = TabNavigator({
screen: userIsSignedInRedux ? SignedInTabNavigator : SignedOutScreen,
},
});
where userIsSignedInRedux is a key within my Redux store.

It is generally possible, but not recommended, because of the potential performance issues with that approach.
React-navigation had documentation on the topic before but removed it as not recommended in the latest version.
You can check out the following: https://reactnavigation.org/docs/en/redux-integration.html
You can connect a specific view (first inside a navigator) to redux, and inside the componentWillMount method do da redirect if your user is logged in for example.
However, if you have a specific case that would save you time if handled through redux, check out this solution:
https://github.com/react-navigation/react-navigation-redux-helpers

Try this:
import React from 'react';
const TabNavigator = ({ isLoggedIn }) => {
const RouteConfigs = {
Screen: isLoggedIn ? SignedInTabNavigator : SignedOutScreen
};
const Navigator = createBottomTabNavigator(RouteConfigs);
return <Navigator />
};
const mapStateToProps = ({ auth }) => ({ isLoggedIn: auth.isLoggedIn });
export default connect(mapStateToProps)(TabNavigator);
But make to not to render your navigation until the value of isLoggedIn is determined .. cause if not, you're going to see a some kind of flickering effect (SignOutScreen will be rendered and a few moments later you're going to see SignInTabNavigator shows up if isLoggedIn resolved to true)

Related

how to auto refresh component when redux state get updated

I currently store user name in redux store and show it in topbar after user logged in. (Shown in screenshots). However, it do not re-render automatically when redux state gets updated. I still need to do some actions in topbar then username does appear. Otherwise, it does not appear. So the question, how to force to re-render topbar component from my login component.
Another question: user name will disapprear when refresh page. So is there any option to persist data with redux state? Or it's impossible.
const Login = () => {
...
const handleLogin = async googleData => {
...
//store user name value als application state into redux store
store.dispatch(nameActionCreator(googleData.profileObj.name));
...
}
}
const TopBar = () => {
...
return(
...
<h5>store.getState().userNameRecuder.name</h5>
...
)
}
Before login
After login
The first question
Short answer:
you must call the selector function with useSelector hook. you are calling it directly in your <h5> element. so it's not working.
const TopBar = () => {
const username = useSelector(() => store.getState().userNameRecuder.name)
return(
<h5>{username}</h5>
)
}
Long answer with best practices:
You need to create a selector and use it in your component to re-render your component after the state changes.
In the selector.js
const selectUsername = (state) => state.userNameRecuder.name
export selectUsername;
Now, in your Topbar component, you need to implement the useSelector hook:
import {useSelector} from 'react-redux';
import {selectUsername} from 'store/selectors';
function TopBar () {
const username = useSelector(selectUsername)
return (
<p>username</p>
)
}
Updating the state with proper action (to update username) will cause a re-render in the Topbar component to get the new username from the store.
The second question
There are many great ways to persist data for example storing them on cookies, localStorage or using third-party libraries like redux-persist, and so on...
take a look at this post.
It works based on #novonimo's answer. Even without apart selector.js. Just connect the redux state with the component's state by using useSelector hooks, so the component will automatically re-rendered when the redux state changes.
const TopBar = () => {
const userName = useSelector(() => store.getState().userNameRecuder.name);
return(
...
<h5>userName</h5>
...
)
}

React Navigation - Get next screen name for another navigator

I have React Native app with React Navigation. I want to log screen name every time when it changes.
In my app I already have several stackNavigator's and one switchNavigator to combine them.
Now I've just added next onTransitionStart property to all stackNavigator's:
export default createStackNavigator(
{
...
},
{
...
onTransitionStart: transitionProps => {
console.log('move to: ', transitionProps.scene.route.routeName);
},
},
);
Mostly it helps and do it just I wanted. However, it doesn't work in cases when I move from one stackNavigator to another. Where it is a better way to leave the callback to know that we moved to another stackNavigator and get its first screen?
You can actually try the below method:
import { NavigationEvents } from 'react-navigation';
render() {
<NavigationEvents
onWillFocus={() => {
console.log('screenA')
}}
/>
}
Quoted from documentation
NavigationEvents is a React component providing a declarative API to subscribe to navigation events. It will subscribe to navigation events on mount, and unsubscribe on unmount.
For me helped next one: createAppContainer creates the container with onNavigationStateChange property. It takes a function with previous and next navigation state as arguments. Both of them have enough data to understand which screen was previous and which will be next one. Since I have this app container in my main App component, it looks next way:
<View>
/* ... */
<RootNavigator
/* ... */
onNavigationStateChange={(prevState, newState, action) => {
if (action.type === 'Navigation/NAVIGATE' || action.type === 'Navigation/BACK') {
const currentNavigator = newState.routes[newState.index];
const screenName = currentRoute.routes[currentNavigator.index].routeName;
console.log('Current screen is: ', screenName);
}
};}
/>
/* ... */
</View>
Both state objects store the index and routes properties. They represent chosen stackNavigator index and the list of stackNavigator's respectively. So we can get the number of chosen navigator and get the index of current route for it.

Re-render component when navigating the stack with React Navigation

I am currently using react-navigation to do stack- and tab- navigation.
Is it possible to re-render a component every time the user navigates to specific screens? I want to make sure to rerun the componentDidMount() every time a specific screen is reached, so I get the latest data from the server by calling the appropriate action creator.
What strategies should I be looking at? I am pretty sure this is a common design pattern but I failed to see documented examples.
If you are using React Navigation 5.X, just do the following:
import { useIsFocused } from '#react-navigation/native'
export default function App(){
const isFocused = useIsFocused()
useEffect(() => {
if(isFocused){
//Update the state you want to be updated
}
}, [isFocused])
}
The useIsFocused hook checks whether a screen is currently focused or not. It returns a boolean value that is true when the screen is focused and false when it is not.
React Navigation lifecycle events quoted from react-navigation
React Navigation emits events to screen components that subscribe to them. There are four different events that you can subscribe to: willFocus, willBlur, didFocus and didBlur. Read more about them in the API reference.
Let's check this out,
With navigation listeners you can add an eventlistener to you page and call a function each time your page will be focused.
const didBlurSubscription = this.props.navigation.addListener(
'willBlur',
payload => {
console.debug('didBlur', payload);
}
);
// Remove the listener when you are done
didBlurSubscription.remove();
Replace the payload function and change it with your "refresh" function.
Hope this will help.
You can also use also useFocusEffect hook, then it will re render every time you navigate to the screen where you use that hook.
useFocusEffect(()=> {
your code
})
At the request of Dimitri in his comment, I will show you how you can force a re-rendering of the component, because the post leaves us with this ambiguity.
If you are looking for how to force a re-rendering on your component, just update some state (any of them), this will force a re-rendering on the component. I advise you to create a controller state, that is, when you want to force the rendering, just update that state with a random value different from the previous one.
Add a useEffect hook with the match params that you want to react to. Make sure to use the parameters that control your component so it rerenders. Example:
export default function Project(props) {
const [id, setId] = useState(props?.match?.params?.id);
const [project, setProject] = useState(props?.match?.params?.project);
useEffect(() => {
if (props.match) {
setId(props.match?.params.id);
setProject(props.match?.params.project);
}
}, [props.match?.params]);
......
To trigger a render when navigating to a screen.
import { useCallback } from "react";
import { useFocusEffect } from "#react-navigation/native";
// Quick little re-render hook
function useForceRender() {
const [value, setValue] = useState(0);
return [() => setValue(value + 1)];
}
export default function Screen3({ navigation }) {
const [forceRender] = useForceRender();
// Trigger re-render hook when screen is focused
// ref: https://reactnavigation.org/docs/use-focus-effect
useFocusEffect(useCallback(() => {
console.log("NAVIGATED TO SCREEN3")
forceRender();
}, []));
}
Note:
"#react-navigation/native": "6.0.13",
"#react-navigation/native-stack": "6.9.0",

How React rerenders tree, if both parent and child components are dependent from one Redux store prop?

I have Layout component, which checks if a user is logged in. It simply examines user prop in Redux store. If it is, it renders children, otherwise Login.
Layout.jsx
const Layout = ({ user, children }) => {
if (user) return children;
return <Login />;
};
export default connect(state => ({ user: state.user }))(Layout);
Profile.jsx
const Profile = ({ user }) => <div>{user.name}</div>;
export default connect(state => ({ user: state.user }))(Profile);
App itself
<Layout>
<Profile />
</Layout>
The issue is when Profile component is rendered and I log out, I get can not read property name of undefined.
I expect Profile component not to be rendered at all, because it is how Layout works. But looks like React first tries updating Profile component and fails.
For now I ended up returning null from Profile if there is no user, but it's not ideal.
So if both parent and child components are dependent on one Redux store prop, React rerenders them from down to the top, from top to down, or simultaneously? Is there a canonical or best practice way to avoid it?

React Redux - Conditional selector

In my application i have many part of the state that is significative only when the user is logged in.
When the user is logged in and navigate directly to a page, router display the page but have to make async call, so a piece of the state is not yet populated.
Now, suppose i have to show a button on navigation bar that have to take a part of the state, but this state is not populated since the async call finis.
More pratically, i have this selector:
export const getSelectedCustomer = state => state.systems.selectedCustomer;
With this, there are no problem, because selectedCustomer is part of the initialState reducer
The problem is in this selector:
export const getSelectedCustomerName = state => {
return state.systems.entities[getSelectedCustomer(state)].customer;
}
The part of the state entities in initialState is an empty array and will be populated on async call.
So, when the application start and i map (with connect - mapStateToProps) the selector getSelectedCustomerName i get the error because entities is empty and sure, customer field do not exist yet
A simple solution if to take in place some if-else, but i will have to do this in "every" part of the application.
I'm asking if there is a better solution to avoid this...
const NullCustomerEntities = {
customer: "Anonymous"
}
export const getSelectedCustomer = state => state.systems.selectedCustomer;
export const getCustomerEntities = state => {
return state.systems.entities[getSelectedCustomer(state)] || NullCustomerEntities;
}
export const getSelectedCustomerName = state => {
return getCustomerEntities(state).customer;
}
This is a classic example of a Null Object pattern
(hope you don't mind the code being in Ruby).

Resources