So I have a slightly weird setup where I redirect all routes where I need dynamic routing to index.tsx and use react-router-dom to handle those routes Client-side. I mainly had to do this because it seems that when exporting Next.js sites to a static site, Next.js generates pages for each route, so I can't generate new routes in Javascript.
However, this setup doesn't seem work properly when using react-router-dom's <Navigate /> component. For example,
// index.tsx
const Home: FC = () => {
// Currently going to generate the room id locally, but we
// should do this server side so that we can guarantee uniqueness.
return (
<Router>
<Routes>
<Route path="/" element={<RedirectToMain />} />
<Route path="/:roomId" element={<Main />} />
</Routes>
</Router>
);
}
const RedirectToMain: FC = () => {
const generatedRoomId = Array(5)
.fill(null)
.map(x => alphabet[Math.floor(26 * Math.random())])
.join('');
console.log('navigating');
return <Navigate to={generatedRoomId} />;
}
const Main: FC = () => {
const { roomId } = useParams();
console.log(roomId);
// We must wait for roomId to load before running.
if (!roomId || roomId.length === 0) {
return <></>;
}
return (
<WebSocketProvider roomId={roomId}>
<MinesAppBar url={`http://mineswpr.io/${roomId}`} />
<Board />
</WebSocketProvider>
);
};
When running this locally with yarn next dev, it redirects to localhost:3000/ABCDE and then the url changes back to localhost:3000/. When running this as a static site which is setup to redirect all traffic to /index.html, it seems to constantly call <Navigate />.
How can I fix this issue?
Related
I'm working with NextJS, Next-auth and Django as backend. I'm using the credentials provider to authenticate users. Users are authenticated against the Django backend and the user info together with the accesstoken is stored in the session.
I'm trying to use useSWR now to fetch data from the backend. (no preloading for this page required, that's why I'm working with SWR) I need to send the access_token from the session in the fetcher method from useSWR. However I don't know how to use useSWR after the session is authenticated. Maybe I need another approach here.
I tried to wait for the session to be authenticated and then afterwards send the request with useSWR, but I get this error: **Error: Rendered more hooks than during the previous render.
**
Could anybody help with a better approach to handle this? What I basically need is to make sure an accesstoken, which I received from a custom backend is included in every request in the Authorization Header. I tried to find something in the documentation of NextJS, Next-Auth or SWR, but I only found ways to store a custom access_token in the session, but not how to include it in the Header of following backend requests.
This is the code of the component:
import { useSession } from "next-auth/react";
import useSWR from 'swr';
import axios from 'axios'
export default function Profile() {
const { data: session, status } = useSession();
// if session is authenticated then fetch data
if (status == "authenticated") {
// create config with access_token for fetcher method
const config = {
headers: { Authorization: `Bearer ${session.access_token}` }
};
const url = "http://mybackend.com/user/"
const fetcher = url => axios.get(url, config).then(res => res.data)
const { data, error } = useSWR(url, fetcher)
}
if (status == "loading") {
return (
<>
<span>Loading...</span>
</>
)
} else {
return (
<>
{data.email}
</>
)
}
}
you don't need to check status every time. what you need to do is to add this function to your app.js file
function Auth({ children }) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated() {
router.push("/sign-in");
},
});
if (status === "loading") {
return (
<div> Loading... </div>
);
}
return children;
}
then add auth proprety to every page that requires a session
Page.auth = {};
finally update your const App like this
<SessionProvider session={pageProps.session}>
<Layout>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</Layout>
</SessionProvider>
so every page that has .auth will be wrapped with the auth component and this will do the work for it
now get rid of all those if statments checking if session is defined since you page will be rendered only if the session is here
Thanks to #Ahmed Sbai I was able to make it work. The component now looks like this:
import { useSession } from "next-auth/react";
import axios from "axios";
import useSWR from 'swr';
Profile.auth = {}
export default function Profile() {
const { data: session, status } = useSession();
// create config with access_token for fetcher method
const config = {
headers: { Authorization: `Bearer ${session.access_token}` }
};
const url = "http://mybackend.com/user/"
const fetcher = url => axios.get(url, config).then(res => res.data)
const { data, error } = useSWR(url, fetcher)
if (data) {
return (
<>
<span>{data.email}</span>
</>
)
} else {
return (
<>
Loading...
</>
)
}
}
App component and function:
function Auth({ children }) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated() {
router.push("/api/auth/signin");
},
});
if (status === "loading") {
return (
<div> Loading... </div>
);
}
return children;
}
function MyApp({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={pageProps.session}>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</SessionProvider>
)
}
I created a wrapper for the pages which will bounce unauthenticated users to the login page.
PrivateRoute Wrapper:
import { useRouter } from 'next/router'
import { useUser } from '../../lib/hooks'
import Login from '../../pages/login'
const withAuth = Component => {
const Auth = (props) => {
const { user } = useUser();
const router = useRouter();
if (user === null && typeof window !== 'undefined') {
return (
<Login />
);
}
return (
<Component {...props} />
);
};
if (Component.getInitialProps) {
Auth.getInitialProps = Component.getInitialProps;
}
return Auth;
};
export default withAuth;
That works \o/, However I noticed a behavior when I log out, using Router.push('/',), to return the user to the homepage the back button contains the state of previous routes, I want the state to reset, as a user who is not authenticated should have an experience as if they're starting from scratch...
Thank you in advance!
You can always use Router.replace('/any-route') and the user will not be able to go back with back button
I am fetching data from firebase with the result being undefined. When I comment out the elements that use the data, the data gets stored in state, I uncomment the elements and they render without any issues. When I make changes and save the file, it reverts back to being undefined and the elements do not render.
Here is the code I am currently working with :
const router = useRouter();
const { make, model, id } = router.query;
const [singleCar, setSingleCar] = useState([]);
const docRef = doc(db, "car listings", `${id}`);
useEffect(() => {
const fetchDetails = async () => {
await getDoc(docRef).then((doc) => {
const carData = doc.data();
setSingleCar(carData);
});
await onSnapshot(docRef, (doc) => {
const carData = doc.data();
setSingleCar(carData);
});
};
fetchDetails();
}, [singleCar]);
I know I am both fetching a single doc and adding in a real-time listener but I wasn't sure which would be the best action, given the getDoc method only runs once after fetching the document so thought it might be better to have a real-time listener also in place.
Here is what I am rendering, my thinking is that if state is undefined, render the Loader component until the state has changed with the appropriate data.
{singleCar === [] ? (
<Loader />
) : (
<Container maxW="container.xl">
<Box display="flex" justifyContent="space-around">
<Image
w="500px"
src={singleCar.carImages[0].fileURL}
borderRadius="10px"
/>
{/* <CarImageGalleryModal isOpen={isOpen} onClose={onClose} /> */}
<Box
display="flex"
flexDirection="column"
justifyContent="space-between"
>
<SingleCarPrimary
make={singleCar.carDetails.make}
model={singleCar.carDetails.model}
year={singleCar.carDetails.year}
doors={singleCar.carDetails.doors}
engineSize={singleCar.carDetails.engine_size}
fuelType={singleCar.carDetails.fuel_type}
body={singleCar.carDetails.year}
price={singleCar.carPrice}
/>
<ContactSection />
</Box>
</Box>
<CarSummary carDetails={singleCar.carDetails} />
<SingleCarDescription carDescription={singleCar.carDescription} />
</Container>
)}
Here is how the information is being stored in Firebase to give you an idea of what data is being retrieved.
Backend: Firebase version 9
Frontend: Next.js / Chakra UI
The issue should be with the queried document identifier that is not referring to any document, hence the undefined properties result you are getting.
Here down a list of fixes you should apply to your implementation to mitigate such an issue.
You are better using a separate loading state that is will be reset once data is loaded so that it can reflect both loading and empty results:
const router = useRouter();
const { make, model, id } = router.query;
const [singleCar, setSingleCar] = useState({}); // the car should be an object and not an array
const [loading, setLoading] = useState(true);
const docRef = doc(db, "cars", `${id}`); // avoid spaces in document names
useEffect(() => {
const fetchDetails = async () => {
await getDoc(docRef).then((doc) => {
setLoading(false);
if (doc.exists()) {
setSingleCar(doc.data());
}
});
};
// fetch the `car` data
fetchDetails();
// then attach the change listener
await onSnapshot(docRef, (doc) => {
const carData = doc.data();
setSingleCar(carData);
});
}, []); // you should not rerun the effect on state changes as this will keep on reattaching state listener again and again and may fall into endless fetching loops
You component declaration then may be updated as follows (you may still need to update it to reflect empty car state => the data is fetched but there is not car reflecting the queried id):
{loading === true ? (
<Loader />
) : (
<Container maxW="container.xl">
<Box display="flex" justifyContent="space-around">
<Image
w="500px"
src={singleCar.carImages[0].fileURL}
borderRadius="10px"
/>
{/* <CarImageGalleryModal isOpen={isOpen} onClose={onClose} /> */}
<Box
display="flex"
flexDirection="column"
justifyContent="space-between"
>
<SingleCarPrimary
make={singleCar.carDetails.make}
model={singleCar.carDetails.model}
year={singleCar.carDetails.year}
doors={singleCar.carDetails.doors}
engineSize={singleCar.carDetails.engine_size}
fuelType={singleCar.carDetails.fuel_type}
body={singleCar.carDetails.year}
price={singleCar.carPrice}
/>
<ContactSection />
</Box>
</Box>
<CarSummary carDetails={singleCar.carDetails} />
<SingleCarDescription carDescription={singleCar.carDescription} />
</Container>
)}
I was able to fix this using the method of getServerSideProps which I think removes the need for useEffect.
This page is the most relevant information I can find but it isn't enough.
I have a generic component that displays an appbar for my site. This appbar displays a user avatar that comes from a separate API which I store in the users session. My problem is that anytime I change pages through next/link the avatar disappears unless I implement getServerSideProps on every single page of my application to access the session which seems wasteful.
I have found that I can implement getInitialProps in _app.js like so to gather information
MyApp.getInitialProps = async ({ Component, ctx }) => {
await applySession(ctx.req, ctx.res);
if(!ctx.req.session.hasOwnProperty('user')) {
return {
user: {
avatar: null,
username: null
}
}
}
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return {
user: {
avatar: `https://cdn.discordapp.com/avatars/${ctx.req.session.user.id}/${ctx.req.session.user.avatar}`,
username: ctx.req.session.user.username
},
pageProps
}
}
I think what's happening is this is being called client side on page changes where the session of course doesn't exist which results in nothing being sent to props and the avatar not being displayed. I thought that maybe I could solve this with local storage if I can differentiate when this is being called on the server or client side but I want to know if there are more elegant solutions.
I managed to solve this by creating a state in my _app.js and then setting the state in a useEffect like this
function MyApp({ Component, pageProps, user }) {
const [userInfo, setUserInfo] = React.useState({});
React.useEffect(() => {
if(user.avatar) {
setUserInfo(user);
}
});
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<NavDrawer user={userInfo} />
<Component {...pageProps} />
</ThemeProvider>
);
}
Now the user variable is only set once and it's sent to my NavDrawer bar on page changes as well.
My solution for this using getServerSideProps() in _app.tsx:
// _app.tsx:
export type AppContextType = {
navigation: NavigationParentCollection
}
export const AppContext = createContext<AppContextType>(null)
function App({ Component, pageProps, navigation }) {
const appData = { navigation }
return (
<>
<AppContext.Provider value={appData}>
<Layout>
<Component {...pageProps} />
</Layout>
</AppContext.Provider>
</>
)
}
App.getInitialProps = async function () {
// Fetch the data and pass it into the App
return {
navigation: await getNavigation()
}
}
export default App
Then anywhere inside the app:
const { navigation } = useContext(AppContext)
To learn more about useContext check out the React docs here.
I have set-up my Firebase project as per the video from David East, as below with this in my app.js file. I have removed my config parameters.
#topName refers to an element on the page that displays the authenticated user's username. Unfortunately what happens is that someone logs in, or is logged in and goes to the page, it initially displays guest and then after some time it switches to the username of that user. This is quick (<500ms) but causes the page to render twice which is confusing.
How can I avoid this, do I need to store something in local storage?
(function() {
//Initialise Firebase
var config = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: ""
};
firebase.initializeApp(config);
//Add a realtime listener.
firebase.auth().onAuthStateChanged(firebaseUser => {
if (firebaseUser) {
console.log(firebaseUser);
$('#topName').text(firebaseUser.email);
}
else
{
console.log('not logged in');
$('#topName').text("Guest");
}
});
}());
This is normal, it happens since the data that is being entered is being sent to the Firebase server, then you wait for a response from Firebase to check if this email is authenticated or not. Also internet connection can effect this.
So lots of stuff are happening in the background, to solve this maybe add a loading spinner widget, or try and store the credentials locally.
To solve this you can use localStorage example:
localStorage.setItem("lastname", "userx"); //store data
document.getElementById("result").innerHTML = localStorage.getItem("lastname") //to retrieve
For more info check this: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
Or you can use sessionStorage, more info here: https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
i solved this with the next code, just show a loader component while waiting for auth.onAuthStateChanged, the var has three values null, true and false.
const Routes = () => {
const dispatch = useDispatch();
// firebase observer for user auth
useEffect(() => {
let unsubscribeFromAuth = null;
unsubscribeFromAuth = auth.onAuthStateChanged(user => {
if (user) {
dispatch(setCurrentUser(user));
} else {
dispatch(clearCurrentUser());
}
});
return () => unsubscribeFromAuth();
}, [dispatch]);
return (
<Switch>
<Route exact path="/" component={Dashboard} />
<Route path="/signin">
<SignIn />
</Route>
<ProtectedRoute path="/protected">
<Dashboard />
</ProtectedRoute>
<Route path="*">
<NoMatch />
</Route>
</Switch>
);
};
const ProtectedRoute = ({ children, ...rest }) => {
const currentUser = useSelector(state => state.auth.currentUser);
if (currentUser === null) {
// handle the delay in firebase auth info if current user is null show a loader if is false user sign out
// TODO create a loading nice component
return <h1>Loading</h1>;
}
return (
<Route
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
render={({ location }) =>
currentUser ? (
children
) : (
<Redirect
to={{
pathname: '/signin',
state: { from: location }
}}
/>
)
}
/>
);
};
export default Routes;