Why a state is not resolved as a variable in nextjs - javascript

I am using passportjs middleware for authentication and it works, but when I'm trying to use the user object in components the property is undefined, although is passed in _app.js.
The app is nextjs based with an express server. I know the user is authenticated because I can trace it in the server, but not in any component.
// _app.js
import React from 'react';
import App, { Container } from 'next/app';
import { ThemeProvider } from '#material-ui/styles';
import theme from '../theme/theme';
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
if (ctx.req && ctx.req.session && ctx.req.session.passport) {
pageProps.user = ctx.req.session.passport.user;
}
return { pageProps };
}
constructor(props) {
super(props);
this.state = {
user: props.pageProps.user
};
}
render() {
const { Component, pageProps } = this.props;
const props = {
...pageProps,
user: this.state.user,
};
return (
<Container>
<ThemeProvider theme={theme}>
<Component {...props} />
</ThemeProvider>
</Container>
);
}
}
export default MyApp;
Webstorm is pointing at user: this.state.user line, complaining unresolved variable user but I do not understand why is not resolved as a variable, it is defined in the constructor.
Edit: and this is the server.js
const express = require("express");
const http = require("http");
const next = require("next");
const session = require("express-session");
const passport = require("passport");
const uid = require('uid-safe');
const authRoutes = require("./auth-routes");
const oAuth2Strategy = require("./lib/passport-oauth2-userinfo");
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
const sessionConfig = {
secret: uid.sync(18),
cookie: {
maxAge: 86400 * 1000 // 24 hours in milliseconds
},
resave: false,
saveUninitialized: true
};
server.use(session(sessionConfig));
passport.use(new oAuth2Strategy(
{
authorizationURL: process.env.REACT_APP_AUTH_URL,
tokenURL: process.env.REACT_APP_AUTH_TOKEN,
clientID: process.env.REACT_APP_CLIENT_ID,
clientSecret: process.env.REACT_APP_SECRET,
callbackURL: process.env.REACT_APP_CALLBACK,
userProfileURL: process.env.REACT_APP_OPENID
},
function(accessToken, refreshToken, extraParams, profile, done) {
console.log(profile);
return done(null, profile);
}
));
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
server.use(passport.initialize());
server.use(passport.session());
server.use(authRoutes);
const restrictAccess = (req, res, next) => {
if (!req.isAuthenticated()) return res.redirect("/login");
next();
};
server.use("/", restrictAccess);
server.use("/profile", restrictAccess);
server.get("*", handle);
http.createServer(server).listen(process.env.PORT, () => {
console.log(`listening on port ${process.env.PORT}`);
});
});
Edit 2: thanks to #SimplyComplexable I troubleshooted a little bit. In my index.js I can access the user prop like this e.g. this.props.user.displayName with no problem.
This works:
import Landing from "./Landing"
import React from "react";
class Home extends React.Component {
render() {
return (
<div>
{this.props.user.displayName}
<Landing />
</div>
)
}
}
export default Home;
But, for example, in Landing component the prop.user is undefined. Maybe I am not accessing correctly or passing the prop somehow?
class Landing extends React.Component {
const {user} = this.props;
return (
<div>
...
)
}
export default withStyles(useStyles)(Landing);

The issue based on the code you have now, is that you're not passing user down to the Landing component.
If you update your home page with the following changes, you're code should work.
import Landing from "./Landing"
import React from "react";
class Home extends React.Component {
render() {
return (
<div>
{this.props.user.displayName}
<Landing user={this.props.user}/>
</div>
)
}
}
export default Home;
With user specifically this would be a great opportunity for context, so that you don't have to explicitly pass down the user prop to every component.
Here's a quick example:
import Landing from "./Landing"
import React from "react";
export const UserContext = React.createContext({});
class Home extends React.Component {
render() {
return (
<div>
{this.props.user.displayName}
<UserContext.Provider value={this.props.user}>
<Landing user={this.props.user}/>
</UserContext.Provider>
</div>
)
}
}
export default Home;
import React from 'react';
import UserContext from './Home';
const Landing = () => {
const user = React.useContext(UserContext);
return (
<div>
{user.displayName}
</div>
)
}
export default withStyles(useStyles)(Landing);

Related

react useContext is reseting a web socket connection each time a page loads

