I'm setting up a generic loading strategy for NextJS apps using framer-motion.
function MyApp({ Component, pageProps, router }) {
const [isFirstMount, setIsFirstMount] = useState(true);
useEffect(() => {
const handleRouteChange = () => {
setIsFirstMount(false);
};
router.events.on("routeChangeStart", handleRouteChange);
// If the component is unmounted, unsubscribe
// from the event with the `off` method:
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
}, [router]);
console.log("My App ran");
return (
<Layout>
<AnimatePresence exitBeforeEnter>
<motion.div exit={{ opacity: 0 }}>
{isFirstMount && <InitialTransition />}
<motion.div
initial="initial"
animate="animate"
variants={content(isFirstMount)}
className="space-y-12"
>
<Component
isFirstMount={isFirstMount}
key={router.route}
{...pageProps}
/>
</motion.div>
</motion.div>
</AnimatePresence>
</Layout>
);
}
I ran into an issue with params. When using params during initial loading (such as clicking on the refresh button on browser), NextJS loads an additional time. I believe its due to NextJS's router query object update. For example,
WEBSITE.com/ // works - you will see the black loading screen, the black loading screens slides down, and the child objects start popping on
// vs
WEBSITE.com/?test=test // fails - you will see the black loading screen flash in and out of view before the rest of the app proceeds
I made a codesandbox sample. On the browser side, test by adding or removing ?test=test to the end of the url.
How can I prevent the additional re-render or get the loader to work the same way regardless of having a param or not?
Update
I tried #Mokhtar 's code. Technically, it did nothing yet the transitioning started worked. Eventually I figured out that removing all the isFirstMount and useEffect logic would make the transitioning correctly.
function MyApp({ Component, pageProps, router }) {
console.log("My App ran");
return (
<Layout>
<AnimatePresence exitBeforeEnter>
<motion.div exit={{ opacity: 0 }}>
<InitialTransition />
<motion.div
initial="initial"
animate="animate"
variants={content(true)}
>
<Component key={router.route} {...pageProps} />
</motion.div>
</motion.div>
</AnimatePresence>
</Layout>
);
}
Does anyone know why its working? I thought the exit opacity / sliding action only works when the Component is removed.
Your <InitialTransition /> component is getting unmounted because of your route being called with a shallow change (https://nextjs.org/docs/routing/shallow-routing) and therefore calling your handleRouteChange handler.
The following handleRouteChange implementation should fix it.
const handleRouteChange = (url, { shallow }) => {
if (shallow && !isFirstMount) {
setIsFirstMount(false);
}
};
Related
I am using ReactTransitionGroup with ReactRouter.
The goal is to reroute smoothly from one component to another. The problem - is that the component is rendered twice.
An example of a component (view) that renders twice
I am using the console to check. You might say that this is not critical. But, the problem is that because of this problem, 2 requests go to the server (one extra). Therefore, it is desirable for me to get rid of this bug.
This is the component itself - the switch
When switching a route, the console issues logs twice
I need to figure out why the side effect is being called twice. If there is not enough information, then write comments. I will try to answer as quickly as possible.
Here's an example from the documentation. I have achieved this effect, but the problem has already been described.
UPD: I remember very well that once it worked like a clockwork. But, probably, I myself did not notice that I changed something, which led to this problem.
UPD: If you need a code, then please, the required elements:
const TabList = ({ tabs }) => {
return (
<nav className="p-my-company__tabs">
{tabs.map(({ to, label, id }) => (
<NavLink to={to} key={id}>
<div>{label}</div>
</NavLink>
))}
</nav>
);
};
const TabViews = ({ tabViews }) => {
const location = useLocation();
return (
<div className="p-my-company__views">
<TransitionGroup>
<SwitchTransition mode="out-in">
<CSSTransition
key={location.pathname}
classNames={{
enter: 'move-enter',
enterActive: 'move-enter-active',
exit: 'move-exit',
}}
timeout={100}>
<Switch>
{tabViews.map(({ path, Component, id }) => (
<Route path={path} render={() => <Component />} key={id} />
))}
</Switch>
</CSSTransition>
</SwitchTransition>
</TransitionGroup>
</div>
);
};
<div className="p-my-company__panel">
<TabList
tabs={[
{ to: ROUTES.COMMON_INFO, label: 'Общая информация', id: 1 },
{ to: ROUTES.SHOWCASE, label: 'Моя витрина', id: 2 },
]}
/>
<TabViews
tabViews={[
{ path: ROUTES.COMMON_INFO, Component: CommonView, id: 1 },
{ path: ROUTES.SHOWCASE, Component: ShowCaseView, id: 2 },
]}
/>
</div>
const ShowCase = () => {
useEffect(() => {
console.log(2);
}, []);
return <div>ShowCase</div>;
};
Looks like the Switch component from React Router and React Transition Group don't work well together. The docs recommend avoiding the usage of the Switch component and passing a function to the Route's children prop. Since the function will be called regardless of whether there is a match or not, you can conditionally render Component if there's one.
<>
{tabViews.map(({ path, Component }) => (
<Route exact path={path} key={path}>
{({ match }) => (
<TransitionGroup>
<SwitchTransition mode="out-in">
<CSSTransition
in={match != null}
classNames={{
enter: 'move-enter',
enterActive: 'move-enter-active',
exit: 'move-exit',
}}
timeout={100}
unmountOnExit
key={location.pathname}
>
<div className="page">{match && <Component />}</div>
</CSSTransition>
</SwitchTransition>
</TransitionGroup>
)}
</Route>
))}
</>
I am fairly new to React Native and currently try to implement pull-to-refresh functionality in my app. Here is my Parent component snippet:
function MainScreenBoss({ navigation }) {
const [refreshing, setRefreshing] = useState(false);
//here I'm trying to refresh (example from docs)
const onRefresh = React.useCallback(async () => {
setRefreshing(true);
wait(2000).then(setRefreshing(false));
}, [refreshing]);
return (
<ScrollView
contentContainerStyle={styles.containerScroll}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
<View style={styles.container}>
//other components here
<View style={styles.containerWidget}>
//need to refresh this component
<Widget style={styles.widget} />
</View>
</View>
</ScrollView>
);
}
I am trying to refresh Widget component as it has the call to API url in it with loading animation. Here is the snipped of Widget component:
function Widget() {
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);
//call to api returns data
useEffect(() => {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}, []);
return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator
loader="blue"
visible={isLoading}
style={styles.animation}
/>
) : (
<>
<View style={styles.Contents}>
//need to refresh data is it used in here when loading is finished
</View>
</>
)}
</View>
);
}
I guess I need to force update the widget component or launch the loading function again somehow, but I do not quite understand what should I do.
Edit:
the api function looks like this:
export function getData(reqOptions, setLoading, setData) {
fetch(apiURL, reqOptions)
.then((response) => response.json())
.then((json) => setData(json.data))
.catch((error) => console.error(error))
.finally(() => setLoading(false));
}
If I understand well, in order to update your widget, you gotta re-do the fetch that you have inside your useEffect.
The useEffect you currently have only executes on mount of the component, as the dep. array is empty. From the looks of your parent component, the Widget does not get unmounted, therefore your useEffect is only called once.
What you have to do is to take your refreshing state and pass it to the Widget, so that it knows when it has to refetch the data.
Try something like this:
function MainScreenBoss({ navigation }) {
const [refreshing, setRefreshing] = useState(false);
//here I'm trying to refresh (example from docs)
const onRefresh = React.useCallback(async () => {
setRefreshing(true);
wait(2000).then(setRefreshing(false));
}, [refreshing]);
return (
<ScrollView
contentContainerStyle={styles.containerScroll}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
<View style={styles.container}>
//other components here
<View style={styles.containerWidget}>
//need to refresh this component
<Widget style={styles.widget} refreshing={refreshing} /> // pass this prop
</View>
</View>
</ScrollView>
);
}
Widget:
function Widget(props) {
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);
//call to api returns data
useEffect(() => {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}, []);
useEffect(() => {
if (props.refreshing) {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}
}, [props.refreshing]); // on every refreshing `true` state
return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator
loader="blue"
visible={isLoading}
style={styles.animation}
/>
) : (
<>
<View style={styles.Contents}>
//need to refresh data is it used in here when loading is finished
</View>
</>
)}
</View>
);
}
Edit:
You can either modify the existing useEffect to include the check of the data, or else create another one which runs only on mount, with another useEffect running on update state. I've added the second case.
Edit 2:
There is nothing wrong with that, actually that is better because the refreshing is handled at a correct time, when the call is finished and the data is fulfilled. With your current config the call may not be finished, but after 2 seconds you're setting refreshing to false, this may bring problems.
The beauty of hooks is that you can use as many of them as you want, as compared to the class methods of React before hooks introduction. So there is no problem with that, but you can actually change it around and have something like:
useEffect(() => {
if (!data || props.refreshing) {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}
}, [data, props.refreshing]);
If you may, one last thing: I would not pass state setters to your fetch fn, I would handle the state updates in the component and just make sure that the fn returns your data. It's just for separation of concerns causes.
I am currently working on a project with React Hooks.
Parent component is a Navigator
Child component is a breadcrumb display in this navigator.
Child component fetches and displays a view with the data.
How can i use the response data in the 2. child component to set name in the 1. Child component?
My Code (omitted large portions of unnecessary code for this example):
Navigator
const { Header, Content } = Layout;
const Navigation = (props: any) => (
<>
<Layout>
<Layout>
<Header>
<Breadcrumbs
style={{ flexGrow: 2, paddingLeft: 20 }}
name='Name of User'
/>
</Header>
<Content style={{ margin: '24px 16px 0', overflow: 'hidden' }}>
<div className="content">
<Switch>
<Route exact path="/" component={MyPatients} />
<Route exact path="/Skjema" component={MySchemas} />
<Route
exact
path="/Pasient"
component={() =>
<PatientInfo
patientID={props.history.location.state}
/>
}
/>
export default withRouter(Navigation);
BreadCrumbs
import React from 'react';
import Breadcrumb from 'antd/lib/breadcrumb';
import { HomeOutlined, UserOutlined } from '#ant-design/icons';
const Breadcrumbs = (props: any) => {
return (
<>
<div className="Breadcrumbcontainer" style={props.style}>
<Breadcrumb>
<Breadcrumb.Item href="/">
<HomeOutlined />
<span style={{ color: 'black' }}>Hjem</span>
</Breadcrumb.Item>
<Breadcrumb.Item href="Pasient">
<UserOutlined />
<span style={{ color: 'black' }}>
{props.name}
</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>Skjema 1 - 17.04.20</span>
</Breadcrumb.Item>
</Breadcrumb>
</div>
</>
);
};
export default Breadcrumbs;
The third file contains a fetch to an api and works fine, the data in question is currently stored as response.name How can i lift this info up to Navigator?
If I understood your question correctly, there's a parent component that has two child components and you want to trigger a change from one child component in another.
You can maintain the state in the parent component, pass state in child1 and setState function in child2.
// Parent Component
const [name, setName] = useState('');
<>
<child1 name={name}/>
<child2 setName={setName}/>
</>
Try this one. I added code sandbox and you can check it out is this what you need. So from parent pass hook as props to a child, and then after the request is made inside of the child component call function from props that will fill the data inside parent component.
Update state in parent from child
I found a solution thanks to the suggestions in this thread. I made my mistake in the passing of the files to the function.
const PatientInfo = ({ patientID, setName }: any) => {
console.log(useFetch<IPatient>( // name of endpoint.... ));
const { response } = useFetch<IPatient>(
'// name of endpoint.... + patientID,
patientID,
);
This ended up fixing my problem. The problem was i initially called the two seperately, like this:
const PatientInfo = ({ patientID }: any, { setName } : any) => {
console.log(useFetch<IPatient>( // name of endpoint.... ));
const { response } = useFetch<IPatient>(
'// name of endpoint.... + patientID,
patientID,
);
So this change worked, although I am not entirely sure as to why.
I'm a newbie in React Native and struggling in cleaning up the state of the screen.
Like screen A has some states --> screen B, back to screen A, old states are clear. I'm using React Navigation V5
What I'm trying to do is:
After navigating from MainMap.js to the last screen TripsListScreen.js (the whole process is a Stack of 4 screens, nested in a Drawer), I got all the data stored in Redux's store, and display it in TripsListScreen.
The problem is when I press the add button in TripsListScreen to comeback at the MainMap.js, it doesn't clean up every state as I expect.
Here's the MainMap.js 's states:
const initialState = {
hasMapPermission: false,
_userLocationDisplayed: null,
userLatitude: 0,
userLongitude: 0,
initial_UserLatitude: 0,
initial_UserLongitude: 0,
userLocationAddress: '',
destination: [],
destinationName: [],
destinationAddress: [],
destinationCoords: [],
destinationImageUri: [],
numOfInput:[0,1],
counter: 1,
wayPoints: [],
markers: [],
}
class MainMap extends React.Component{
constructor(props){
super(props);
this.state = initialState;
};
componentDidMount(){
console.log('Hello',this.props)
if(this.props.route.params === true){
this.setState(initialState)
}
this._requestUserLocation();
};
Basically, I tried to pass a boolean param from TripsListScreen to MainMap, and if the param is true, I'll set all the states back to the beginning. However, it doesn't work as expected.
Here's TripsListScreen:
//...
<Layout style={styles.bottom}>
<TouchableOpacity onPress={() => props.navigation.navigate('PlanningProcess', {
screen: 'MainMapScreen',
params: {doAddPlace: true}
})} style={styles.createTrip}>
<Layout style={styles.button} >
<Icon name='add' size={35} />
</Layout>
</TouchableOpacity>
</Layout>
//...
Here's the Navigation:
StackNavigators:
const Stack = createStackNavigator();
const StackNavigator = (props) => {
return(
<Stack.Navigator screenOptions={{headerShown: false}}>
<Stack.Screen name='MainMapScreen' component={MainMap} />
<Stack.Screen name='TripDescription' component={TripDescription} />
<Stack.Screen name='TripsListDetailScreen' component={TripsListDetailScreen} />
<Stack.Screen
name='TripsListScreen'
component={TripsListScreen}
options={{
headerLeft: () => (<Icon style={{marginHorizontal: 30, marginTop: 30}} color='white' name='menu' size={30} onPress={() => props.navigation.dispatch(DrawerActions.openDrawer())}/>),
title:'Something'
}}
/>
</Stack.Navigator>
);
};
export default StackNavigator;
Main Navigators:
const Navigator = () => {
return(
<NavigationContainer>
<Drawer.Navigator
statusBarAnimation='slide'
drawerContent={props =>
<DrawerContent {...props} />}>
<Drawer.Screen name='Welcome' component={WelcomeStackScreen} />
<Drawer.Screen name='TripsListScreen' component={TripsListScreen} />
<Drawer.Screen name='PlanningProcess' component={StackNavigators} />
</Drawer.Navigator>
</NavigationContainer>
);
};
export default Navigator;
This is what MainMap renders:
This is what I expected, when navigating from TripsListScreen ( to create a new trip):
PLEASE HELP !!!
ComponentDidMount in MainMap.js doesn't get triggered because the screen is mounted already. Please look at this `componentDidMount()` function is not called after navigation
The method ComponentDidMount() only triggers for the first time at the mounting of the component and as you are navigating to a new screen, the previous screen is still mounted in the stack.
If you want to re-initialize your state every time your component gets the focus, you can set a listener on the focus on the navigation.
Like this,
const unsubscribe = navigation.addListener('willFocus', () => {
// Do something
// re-initialise the state
});
in StackNavigator, screens don't unmount when you open new screens on top of them. So if you go from A to B, then from B to C, both A and B will stay mounted. If you go back from C to B, C will unmount. It's like push and pop methods on array. componentDidMount in MainMap is not being called when you go back to it, as it doesn't unmount in first place. It is explained here Navigation Lifecycle.
As you are using Redux and saying that all the data is stored in Redux store, make your MainMap component render solely from the data from store, not from own state. You can then manipulate this data from TripsListScreen by dispatching actions. The easiest would be creating something like RESET_MAIN_MAP action that will reset that part of the state for MainMap screen
I'm trying to render a print page using React Router. So I have two components:
export default class PurchaseOrder extends React.Component{
....
render(){
const {orderDate, client} = this.state.order;
//omitted for brevity
return(
<BrowserRoute>
<Button
component={Link}
to="/order/print"
target="_blank"
>
Print
</Button>
<Route
path="/order/print"
render={props => (
<OrderPrint
{...props}
orderDate={orderDate}
client={client}
/>
)}
/>
</BrowserRoute>
}
}
And the OrderPrint:
export default function OrderPrint(props) {
return (
<div>props.orderDate</div>
<div>props.client.name</div>
);
}
As you can see, I'm trying to present the printable version of the purchase order with a click of a button. The OrderPrint component gets rendered, but it's rendered right below the button. I could put the Route inside my root component, which is App, that way making sure that I get only the contents of the OrderPrint component rendered like this:
class App extends Component {
render() {
return (
<div className="App">
<Router>
<Route exact path="/" component={PurchaseOrder} />
<Route exact path="/order/print" component={OrderPrint} />
</Router>
</div>
);
}
}
But in that case, I won't be able to pass the necessary props to it. So in this particular case, how to replace entire page content with the contents of OrderPrint component and still be able to pass the necessary input to it?
Update
As #Akalanka Weerasooriya mentioned in comments, I could have the entire state kept in the App component. But one thing stopped me from doing this: This means I'll practically always have to use the render prop of the Route component, instead of the component prop. Ok, that's not a problem, but if it's the way to go, then why does React Router documentation almost always use the
<Route path="/about" component={About} />
pattern as the standard way of using it? So to recap it, if I go the Single Source of Truth way and store all my state in one place, then doesn't it mean that I will always use
<Route path="/about" render={props=>(<div>props.someProp</div>)} />
I don't say there's a problem with it, it's just mentioning it in the documentation only after component={SomeComponent} pattern confuses me.
Not sure why you need a different route for a print page, but anyway if you want it on a new empty page, you can take advantage of the ReactDOM.createPortal feature.
You can create a new page and or even a new window using window.open while keeping the flow of react data in sync.
Here is a running example of a portal on a new window with live state updates from the component that triggered this window using a portal:
running example, i'm sharing an external snippet and not using stack-snippets here because window.open returns null in the contexts of stack-snippets
Source code:
class WindowPortal extends React.PureComponent {
containerEl = document.createElement("div");
externalWindow = null;
componentDidMount() {
const { width = 450, height = 250, left = 150, top = 150 } = this.props;
const windowFetures = `width=${width},height=${height},left=${left},top=${top}`;
this.externalWindow = window.open("", "", windowFetures);
this.externalWindow.document.body.appendChild(this.containerEl);
}
componentWillUnmount() {
this.externalWindow.close();
}
render() {
return ReactDOM.createPortal(this.props.children, this.containerEl);
}
}
class App extends React.PureComponent {
state = {
counter: 0,
showWindowPortal: false
};
componentDidMount() {
window.setInterval(() => {
this.setState(state => ({
counter: state.counter + 1
}));
}, 1000);
}
toggleWindowPortal = () => {
this.setState(state => ({
...state,
showWindowPortal: !state.showWindowPortal
}));
};
closeWindowPortal = () => {
this.setState({ showWindowPortal: false });
};
render() {
return (
<div>
<h1>Counter: {this.state.counter}</h1>
<button onClick={this.toggleWindowPortal}>
{this.state.showWindowPortal ? "Close the" : "Open a"} Portal
</button>
{this.state.showWindowPortal && (
<WindowPortal closeWindowPortal={this.closeWindowPortal}>
<h2>We are in a portal on a new window</h2>
<h3>{`This is the current state: ${this.state.counter}`}</h3>
<p>different window but sharing the state!!</p>
<button onClick={() => this.closeWindowPortal()}>Close me!</button>
</WindowPortal>
)}
</div>
);
}
}
here you have a PrivateRoute which is a custom route which holds a header and header is rendered in PrivateRoute routes only so when you try to navigate to new route like path="/order/print" then you won't get header which has button in it.
function Header(props) {
return (
<div>
<Button
component={Link}
to="/order/print"
target="_blank">
Print</Button>
{props.children}
</div>
)
}
const PrivateRoute = ({ component: Component, layout: Layout, ...rest }) => {
return <Route {...rest} render={props => {
return <Layout>
<Component {...props} />
</Layout>
}} />
}
export default class PurchaseOrder extends React.Component{
render(){
const {orderDate, client} = this.state.order;
//omitted for brevity
return(
<BrowserRouter>
<div>
<PrivateRoute exact path="/" layout={Header} component={Landing} />
<Route
path="/order/print"
render={props => (
<OrderPrint
{...props}
orderDate={orderDate}
client={client}
/>
)}
/>
</div>
</BrowserRouter>
}
}