Client components, localstorage in nextjs13 - next.js

I am moving the project from react to nextjs and have a problem. I have a global wrapper that handles the login context and is using local storage.
I set "use client" directive at the top, but the component is trying to be rendered on the server too, and this code gives a hydration error:
export function AuthProvider(props) {
const initialParsedToken = typeof localStorage !== 'undefined' ? localStorage.getItem("jwt") : null
if (initialParsedToken) {
const decodedToken = jwtDecode<JwtInterface>(initialParsedToken);
if (decodedToken.exp * 1000 < Date.now()) {
localStorage.removeItem("token");
} else {
initialState.user = decodedToken;
}
}
const [state, dispatch] = useReducer(authReducer, initialState);
if (typeof localStorage === 'undefined'){
return null
}
const login = (userData: any) => {
localStorage.setItem("jwt", userData.token);
dispatch({ type: "LOGIN", payload: userData });
};
function logout() {
localStorage.removeItem("jwt");
dispatch({ type: "LOGOUT" });
}
return (
<AuthContext.Provider
value={{ user: state.user, login, logout }}
{...props}
/>
);
}
The error:
Hydration failed because the initial UI does not match what was rendered on the server.
How can i use the useReducer hook in next, or make auth logic work with this framework?
I was planning to make render static content as server components, having client parts, but can I avoid using ugly useffect hack in each client component and just add a use client in nested dynamic content?
I tried using useffect hack in parent component, but nested components still don't work.

You should check on the type of window instead to make sure you are on the client side
if (typeof window !== undefined){
....your local storage logic
}
Also, you should consider moving your token to cookies instead of local storage as it doesn't cause conflicts and you can check your cookies on the server side using the context parameter, see this

Related

Unable to catch the 'auth' event in Hub.listen while calling Auth.federatedSignIn

I am using SolidJS and building a SPA (no server rendering). For authentication, I use the #aws-amplify/core and #aws-amplify/auth packages. At the application root I call the Hub.listen function:
Hub.listen('auth', ({ payload }) => console.log(payload));
In the SignUp component I call Auth.federatedSignIn:
const SignUp = () => {
return (
<button onClick={() => {
Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google });
}}>
Sign up
</button>
);
}
I have configured the Amplify as such:
Amplify.configure({
Auth: {
region: import.meta.env.VITE_AWS_REGION,
userPoolId: import.meta.env.VITE_AWS_POOL_ID,
userPoolWebClientId: import.meta.env.VITE_AWS_POOL_CLIENT_ID,
oauth: {
domain: import.meta.env.VITE_AUTH_URL,
responseType: 'code',
redirectSignIn: location.origin + '/account/external',
redirectSignOut: location.origin + '/my',
},
},
});
When I click on the button I am redirected to the import.meta.env.VITE_AUTH_URL (simply outside of my app), choose an account, and then return back to the /account/external page. At that time I expect a consoled payload object in Web tools, but there is nothing. I get it when I call Auth.signOut(), so I assume that I configured Amplify correctly and Hub is subscribed to the auth channel.
My thoughts were that Hub cannot catch any events because after returning the application basically renders again in a new context and Hub simply isn't able to catch anything (events aren't sent from AWS?). I tried to declare the urlOpener function under the oauth property in the config and Google's sign page opened in a new tab, but even then I couldn't get any events in the preserved old page (from which I called Auth.federatedSignIn).
Questions:
How should I organize the code to get the signIn and signUp events?
Can I pass some data into the Auth.federatedSignIn to get it back in the Hub.listen, so I will be able to join the CognitoUser with the data that existed at the time of starting Sign in/Sign up (I want to add a new login type to existed user)?
Here is an example regarding the first question. Just check that your listener is set before you call the Auth.federatedSignIn() method.
export default class SignInService {
constructor(private landingFacade: LandingFacade) {
this.setupAuthListeners(); // Should be called at the top level.
}
private setupAuthListeners() {
Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
this.landingFacade.signInSuccess();
break;
case 'signIn_failure':
console.log('Sign in failure', data);
break;
case 'configured':
console.log('the Auth module is configured', data);
}
});
}
public async signIn(): Promise<void> {
await Auth.federatedSignIn();
}
}
For the second one: I'll use a local state and set/query the object you need.

