nextjs getInitialProps behaving strangely when navigating via link - javascript

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...

Related

How do I make a client-side only component for GatsbyJS?

How do I create a component for Gatsby that will load on the client-side, not at build time?
I created this one and it renders with gatsby develop but not with the rendered server-side rendering
import React from 'react';
import axios from 'axios';
import adapter from 'axios-jsonp';
export default class Reputation extends React.Component<{}, { reputation?: number }> {
constructor(props) {
super(props);
this.state = {};
}
async componentDidMount() {
const response = await axios({
url: 'https://api.stackexchange.com/2.2/users/23528?&site=stackoverflow',
adapter
});
if (response.status === 200) {
const userDetails = response.data.items[0];
const reputation = userDetails.reputation;
this.setState({
reputation
});
}
}
render() {
return <span>{ this.state.reputation?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") }</span>
}
}
If you don't want the component to be bundled in the main js file at build time, use loadable-components
Install loadable-components and use it as a wrapper for a component that wants to use a client-side only package. docs
import React, { Component } from "react";
import Loadable from "#loadable/component";
const LoadableReputation = Loadable(() =>
import("../components/Reputation")
);
const Parent = () => {
return (
<div>
<LoadableReputation />
</div>
);
};
export default Parent;
before render this component, make sure you have a window, to detect if there is a window object. I would write a hook for that:
function hasWindow() {
const [isWindow, setIsWindow] = React.useState(false);
React.useEffect(() => {
setIsWindow(true);
return ()=> setIsWindow(false);
}, []);
return isWindow;
}
In the parent component check if there is a window object:
function Parent(){
const isWindow = hasWindow();
if(isWindow){
return <Reputation />;
}
return null;
}

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

Why a state is not resolved as a variable in nextjs

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);

React: Context to pass state between two hierarchies of components

I am developing a website in which I want to be able to access the state information anywhere in the app. I have tried several ways of implementing state but I always get following error message:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of SOS.
Here is my SOS->index.js file:
import React, { useContext } from 'react';
import axios from 'axios';
import CONST from '../utils/Constants';
import { Grid, Box, Container } from '#material-ui/core';
import { styled } from '#material-ui/styles';
import { Header } from '../Layout';
import ListItem from './ListItem';
import SOSButton from './SOSButton';
import FormPersonType from './FormPersonType';
import FormEmergencyType from './FormEmergencyType';
import StateContext from '../App';
import Context from '../Context';
export default function SOS() {
const { componentType, setComponentType } = useContext(Context);
const timerOn = false;
//'type_of_person',
const ambulance = false;
const fire_service = false;
const police = false;
const car_service = false;
//static contextType = StateContext;
const showSettings = event => {
event.preventDefault();
};
const handleComponentType = e => {
console.log(e);
//this.setState({ componentType: 'type_of_emergency' });
setComponentType('type_of_emergency');
};
const handleEmergencyType = new_emergency_state => {
console.log(new_emergency_state);
// this.setState(new_emergency_state);
};
const onSubmit = e => {
console.log('in OnSubmit');
axios
.post(CONST.URL + 'emergency/create', {
id: 1,
data: this.state //TODO
})
.then(res => {
console.log(res);
console.log(res.data);
})
.catch(err => {
console.log(err);
});
};
let component;
if (componentType == 'type_of_person') {
component = (
<FormPersonType handleComponentType={this.handleComponentType} />
);
} else if (componentType == 'type_of_emergency') {
component = (
<FormEmergencyType
handleComponentType={this.handleComponentType}
handleEmergencyType={this.handleEmergencyType}
emergencyTypes={this.state}
timerStart={this.timerStart}
onSubmit={this.onSubmit}
/>
);
}
return (
<React.Fragment>
<Header title="Send out SOS" />
<StateContext.Provider value="type_of_person" />
<Container component="main" maxWidth="sm">
{component}
</Container>
{/*component = (
<HorizontalNonLinearStepWithError
handleComponentType={this.handleComponentType}
/>*/}
</React.Fragment>
);
}
I would really appreciate your help!
Just for reference, the Context file is defined as follows:
import React, { useState } from 'react';
export const Context = React.createContext();
const ContextProvider = props => {
const [componentType, setComponentType] = useState('');
setComponentType = 'type_of_person';
//const [storedNumber, setStoredNumber] = useState('');
//const [functionType, setFunctionType] = useState('');
return (
<Context.Provider
value={{
componentType,
setComponentType
}}
>
{props.children}
</Context.Provider>
);
};
export default ContextProvider;
EDIT: I have changed my code according to your suggestions (updated above). But now I get following error:
TypeError: Cannot read property 'componentType' of undefined
Context is not the default export from your ../Context file so you have to import it as:
import { Context } from '../Context';
Otherwise, it's trying to import your Context.Provider component.
For your file structure/naming, the proper usage is:
// Main app file (for example)
// Wraps your application in the context provider so you can access it anywhere in MyApp
import ContextProvider from '../Context'
export default () => {
return (
<ContextProvider>
<MyApp />
</ContextProvider>
)
}
// File where you want to use the context
import React, { useContext } from 'react'
import { Context } from '../Context'
export default () => {
const myCtx = useContext(Context)
return (
<div>
Got this value - { myCtx.someValue } - from context
</div>
)
}
And for godsakes...rename your Context file, provider, and everything in there to something more explicit. I got confused even writing this.

