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

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?

Related

React Mobx can't display observable contents, very simple app

Very simple app, I'm trying to display content from my API using Mobx and Axios, here's my Axios agent.ts:
import { ITutorialUnit } from './../model/unit';
import axios, { AxiosResponse } from "axios";
//set the base URL
axios.defaults.baseURL = "http://localhost:5000/api";
//store our request in a const
const responseBody = (response: AxiosResponse) => response.data;
const requests = {
get: (url: string) => axios.get(url).then(responseBody),
};
//create a const for our activty's feature,all our activities' request are go inside our Activities object
const TutorialUnits = {
list: ():Promise<ITutorialUnit[]> => requests.get("/tutorialunits"),
};
export default{
TutorialUnits
}
then I call this agent.s in a store:
import { ITutorialUnit } from "./../model/unit";
import { action, observable } from "mobx";
import { createContext } from "react";
import agent from "../api/agent";
class UnitStore {
#observable units: ITutorialUnit[] = [];
//observable for loading indicator
#observable loadingInitial = false;
#action loadUnits = async () => {
//start the loading indicator
this.loadingInitial = true;
try {
//we use await to block anything block anything below list() method
const units = await agent.TutorialUnits.list();
units.forEach((unit) => {
this.units.push(unit);
// console.log(units);
});
this.loadingInitial = false;
} catch (error) {
console.log(error);
this.loadingInitial = false;
}
};
}
export default createContext(new UnitStore());
then I call this in my App component:
import React, { Fragment, useContext, useEffect } from "react";
import { Container } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import NavBar from "../../features/nav/NavBar";
import { ActivityDashboard } from "../../features/Units/dashboard/tutorialUnitDashboard";
import UnitStore from "../stores/unitStore";
import { observer } from "mobx-react-lite";
import { LoadingComponent } from "./LoadingComponent";
const App = () => {
const unitStore = useContext(UnitStore);
useEffect(() => {
unitStore.loadUnits();
//need to specify the dependencies in dependenciy array below
}, [unitStore]);
//we are also observing loading initial below
if (unitStore.loadingInitial) {
return <LoadingComponent content="Loading contents..." />;
}
return (
<Fragment>
<NavBar />
<Container style={{ marginTop: "7em" }}>
<ActivityDashboard />
</Container>
</Fragment>
);
};
export default observer(App);
Finally, I want to use this component to display my content:
import { observer } from "mobx-react-lite";
import React, { Fragment, useContext } from "react";
import { Button, Item, Label, Segment } from "semantic-ui-react";
import UnitStore from "../../../app/stores/unitStore";
const UnitList: React.FC = () => {
const unitStore = useContext(UnitStore);
const { units } = unitStore;
console.log(units)
return (
<Fragment>
{units.map((unit) => (
<h2>{unit.content}</h2>
))}
</Fragment>
);
};
export default observer(UnitList);
I can't see the units..
Where's the problem? My API is working, I tested with Postman.
Thanks!!
If you were using MobX 6 then you now need to use makeObservable method inside constructor to achieve same functionality with decorators as before:
class UnitStore {
#observable units: ITutorialUnit[] = [];
#observable loadingInitial = false;
constructor() {
// Just call it here
makeObservable(this);
}
// other code
}
Although there is new thing that will probably allow you to drop decorators altogether, makeAutoObservable:
class UnitStore {
// Don't need decorators now anywhere
units: ITutorialUnit[] = [];
loadingInitial = false;
constructor() {
// Just call it here
makeAutoObservable(this);
}
// other code
}
More info here: https://mobx.js.org/react-integration.html
the problem seems to be the version, I downgraded my Mobx to 5.10.1 and my mobx-react-lite to 1.4.1 then Boom everything's fine now.

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

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

Next.js. How to call component's getInitialProps in Layout

I add header component to layout but i don't want to send props from layout to header for every page I want use getInitialProps
layout.js
import Header from './header'
export default ({title}) => (
<div>
<Head>
<title>{ title }</title>
<meta charSet='utf-8' />
</Head>
<Header />
{children}
</div>
)
header.js
export default class Header extends Component {
static async getInitialProps () {
const headerResponse = await fetch(someapi)
return headerResponse;
}
render() {
console.log({props: this.props})
return (
<div></div>
);
}
}
console: props: {}
App.js
import Layout from './layout'
import Page from './page'
import axios from 'axios'
const app = (props) => (
<Layout >
<Page {...props}/>
</Layout>
)
app.getInitialProps = async function(){
try {
const response = await axios.get(someUrl)
return response.data;
} catch(e) {
throw new Error(e);
}
}
export default app
I want to use get initial props in Header component but it not firing
EDIT: 2021 way of doing this :
// with typescript remove type if you need a pure javascript solution
// _app.tsx or _app.jsx
import type { AppProps } from 'next/app';
import Layout from '../components/Layout';
export default function App({ Component, pageProps }: AppProps) {
return (
<Layout {...pageProps}>
<Component {...pageProps} />
</Layout>
);
}
// In order to pass props from your component you may need either `getStaticProps` or `getServerSideProps`.
// You should definitely not do that inside your `_app.tsx` to avoid rendering your whole app in SSR;
// Example for SSR
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch(`https://.../data`)
const data = await res.json()
// Pass data to the page via props
return { props: { data } }
}
Next.js uses the App component to initialize pages. You can override it and control the page initialization.
What you could do is, put your logic inside the _app override and pass it to your children components, example with a Layout component.
Create a page/_app.js
import React from 'react'
import App, { Container } from 'next/app'
import Layout from '../components/Layout'
export default class MyApp extends App {
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
/* your own logic */
return { pageProps }
}
render () {
const { Component, pageProps } = this.props
return (
<Container>
<Layout {...pageProps}>
<Component {...pageProps} />
</Layout>
</Container>
)
}
}
There is a good example from zeit at github

Categories

Resources