I am using websocket for various parts of my nextJS app and need to export them everywhere while not resetting the socket connection. If it resets it drops all the rooms it was following which causes issues in my application. I use a context to make them global and initialize them inside my _app.tsx
this is my socket contexts in /contexts/socket.tsx
import socketio from "socket.io-client";
import React from "react";
export const socket = socketio();
export const SocketContext = React.createContext(socket);
here is the page that has issues at pages/chat/index.tsx
import { useState, useContext, useEffect } from "react";
import AppLayout from "../../layouts/AppLayout";
import useUser from '../../hooks/useUser';
import { SocketContext } from '../../contexts/socket';
export default function Chat() {
const username = useUser().username;
const [messages, setMessages] = useState<Array<Message>>([]);
const socket = useContext(SocketContext); //THIS resets if the page reloads despite being a context
useEffect(() => {
socket.on("newIncomingMessage", (msg: any) => {
setMessages((currentMsg) => [
...currentMsg,
{ author: msg.author, message: msg.message },
]);
});
}, [])
}
this is how I initialize the socket in pages/_app.tsx
import { SocketContext, socket } from '../contexts/socket';
const App = ({ Component, pageProps }: AppProps) => {
return (
<SocketContext.Provider value={socket}>
<main>
<Component {...pageProps} key={router.asPath} />
<Toaster position="bottom-center" toastOptions={{ duration: 3000, }} />
</main>
</SocketContext.Provider>
)
};
export default wrapper.withRedux(App);
I am using this for the backend for now pages/api/socket.tsx
import { Server } from "socket.io";
export default function SocketHandler(req: any, res: any) {
// if socket server already initialized do nothing
if (res.socket.server.io) {
console.log("server socket already set up");
res.end();
} else {
const io = new Server(res.socket.server);
res.socket.server.io = io;
const onConnection = (socket: any) => {
console.log('server new socket.id: ', socket.id)
messageHandler(io, socket);
};
// Define actions inside
io.on("connection", onConnection);
}
res.end();
}
//export messageHandler (io, socket) => { //if you take to another fule
function messageHandler(io: any, socket: any) {
const createdMessage = (msg: any) => {
socket.broadcast.emit("newIncomingMessage", msg);
};
socket.on("createdMessage", createdMessage);
};
Maybe move the socket creation to a higher level - simular to the way you would use Authentication or a Connection SQL Server. ~ like a singleton.

React Context and Provider data doesn't get passed down as expected