Accessing consumed React.Context in Next.js getInitialProps using HOC

I am attempting to abstract my API calls by using a simple service that provides a very simple method, which is just an HTTP call. I store this implementation in a React Context, and use its provider inside my _app.js, so that the API is globally available, but I have a problem at actually consuming the context in my pages.
pages/_app.js
import React from 'react'
import App, { Container } from 'next/app'
import ApiProvider from '../Providers/ApiProvider';
import getConfig from 'next/config'
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig()
export default class Webshop extends App
{
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
render () {
const { Component, pageProps } = this.props
return (
<Container>
<ApiProvider endpoint={publicRuntimeConfig.api_endpoint}>
<Component {...pageProps} />
</ApiProvider>
</Container>
);
}
}
Services/Api.js
import fetch from 'unfetch'
function Api (config)
{
const apiUrl = config.endpoint;
async function request (url) {
return fetch(apiUrl + '/' + url);
};
this.decode = async function (code) {
const res = request('/decode?code=' + code);
const json = await res.json();
return json;
}
return this;
}
export default Api;
Providers/ApiProvider.js
import React, { Component } from 'react';
import Api from '../Services/Api';
const defaultStore = null;
class ApiProvider extends React.Component
{
state = {
api: null
};
constructor (props) {
super(props);
this.state.api = new Api({ endpoint: props.endpoint });
}
render () {
return (
<ApiContext.Provider value={this.state.api}>
{this.props.children}
</ApiContext.Provider>
);
}
}
export const ApiContext = React.createContext(defaultStore);
export default ApiProvider;
export const ApiConsumer = ApiContext.Consumer;
export function withApi(Component) {
return function withApiHoc(props) {
return (
<ApiConsumer>{ context => <Component {...props} api={context} /> }</ApiConsumer>
)
}
};
pages/code.js
import React, { Component } from 'react';
import Link from 'next/link';
import { withApi } from '../Providers/ApiProvider';
class Code extends React.Component
{
static async getInitialProps ({ query, ctx }) {
const decodedResponse = this.props.api.decode(query.code); // Cannot read property 'api' of undefined
return {
code: query.code,
decoded: decodedResponse
};
}
render () {
return (
<div>
[...]
</div>
);
}
}
let hocCode = withApi(Code);
hocCode.getInitialProps = Code.getInitialProps;
export default hocCode;
The problem is that I am unable to access the consumed context. I could just make a direct fetch call within my getInitialProps, however I wanted to abstract it by using a small function that also takes a configurable URL.
What am I doing wrong?
You can't access an instance of your provider in as static method getInitialProps, it was called way before the React tree is generated (when your provider is available).
I would suggest you to save an Singelton of your API in the API module, and consume it inside the getInitialProps method via regular import.
Or, you can inject it to your componentPage inside the _app getInitialProps, something like that:
// _app.jsx
import api from './path/to/your/api.js';
export default class Webshop extends App {
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {}
ctx.api = api;
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
render () {
const { Component, pageProps } = this.props
return (
<Container>
<Component {...pageProps} />
</Container>
);
}
}
// PageComponent.jsx
import React, { Component } from 'react';
class Code extends React.Component
{
static async getInitialProps ({ query, ctx }) {
const decodedResponse = ctx.api.decode(query.code); // Cannot read property 'api' of undefined
return {
code: query.code,
decoded: decodedResponse
};
}
render () {
return (
<div>
[...]
</div>
);
}
}
export default Code;
Does it make sense to you?

Categories

Resources