React Navigation - Get next screen name for another navigator - reactjs

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.

Related

Window.location.reload() in react-native

I often use window.location.reload on my React Web project.
window.location.reload();
Is there any similar way to reload page(component) in react-native?
The thing that you mention is a browser feature. React Native uses native capabilities to render your app, so there are no browser features like window.location.reload()
So I am not sure what is your particular use-case for this. I guess it is needed to reload the current screen, not the full app.
In this case, you should use a react-way to re-render your screens. In React the views are re-rendered when the props or state changes. So if you want to trigger a full reload, you need to have some internal state that will trigger a reload. For example, you can use key property and a container with an internal state that toggles this key.
But I would consider it a hack. You should really use the react data-driven views
You can read more about key prop either in official docs or check out this article from Google: https://kentcdodds.com/blog/understanding-reacts-key-prop/
import React from 'react';
import { View, Text } from 'react-native';
const Container = () => {
const [key, setKey] = React.useState(0);
const reload = React.useCallback(() => setKey((prevKey) => prevKey + 1), []);
return <Child reload={reload} key={key} />;
}
const Child = ({ reload }) => {
const getRandomId = () => parseInt(Math.random() * 100, 10);
// We use useRef to showcase that the view is fully re-rendered. Use ref is initialized once per lifecycle of the React component
const id = React.useRef(getRandomId());
return (
<View>
<Text>My random id is {id}</Text>
<Button onPress={reload} />
</View>
)
}
#Coding is Life
import { NavigationEvents } from 'react-navigation';
<NavigationEvents onWillFocus={() => this.goBackReload()}/>
This is the way to reload the page, ie. when you go back to page you got the call back method. Using this method clear all state value and refresh the page. Another way is refresh control using to reload the app.
<ScrollView refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this._onRefresh} />
}>
</ScrollView>
When you scroll down the screen onRefresh method trigger.

React Router <Link> to dynamic page and go back to previous state in useEffect

I'm trying to make a React app with dynamic pages and navigational fetch with next and previous buttons, when click on the Item it shows the page dynamic page but when I press the back button on the browser it forgets the state where count and input value and shows the initial state. What should I write to save the state so that when I go back it stays on the same count and value and not start from initial state?
const App = () => {
return (
<Router>
Route path='/item/:id' component={Item} />
Route path='/' exact component={Search} />
</Router>
)
}
const Items = (props) => {
return (
<div>
{props.data.map(image => (
/* Link to component Item */
<Link to={`/item/${image.id}`} key={image.id}>
<img src={image.urls.small} alt={image.description} />
</Link>
))}
</div>
);
}
const Search = () => {
useEffect(() => {
getData();
},[count]);
const nextPage = (event) => {
setCount(count + 1);
getData();
event.preventDefault();
}
const prevPage = event => {
if (count > 1) {
setCount(count - 1);
getData();
}
event.preventDefault();
}
return (
<div>
<Items data={images} />
<button onClick={prevPage}>PREV</button>
<button onClick={nextPage}>NEXT</button>
</div>
);
}
It looks like you may need to look into state management systems/patterns. What you are looking for is called persistence, where you can revisit a page and maintain the same values in the same session.
Your form state is being cleared because you're holding it in a "local" component state - a component that is being re-rendered every time you switch pages. The "easy" way to solve this is to manage the Search component's state inside of App and add all the handlers to App as well, this is also called "raising" the state. While this is the "easy" way to do things, it is also quick and dirty (you will probably need to refactor in the future) because it will eventually overcomplicate your App component if you add other page/form states, etc.
The other way (also quick and easy and what I would recommend) is to store your search values in localStorage. You can save them in a JSON format to be read and used to update the form as soon as the component mounts. I think if this is a small app, this is probably the best way.
The approach for larger applications (3+ pages) is to use a global state management system, Flux, Redux, Mobx etc. These systems make it much easier to maintain a global app state and persist information even if you navigate through different pages in the app (as long as you maintain the session). This approach, while I recommend you look into for practice is usually left fort larger applications and can add too much overhead for what its worth on smaller apps.
Another 'quick win' approach is to utilize ReactRouter's state. Basically you can set state properties to a particular point in the router's history. I've used this before to remember scroll position of a lazy-loading grid.
What this does is applies your state to the current browser history position, so when you navigate elsewhere and then hit back on your browser navigation bar, the previous state is also restored (not just the URL).
In essence, create your own Link component wrapper:
import React from 'react';
import { withRouter } from 'react-router';
import { Link } from 'react-router-dom';
class CustomLink extends React.Component {
handleClick(e) {
const { count, input, history } = this.props;
state = { count, input }
history.replace({ state });
}
render() {
const { to, children } = this.props;
return (
<Link
onClick={this.handleClick}
to={to}
>
{children}
</Link>
);
}
}
export default withRouter(CustomLink);

