How to add/append routes programatically in React18 - reactjs

I'm a quite new to React, but I'd like to achieve the following:
FrontendApp should:
check if user is authenticated -> draw MainLayout and build menu dynamically, based on JSON received from server api. For the menu to work, I accordingly need to add data to Routes
if user is not authenticated -> draw Login
What I did:
component, which is checking isAuthenticated and loading json from data from server:
const Preloader = () =>
{
const [menu, setMenu] = useState([]);
const {isAuthenticated} = useAuth()
async function fetchMenu()
{
const loadedMenu = await MenuService.getMenu();
setMenu(loadedMenu)
}
useEffect(() =>
{
fetchMenu()
}, [])
if(isAuthenticated)
return <Login/>;
return <MainLayout/>
}
And I'm using this component Preloading in my App
const App = () => {
return (
<BrowserRouter>
<Preloader />
</BrowserRouter>
);
};
auth check works fine, I receive MainLayout or Login depending on isAuthenticated status.
But, how to add all received from server routes to BrowserRouter?
UPD 1:
I'm receiving routes from server to determine, what routes and components are available to current authenticated user. And draw MainMenu correspondingly.

Related

How do I make Next.js 13 server-side components in the app directory that depend on useEffect for props?

I'm trying to write a Next.js 13 newsletter page in the app directory that uses server-side components that depend on useEffect for props. The useEffect fetches data from a REST API to get newsletters which will render the content of the page. The code I'm using is below. I'm having trouble figuring out how to configure the server-side components to work when I need to "use client" for interactivity. How can I make sure that the server-side components are rendered before it is sent to the client?
Code:
import Navbar from '#/components/navbar'
import Footer from '#/components/footer'
import Pagination from './pagination'
import IssueCards from './issueCards';
import { useState, useEffect } from 'react';
import axios from 'axios';
const Newsletters = () => {
const [issues, setIssues] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [issuesPerPage, setIssuesPerPage] = useState(5);
useEffect(() => {
const fetchIssue = async () => {
const res = await axios.get(`${process.env.NEXT_PUBLIC_BACKEND_API}/newsletters`)
setIssues(res.data)
}
fetchIssue()
}, [])
// Change page
const paginate = (pageNumber) => setCurrentPage(pageNumber);
const indexOfLastIssue = currentPage * issuesPerPage;
const indexOfFirstIssue = indexOfLastIssue - issuesPerPage;
const currentIssues = issues.slice(indexOfFirstIssue, indexOfLastIssue)
return (
<>
<Navbar />
<div className="newsletter-container" id='newsletter-container'>
<h1>Newsletters</h1>
<hr></hr>
<div className="newsletter-wrapper">
<IssueCards issues={currentIssues} />
<Pagination
issuesPerPage={issuesPerPage}
totalIssues={issues.length}
paginate={paginate}
/>
</div>
</div>
<Footer />
</>
);
}
export default Newsletters;
How do I configure Next.js 13 server-side components that depend on useEffect for props and ensure that the content is rendered before it is sent to the client?
I tried following the Nextjs docs on Server and Client components but I am unsure of how I can pass down the props information onto the server.
Unfortunately, server components don't allow for hooks such as useEffect, see documentation here.
You have two main options:
New way of fetching data
Server components allow for a new way of fetching data in a component, described here.
This approach would look something this:
async function getData() {
const res = await fetch('https://api.example.com/...');
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
// Recommendation: handle errors
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <main></main>;
}
Revert to client components
Your other option is to use the use client directive at the top of your file and leaving Newsletter as a client component. Of course, this way, you wouldn't get the benefits of server components, but this would prevent you from having to change your code substantially. Also, keep in mind that server components are still in beta.

This React Private Route isn't catching Firebase Auth in time?