Nextjs urql auth exchange running on server when it should run on client

When trying to add an auth exchange to my urql client, it gets run on the server when the app starts and on the client subsequent times until refresh. The problem is in my getAuth function, which is as follows:
const getAuth = async ({ authState }) => {
const token = localStorage.getItem('5etoken');
if (!authState) {
if (token) {
return { token };
}
return null;
}
if (token) {
const decoded = jwt.decode(token) as jwt.JwtPayload;
if (decoded.exp !== undefined && decoded.exp < Date.now() / 1000) {
return { token };
}
}
return null;
};
When I run my app, I get an error saying localStorage is undefined. If I check that the function is running in the browser, then my token never gets set on app start and I'm logged out when I refresh the page, so I can't use that approach. I've tried multiple approaches:
Using dynamic imports with ssr set to false
Creating the client in a useEffect hook
Using next-urql's withUrqlClient HOC only using the auth exchange when in the browser
None of what I tried worked and I'm running out of ideas.
I eventually figured out that createClient was being called on the server side. I managed to force it to run in the browser by creating the client in a useEffect hook. I'm not sure why creating it in a useEffect didn't work months ago.

How to use secret key for on-demand revalidation for ISR (Next.js) in the frontend without exposing it?

According to the documentation, you should use a SECRET_TOKEN to prevent unauthorized access to your revalidate API route i.e.
https://<your-site.com>/api/revalidate?secret=<token>
But how are you supposed to call that route from the frontend and keep the token secret?
For example, if you have a simple POST that you then want to trigger the revalidate off of, you'd have to expose your secret token via NEXT_PUBLIC to be able to use it:
function handleSubmit(payload) {
axios.post(POST_URL, payload)
.then(() => {
axios.get(`/api/revalidate?secret=${process.env.NEXT_PUBLIC_SECRET_TOKEN}`)
})
.then(() => {
// redirect to on-demand revalidated page
})
}
What am I missing here? How can you call the API route through the frontend without exposing the SECRET_TOKEN?
I've been trying out On-Demand ISR and stumbled on a similar problem. I was trying to revalidate data after CRUD actions from my Admin dashboard living on the client, behind protected routes ("/admin/...").
If you have an authentication process setup and you're using Next-Auth's JWT strategy, it gives you access to the getToken() method, which decrypts the JWT of the current authenticated user.
You can then use whatever information you have passed through your callbacks to validate the request instead of relying on a SECRET_TOKEN.
import type { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const user = await getToken({ req, secret });
if (!user || user.role !== "ADMIN") {
return res.status(401).json({ message: "Revalidation not authorized"});
}
try {
// unstable_revalidate is being used in Next 12.1
// I'm passing the revalidation url through the query params
await res.unstable_revalidate(req.query.url as string);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send("Error revalidating");
}
}
The Next.js video demo don't actually use a SECRET_KEY.
https://www.youtube.com/watch?v=BGexHR1tuOA
So I guess I'll just have to omit it and hope nobody abuses the revalidate API?
I think you need to create one file called ".env".
Inside the file, you put the params .env like this:
NEXT_PUBLIC_SECRET_TOKEN=123password
You must install the dependency dotenv:
npm i dotenv
and then you can call inside your function like this
function handleSubmit(payload) {
axios.post(POST_URL, payload)
.then(() => {
axios.get(`/api/revalidate?secret=${process.env.NEXT_PUBLIC_SECRET_TOKEN}`)
})
.then(() => {
// redirect to on-demand revalidated page
})
}

Issues with NextJs SSR and cookies in Apollo

