SO Relay QueryRenderer - TypeError:this.props.render 不是函数

Posted

技术标签:

【中文标题】SO Relay QueryRenderer - TypeError:this.props.render 不是函数【英文标题】:SO Relay QueryRenderer - TypeError: this.props.render is not a function 【发布时间】:2018-12-09 16:26:12 【问题描述】:

我在 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 渲染我的组件?

【问题讨论】:

很难弄清楚,但是错误指向根索引组件错误,所以尝试只使用没有翻译的简单组件,然后尝试使用翻译的简单组件,我不认为 graphql 中继有问题,它可能是 t =>渲染是错误的,但它可能不会...先尝试简单的版本 【参考方案1】:

QueryRenderer 非常明确地要求 render 属性必须是:

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

如果查看relay源码,可以看到这个render prop被调用为函数here。

这会在尝试将 redux 连接的组件、类组件或除函数之外的任何东西传递给 QueryRendererrender 属性时导致问题。

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

解决这个问题的一个简单方法是包装有问题的connected/class/etc。一个简单的渲染函数中的组件,如下所示:

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

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

在您的情况下,那个杂散的尖括号导致根本没有 render 属性被传递给 QueryRenderer,但如上所述,通过许多其他方式也很容易遇到此问题。

【讨论】:

【参考方案2】:

问题现在解决了! 我删除了 Redux 并修复了 query渲染道具。

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

我还修改了渲染功能。似乎第二个参数必须按字面意思命名为 props。不能叫别的名字! (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>
  );

【讨论】:

【参考方案3】:

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

【讨论】:

以上是关于SO Relay QueryRenderer - TypeError:this.props.render 不是函数的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Relay QueryRenderer 道具更新状态

如何在 Relay Modern 的父组件中最好地访问来自 QueryRenderer 的数据?

React Relay 实现

在 Relay Modern 中处理身份验证

在 QueryRenderer 中传递中继现代变量

如何将自定义道具传递给 QueryRenderer 渲染函数?