I'm working with React to build a admin panel for a website, and using Firebase as the authentication system (and data storage, etc).
I've gone through a few Private Route versions, but finally settled on the one that seems to work best with Firebase. However, there is a minor problem. It works well when the user logins in, and according to Firebase Auth documentation, by default, it should be caching the user.
However, if I log in, and then close the tab and re-open it in a new tab, I get ejected back to the login page (as it should if the user isn't logged in).
I am running the site on localhost via node, but that probably shouldn't matter. A console.log reports that the user is actually logged in, but then gets kicked back anyway. Everything is encapsulated in a useEffect, which watches the LoggedIn value, and checks the Auth State.
Is there any way to prevent this from kicking a logged-in user out when they re-open the tab?
import { FunctionComponent, useState, useEffect } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
import Routes from '../../helpers/constants';
export const PrivateRoute: FunctionComponent<any> = ({
component: Component,
...rest
}) => {
const [LoggedIn, SetLoggedIn] = useState(false);
// Check if the User is still authenticated first //
useEffect(() => {
const auth = getAuth();
onAuthStateChanged(auth, (user:any) => {
if (user.uid !== null)
{
SetLoggedIn(true);
// This gets reached after the tab is re-opened, indicating it's cached.
console.log("Logged In");
}
});
}, [LoggedIn]);
// On tab reload however, this acts as if LoggedIn is set to false after the cache check
return (
<Route
{...rest}
render={(props:any) => {
console.log(LoggedIn);
return LoggedIn ? (
<Component {...props} />
) : (
<Redirect to={Routes.LOGIN} />
);
}}
/>
);
};
It redirects because in the first render of your private route the code that sets the LoggedIn state to true hasn't been executed yet. You could use an extra boolean state to avoid rendering the Routes when you haven't checked the auth status.
...
const [LoggedIn, SetLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
...
if (user.uid !== null) {
setLoading(false);
SetLoggedIn(true);
}
...
// On tab reload however, this acts as if LoggedIn is set to false after the cache check
if(loading) return <div>Loading...</div>; // or whatever UI you use to show a loader
return (
<Route
...
/>
);
};
You'll need to check for the user only on the component mount, you can have an empty dependency array in the useEffect hook, and also stop listening for updates in the hook clean up
useEffect(() => {
const auth = getAuth();
const unsubscribe = onAuthStateChanged(auth, (user:any) => {
...
});
return unsubscribe; // stop listening when unmount
}, []);
But you'll be reinventing the wheel a little, there is already a hook you could use https://github.com/CSFrequency/react-firebase-hooks/tree/master/auth#useauthstate

How do I provide API client so it has access to auth provider state?

I am working in a project that handles showing authenticated/non-authenticated components by setting a state variable in the auth provider.
function App() {
const { user, loading } = useAuth();
if (loading) {
return <LoadPanel visible={true} />;
}
if (user) {
return <Content />;
}
return <UnauthenticatedContent />;
}
export default function Root() {
const screenSizeClass = useScreenSizeClass();
return (
<Router history={customHistory}>
<AuthProvider>
<NavigationProvider>
<div className={`app ${screenSizeClass}`}>
<App />
</div>
</NavigationProvider>
</AuthProvider>
</Router>
);
So if user is set to null then the behaviour is that user is logged out.
The user is stored inside the auth context like so
function AuthProvider(props) {
const [user, setUser] = useState();
const [loading, setLoading] = useState(true);
///...
}
This causes me some problems as sometimes cookies get cleared or a session times out. The API client I am using allows for a method that gets triggered when the client gets a 401 status on a request. I want to set the auth user to null inside this method but I do not know how to call into the auth context from the client which I am declaring in global scope like this:
let _client = new JsonServiceClient(environment.apiUrl);
_client.onAuthenticationRequired = () => {
//how do I clear the auth provider user state from here?
return new Promise((resolve, reject) => resolve(true))
};
export let client = _client;
My thoughts are that I probably shouldn't be exposing the client on the global scope and I should be providing it inside the context somewhere but I can't quite get my head around how to structure this. All API requests go through this client.
Can someone point me in right direction for how to get this client working right with the way the auth works in this app I am working on please?

Fetch data after login

I have a login component:
const login = async (e) => {
e.preventDefault();
const {data} = await loginUserRes({variables: {
...formData,
}});
if (data) {
currentUserVar({
...data.loginUser,
isLoggedIn: true,
});
}
};
Which allows user to send some data to the server, if the credentials are correct a JWT is stored in a cookie for authentication.
I'm wondering what he best approach is to fetching data after a log in, on the dashboard I have a list of movies that the user has added. Should I use a react effect to react to the currentUserVar changing and then do request for the movies? Or perhaps I could add all the dashboard data to the return object of the apollo server, that way the data is present on login.
Yes you should react to your authorization data, But this info always considered as global state that you will need it in the whole app. so It is recommended to save your currentUserVar in a react context or whatever you are using to handle global state, Even if the app was small and you don't have global state then you can left it to your root component so you can reach this state in all your components.
Here how I handle auth in reactjs, I don't know if it is the better approach but I'm sure it is a very good solution:
Create a wrapper component named AuthRoute that checks if the user is logedin or
not.
Wrap all components that require user auth with AuthRoute
If we have user data then render wrapped component.
If there is no user data then redirect to Login component
const AuthRoute = ({component, children, ...rest}) => {
const Component = component;
const {globalState} = useContext(GlobalContext);
// Return Component if user is authed, else redirect it to login page
return (
<Route
{...rest}
render={() =>
!!globalState.currentUser ? ( // if current user is exsits then its login user
children
) : (
<Redirect
to={ROUTES_PATH.SIGN_IN.index}
/>
)
}
/>
);
};
Then any component can fetch the needed data in it's mount effect useEffect(() => { fetchData() }, []). and this is an indirect react to userData.
Note: if you are not using routing in your app, you still can apply this solution by converting AuathRoute component to HOC.

Component shows previous data when mount for fractions of seconds

I am developing an app named "GitHub Finder".
I am fetching the date in App component using async function and pass these function to User component as props and I call these functions in useEffect.
The problem is here, when I goto user page for second time it shows previous data which I passed in props from App component and then it shows loader and shows new data.
Here is App component code where I am fetching date from APIs and passing to User component through props.
// Get single GitHub user
const getUser = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setUser(res.data);
setLoading(false);
}
// Get user repos
const getUserRepos = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/users/${username}/repos?
per_page=5&sort=created:asc&client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setRepos(res.data);
setLoading(false);
}`
User component code.
useEffect(() => {
getUser(match.params.login);
getUserRepos(match.params.login);
// eslint-disable-next-line
}, []);
I've recorded a video, so you guys can easily understand what I am trying to say.
Video link
Check live app
How can I solve this problem?
Thank in advance!
Here is what happens in the app :
When the App component is rendered the first time, the state is user={} and loading=false
When you click on a user, the User component is rendered with props user={} and loading=false, so no spinner is shown and no data.
After the User component is mounted, the useEffect hooks is triggered, getUser is called and set loading=true (spinner is shown) then we get the user data user=user1 and set loading=false (now the user data is rendered)
When you go back to search page, the app state is still user=user1 and loading=false
Now when you click on another user, the User component is rendered with props user=user1 and loading=false, so no spinner is shown and the data from previous user is rendered.
After the User component is mounted, the useEffect hooks is triggered, getUser is called and set loading=true (spinner is shown) then we get the user data user=user2 and set loading=false (now the new user data is rendered)
One possible way to fix this problem :
instead of using the loading boolean for the User component, inverse it and use loaded
When the User component is unmounted clear the user data and the loaded boolean.
App component:
const [userLoaded, setUserLoaded] = useState(false);
const getUser = async username => {
await setUserLoaded(false);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID
}&client_secret=${process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
await setUser(res.data);
setUserLoaded(true);
};
const clearUser = () => {
setUserLoaded(false);
setUser({});
};
<User
{...props}
getUser={getUser}
getUserRepos={getUserRepos}
repos={repos}
user={user}
loaded={userLoaded}
clearUser={clearUser}
/>
User component:
useEffect(() => {
getUser(match.params.login);
getUserRepos(match.params.login);
// eslint-disable-next-line
return () => clearUser();
}, []);
if (!loaded) return <Spinner />;
You can find the complete code here
Please make your setUser([]) empty at the start of getUser like this:
const getUser = async (username) => {
setLoading(true);
setUser([]);
const res = await axios.get(
`https://api.github.com/users/${username}?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${
process.env.REACT_APP_GITHUB_CLIENT_SECRET}`
);
setUser(res.data);
setLoading(false);
}

Resources