relay-tools / found-relay

Relay integration for Found
MIT License
276 stars 32 forks source link

Invalid prop/context `relay` supplied to `Relay(VideoList)` #55

Closed donut closed 7 years ago

donut commented 7 years ago

I'm writing an isomorphic app and the server is rendering everything fine, but in the browser (Chrome) I get these errors:

warning.js:36 Warning: Failed context type: Invalid prop/context `relay` supplied to `Relay(VideoList)`, expected `undefined` to be an object with an `environment` and `variables`.
    in Relay(VideoList)
    in section (created by Main)
    in section (created by Main)
    in Main (created by AppFrame)
    in AppFrame
    in Renderer (created by BaseRouter)
    in StaticContainer (created by BaseRouter)
    in StaticContainer (created by BaseRouter)
    in BaseRouter (created by Connect(BaseRouter))
    in Connect(BaseRouter) (created by FarceRouter)
    in Provider (created by FarceRouter)
    in FarceRouter

buildReactRelayContainer.js:40 Uncaught (in promise) TypeError: Cannot read property 'environment' of undefined
    at ContainerConstructor (buildReactRelayContainer.js:40)
    at ReactCompositeComponent.js:305
    at measureLifeCyclePerf (ReactCompositeComponent.js:75)
    at ReactCompositeComponentWrapper._constructComponentWithoutOwner (ReactCompositeComponent.js:304)
    at ReactCompositeComponentWrapper._constructComponent (ReactCompositeComponent.js:279)
    at ReactCompositeComponentWrapper.mountComponent (ReactCompositeComponent.js:187)
    at Object.mountComponent (ReactReconciler.js:45)
    at ReactDOMComponent.mountChildren (ReactMultiChild.js:236)
    at ReactDOMComponent._createContentMarkup (ReactDOMComponent.js:659)
    at ReactDOMComponent.mountComponent (ReactDOMComponent.js:526)

I'm trying to follow the todo app example, which is running fine for me, but I can't figure out what I'm doing differently. It just seems that the relay context isn't getting passed to the components for some reason.

I'm using found-relay@0.3.0-alpha.4

Browser entry point (browser.tsx):


import 'babel-polyfill'

import BrowserProtocol = require('farce/lib/BrowserProtocol')
import createInitialBrowserRouter
    = require('found/lib/createInitialBrowserRouter')
import * as React from 'react'
import * as ReactDOM from 'react-dom'

import { createResolver, historyMiddlewares, renderConfig, routeConfig }
    from './App'
import { ClientFetcher } from './fetcher'

(async () => {

    const fetcher = new ClientFetcher('http://localhost:3000/graphql',
                                      window['__RELAY_PAYLOADS__'])
    const resolver = createResolver(fetcher)

    const Router = await createInitialBrowserRouter({
        historyProtocol: new BrowserProtocol(),
        historyMiddlewares,
        routeConfig,
        resolver,
        render: renderConfig,
    })

    ReactDOM.render(
        <Router resolver={resolver} />,
        document.getElementById('root')
    )

})()

Router/App File:


import queryMiddleware = require('farce/lib/queryMiddleware')
import createRender = require('found/lib/createRender')
import makeRouteConfig = require('found/lib/makeRouteConfig')
import Route = require('found/lib/Route')
import * as React from 'react'
const graphql = require('react-relay').graphql
import { Resolver } from 'found-relay'
import { Environment, Network, RecordSource, Store } from 'relay-runtime'

import RouteComponentProps from './props/RouteComponentProps'
import Main from './components/Main'
import VideoList from './components/VideoList'
import ErrorPage from './components/ErrorPage'

export class AppFrame extends React.Component<RouteComponentProps, any> {

    render() { return (
        <Main>
            {this.props.children}
        </Main>
    )}

}

export const historyMiddlewares = [queryMiddleware]

export function createResolver(fetcher) {
    const environment = new Environment({
        network: Network.create((...args) => fetcher.fetch(...args)),
        store: new Store(new RecordSource()),
    });

    return new Resolver(environment);
}

export const renderConfig = createRender({
    renderError: ({ error }) => { return (
        <AppFrame>
            <ErrorPage error={error}/>
        </AppFrame>
    ) }
})

