5

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?

GiamPy
  • 3,543
  • 3
  • 30
  • 51
  • I have faced similar issues when trying to access contexts in functional components using `Context.Consumer` API. Have you tried to convert it into a class component using `static contextType = ApiContext`. Not a solution but it will help you find out if this is the issue – ManavM Feb 15 '19 at 12:32
  • 1
    The proben is that getInitialProps is a static method and hence you can't access class instance properties from within it – Shubham Khatri Feb 15 '19 at 12:37
  • @ShubhamKhatri Oh right... That's so obvious. How can I do something like this, then? I need to pass it as an argument of the getInitialProps from my HOC, right? That's also what I have tried, however I can not access React.Context data programmatically, if I wrap my Component with the Consumer context, getInitialProps won't be called properly. – GiamPy Feb 15 '19 at 12:38

1 Answers1

5

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?

felixmosh
  • 32,615
  • 9
  • 69
  • 88