connect a react-navigation directly to redux?

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)

react-navigation: connect with component/screen without props/state

I'm looking for a way to connect the headerLeft and headerRight buttons of react-navigation with the screen/component it should be showed on, without using the props/state of that component.
We moved all our navigation transitions to one single service (at first because we needed it outside our react-components, but decided to replace it app-wide to keep one consistent flow).
Now, besides this service, we have few header-buttons, of which 2 require interaction with the screen: a cancel/finish and an edit button... Ideally, they would have a a simple callback which can be provided in the Component.
As suggested in the docs, this is the one class we use as a service:
import {StackActions, NavigationActions} from 'react-navigation';
import type {NavigationRoute, NavigationParams} from 'react-navigation';
let _navigator;
function setNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName: string, params?: NavigationParams) {
_navigator.dispatch(NavigationActions.navigate({routeName, params}));
}
function reset(routeName: string, params?: NavigationParams) {
//...
}
function getCurrentRoute(): NavigationRoute | undefined {
//...
}
function goBack() {
_navigator._navigation.goBack();
}
function getCurrentParams() {
return _navigator.state.params || {};
}
function setParams(params) {
_navigator._navigation.setParams(params);
}
export default {
//all functions
};
This is the button we have:
FinishCancelButton = () => (
<Button onPress={() => NavigationService.getCurrentParams().canFinish ? NavigationService.setParams({finish: true}) : NavigationService.setParams({cancel: true})}>
<Text>
{NavigationService.getCurrentParams().canFinish ? 'finish' : 'cancel'}
</Text>
</Button>
),
Before, the navigation object of the state of the component was passed, which would trigger state changes (canFinish from the component and pushback finish/cancel) and with componentWillReceiveProps (which is deprecated now) it updated the screen. But now this connection is gone, and we're looking for way to connect the two...
Ideally this would be a a simple callback:
FinishCancelButton = (onFinish) => (<Button onPress={onFinish} ... />)
But I don't see a way to do this. An other possible solution I've thought of is to attach a listener from the component to the service: which notifies when a certain property is changed.
Is there a way to create the button in the component itself, which can access the state?
Does anyone have any idea? Or if you have some more questions or other approaches, please share!

How do people handle scroll restoration with react-router v4?

