简体   繁体   中英

SO Relay QueryRenderer - TypeError: this.props.render is not a function

I have a project in React using Redux and Relay . The client connects to an API Server using GraphQL. I was trying to use the component QueryRenderer and I'm getting the following error:

TypeError: this.props.render is not a function
render
src/react-landing/node_modules/react-relay/lib/ReactRelayQueryRenderer.js:164

  161 |   if (process.env.NODE_ENV !== 'production') {
  162 |     deepFreeze(renderProps);
  163 |   }
> 164 |   return this.props.render(renderProps);
  165 | };
  166 | 
  167 | return ReactRelayQueryRenderer;

View compiled
finishClassComponent
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:13193

  13190 | } else {
  13191 |   {
  13192 |     ReactDebugCurrentFiber.setCurrentPhase('render');
> 13193 |     nextChildren = instance.render();
  13194 |     if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {
  13195 |       instance.render();
  13196 |     }

View compiled
updateClassComponent
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:13155

  13152 |   } else {
  13153 |     shouldUpdate = updateClassInstance(current, workInProgress, renderExpirationTime);
  13154 |   }
> 13155 |   return finishClassComponent(current, workInProgress, shouldUpdate, hasContext, renderExpirationTime);
  13156 | }
  13157 | 
  13158 | function finishClassComponent(current, workInProgress, shouldUpdate, hasContext, renderExpirationTime) {

View compiled
beginWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:13824

  13821 | case FunctionalComponent:
  13822 |   return updateFunctionalComponent(current, workInProgress);
  13823 | case ClassComponent:
> 13824 |   return updateClassComponent(current, workInProgress, renderExpirationTime);
  13825 | case HostRoot:
  13826 |   return updateHostRoot(current, workInProgress, renderExpirationTime);
  13827 | case HostComponent:

View compiled
performUnitOfWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:15863

  15860 |   startBaseRenderTimer();
  15861 | }
  15862 | 
