I have React app in framework Next.js (but framework is not important) with authentification implemented.
Current app is getting user session data by API call after first load (I am using next-auth for this). This causes that there is interval between load and session fetch which results in small interval where user see loading spinners all over app.
I want to mitigate this with implementation of SSR (Server-side-rendering) where I can hydrate initial response with session data.
This can be easily done in Next.js by getServerProps method in Component, but the problem is that I need to implement this method on every single page I have.
Thats why I am searching for a way to make session state global across whole app so every page (component) can access this session data.
My main question is, if I should use HOC wrapper where I will implement getServerProps to receive session data, or I should use Redux store and hydrate this store with by same method.
I am not exactly sure if HOCs are still widely used, or they are just deprecated now.
I do not want to implement getInitialProps in Next.js custom _app.js file because this will disable ability to have statically genererated pages and session will be available in every page. I am planning that app needs this session data only in about half of pages.
So should I go with HOC or just inplement redux store for this?
You could do the following:
Create an authentication Context Provider that will pass auth data down to your components. As an example:
interface ContextProps {
authenticated: boolean;
handleLogin: () => void;
handleLogout: () => void;
}
const authContext = createContext<Partial<ContextProps>>(undefined);
Along with Context, create a hook that will make use of useEffect to determine authenticated state - could be checking cookie exists, or await call to API to check auth status. Set this as the relevant status and use this value in your provider.
interface AuthProviderProps {
children: any;
}
const AuthProvider = (({ children }: AuthProviderProps) => {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
});
export const useAuth = () => useContext(authContext);
export default AuthProvider;
Wrap in your _app.js:
...
<AuthProvider>
<Component pageProps={pageProps} />
</AuthProvider>
...
Access in relevant pages:
const Page = () => {
const { authenticated } = useAuth();
if (!authenticated) {
return (
<>
Not authenticated
</>
);
}
...
Then you can consume in your relevant components
Related
I am pretty new with NextJS (12) and I am creating an ecommerce website in which I have some UI filters; I am using getServerSideProps to fetch data since I need the site to be SEO compliant.
When a user clicks on the filters, I am updating the global state and triggering next/router that fetches fresh data and populate the page simply passing query params to the url. Everything seems to work pretty well.
When a user lands on the site with active filters (via url query params), I am populating the page with correct data via getServerSideProps (parsing url query params), but now I should also enrich Redux state with the active filters, to show the correct UI.
Is there any way to initialize Redux Toolkit store via url params on page load (SSR)?
I know I am not posting any code here, but I need to know if using Redux Toolkit in this scenario could be overkill in the first place, and if there's any way to achieve what I need without over complicated sync libraries.
Thank you very much
I don't know if this could help you and even if this could be a good practice or an anti-pattern instead, but - since I suppose that you've passed your store to the app there - why don't you just grab your query params via useRouter in the _app, compose the actual store in there with query string and then pass it to the store provider?
import { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import { Provider as StoreProvider } from 'react-redux';
import store from '../store';
const App = ({ Component, pageProps }: AppProps) => {
const { query } = useRouter();
const realStore = doSomethingWithQueryAndReturnRealStore(store, query);
return (
<StoreProvider store={realStore}>
<Component {...pageProps} />
</StoreProvider>
);
};
Keep in mind that I haven't tested it, it's just an assumption :)
I'm having a lot of trouble learning to properly load data into state in my todo app.
I have a next.js page component pages/index.tsx where I load data from my API via getServerSideProps and return it as a page prop called tasksData.
The tasksData is being returned properly and I can access them in my page component just fine via prop destructuring: const Home = ({ tasksData }: Home) => { }
I also have a React Context provider in _app.tsx called BoardProvider. This stores state for my task board, and employs useReducer() from the React Context API to update this state in context consumers, such as pages/index.tsx.
The challenge I am facing is how to make my UI's "source of truth" the state stored in my context provider (eg. const { { tasks }, dispatch } = useBoard();, rather than the page page props returned from my API (eg. the tasksData prop).
One approach I considered was to simply load the data in getServerSideProps and then set the state via a dispatched action in a useEffect hook:
useEffect(() => {
// On first render only, set the Context Provider state with data from my page props.
dispatch({ type: TaskAction.SET_TASKS, payload: tasksData });
});
However, this doesn't seem to be working because sometimes tasksData is undefined, presumably because Next.js has not yet made it available on page mount.
Another suggestion I heard was to fetch the data and pass it as pageProps to my Context Provider in _app.tsx. I believe this means using getInitialProps() in _app.tsx so that my provider's initial state is populated by my API. However, this disabled static optimization and other useful features.
Can anyone help me out with some pseudocode, documentation, or examples of how to use getServerSideProps in combination with React Context API?
Couple of points:
getServerSideProps should be invoked before the page is even rendered. So theoretically your tasksData is undefined is a bug! You can't have a server data to be unavailable unless you really really intend to have that happen in the first place.
Assuming getServerSideProps is always returning the right data, but you want to use your own data to override it. In your context, you can have this logic.
const Home = ({ tasksData }) => {
const value = { tasksData: {
// let me override it
}}
return (
<Context.Provider value={value}>
...
<Context.Provider>
)
}
If you have the context provided under a page, the above code is all you need. But if your context is provided in a root (parent of a page), you can still add the above code to re-provide the same context again with overridden value. Because this is how a context is designed, read https://javascript.plainenglish.io/react-context-is-a-global-variable-b4b049812028 for more info.
I am a little bit confused about protected routes in next.js.
First of all, I don't want to use any server side rendering. I want to statically export
via next export. In that case, how do I implement client side protected routes?
Say, I have a back-end server with basic JWT authentication. How do I make certain routes protected from certain user and redirect them in /login page?
Since you're wanting to create protected routes with a static export, you'll need to do everything in the browser.
Verify their JWT in the browser
If their JWT is valid, render the page (including all requests to fetch data)
If their JWT is not valid, redirect them
For this, we're going to create a wrapper AuthCheck component.
Related:
How can you create a Private route in next.js?
AuthCheck
For verifying the JWT, you can use any method you'd like, including sending it to an api endpoint to verify it. Though I'm unsure if you can use Next.js api endpoints with static exports.
import { useRouter } from 'next/router'
export const AuthCheck = (props) => {
const router = useRouter()
const isJWTValid = useIsJWTValid() // you need to implement this. In this example, undefined means things are still loading, null means user is not signed in, anything truthy means they're signed in
if (typeof window !== 'undefined' && user === null) router.push('/')
if(!user) return <Loading /> // a loading component that prevents the page from rendering
return props.children
}
You can then add this to your app.js.
const MyApp = ({ Component, pageProps }) => {
return (
<AuthCheck>
<Component {...pageProps} />
</AuthCheck>
)
}
export default MyApp
Alternatively, you can also add this to any individual page. With this, you may need to debug the timing of any fetched data.
export default const ProtectedPage = () => {
return (
<AuthCheck>
<!-- contents of the page -->
</AuthCheck>
)
}
I have currently working on a next.js project using react, redux and firebase.
When user enters a page that need authorization I use the following code to redirect them if they are not authenticated.
import React from 'react';
import Router from 'next/router';
import { firebase } from '../../firebase';
import * as routes from '../../constants/routes';
const withAuthorization = (needsAuthorization) => (Component) => {
class WithAuthorization extends React.Component {
componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
if (!authUser && needsAuthorization) {
Router.push(routes.SIGN_IN)
}
});
}
render() {
return (
<Component { ...this.props } />
);
}
}
return WithAuthorization;
}
export default withAuthorization;
I am using the following repo as my base project. The problem is the app seems to work fine but when I try to navigate to a page that requires authentication when I am not authenticated. I am not redirected immediately rather it shows the page first then redirects. Since the HOC uses
firebase.auth.onAuthStateChanged()
which is asynchronous. Is there a faster way of checking if the user is logged in.
I have so far considered the
firebase.auth().currentUser
but my internal state depends on the update of the onAuthStateChanged function when the user logs out in another page.
Using a listener to onAuthStateChanged is usually the correct way to restore authentication state upon a full page transition. Since there is a reload happening at such a transition, there is a chance that the authentication state has changed. Using an onAuthStateChanged listener ensures that Firebase calls your code after it has validated the authentication state of the user. Since this may require a call to the server, this can indeed takes some time, so you'll want to show a "loading..." animation.
If the state transition doesn't require a reload, like when you're just rendering a new route in the same page, then you can be reasonably certain that the authentication state doesn't change on the transition. In that case you could pass the current user from the previous component into the new one. Tyler has a good article on this: Pass props to a component rendered by React Router.
In the latter case firebase.auth().currentUser should also retain its state, and you can use it safely. You'd then still use an onAuthStateChanged to detect changes in authentication state after the new route has loaded.
Let's say we have a context provider set up, along with some initial data property values.
Somewhere along the line, let's say a consumer then modifies those properties.
On page reload, those changes are lost. What is the best way to persist the data so we can retain those data modifications? Any method other than simply local storage?
Yeah, if you want the data to persist across reloads, your options are going to be storing that info server-side (via an api call) or in browser storage (local storage, session storage, cookies). The option you'll want to use depends on what level of persistence you're looking to achieve. Regardless of storage choice, it would likely look something along the lines of
const MyContext = React.createContext(defaultValue);
class Parent extends React.Component {
setValue = (value) => {
this.setState({ value });
}
state = {
setValue: this.setValue,
value: localStorage.getItem("parentValueKey")
}
componentDidUpdate(prevProps, prevState) {
if (this.state.value !== prevState.value) {
// Whatever storage mechanism you end up deciding to use.
localStorage.setItem("parentValueKey", this.state.value)
}
}
render() {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
)
}
}
Context doesn't persist in the way you want. Here's a sample of what I've done, using stateless functional with React hooks.
import React, {useState, useEffect} from 'react'
export function sample(){
// useState React hook
const [data, setData] = useState({})
const [moreData, setMoreData] = useState([])
// useState React hook
useEffect(() => {
setData({test: "sample", user: "some person"})
setMoreData(["test", "string"])
}, [])
return data, moreData
}
export const AppContext = React.createContext()
export const AppProvider = props => (
<AppContext.Provider value={{ ...sample() }}>
{props.children}
</AppContext.Provider>
)
Understand from the start that this isa workaround, not a permanent solution. Persisting data is the job of a database, not the client. However, if you need persisted data for development, this is one way. Notice first that I'm using React hooks. This is a fully supported feature as of 16.8. The useEffect() replaces the lifecycle methods found in class declarations like that of TLadd above. He's using componentDidUpdate to persist. The most up-to-date way of doing this is useEffect. When the app is refreshed this method will be called and set some hard-coded data in context.
To use the provider:
import React from 'react'
import Component from './path/to/component'
import { AppProvider } from './path/to/context'
const App = () => {
return (
<AppProvider>
<Component />
</AppProvider>
)
}
When you refresh, data and moreData will still have whatever default values you assign to them.
I am assuming that you are already familiar with setting context and setting up the context provider.
One of the things you can do is to store the value in the browser's Cookie or any storage available to you, and then, in your Context Provider, retrieve the value, if you can find it, you set it, if not, set the default. If the provider file is a class based component, you would like to retrieve this value in the constructor(), otherwise if it is functional, you can use useLayoutEffect() to set it.