I'm experiencing some problems with scroll positions on the back button (history popstate) when using react-router. React router v4 doesn't handle scroll management out of the box because browsers are implementing some automatic scroll behavior. This is great except when the height of the browser window changes too dramatically from one view to another. I have implemented the ScrollToTop component as described here: https://reacttraining.com/react-router/web/guides/scroll-restoration
This works great. When you click a link and go to a different component, the browser scrolls to the top (like a normal server-rendered website would). The issue only happens when you go back (via the browser back button) to a view with a much taller window height. It seems that (chrome) tries to go to the scroll position of the previous page before react has rendered the content (and browser height). This results in the scroll only going as far down as it can based on the height of the view it's coming from. Picture this scenario:
View1: Long list of movies (window height 3500px).
(movie is clicked)
View2: Details view of the selected movie (window height: 1000px).
(Browser back button is clicked)
Back to view 1, but scroll position can't go further than 1000px, because chrome is trying to set the position before react renders the long movie list.
For some reason this is only a problem in Chrome. Firefox and Safari seem to handle it fine. I wonder if anyone else have had this problem, and how you guys generally handle scroll restoration in React.
Note: all the movies are imported from a sampleMovies.js — so I'm not waiting for an API response in my example.
Note that history.scrollRestoration is just a way of disabling the browser's automatic attempts at scroll restoration, which mostly don't work for single-page apps, so that they don't interfere with whatever the app wants to do. In addition to switching to manual scroll restoration, you need some sort of library that provides integration between the browser's history API, React's rendering, and the scroll position of the window and any scrollable block elements.
After not being able to find such a scroll restoration library for React Router 4, I created one called react-scroll-manager. It supports scrolling to top on navigation to a new location (aka history push) and scroll restoration on back/forward (aka history pop). In addition to scrolling the window, it can scroll any nested element that you wrap in an ElementScroller component. It also supports delayed/asynchronous rendering by using a MutationObserver to watch the window/element content up to a user-specified time limit. This delayed rendering support applies to scroll restoration as well as scrolling to a specific element using a hash link.
npm install react-scroll-manager
import React from 'react';
import { Router } from 'react-router-dom';
import { ScrollManager, WindowScroller, ElementScroller } from 'react-scroll-manager';
import { createBrowserHistory as createHistory } from 'history';
class App extends React.Component {
constructor() {
super();
this.history = createHistory();
}
render() {
return (
<ScrollManager history={this.history}>
<Router history={this.history}>
<WindowScroller>
<ElementScroller scrollKey="nav">
<div className="nav">
...
</div>
</ElementScroller>
<div className="content">
...
</div>
</WindowScroller>
</Router>
</ScrollManager>
);
}
}
Note that an HTML5 browser (10+ for IE) and React 16 are required. HTML5 provides the history API, and the library uses the modern Context and Ref APIs from React 16.
How do you handle your scroll restoration?
Turns out browsers have implementations of the history.scrollRestoration.
Maybe you can use that? Check these links out.
https://developer.mozilla.org/en/docs/Web/API/History#Specifications
https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration
In addition, I found an npm module that might be able to handle scroll restoration in react with ease, but this library only works with react router v3 and below
https://www.npmjs.com/package/react-router-restore-scroll
https://github.com/ryanflorence/react-router-restore-scroll
I hope this can help.
my solution:save window.scrollY for every pathname with a Map (ES6)
scroll-manager.tsx
import { FC, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function debounce(fn: (...params: any) => void, wait: number): (...params: any) => void {
let timer: any = null;
return function(...params: any){
clearTimeout(timer);
timer = setTimeout(()=>{
fn(...params)
}, wait);
}
}
export const pathMap = new Map<string, number>();
const Index: FC = () => {
const { pathname } = useLocation();
useEffect(() => {
if (pathMap.has(pathname)) {
window.scrollTo(0, pathMap.get(pathname)!)
} else {
pathMap.set(pathname, 0);
window.scrollTo(0, 0);
}
}, [pathname]);
useEffect(() => {
const fn = debounce(() => {
pathMap.set(pathname, window.scrollY);
}, 200);
window.addEventListener('scroll', fn);
return () => window.removeEventListener('scroll', fn);
}, [pathname]);
return null;
};
export default Index;
App.tsx
function App() {
return (
<Router>
<ScrollManager/>
<Switch>...</Switch>
</Router>
);
}
You can also use pathMap.size === 1 to determine if the user entered the app for the first time
I wound up using localStorage to track the scroll position - not sure this would handle all situations.
In this example, there's a Company page with a set of Stores, and each Store has a set of Display cases. I needed to track the scroll position of the display cases, so saved that to a 'storeScrollTop' key. There were 6 lines of code to add.
company.jsx:
// click on a store link
const handleClickStore = (evt) => {
window.localStorage.removeItem('storeScrollTop') // <-- reset scroll value
const storeId = evt.currentTarget.id
history.push(`/store/${storeId}`)
}
store.jsx:
// initialize store page
React.useEffect(() => {
// fetch displays
getStoreDisplays(storeId).then(objs => setObjs(objs)).then(() => {
// get the 'store' localstorage scrollvalue and scroll to it
const scrollTop = Number(window.localStorage.getItem('storeScrollTop') || '0')
const el = document.getElementsByClassName('contents')[0]
el.scrollTop = scrollTop
})
}, [storeId])
// click on a display link
const handleClickDisplay = (evt) => {
// save the scroll pos for return visit
const el = document.getElementsByClassName('contents')[0]
window.localStorage.setItem('storeScrollTop', String(el.scrollTop))
// goto the display
const displayId = evt.currentTarget.id
history.push(`/display/${displayId}`)
}
The trickiest part was figuring out which element had the correct scrollTop value - I had to inspect things in the console until I found it.
This component scroll to up if page is new but if page seen before restore the scroll.
scroll-to-top.tsx file:
import { useEffect, FC } from 'react';
import { useLocation } from 'react-router-dom';
const pathNameHistory = new Set<string>();
const Index: FC = (): null => {
const { pathname } = useLocation();
useEffect((): void => {
if (!pathNameHistory.has(pathname)) {
window.scrollTo(0, 0);
pathNameHistory.add(pathname);
}
}, [pathname]);
return null;
};
export default Index;
app.tsx file:
<BrowserRouter>
<ScrollToTop />
<App />
</BrowserRouter>
Use this library
https://www.npmjs.com/package/react-scroll-restoration
React Router does not provide out of the box support for scroll restoration and as it currently stands they won't either, because browsers are implementing some automatic scroll behavior

Resources