I would like to use cookies for authentication in my nextjs app. I have a bug in my code where the SSR won't work because somewhere in the execution process of the code it does not find the cookie on the first render of the page so it will throw an error. I have played with the code a lot now and have gotten it to a state where the data will eventually load but will not be a SSR page. Has anyone else dealt with this problem?
I am using next, apollo client and apollo server express.
When you do an SSR, the code runs on the server. The cookies you added in browser are not available as default. You can access then in getInitialProps or getServerSideProps via req.headers.cookie and pass it to the authentication code again.
Alternately, you can use an npm module like react-cookie https://www.npmjs.com/package/react-cookie which support isomorphic cookies. More examples on integration are available on the link.
We can custom the headers before sending.
Please check my full answer at this link https://github.com/apollographql/apollo-client/issues/5089#issuecomment-749301669
async function getHeaders(ctx) {
if (ctx?.req?.cookies) {
const cookieItems = []
for (let key of Object.keys(ctx?.req?.cookies)) {
cookieItems.push(`${key}=${ctx.req.cookies[key]}`)
}
return {
cookie: cookieItems.join('; ')
}
}
return {
}
}
WithApollo.getInitialProps = async (ctx) => {
const { AppTree } = ctx
// Initialize ApolloClient, add it to the ctx object so
// we can use it in `PageComponent.getInitialProp`.
const apolloClient = (ctx.apolloClient = initApolloClient(null, await getHeaders(ctx)))
// Run wrapped getInitialProps methods
let pageProps = {}
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx)
}
............
}
}

Aspnet server rendering debugging