const ActiveVideosQuery = graphql`
    query App_ActiveVideos_Query {
        activeVideos {
            ...VideoList_activeVideos
        }
    }
`

export const routeConfig = makeRouteConfig(
    <Route path="/" Component={AppFrame}>
        <Route Component={VideoList} query={ActiveVideosQuery} />
        {/*<Route Component={() => <h2>Video List</h2>} />*/}
        <Route path="videos/forms/replace" Component={() => <h2>Form</h2>} />
        <Route path="goomba" Component={() => <h2>Goomba</h2>} />
    </Route>,
)

VideoList component:

import * as React from 'react'
import * as Relay from 'react-relay'
const graphql = Relay.graphql

import Video from './Video'

interface VideoListProps {
    activeVideos: any[]
}

class VideoList extends React.Component<VideoListProps, any> {

    render() {
        const videos = this.props.activeVideos.map((vid) => { return (
            <Video key={vid.id} video={vid} />
        )})

        return (
            <section className="video-list">
                { videos }
            </section>
        )
    }

}

export default Relay.createFragmentContainer(VideoList, graphql`
    fragment VideoList_activeVideos on Video @relay(plural: true) {
        id
        ...Video_video
    }
`)
taion commented 7 years ago

Use createInitialFarceRouter per https://github.com/4Catalyzer/found-relay/blob/master/examples/todomvc-modern-universal/src/client.js#L21.

createInitialBrowserRouter ignores custom resolvers.

donut commented 7 years ago

That worked! I looked over the example so many times and just kept missing that difference. Thank you!

taion commented 7 years ago

Adding it to a list of things to document in https://github.com/4Catalyzer/found-relay/issues/56

creatzor commented 6 years ago

Im migrating from react-router to found-relay (ssr with codesplitting by route) and relay modern. I was unable to incrementally migrate to modern (lot of fragment and environment issues) so i decided to make the app as we have it now, work in the stack that can support modern, that way we can take our time with the migration.

However, im running into issues with context. The app seemingly renders correctly but passes the relay modern environment instead of the classic environment as the context in Component.context.relay.. any clue where I may have went wrong?

I've been looking everywhere trying to identify the problem but have come up empty handed. I can create a new issue if necessary, I just thought i'd try here to see if it was related in any way before polluting your issues.

the component requires the relay context in this way:

    CheckingComponent.contextTypes = {
      router: PropTypes.object,
      relay: Relay.PropTypes.Environment,
    }

console error:

Warning: Failed context type: Invalid prop/context `relay` supplied to `CheckingComponent`, expected `[object Object]` to be an object conforming to the `RelayEnvironment` interface.
    in CheckingComponent (created by Relay(CheckingComponent))
    in Relay(CheckingComponent) (created by RelayReadyStateRenderer)
    in StaticContainer (created by RelayReadyStateRenderer)
    in RelayReadyStateRenderer (created by RelayRouteRenderer)
    in RelayRouteRenderer
    in ElementsRenderer (created by BaseRouter)
    in StaticContainer (created by BaseRouter)
    in StaticContainer (created by BaseRouter)
    in BaseRouter (created by Connect(BaseRouter))
    in Connect(BaseRouter) (created by FarceRouter)
    in Provider (created by FarceRouter)
    in FarceRouter

router.jsx

import React from 'react'
//classic
import Relay from 'react-relay/classic'

import {
  RelayNetworkLayer,
  urlMiddleware,
} from 'react-relay-network-layer'
import cookie from 'react-cookie'
import {
  GRAPHQL_SERVER_URL
} from '../configuration/webapp/config'

import queryMiddleware from 'farce/lib/queryMiddleware'
import makeRouteConfig from 'found/lib/makeRouteConfig'
import Route from 'found/lib/Route'

import schema from '../schema/schema.json'

// import App from './scenes'
import routesRoot from './routes'

export const historyMiddlewares = [queryMiddleware]

//classic
//reqToken contains req.token for server side rendering
export function createEnvironment(reqToken) {
  const environment = new Relay.Environment()
  let token = reqToken || cookie.load('token')

  environment.injectNetworkLayer(
    new RelayNetworkLayer([
      urlMiddleware({
        url: req => GRAPHQL_SERVER_URL,
      }),
      next => req => {
        req.headers = {
          token,
          credentials: 'same-origin',
        }
        if (reqToken) {
          req.headers['Content-type'] = 'application/json'
        } 
        return next(req)
      },
    ]),
  )

  return environment
}