I am trying to update an object that another component is using.
My Context:
import React, {Component, createContext} from "react";
import jwt from 'jsonwebtoken';
const LOCAL_STORAGE_AUTH_TOKEN = 'authToken';
interface AuthenticatedUser {
username?: string;
guildName?: string;
guildId?: string;
}
interface AuthContextType {
authenticated: boolean; // to check if authenticated or not
user: AuthenticatedUser; // store user details
token: string; //jwt token
refreshToken: string; //jwt refresh token
handleAuthentication: (username: string, password: string) => Promise<void>; // handle login process
logout: () => Promise<void>; // log out the user
}
export const AuthContext = createContext<AuthContextType>({
authenticated: false, // to check if authenticated or not
user: {}, // store all the user details
token: '', // store all the user details
refreshToken: '', //jwt refresh token
handleAuthentication: (username: string, password: string): Promise<void> =>
Promise.resolve(), // handle login process
logout: (): Promise<void> => Promise.resolve(), // logout the user
});
AuthContext.displayName = 'AuthContext';
export class AuthProvider extends Component {
state = {
authenticated:false,
user: {},
token:'',
refreshToken:''
};
constructor(props: any) {
super(props);
const token = window.localStorage.getItem(LOCAL_STORAGE_AUTH_TOKEN) || '';
const jwtData = jwt.decode(token);
let user = {};
let authenticated = false;
let refreshToken = '';
if(jwtData && typeof jwtData !== 'string'){
authenticated = true;
user = {
username: jwtData.data?.username || '',
// TODO: Add the other sources too
}
}
this.state = {
authenticated,
user,
token,
refreshToken
}
}
handleAuthentication = async (username: string, password: string):Promise<void> => {
fetch(process.env.REACT_APP_API_ENDPOINT + "users/login", {
method:"POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({username:username, password:password})
})
.then(response => response.json())
.then((data) => {
/*
This data is coming back correctly
*/
const user = {
username: data?.user.username,
guildName: data?.user.guild_information[0].guildName,
guildId: "123123"
}
this.setState({
authenticated: true,
user: user,
token: data?.token,
refreshToken: data?.refreshToken
})
window.localStorage.setItem(LOCAL_STORAGE_AUTH_TOKEN, data?.token)
//TODO: Also save token in localstorage
})
.catch((err) => {
console.log(err)
})
}
logout = async ():Promise<void> => {
//TODO: Log out the current user
}
render() {
const authProviderValue = {
...this.state,
handleAuthentication: this.handleAuthentication,
logout: this.logout
}
return (
<AuthContext.Provider value={authProviderValue}>
{this.props.children}
</AuthContext.Provider>
)
}
}
And the App component where I use it:
import React, {useContext} from "react";
import { useStyles} from "./style";
import {AuthContext, AuthProvider} from "../../context/UserContext";
import RoutesProvider from "./Routes";
import Login from "../Login";
export default function App() {
const classes = useStyles();
const { user } = useContext(AuthContext)
return (
<AuthProvider>
{typeof user.username !== "undefined" ? (
<div className={classes.content}>
<RoutesProvider />
</div>
):(
<Login />
)}
</AuthProvider>
)
}
Also wrapped the App component within the AuthProvider as recommended:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import { SnackbarProvider } from 'notistack';
import { AuthProvider} from "./context/UserContext";
import './styles/index.css'
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<SnackbarProvider maxSnack={3}>
<App />
</SnackbarProvider>
</AuthProvider>
</React.StrictMode>,
document.getElementById('root')
);
It does take the initial value if I change it in the AuthContext. But after updating it does not update in the App component. I'm a little confused as to why this doesn't work as expected. If anyone knows what causes this (or if I'm doing it wrong) let me know please. Currently 5 hours into debugging...
If I understand your question correctly, you're asking:
"Why your user constant doesn't update when your handleAuthentication method is successful"
The answer is because you're initializing your user constant outside of your provider.
Use your AuthProvider in your index.tsx instead of your App component like this:
<AuthProvider>
<App />
</AuthProvider>
Or transfer your context logic in a children component

nextjs getInitialProps behaving strangely when navigating via link

I need getInitialProps to run server side very time a page is rendered, but it seems it only runs on first render of a page in my project. Every subsequent render (e.g., I followed a link or pushed a new route via Router.push(\link)), only runs client side and I don't have access to ENV variables defined on the server-side, e.g. my GraphQL API_URL.
This is my project structure.
./pages
_app.tsx
index.tsx
other.tsx
In _app.tsx I have the following
class CustomApp extends App {
static async getInitialProps({ Component, ctx }: AppContext) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
render() {
const { Component, pageProps } = this.props;
return (
<>
<Head>
<title>My cool app</title>
</Head>
<Component {...pageProps} />
</>
);
}
}
export default CustomApp;
In index.tsx
import React, { Component } from 'react';
import Router from 'next/router';
class Index extends Component {
navigateToApplication = () => {
Router.push('/other');
};
render() {
return (
<div style={{width:100px, height:20px, color:red}} onClick={this.navigateToApplication}>
Click me!
</div>
);
}
}
export default Index;
In other.tsx I have the following:
import React, { Component } from 'react';
import dynamic from 'next/dynamic';
import config from '../src/config';
//Ensure the WizardComponent loads CLIENT SIDE. This makes the 'fetch' utility available for the gqlClient
const WizardComponent = dynamic(() => import('../src/components/layout/WizardComponent'), { ssr: false });
class Application extends Component {
static async getInitialProps() {
return { config };
}
render() {
return <ApplicationWizard apiUrl={config.API_URL} />;
}
}
export default Application;
In config.ts the following:
import getConfig from 'next/config';
const nextConfig = getConfig();
const clientConfig = (nextConfig && nextConfig.publicRuntimeConfigÄ) || {};
const settings = Object.keys(process.env).length > 1 ? process.env : clientConfig;
const config = new (class {
constructor(private readonly settings: Record<string, string | undefined>) {
console.log('getting details', settings);
console.log('NODE_ENV', this.settings['NODE_ENV']);
console.log('API_URL', this.settings['API_URL']);
}
readonly ENVIRONMENT = process.env.ENVIRONMENT || this.settings['NODE_ENV'] || 'development';
readonly API_URL = process.env.API_URL || this.settings['API_URL'] || 'http://localhost:4000/graphql';
})(settings);
export default config;
In next.config.js I have the following:
const pick = require('lodash/pick');
const withCSS = require('#zeit/next-css');
const withSass = require('#zeit/next-sass');
const withPlugins = require('next-compose-plugins');
const withGraphql = require('next-plugin-graphql');
const path = require('path');
const nextConfig = {
webpack: (config, options) => {
config.resolve.alias['src'] = path.join(__dirname, 'src/');
return config;
}
,publicRuntimeConfig : pick(process.env, ['NODE_ENV', 'API_URL'])
};
module.exports = withPlugins([withCSS, withSass, withGraphql], nextConfig);
If I visit http://host.url/other directly, the getInitialProps method executes as expected. If I navigate to the page via a nextjs method, e.g. Router.push or a <Link> HOC, then the env variables returns undefined and I fall back to default values...
I'm obviously missing something here..please help!
All I want to do is have env variable API_URL available on my other.tsx page regardless of how I get there...

How can i get a client side cookie with next.js?

I can't find a way to get a constant value of isAuthenticated variable across both server and client side with next.js
I am using a custom app.js to wrap the app within the Apollo Provider. I am using the layout to display if the user is authenticated or not. The defaultPage is a HOC component.
When a page is server side, isAuthenticated is set a true. But as soon as I change page - which are client side rendering (no reload) - the isAuthenticated remain at undefined all the way long until I reload the page.
_app.js
import App from 'next/app';
import React from 'react';
import withData from '../lib/apollo';
import Layout from '../components/layout';
class MyApp extends App {
// static async getInitialProps({ Component, router, ctx }) {
// let pageProps = {};
// if (Component.getInitialProps) {
// pageProps = await Component.getInitialProps(ctx);
// }
// return { pageProps };
// }
render() {
const { Component, pageProps, isAuthenticated } = this.props;
return (
<div>
<Layout isAuthenticated={isAuthenticated} {...pageProps}>
<Component {...pageProps} />
</Layout>
</div>
);
}
}
export default withData(MyApp);
layout.js
import React from "react";
import defaultPage from "../hoc/defaultPage";
class Layout extends React.Component {
constructor(props) {
super(props);
}
static async getInitialProps(ctx) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps, isAuthenticated };
}
render() {
const { isAuthenticated, children } = this.props;
return (
<div>
{isAuthenticated ? (
<h2>I am logged</h2>
) : (
<h2>I am not logged</h2>
)}
{children}
</div>
)
}
}
export default defaultPage(Layout);
defaultPage.js
/* hocs/defaultPage.js */
import React from "react";
import Router from "next/router";
import Auth from "../components/auth";
const auth = new Auth();
export default Page =>
class DefaultPage extends React.Component {
static async getInitialProps(ctx) {
const loggedUser = process.browser
? auth.getUserFromLocalCookie()
: auth.getUserFromServerCookie(ctx);
const pageProps = Page.getInitialProps && Page.getInitialProps(ctx);
let path = ""
return {
...pageProps,
loggedUser,
currentUrl: path,
isAuthenticated: !!loggedUser
};
}
render() {
return <Page {...this.props} />;
}
};
What am I missing here?
I think client side and server side are not use the same cookie. So here is how you get client side cookie and you have to attach this cookie in your server side request.
static async getInitialProps(ctx) {
// this is client side cookie that you want
const cookie = ctx.req ? ctx.req.headers.cookie : null
// and if you use fetch, you can manually attach cookie like this
fetch('is-authenticated', {
headers: {
cookie
}
}
}
With NextJs you can get client aide cookie without any extra library, but what I'll encourage you to do is install
js-cookie
import cookie from "js-cookie"
export default () => {
//to get a particular cookie
const authCookie = cookies.get("cookieName")
return "hey"
}
export const getServerSideProps = async ({req: {headers: {cookie}} => {
console.log(cookie)
return {
props: {key: "whatever you want to return"
}
}
Its been long, I used class components, but you get the concept in case you'll need a class component

Isomorphic-style-loader, Cannot read property 'apply' of null

Hi I've seen this same error and multiple possible solutions but none has been able to solve my issue (Probably because I'm lacking in depth understanding of the whole React structure).
I know that context.insertCss.apply(context, styles); isn't receiving the context and that's why the error is thrown, I've added the ContextProvider but I'm afraid this could be conflicting with my routing setup. Also used Daniel's answer to this question [Why does isomorphic-style-loader throw a TypeError: Cannot read property 'apply' of undefined when being used in unison with CSS-Modules
server index.js
app.get('/*', (req, res) => {
const matchingRoutes = matchRoutes(Routes, req.url);
let promises = [];
matchingRoutes.forEach(route => {
if (route.loadData) {
promises.push(route.loadData());
}
});
// promise.then(data => {
Promise.all(promises).then(dataArr => {
// Let's add the data to the context
// const context = { data };
// const context = { dataArr };
const css = new Set()
const context = { insertCss: (...styles) => styles.forEach(style => css.add(style._getCss()))}
const app = React.renderToString(
<StaticRouter location={req.url}>
<ContextProvider context={context}>
<App/>
</ContextProvider>
</StaticRouter>
)
const indexFile = path.resolve('./build/index.html');
fs.readFile(indexFile, 'utf8', (err, indexData) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Oops, better luck next time!');
}
if (context.status === 404) {
res.status(404);
}
if (context.url) {
return res.redirect(301, context.url);
}
return res.send(
indexData
.replace('<style id="myStyle"></style>',`<style type="text/css" id="myStyle">${[...css].join('')}</style>`)
.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
.replace(
'</body>',
`<script>window.__ROUTE_DATA__ = ${serialize(dataArr)}</script></body>`
)
);
});
});
});
Added on the server the ContextProvider in the renderToString(..) method, also I'm replacing the html body so the received CSS is attached to the HTML response.
ContextProvider.js
import React from 'react';
import PropTypes from 'prop-types'
import App from './App'
class ContextProvider extends React.Component {
static childContextTypes = {
insertCss: PropTypes.func,
}
getChildContext() {
return {
...this.props.context
}
}
render() {
return <App {
...this.props
}
/>
}
}
export default ContextProvider
Used the context provider from Daniel's answer (Reference above)
Client index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import ContextProvider from './ContextProvider';
const context = {
insertCss: (...styles) => {
const removeCss = styles.map(x => x._insertCss());
return () => {
removeCss.forEach(f => f());
};
},
}
ReactDOM.hydrate(
<BrowserRouter>
<ContextProvider context={context} />
</BrowserRouter>,
document.getElementById('root')
);
Passing the context through the ContextProvider as supposed.
App.js used inside the ContextProvider
import React from 'react';
import { renderRoutes } from 'react-router-config';
import { Switch, NavLink } from 'react-router-dom';
import Routes from './routes';
export default props => {
return (
<div>
<ul>
<li>
<NavLink to="/">Home</NavLink>
</li>
<li>
<NavLink to="/todos">Todos</NavLink>
</li>
<li>
<NavLink to="/posts">Posts</NavLink>
</li>
</ul>
<Switch>
{renderRoutes(Routes)}
</Switch>
</div>
);
};
Home.js where I'm trying to test the custom style
import React from 'react';
import withStyles from '../../node_modules/isomorphic-style-loader/withStyles'
import styles from '../scss/Home.scss';
function Home(props, context) {
return (
<h1>Hello, world!</h1>
)
}
export default withStyles(styles)(Home);
routes.js describes the routes used.
import Home from './components/Home';
import Posts from './components/Posts';
import Todos from './components/Todos';
import NotFound from './components/NotFound';
import loadData from './helpers/loadData';
const Routes = [
{
path: '/',
exact: true,
component: Home
},
{
path: '/posts',
component: Posts,
loadData: () => loadData('posts')
},
{
path: '/todos',
component: Todos,
loadData: () => loadData('todos')
},
{
component: NotFound
}
];
export default Routes;
Almost sure there is an easy fix for this issue but it doesn't seem so trivial to me. Thank you in advance.
Please try to use the built in StyleContext of isomorphic-style-loader instead of custom context provider.
server.js:
import StyleContext from 'isomorphic-style-loader/StyleContext';
const insertCss = (...styles) => {
const removeCss = styles.map(style => style._insertCss());
return () => removeCss.forEach(dispose => dispose());
};
ReactDOM.render(
<StyleContext.Provider value={{ insertCss }}>
<Router>{renderRoutes(Routes)}</Router>
</StyleContext.Provider>,
document.getElementById('root')
);
client.js:
app.get('/*', function(req, res) {
const context = {};
const css = new Set(); // CSS for all rendered React components
const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()));
const component = ReactDOMServer.renderToString(
<StyleContext.Provider value={{ insertCss }}>
<StaticRouter location={req.url} context={context}>
{renderRoutes(Routes)}
</StaticRouter>
</StyleContext.Provider>
);
if (context.url) {
res.writeHead(301, { Location: context.url });
res.end();
} else {
res.send(Html('React SSR', component));
}
});
You can see example project here: https://github.com/digz6666/webpack-loader-test/tree/ssr-2

Categories

Resources