1

我在React中有一个使用ReduxRelay的项目。客户端使用 GraphQL 连接到 API 服务器。我试图使用组件QueryRenderer并且我收到以下错误:

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.

这是源文件:

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

如何解决此问题并使用QueryRenderer呈现我的组件?

4

3 回答 3

1

现在问题解决了!我删除了Redux并修复了查询渲染道具之间的额外“ > ”。

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

我还修改了渲染功能。似乎第二个参数必须从字面上命名为道具。不能叫别的名字!(stores是之前在我的示例代码中使用的名称)。

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>
  );
}
于 2018-07-01T21:53:11.790 回答
0

在添加 Redux 和 Relay 之前,翻译工作得很好。有人告诉我 Relay 可以管理中心化状态,所以我会在尝试其他方法之前尝试删除 Redux。也许两者都在折叠渲染。

于 2018-07-01T14:08:58.703 回答
0

QueryRenderer非常特别地要求render道具必须是:

({error, props, retry}) 类型的函数 => React.Node。
https://relay.dev/docs/en/query-renderer.html#props

如果你查看中继源代码,你可以看到这个prop在这里render被作为函数调用。

这会在尝试将 redux 连接的组件、类组件或除函数以外的任何东西传递给QueryRenderer's renderprop 时导致问题。

例如,connect函数 fromreact-redux不返回函数。它返回一个对象。因此,如果您将一个与 redux 连接的组件传递给QueryRenderer,当它尝试调用此render道具时,"TypeError: this.props.render is not a function"您看到的错误将被抛出。尝试传递类组件会导致类似的错误,"TypeError: Cannot call a class as a function".

这个问题的一个简单解决方案是包装有问题的连接/类/等。一个简单的渲染函数中的组件,如下所示:

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

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

在您的情况下,那个杂散的尖括号导致根本没有任何render道具被传递给QueryRenderer,但是如上所述,在许多其他方面也很容易遇到这个问题。

于 2020-05-02T21:37:51.997 回答