> 15863 | next = beginWork(current, workInProgress, nextRenderExpirationTime);
  15864 | 
  15865 | if (workInProgress.mode & ProfileMode) {
  15866 |   // Update "base" time if the render wasn't bailed out on.

View compiled
workLoop
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:15902

  15899 | if (!isAsync) {
  15900 |   // Flush all expired work.
  15901 |   while (nextUnitOfWork !== null) {
> 15902 |     nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  15903 |   }
  15904 | } else {
  15905 |   // Flush asynchronous work until the deadline runs out of time.

View compiled
callCallback
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:100

   97 |   // nested call would trigger the fake event handlers of any call higher
   98 |   // in the stack.
   99 |   fakeNode.removeEventListener(evtType, callCallback, false);
> 100 |   func.apply(context, funcArgs);
  101 |   didError = false;
  102 | }
  103 | 

View compiled
invokeGuardedCallbackDev
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:138

  135 | // Synchronously dispatch our fake event. If the user-provided function
  136 | // errors, it will trigger our global error handler.
  137 | evt.initEvent(evtType, false, false);
> 138 | fakeNode.dispatchEvent(evt);
  139 | 
  140 | if (didError) {
  141 |   if (!didSetError) {

View compiled
invokeGuardedCallback
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:187

  184 |  * @param {...*} args Arguments for function
  185 |  */
  186 | invokeGuardedCallback: function (name, func, context, a, b, c, d, e, f) {
> 187 |   invokeGuardedCallback$1.apply(ReactErrorUtils, arguments);
  188 | },
  189 | 
  190 | /**

View compiled
replayUnitOfWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:15310

  15307 | // Replay the begin phase.
  15308 | isReplayingFailedUnitOfWork = true;
  15309 | originalReplayError = thrownValue;
> 15310 | invokeGuardedCallback$2(null, workLoop, null, isAsync);
  15311 | isReplayingFailedUnitOfWork = false;
  15312 | originalReplayError = null;
  15313 | if (hasCaughtError()) {

View compiled
renderRoot
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:15962

  15959 | 
  15960 | var failedUnitOfWork = nextUnitOfWork;
  15961 | if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
> 15962 |   replayUnitOfWork(failedUnitOfWork, thrownValue, isAsync);
  15963 | }
  15964 | 
  15965 | // TODO: we already know this isn't true in some cases.

View compiled
performWorkOnRoot
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16560

  16557 |   // This root is already complete. We can commit it.
  16558 |   completeRoot(root, finishedWork, expirationTime);
  16559 | } else {
> 16560 |   finishedWork = renderRoot(root, expirationTime, false);
  16561 |   if (finishedWork !== null) {
  16562 |     // We've completed the root. Commit it.
  16563 |     completeRoot(root, finishedWork, expirationTime);

View compiled
performWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16482

  16479 |   }
  16480 | } else {
  16481 |   while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || minExpirationTime >= nextFlushedExpirationTime)) {
> 16482 |     performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
  16483 |     findHighestPriorityRoot();
  16484 |   }
  16485 | }

View compiled
performSyncWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16454

  16451 | }
  16452 | 
  16453 | function performSyncWork() {
> 16454 |   performWork(Sync, false, null);
  16455 | }
  16456 | 
  16457 | function performWork(minExpirationTime, isAsync, dl) {

View compiled
requestWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16354

  16351 | 
  16352 | // TODO: Get rid of Sync and use current time?
  16353 | if (expirationTime === Sync) {
> 16354 |   performSyncWork();
  16355 | } else {
  16356 |   scheduleCallbackWithExpiration(expirationTime);
  16357 | }

View compiled
scheduleWork$1
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16218

  16215 | !isWorking || isCommitting$1 ||
  16216 | // ...unless this is a different root than the one we're rendering.
  16217 | nextRoot !== root) {
> 16218 |   requestWork(root, nextExpirationTimeToWorkOn);
  16219 | }
  16220 | if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
  16221 |   invariant(false, 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.');

View compiled
scheduleRootUpdate
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16785

  16782 |   }
  16783 |   enqueueUpdate(current, update, expirationTime);
  16784 | 
> 16785 |   scheduleWork$1(current, expirationTime);
  16786 |   return expirationTime;
  16787 | }
  16788 | 

View compiled
updateContainerAtExpirationTime
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16812

  16809 |     container.pendingContext = context;
  16810 |   }
  16811 | 
> 16812 |   return scheduleRootUpdate(current, element, expirationTime, callback);
  16813 | }
  16814 | 
  16815 | function findHostInstance(component) {

View compiled
updateContainer
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16839

  16836 |   var current = container.current;
  16837 |   var currentTime = recalculateCurrentTime();
  16838 |   var expirationTime = computeExpirationForFiber(currentTime, current);
> 16839 |   return updateContainerAtExpirationTime(element, container, parentComponent, expirationTime, callback);
  16840 | }
  16841 | 
  16842 | function getPublicRootInstance(container) {

View compiled
./node_modules/react-dom/cjs/react-dom.development.js/ReactRoot.prototype.render
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:17122

  17119 |   if (callback !== null) {
  17120 |     work.then(callback);
  17121 |   }
> 17122 |   updateContainer(children, root, null, work._onCommit);
  17123 |   return work;
  17124 | };
  17125 | ReactRoot.prototype.unmount = function (callback) {

View compiled
legacyRenderSubtreeIntoContainer/<
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:17262

  17259 |     if (parentComponent != null) {
  17260 |       root.legacy_renderSubtreeIntoContainer(parentComponent, children, callback);
  17261 |     } else {
> 17262 |       root.render(children, callback);
  17263 |     }
  17264 |   });
  17265 | } else {

View compiled
unbatchedUpdates
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16679

  16676 |       isUnbatchingUpdates = false;
  16677 |     }
  16678 |   }
> 16679 |   return fn(a);
  16680 | }
  16681 | 
  16682 | // TODO: Batching should be implemented at the renderer level, not within

View compiled
legacyRenderSubtreeIntoContainer
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:17258

  17255 |   };
  17256 | }
  17257 | // Initial mount should not be batched.
> 17258 | unbatchedUpdates(function () {
  17259 |   if (parentComponent != null) {
  17260 |     root.legacy_renderSubtreeIntoContainer(parentComponent, children, callback);
  17261 |   } else {

View compiled
render
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:17317

  17314 |   return legacyRenderSubtreeIntoContainer(null, element, container, true, callback);
  17315 | },
  17316 | render: function (element, container, callback) {
> 17317 |   return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
  17318 | },
  17319 | unstable_renderSubtreeIntoContainer: function (parentComponent, element, containerNode, callback) {
  17320 |   !(parentComponent != null && has(parentComponent)) ? invariant(false, 'parentComponent must be a valid React Component') : void 0;

View compiled
./src/index.js
src/react-landing/src/index.js:15

  12 | import '../node_modules/font-awesome/css/font-awesome.min.css';
  13 | 
  14 | 
> 15 | ReactDOM.render(
  16 |   <Provider store={ store }>
  17 |     <I18nextProvider i18n={ i18n }>
  18 |       <App />

View compiled
▶ 6 stack frames were collapsed.

This are the source files:

src/components/HomePage/Header/Header.jsx

import React from 'react';
import { connect } from "react-redux";
import { I18n } from 'react-i18next';
import { QueryRenderer } from 'react-relay';

import environment from '../../../relay/environment';
import featuredStores from './FeaturedStores';
import SearchBox from '../../SearchBox/SearchBox';

import './Header.css';


const mapStateToProps = state => {
  return {
    query: state.storeService.getAllFeatured()
  };
};

const Header = ({ query }) => (
  <I18n>
    {
      (t) => (
        <div className="background">
          <ul className="cb-slideshow">
            <li><span>Image 01</span></li>
            <li><span>Image 02</span></li>
            <li><span>Image 03</span></li>
          </ul>
          <div className="banner">
            <div className="container">
              <div className="banner-info">
                <h2>{ t('home-page.header.title') }</h2>
                <p>{ t('home-page.header.description') }</p>
              </div>
              <div className="banner-grads">
                <QueryRenderer environment={ environment } query={ query }> render={ featuredStores }></QueryRenderer>

                <div className="clearfix"></div>

                <SearchBox />
              </div>
            </div>
          </div>
        </div>
      )
    }
  </I18n>
);

export default connect(mapStateToProps)(Header);

src/components/HomePage/Header/FeaturedStores.jsx

import React from 'react';
import Spinner from 'react-spinkit';

/**
 * FeaturedStores component.
 */
export default ({ error, stores }) => {
  if (error) {
    return <div>Error!</div>;
  }

  if (!stores) {
    return <Spinner name="line-scale" color="blue" />;
  }

  return (
    <div>
      {
        stores.map((store, key) => {
          return (
            <div className="col-md-4 banner-grad" key={ key }>
              <div className="banner-grad-img">
                <img src={ store.image } alt={ store.name } />
                <h4>{ store.name }</h4>
                <p>
                  <span className="storeDescription">{ store.description }</span>
                  <br /> { store.address }, { store.city }
                </p>
              </div>
            </div>
          );
        })
      }
    </div>
  );
}

src/relay/services/StoreService.jsx

import storesQuery from '../queries/StoresQuery';
import featuredStoresQuery from '../queries/FeaturedStoresQuery';
import storeQuery from '../queries/StoreQuery';
import storesByMenuItemQuery from '../queries/StoresByMenuItemQuery';


/** Limit of stores per request. */
const LIMIT = 24

/**
 * class :: StoreService
 *
 * Service for Store types.
 */
class StoreService {
  /**
   * Constructor.
   */
  constructor() {
    this.storesQuery = storesQuery;
    this.storeQuery = storeQuery;
    this.featuredStoresQuery = featuredStoresQuery;
    this.storesByMenuItemQuery = storesByMenuItemQuery;
    this.searchFrom404 = false
    this.skipCounter = 0
  }

  /**
   * Resets the skip counter.
   */
  resetSkipCounter() {
    this.skipCounter = 0
  }

  /**
   * Gets all the stores using pagination.
   *
   * @returns {any} GraphQL query for retrieving the stores from the API server.
   */
  getAll() {
    this.skipCounter += LIMIT

    return this.storesQuery;
  }

  /**
   * Gets all the featured stores.
   *
   * @returns {any} GraphQL query for retrieving the featured stores from the API server.
   */
  getAllFeatured() {
    return this.featuredStoresQuery;
  }

  /**
   * Gets an store from the API server by its URI.
   *
   * @returns {any} GraphQL query for retrieving the store from the API server.
   */
  getStore() {
    return this.storeQuery;
  }

  /**
   * Gets all the stores from the API server that have the given item in their menues.
   *
   * @param {Boolean} searchFrom404 True if the search was performed from the SearchBox component.
   * @returns {any} GraphQL query for retrieving the stores from the API server.
   */
  getAllByMenuItem(searchFrom404) {
    this.searchFrom404 = searchFrom404 || false

    return this.storesByMenuItemQuery;
  }
}

/**
 * Singleton implementation.
 */
export default (function () {
  /** StoreService instance reference. */
  let instance = null

  return {
    /**
     * Gets a unique instance of StoreService.
     *
     * @returns {StoreService} A unique instance of StoreService.
     */
    getInstance: function () {
      if (!instance) {
        instance = new StoreService()
      }
      return instance
    }
  }
})()

src/relay/queries/FeaturedStoresQuery.js

import { graphql } from 'react-relay';


/**
 * Gets all the featured stores.
 */
export default graphql`
  query FeaturedStoresQuery {
    featuredStores {
      URI
      name
      category
      address
      city
      image
    }
  }
`;

How can I solve this issue and render my component with QueryRenderer ?

The problem is solved now! I removed Redux and fixed that extra " > " between query and render props.

src/components/HomePage/Header/Header.jsx

import React, { Component } from 'react';
import { I18n } from 'react-i18next';
import { QueryRenderer } from 'react-relay';

import environment from '../../../relay/environment';
import query from '../../../relay/queries/FeaturedStoresQuery';
import featuredStores from './FeaturedStores';
import SearchBox from '../../SearchBox/SearchBox';

import './Header.css';


export default class Header extends Component {
  /**
   * Renders the component.
   * 
   * @returns {string} The component's JSX code.
   */
  render() {
    return (
      <I18n>
        {
          (t) => (
            <div className="background">
              <ul className="cb-slideshow">
                <li><span>Image 01</span></li>
                <li><span>Image 02</span></li>
                <li><span>Image 03</span></li>
              </ul>
              <div className="banner">
                <div className="container">
                  <div className="banner-info">
                    <h2>{ t('home-page.header.title') }</h2>
                    <p>{ t('home-page.header.description') }</p>
                  </div>
                  <div className="banner-grads">
                    <QueryRenderer environment={ environment } query={ query } render={ featuredStores } />

                    <div className="clearfix"></div>

                    <SearchBox />
                  </div>
                </div>
              </div>
            </div>
          )
        }
      </I18n>
    );
  }
}

I've also modified the rendering function. It seems that the second param has to be named as props literally. It can't be named differently! ( stores was the named used before in my example code).

src/components/HomePage/Header/FeaturedStores.jsx

import React from 'react';
import Spinner from 'react-spinkit';

/**
 * FeaturedStores component.
 */
export default ({ error, props }) => {
  if (error) {
    return <div>Error!</div>;
  }

  if (!props) {
    return <Spinner name="line-scale" color="blue" />;
  }

  return (
    <div>
      {
        props.featuredStores.map((store, key) => {
          return (
            <div className="col-md-4 banner-grad" key={ key }>
              <div className="banner-grad-img">
                <img src={ store.image } alt={ store.name } />
                <h4>{ store.name }</h4>
                <p>
                  <span className="storeDescription">{ store.description }</span>
                  <br /> { store.address }, { store.city }
                </p>
              </div>
            </div>
          );
        })
      }
    </div>
  );
}

The translations was working just fine before adding Redux and Relay. Someone told me that Relay can manage centralized states so I'll try to remove Redux before trying something else. Maybe both together are collapsing the rendering.

QueryRenderer very specifically requires that the render prop must be a:

Function of type ({error, props, retry}) => React.Node.
https://relay.dev/docs/en/query-renderer.html#props

If you look at the relay source code, you can see that this render prop is called as a function here .

This causes problems when trying to pass a redux-connected component, or a class component, or anything other than a function to QueryRenderer 's render prop.

As an example, the connect function from react-redux does not return a function. It returns an object. And so, were you to pass a redux-connected component to QueryRenderer , when it would attempt to call this render prop, the "TypeError: this.props.render is not a function" error that you're seeing would be thrown. Attempting to pass a class component would result in a similar error, "TypeError: Cannot call a class as a function" .

An easy solution to this problem is to wrap the problematic connected/class/etc. component in a simple render function, like so:

const ConnectedComponent =
    connect(mapStateToProps, mapDispatchToProps)(Component);

const QueryComponent = () => (
    <QueryRenderer
        environment={environment}
        query={query}
        render={(props) => <ConnectedComponent {...props} />}
    />
);

In your case, that stray angle bracket was resulting in no render prop at all being passed to QueryRenderer , but it's also very easy to encounter this issue in a number of other ways, as outlined above.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM