I'm currently trying to develop an app with multiple screens. Specifically, I'm working on the navigator component that directs the user to the login screen or the home screen based on whether they are logged in or not.
To do this, I'm making use of hooks, React Navigation and Firebase. I have a state which tracks the user, and this state is updated using onAuthStateChanged() from Firebase, which is inside a useEffect hook.
import { useState, useEffect } from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import {
HomeScreen,
LoginScreen,
TimerScreen
} from '../screens';
import { auth } from '../firebase';
import { onAuthStateChanged } from 'firebase/auth';
const MainStack = createNativeStackNavigator();
const AppNavigator = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const subscriber = onAuthStateChanged(auth, authUser => {
if (authUser) {
setUser(authUser);
} else {
setUser(null);
}
});
return subscriber;
});
const MainNavigator = () => (
...
);
return (
<NavigationContainer>
{ user ? MainNavigator() : LoginScreen() }
</NavigationContainer>
);
};
export default AppNavigator;
AppNavigator is then called in my App.js:
export default function App() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<AppNavigator />
</View>
);
}
However, whenever I run the app, I get
Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I've read a few posts with the same error message, and a common recommendation is to avoid having hooks inside conditional statements / loops. I did check that my useState and useEffect were at the top level of my component, so that doesn't seem to be the issue.
Right now I'm thinking that the problem could be arising because I'm navigating between screens, but I'll have to look more into it though.
Does anyone know what might be the issue, or any other possible fixes I could try? Any help would be great. Thanks!
user ? MainNavigator() : LoginScreen()
You are calling components as regular functions instead of creating elements from them. To create an element, use the JSX syntax, i.e.:
user ? <MainNavigator /> : <LoginScreen />
(Which will then be transpiled to React.createElement.)
The error occurs because when calling these components as functions, the code inside becomes a part of the render phase of the AppNavigator component. If, for example, MainNavigator contains hooks, and LoginScreen does not, then toggling between which function is (incorrectly) called also changes the number of hooks rendered, as suggested in the error message.
Related
I just started using auth0 sdk for nextjs projects. Everything seems to be ok, but I have one little problem. Everytime I change route (while I am logged in) there is invoked the /me api. This means that the user that I got through useUser ctx becomes undefined and there is a little flash of the pages rendered only if logged in.
This is the general structure of my project
_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { appWithTranslation } from "next-i18next";
import { UserProvider } from "#auth0/nextjs-auth0/client";
import AppMenu from "../components/AppMenu";
function MyApp({ Component, pageProps }: AppProps) {
return (
<UserProvider>
<AppMenu>
<Component {...pageProps} />
</AppMenu>
</UserProvider>
);
}
export default appWithTranslation(MyApp);
AppMenu is a component that I render only if I have the user. It's something like:
import { NextPage } from "next";
import { useUser } from "#auth0/nextjs-auth0/client";
interface Props {
children: JSX.Element;
}
const AppMenu: NextPage<Props> = ({ children }) => {
const { user } = useUser();
return (
<div>
{user && (<nav>{/* more stuff...*/}</nav>) }
{children}
</div>
);
};
export default AppMenu;
In this component I have the app's menu.
As I said before, when I switch from a route to another I can see in the network tab that there is called the /me endpoint and "user" is undefined. This implies that the app's menu (and all the protected components) is not rendered and there is a nasty flash of the page waiting for the response.
Is there a way to fix this? I was looking for a isLoggedIn prop but I haven't found anything.
What do you suggest?
p.s. Possibly, I would avoid a "loading" component
useUser() is a hooks call. Therefore, you need to use useEffect() with a Promise and wait until load the user. However, by the end of the day loading component is a must.
In the meantime, A feature called isLoading comes up with the useUser() state. You can improve your code like the below.
const { user, error, isLoading } = useUser();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>{error.message}</div>;
Source
I found that the issue is stemming from a Higher Order Component that wraps around a react-router-dom hook.
This Higher Order Component is imported from #auth0/auth0-react and is a requirement in our project to handle logging out with redirect.
However, even just a basic HOC, the issue is persisting.
in my App.js file, I have a react-redux provider. And inside the provider I have a ProtectLayout component.
ProtectLayout checks for an error reducer, and if the error property in the reducer has a value, it sets a toast message, as seen below.
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import PageLoader from "../loader/PageLoader";
import { useToast } from "../toast/ToastContext";
import { selectError } from "../../store/reducers/error/error.slice";
import ProtectedRoute from "../routes/ProtectedRoute";
const JobsPage = Loadable({
loader: () => import("../../screens/jobs/JobsPage"),
loading: () => <PageLoader loadingText="Getting your jobs..." />
});
const ProtectedLayout = () => {
const { openToast } = useToast();
const { error } = useSelector(selectError);
const getErrorDetails = async () => {
if (error) {
if (error?.title || error?.message)
return { title: error?.title, message: error?.message };
return {
title: "Error",
message: `Something went wrong. We couldn't complete this request`
};
}
return null;
};
useEffect(() => {
let isMounted = true;
getErrorDetails().then(
(e) =>
isMounted &&
(e?.title || e?.message) &&
openToast({ type: "error", title: e?.title, message: e?.message })
);
return () => {
isMounted = false;
};
}, [error]);
return (
<Switch>
<ProtectedRoute exact path="/" component={JobsPage} />
</Switch>
);
};
export default ProtectedLayout;
ProtectLayout returns another component ProtectedRoute. ProtectedRoute renders a react-router-dom Route component, which the component prop on the Route in the component prop passed into ProtectedRoute but wrapped in a Higher Order Component. In my actual application, as aforementioned, this is the withAuthenticationRequired HOC from #auth0/auth0-react which checks if an auth0 user is logged in, otherwise it logs the user out and redirects to the correct URL.
import React from "react";
import { Route } from "react-router-dom";
const withAuthenticationRequired = (Component, options) => {
return function WithAuthenticationRequired(props) {
return <Component {...props} />;
};
};
const ProtectedRoute = ({ component, ...args }) => {
return <Route component={withAuthenticationRequired(component)} {...args} />;
};
export default ProtectedRoute;
However, in one of the Route components, JobsPage the error reducer state is updated on mount, so what happens is the state gets updated, the ProtectedLayout re-renders, which then re-renders ProtectedRoute, which then re-renders JobPage which triggers the useEffect again, which updates the state, so you end up in an infinite loop.
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getGlobalError } from "../../store/reducers/error/error.thunk";
const JobsPage = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getGlobalError(new Error("test")));
}, []);
return (
<div>
JOBS PAGE
</div>
);
};
export default JobsPage;
I have no idea how to prevent this rendering loop?
Really all I want to do, is that when there is an error thrown in a thunk action, it catches the error and updates the error reducer state. That will then trigger a toast message, using the useToast hook. Perhaps there is a better way around this, that what I currently have setup?
I have a CodeSandbox below to recreate this issue. If you click on the text you can see the re-renders occur, if you comment out the useEffect hook, it will basically crash the sandbox, so might be best to only uncomment when you think you have resolved the issue.
Any help would be greatly appreciated!
I am making a mern stack application and currently I am trying to switch between the login route and main page depending if you are logged in or not. However, this only works once I refresh the page, is there any way I can make it work without having to refresh the page?
App.js
{!localStorage.getItem('token') ? (
<Redirect exact from='/' to='/login' />
):
<>
<Navbar />
<Redirect to='/' />
</>
}
Reacting to changes in local storage is -at best- a weird approach. In practice, the only way for a component to re-render, is by the props that it receives to change, or by using component state via useState.
I'll write this imaginary piece of code to illustrate my point:
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom'
// ...
const LoginPage = _props {
const [token, setToken] = useState(localStorage.getItem('token'))
if (token) {
return <Redirect to='/' />
}
// I have no idea how you login your users
return (
<div>
<LoginForm onToken={setToken} />
</div>
)
}
If you need component A to react to changes done by component B, where neither of them is a direct child of the other, you will need global state.
Global state is similar to component state in that changes on it should trigger a re-render on the component that depends on it. But it is global, not local to a particular component.
To achieve this, there are complex solutions like redux, but you can implement a very simple version of it using a React Context:
// src/providers/GlobalStateProvider.js
import React, { createContext, useContext, useState } from 'react'
const Context = createContext()
const GlobalStateProvider = ({ children }) => {
const [token, doSetToken] = useState(localStorage.getItem('token'))
const setToken = t => {
doSetToken(t)
localStorage.setItem('token', token)
}
return (
<Context.Provider value={{ token, setToken }}>
{children}
</Context>
)
}
export { Context }
export default GlobalStateProvider
// src/App.js
import GlobalStateProvider from './providers/GlobalStateProvider'
// ...
const App = _props => {
return (
{/* Any component that is descendant of this one can access the context values, an will re-render if they change */}
<GlobalStateProvider>
{/* ... the rest of your components */}
</GlobalStateProvider>
)
}
// ...
// your particular component
import React, { useContext } from 'react'
import { Context } from 'path/to/GlobalStateProvider'
const SomeComponent = _props => {
// Component will re-render if token changes
// you can change token from wherever by using `setToken`
const { token, setToken } = useContext(Context)
if (token) {
// do this
} else {
// do that
}
}
I've been spending a bunch of time reading up on React Hooks, and while the functionality seems more intuitive, readable, and concise than using classes with local state and lifecycle methods, I keep reading references to Hooks being a replacement for HOCs.
The primary HOC I have used in React apps is withAuth -- basically a function that checks to see if the currentUser (stored in Redux state) is authenticated, and if so, to render the wrapped component.
Here is an implementation of this:
import React, { Component } from "react";
import { connect } from "react-redux";
export default function withAuth(ComponentToBeRendered) {
class Authenticate extends Component {
componentWillMount() {
if (this.props.isAuthenticated === false) {
this.props.history.push("/signin");
}
}
componentWillUpdate(nextProps) {
if (nextProps.isAuthenticated === false) {
this.props.history.push("/signin");
}
}
render() {
return <ComponentToBeRendered {...this.props} />;
}
}
function mapStateToProps(state) {
return { isAuthenticated: state.currentUser.isAuthenticated };
}
return connect(mapStateToProps)(Authenticate);
}
What I can't see is how I can replace this HOC with hooks, especially since hooks don't run until after the render method is called. That means I would not be able to use a hook on what would have formerly been ProtectedComponent (wrapped with withAuth) to determine whether to render it or not since it would already be rendered.
What is the new fancy hook way to handle this type of scenario?
render()
We can reframe the question of 'to render or not to render' a tiny bit. The render method will always be called before either hook-based callbacks or lifecycle methods. This holds except for some soon-to-be deprecated lifecycle methods.
So instead, your render method (or functional component) has to handle all its possible states, including states that require nothing be rendered. Either that, or the job of rendering nothing can be lifted up to a parent component. It's the difference between:
const Child = (props) => props.yes && <div>Hi</div>;
// ...
<Parent>
<Child yes={props.childYes} />
</Parent>
and
const Child = (props) => <div>Hi</div>;
// ...
<Parent>
{props.childYes && <Child />}
</Parent>
Deciding which one of these to use is situational.
Hooks
There are ways of using hooks to solve the same problems the HOCs do. I'd start with what the HOC offers; a way of accessing user data on the application state, and redirecting to /signin when the data signifies an invalid session. We can provide both of those things with hooks.
import { useSelector } from "react-redux";
const mapState = state => ({
isAuthenticated: state.currentUser.isAuthenticated
});
const MySecurePage = props => {
const { isAuthenticated } = useSelector(mapState);
useEffect(
() => {
if (!isAuthenticated) {
history.push("/signin");
}
},
[isAuthenticated]
);
return isAuthenticated && <MyPage {...props} />;
};
A couple of things happening in the example above. We're using the useSelector hook from react-redux to access the the state just as we were previously doing using connect, only with much less code.
We're also using the value we get from useSelector to conditionally fire a side effect with the useEffect hook. By default the callback we pass to useEffect is called after each render. But here we also pass an array of the dependencies, which tells React we only want the effect to fire when a dependency changes (in addition to the first render, which always fires the effect). Thus we will be redirected when isAuthenticated starts out false, or becomes false.
While this example used a component definition, this works as a custom hook as well:
const mapState = state => ({
isAuthenticated: state.currentUser.isAuthenticated
});
const useAuth = () => {
const { isAuthenticated } = useSelector(mapState);
useEffect(
() => {
if (!isAuthenticated) {
history.push("/signin");
}
},
[isAuthenticated]
);
return isAuthenticated;
};
const MySecurePage = (props) => {
return useAuth() && <MyPage {...props} />;
};
One last thing - you might wonder about doing something like this:
const AuthWrapper = (props) => useAuth() && props.children;
in order to be able to do things like this:
<AuthWrapper>
<Sensitive />
<View />
<Elements />
</AuthWrapper>
You may well decide this last example is the approach for you, but I would read this before deciding.
Building on the answer provided by backtick, this chunk of code should do what you're looking for:
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
const withAuth = (ComponentToBeRendered) => {
const mapState = (state) => ({
isAuthenticated: state.currentUser.isAuthenticated,
});
const Authenticate = (props) => {
const { isAuthenticated } = useSelector(mapState);
useEffect(() => {
if (!isAuthenticated) {
props.history.push("/signin");
}
}, [isAuthenticated]);
return isAuthenticated && <ComponentToBeRendered {...props} />;
};
return Authenticate;
};
export default withAuth;
You could render this in a container using React-Router-DOM as such:
import withAuth from "../hocs/withAuth"
import Component from "../components/Component"
// ...
<Route path='...' component={withAuth(Component)} />
With React Navigation, is there a way to link from outside to a specific path/screen inside a Navigator?
For example to implement a global footer, like this:
<Provider store={store}>
<View>
<AppNavigator />
<MyFooter /> // Link from here to a path/screen inside AppNavigator
</View>
</Provider>
I think refs might work here. If you want to use Navigator from the same level you declare it you can use react's refs and pass props to MyFooter. Look at example in official documentation.
const AppNavigator = StackNavigator(SomeAppRouteConfigs);
class App extends React.Component {
someFunction = () => {
// call navigate for AppNavigator here:
this.navigator && this.navigator.dispatch({ type: 'Navigate', routeName, params });
}
render() {
return (
<View>
<AppNavigator ref={nav => { this.navigator = nav; }} />
<MyFooter someFunction={this.someFunction} />
</View>
);
}
}
Go to this link:
https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html
React Navigation Version: 5.x
Sometimes you need to trigger a navigation action from places where you do not have access to the navigation prop, such as a Redux middleware. For such cases, you can dispatch navigation actions from the navigation container.
If you're looking for a way to navigate from inside a component without needing to pass the navigation prop down, see useNavigation.
You can get access to the root navigation object through a ref and pass it to the RootNavigation which we will later use to navigate.
// App.js
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}
In the next step, we define RootNavigation, which is a simple module with functions that dispatch user-defined navigation actions.
// RootNavigation.js
import * as React from 'react';
export const navigationRef = React.createRef();
export function navigate(name, params) {
navigationRef.current?.navigate(name, params);
}
// add other navigation functions that you need and export them
Then, in any of your javascript modules, just import the RootNavigation and call functions which you exported from it. You may use this approach outside of your React components and, in fact, it works just as well when used from within them.
// any js module
import * as RootNavigation from './path/to/RootNavigation.js';
// ...
RootNavigation.navigate('ChatScreen', { userName: 'Lucy' });
Apart from navigate, you can add other navigation actions:
import { StackActions } from '#react-navigation/native';
export function push(...args) {
navigationRef.current?.dispatch(StackActions.push(...args));
}
Note that a stack navigators needs to be rendered to handle this action. You may want to check the docs for nesting for more details.
When writing tests, you may mock the navigation functions, and make assertions on whether the correct functions are called with the correct parameters.
Handling initialization
When using this pattern, you need to keep few things in mind to avoid crashes in your app.
The ref is set only after the navigation container renders
A navigator needs to be rendered to be able to handle actions
If you try to navigate without rendering a navigator or before the navigator finishes mounting, it will throw and crash your app if not handled. So you'll need to add an additional check to decide what to do until your app mounts.
For an example, consider the following scenario, you have a screen somewhere in the app, and that screen dispatches a redux action on useEffect/componentDidMount. You are listening for this action in your middleware and try to perform navigation when you get it. This will throw an error, because by this time, the parent navigator hasn't finished mounting. Parent's useEffect/componentDidMount is always called after child's useEffect/componentDidMount.
To avoid this, you can set a ref to tell you that your app has finished mounting, and check that ref before performing any navigation. To do this, we can use useEffect in our root component:
// App.js
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef, isMountedRef } from './RootNavigation';
export default function App() {
React.useEffect(() => {
isMountedRef.current = true;
return () => (isMountedRef.current = false);
}, []);
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}
Also export this ref from our RootNavigation:
// RootNavigation.js
import * as React from 'react';
export const isMountedRef = React.createRef();
export const navigationRef = React.createRef();
export function navigate(name, params) {
if (isMountedRef.current && navigationRef.current) {
// Perform navigation if the app has mounted
navigationRef.current.navigate(name, params);
} else {
// You can decide what to do if the app hasn't mounted
// You can ignore this, or add these actions to a queue you can call later
}
}