I have a react-redux app running on aspnet core, with server side rendering using aspnet prerendering.
Lets say i make a programming error, where in child component I try to access a undefined prop because of a stupid typo.
import {Child} from './child'
export class Parent extends React.Component {
render () {
const someProp = {
something: "something"
};
return <Child someProp={someProp} />;
}
}
export class Child extends React.Component {
render() {
return <div>this.props.someprop.something</div>;
//typo: should be someProp instead of someprop
}
Without server rendering I would have got an error similar to this: cannot access something of undefined at line x:yy
But with serverrendering i get a:
An unhandled exception occurred while processing the request.
Exception: Call to Node module failed with error: Prerendering timed out after 30000ms because the boot function in 'ClientApp/src/boot-server' returned a promise that did not resolve or reject. Make sure that your boot function always resolves or rejects its promise. You can change the timeout value using the 'asp-prerender-timeout' tag helper.
this makes debugging quite hard, when you dont get any feedback on what went wrong.
Any one knows how to setup a reject if something fails ? or is it even possible to debug a server side rendered code ?
here is my boot-server file, tell me if you need some more files.
import * as React from 'react';
import { Provider } from 'react-redux';
import { renderToString } from 'react-dom/server';
import configureStore from './store/configureStore';
import {getFormById} from './actions/getFormActions';
import {updateUserLocale} from './actions/userLocaleActions';
import FormResponder from './components/mainComponents/formResponder';
export default function renderApp (params) {
return new Promise((resolve, reject) => {
const store = configureStore();
store.dispatch(getFormById(params.data.id, params.data.config, params.data.authenticationToken));
store.dispatch(updateUserLocale(params.data.userLocale));
const app = (
<Provider store={ store }>
<FormResponder />
</Provider>
);
// Perform an initial render that will cause any async tasks (e.g., data access) to begin
renderToString(app);
// Once the tasks are done, we can perform the final render
// We also send the redux store state, so the client can continue execution where the server left off
params.domainTasks.then(() => {
resolve({
html: renderToString(app),
globals: {
initialReduxState: store.getState(),
authenticationToken: params.data.authenticationToken,
config: params.data.config
}
});
}, reject); // Also propagate any errors back into the host application
});
}
I have had similar experience working with Visual Studio 2017. I eventually realized that the diagnostic information for the original error(s) was actually in the Output window.
I have done som research and have come to the conclusion that is not possible for the time beeing to debug the initial server rendered code.
what i have done instead is to implement logic, so that i can disable server rendering.
this is how it looks like:
public async Task<IActionResult> Index(string id, string userLocale = "en", bool server = true)
{
Guid positionId;
if (!Guid.TryParse(id, out positionId))
{
throw new Exception("Invalid position id");
}
var token = await _apiClient.GetToken();
var formData = new ApplicationFormViewModel()
{
Id = positionId,
UserLocale = userLocale,
AuthenticationToken = token.AccessToken,
Server = server
};
return View(formData);
}
view.cshtml:
#{if (#Model.Server) {
<div
class="container"
id="react-app"
asp-prerender-module="ClientApp/src/boot-server"
asp-prerender-data="new {
Id = #Model.Id,
UserLocale = #Model.UserLocale,
AuthenticationToken = #Model.AuthenticationToken,
Config = new {
ApplicationPostUrl = #Url.Action("SaveApplication"),
AttachmentPostUrl = #Url.Action("UploadAttachment"),
FormGetUrl = #Url.Action("GetForm")
}
}"
asp-prerender-webpack-config="webpack.config.js" >
Loading...
</div>
}
else {
<script>
var id= '#Model.Id';
var config= {
applicationPostUrl: '#Url.Action("SaveApplication")',
attachmentPostUrl: '#Url.Action("UploadAttachment")',
formGetUrl: '#Url.Action("GetForm")'
};
var userLocale='#Model.UserLocale';
var authenticationToken='#Model.AuthenticationToken';
var server = false;
</script>
<div class="container" id="react-app">loading</div>
}
}
#section scripts {
<script src="~/dist/main.js" asp-append-version="true"></script>
}
boot-server.jsx:
export default function renderApp (params) {
return new Promise((resolve, reject) => {
const store = configureStore();
store.dispatch(getFormById(params.data.id, params.data.config, params.data.authenticationToken));
store.dispatch(updateUserLocale(params.data.userLocale));
const app = (
<Provider store={ store }>
<FormResponder />
</Provider>
);
// Perform an initial render that will cause any async tasks (e.g., data access) to begin
renderToString(app);
// Once the tasks are done, we can perform the final render
// We also send the redux store state, so the client can continue execution where the server left off
params.domainTasks.then(() => {
resolve({
html: renderToString(app),
globals: {
initialReduxState: store.getState(),
authenticationToken: params.data.authenticationToken,
config: params.data.config,
server: true
}
});
}, reject); // Also propagate any errors back into the host application
});
}
boot-client.jsx:
// Grab the state from a global injected into server-generated HTML
const {id, initialReduxState, authenticationToken, config, server, userLocale } = window;
if (server) {
// Get the application-wide store instance, prepopulating with state from the server where available.
const store = configureStore(initialReduxState);
// This code starts up the React app when it runs in a browser.
ReactDOM.render(
<Provider store={ store }>
<FormResponder authenticationToken={authenticationToken} config={config} />
</Provider>,
document.getElementById('react-app')
);
}
else {
const store = configureStore();
store.dispatch(getFormById(id, config, authenticationToken));
store.dispatch(updateUserLocale(userLocale));
render(
<Provider store ={store}>
<FormResponder authenticationToken={authenticationToken} config={config} />
</Provider>,
document.getElementById('react-app')
); // Take our FormBuilder component and attach it with DOM element "app"
}
so now i can simply turn of server rendering by adding a ?server=false at the end of the url, and start debugging :)
Found a solution that works for me:
I inserted a try/catch on final renderToString.
where in catch i send a dispatch with the error.
updated boot-server.jsx
params.domainTasks.then(() => {
let html;
try {
html = renderToString(app);
}
catch (err) {
store.dispatch(loadFormFailed( {message: err.toString() } ));
}
resolve({
html: html,
globals: {
initialReduxState: store.getState(),
authenticationToken: params.data.authenticationToken,
config: params.data.config,
disableReactServerRendring: false
}
});
}, reject);
// Also propagate any errors back into the host application
});

Resources