export const routeConfig = makeRouteConfig(routesRoot)

server.js

// @flow
// import { JssProvider, SheetsRegistry } from 'react-jss'

import path from 'path'

import createRender from 'found/lib/createRender'
import { getFarceResult } from 'found/lib/server'
import express from 'express'
import Helmet from 'react-helmet'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import serialize from 'serialize-javascript'
import { Resolver } from 'found-relay/lib/classic'

import {
  GRAPHQL_SERVER_URL,
} from '../configuration/webapp/config'

import ErrorComponent from './ErrorComponent'
import { createEnvironment, historyMiddlewares, routeConfig } from './router'

// Read environment
require('dotenv').load()

const envPortWebpack = process.env.PORT_WEBPACK

const server = express()
async function gatherLocationAndSiteInformation(req: Object, res: Object) {
  let assetsPath
  if (process.env.NODE_ENV === 'production') {
    assetsPath = `/`
  } else {
    assetsPath = `http://localhost:${envPortWebpack}/dev`
  }
  return { assetsPath }
}

const render = createRender({
  renderError(obj: Object): React$Element<*> {
    const { error } = obj
    return <ErrorComponent httpStatus={error.status} />
  },
})

server.use(async (req, res) => {
  try {
    const { assetsPath } = await gatherLocationAndSiteInformation(req, res)

    const environment = createEnvironment(req.cookies.token)
    const { redirect, status, element } = await getFarceResult({
      url: req.url,
      historyMiddlewares,
      routeConfig,
      resolver: new Resolver(environment),
      render,
    })
    if (redirect) {
      res.redirect(302, redirect.url)
      return
    }

    const helmet = Helmet.rewind()
    const rootHTML = ReactDOMServer.renderToString(element)

    res.render(
      path.resolve(
        __dirname,
        process.env.NODE_ENV === 'production'
          ? 'renderOnServer.ejs'
          : 'renderOnServer-template.ejs',
      ),
      {
        assets_path: assetsPath,
        root_html: rootHTML,
        helmet,
        relay_payload: serialize(environment, { isJSON: true })
      },
    )
  } catch (err) {
    log.log('error', 'Error: Render on server request', err)
    res.status(500).send(ReactDOMServer.renderToString(<ErrorComponent httpStatus={500} />))
  }
})

export default server

client.js


// @flow

// In order to use ES7 async/await
import 'babel-polyfill'

import BrowserProtocol from 'farce/lib/BrowserProtocol'
import createInitialFarceRouter from 'found/lib/createInitialFarceRouter'
import createRender from 'found/lib/createRender'
import React from 'react'
import Relay from 'react-relay/classic'
import ReactDOM from 'react-dom'
import injectTapEventPlugin from 'react-tap-event-plugin'

import { GRAPHQL_SERVER_URL } from '../configuration/webapp/config'

import { createEnvironment, historyMiddlewares, routeConfig } from './router'

import { Resolver } from 'found-relay/lib/classic'

const render = createRender({})

;(async () => {
  try {
    injectTapEventPlugin()
  } catch (e) {
    console.log('already injected tap plugin', e)
  }
  const resolver = new Resolver(createEnvironment())
  const Router = await createInitialFarceRouter({
    historyProtocol: new BrowserProtocol(),
    historyMiddlewares,
    routeConfig,
    resolver,
    render,
  })

  ReactDOM.render(<Router />, document.getElementById('root'))
})()

==== more supporting files ====

before found:

context-before-found

after found:

context-after-found
creatzor commented 6 years ago

@taion I would deeply appreciate any insight you may have on this issue

taion commented 6 years ago

v1 changes the shape of context.relay, even when using classic mode. See e.g. the changes I made for https://github.com/edvinerikson/relay-subscriptions/pull/35/files#diff-ca27f0129c75f4c86b6b281186ae94c3.

taion commented 6 years ago

That's a change in Relay proper, BTW – nothing to do directly with this library.

creatzor commented 6 years ago

Oh wow! Thank you very much, adapting to Relay.PropTypes.ClassicRelay solves my issue.