没有正确使用反应生命周期

Posted

技术标签:

【中文标题】没有正确使用反应生命周期【英文标题】:not proper use of react lifecycle 【发布时间】:2018-10-25 15:44:58 【问题描述】:

我有一个 Sharepoint Framework webpart,它基本上有一个属性侧栏,我可以在其中选择 Sharepoint 列表,并根据选择将该列表中的列表项呈现到 Office UI DetailsList 组件中。

当我调试 REST 调用时一切正常,但问题是我从未在屏幕上呈现任何数据。

所以如果我选择 GenericList 它应该查询 Generic LIst,如果我选择 Directory 它应该查询 Directory 列表,但是当我选择 Directory 时它仍然说选择的是 GenericList,而不是目录。

这是我的网络部件代码

import * as React from "react";
import * as ReactDom from "react-dom";
import  Version  from "@microsoft/sp-core-library";
import 
  BaseClientSideWebPart,
  IPropertyPaneConfiguration,
  PropertyPaneTextField,
  PropertyPaneDropdown,
  IPropertyPaneDropdownOption,
  IPropertyPaneField,
  PropertyPaneLabel
 from "@microsoft/sp-webpart-base";

import * as strings from "FactoryMethodWebPartStrings";
import FactoryMethod from "./components/FactoryMethod";
import  IFactoryMethodProps  from "./components/IFactoryMethodProps";
import  IFactoryMethodWebPartProps  from "./IFactoryMethodWebPartProps";
import * as lodash from "@microsoft/sp-lodash-subset";
import List from "./components/models/List";
import  Environment, EnvironmentType  from "@microsoft/sp-core-library";
import IDataProvider from "./components/dataproviders/IDataProvider";
import MockDataProvider from "./test/MockDataProvider";
import SharePointDataProvider from "./components/dataproviders/SharepointDataProvider";

export default class FactoryMethodWebPart extends BaseClientSideWebPart<IFactoryMethodWebPartProps> 
  private _dropdownOptions: IPropertyPaneDropdownOption[];
  private _selectedList: List;
  private _disableDropdown: boolean;
  private _dataProvider: IDataProvider;
  private _factorymethodContainerComponent: FactoryMethod;

  protected onInit(): Promise<void> 
    this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");

    /*
    Create the appropriate data provider depending on where the web part is running.
    The DEBUG flag will ensure the mock data provider is not bundled with the web part when you package the
     solution for distribution, that is, using the --ship flag with the package-solution gulp command.
    */
    if (DEBUG && Environment.type === EnvironmentType.Local) 
      this._dataProvider = new MockDataProvider();
     else 
      this._dataProvider = new SharePointDataProvider();
      this._dataProvider.webPartContext = this.context;
    

    this.openPropertyPane = this.openPropertyPane.bind(this);

    /*
    Get the list of tasks lists from the current site and populate the property pane dropdown field with the values.
    */
    this.loadLists()
      .then(() => 
        /*
         If a list is already selected, then we would have stored the list Id in the associated web part property.
         So, check to see if we do have a selected list for the web part. If we do, then we set that as the selected list
         in the property pane dropdown field.
        */
        if (this.properties.spListIndex) 
          this.setSelectedList(this.properties.spListIndex.toString());
          this.context.statusRenderer.clearLoadingIndicator(this.domElement);
        
      );

    return super.onInit();
  

  // render method of the webpart, actually calls Component
  public render(): void 
    const element: React.ReactElement<IFactoryMethodProps > = React.createElement(
      FactoryMethod,
      
        spHttpClient: this.context.spHttpClient,
        siteUrl: this.context.pageContext.web.absoluteUrl,
        listName: this._dataProvider.selectedList === undefined ? "GenericList" : this._dataProvider.selectedList.Title,
        dataProvider: this._dataProvider,
        configureStartCallback: this.openPropertyPane
      
    );

    // reactDom.render(element, this.domElement);
    this._factorymethodContainerComponent = <FactoryMethod>ReactDom.render(element, this.domElement);

  

  // loads lists from the site and fill the dropdown.
  private loadLists(): Promise<any> 
    return this._dataProvider.getLists()
      .then((lists: List[]) => 
        // disable dropdown field if there are no results from the server.
        this._disableDropdown = lists.length === 0;
        if (lists.length !== 0) 
          this._dropdownOptions = lists.map((list: List) => 
            return 
              key: list.Id,
              text: list.Title
            ;
          );
        
      );
  

  protected get dataVersion(): Version 
    return Version.parse("1.0");
  

  protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void 
    /*
    Check the property path to see which property pane feld changed. If the property path matches the dropdown, then we set that list
    as the selected list for the web part.
    */
    if (propertyPath === "spListIndex") 
      this.setSelectedList(newValue);
    

    /*
    Finally, tell property pane to re-render the web part.
    This is valid for reactive property pane.
    */
    super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
  

  // sets the selected list based on the selection from the dropdownlist
  private setSelectedList(value: string): void 
    const selectedIndex: number = lodash.findIndex(this._dropdownOptions,
      (item: IPropertyPaneDropdownOption) => item.key === value
    );

    const selectedDropDownOption: IPropertyPaneDropdownOption = this._dropdownOptions[selectedIndex];

    if (selectedDropDownOption) 
      this._selectedList = 
        Title: selectedDropDownOption.text,
        Id: selectedDropDownOption.key.toString()
      ;

      this._dataProvider.selectedList = this._selectedList;
    
  


  // we add fields dynamically to the property pane, in this case its only the list field which we will render
  private getGroupFields(): IPropertyPaneField<any>[] 
    const fields: IPropertyPaneField<any>[] = [];

    // we add the options from the dropdownoptions variable that was populated during init to the dropdown here.
    fields.push(PropertyPaneDropdown("spListIndex", 
      label: "Select a list",
      disabled: this._disableDropdown,
      options: this._dropdownOptions
    ));

    /*
    When we do not have any lists returned from the server, we disable the dropdown. If that is the case,
    we also add a label field displaying the appropriate message.
    */
    if (this._disableDropdown) 
      fields.push(PropertyPaneLabel(null, 
        text: "Could not find tasks lists in your site. Create one or more tasks list and then try using the web part."
      ));
    

    return fields;
  

  private openPropertyPane(): void 
    this.context.propertyPane.open();
  

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration 
    return 
      pages: [
        
          header: 
            description: strings.PropertyPaneDescription
          ,
          groups: [
            
              groupName: strings.BasicGroupName,
              /*
              Instead of creating the fields here, we call a method that will return the set of property fields to render.
              */
              groupFields: this.getGroupFields()
            
          ]
        
      ]
    ;
  

这是我的组件代码

//#region Imports
import * as React from "react";
import styles from "./FactoryMethod.module.scss";
import   IFactoryMethodProps  from "./IFactoryMethodProps";
import 
  IDetailsListItemState,
  IDetailsNewsListItemState,
  IDetailsDirectoryListItemState,
  IDetailsAnnouncementListItemState,
  IFactoryMethodState
 from "./IFactoryMethodState";
import  IListItem  from "./models/IListItem";
import  IAnnouncementListItem  from "./models/IAnnouncementListItem";
import  INewsListItem  from "./models/INewsListItem";
import  IDirectoryListItem  from "./models/IDirectoryListItem";
import  escape  from "@microsoft/sp-lodash-subset";
import  SPHttpClient, SPHttpClientResponse  from "@microsoft/sp-http";
import  ListItemFactory from "./ListItemFactory";
import  TextField  from "office-ui-fabric-react/lib/TextField";
import 
  DetailsList,
  DetailsListLayoutMode,
  Selection,
  buildColumns,
  IColumn
 from "office-ui-fabric-react/lib/DetailsList";
import  MarqueeSelection  from "office-ui-fabric-react/lib/MarqueeSelection";
import  autobind  from "office-ui-fabric-react/lib/Utilities";
import PropTypes from "prop-types";
//#endregion

export default class FactoryMethod extends React.Component<IFactoryMethodProps, IFactoryMethodState> 
  constructor(props: IFactoryMethodProps, state: any) 
    super(props);
    this.setInitialState();
  


  // lifecycle help here: https://staminaloops.github.io/undefinedisnotafunction/understanding-react/
  //#region Mouting events lifecycle
  // the data returned from render is neither a string nor a DOM node.
  // it's a lightweight description of what the DOM should look like.
  // inspects this.state and this.props and create the markup.
  // when your data changes, the render method is called again.
  // react diff the return value from the previous call to render with
  // the new one, and generate a minimal set of changes to be applied to the DOM.
  public render(): React.ReactElement<IFactoryMethodProps> 
    if (this.state.hasError) 
      // you can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
     else 
      switch(this.props.listName) 
          case "GenericList":
            // tslint:disable-next-line:max-line-length
            return <this.ListMarqueeSelection items=this.state.DetailsListItemState.items columns=this.state.columns />;
          case "News":
            // tslint:disable-next-line:max-line-length
            return <this.ListMarqueeSelection items=this.state.DetailsNewsListItemState.items columns=this.state.columns/>;
          case "Announcements":
            // tslint:disable-next-line:max-line-length
            return <this.ListMarqueeSelection items=this.state.DetailsAnnouncementListItemState.items columns=this.state.columns/>;
          case "Directory":
            // tslint:disable-next-line:max-line-length
            return <this.ListMarqueeSelection items=this.state.DetailsDirectoryListItemState.items columns=this.state.columns/>;
          default:
            return null;
      
    
  

  public componentDidCatch(error: any, info: any): void 
    // display fallback UI
    this.setState( hasError: true );
    // you can also log the error to an error reporting service
    console.log(error);
    console.log(info);
  



  // componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here.
  // if you need to load data from a remote endpoint, this is a good place to instantiate the network request.
  // this method is a good place to set up any subscriptions. If you do that, don’t forget to unsubscribe in componentWillUnmount().
  // calling setState() in this method will trigger an extra rendering, but it is guaranteed to flush during the same tick.
  // this guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state.
  // use this pattern with caution because it often causes performance issues. It can, however, be necessary for cases like modals and
  // tooltips when you need to measure a DOM node before rendering something that depends on its size or position.
   public componentDidMount(): void 
    this._configureWebPart = this._configureWebPart.bind(this);
    this.readItemsAndSetStatus();
  

  //#endregion
  //#region Props changes lifecycle events (after a property changes from parent component)
  // componentWillReceiveProps() is invoked before a mounted component receives new props.
  // if you need to update the state in response to prop
  // changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions
  // using this.setState() in this method.
  // note that React may call this method even if the props have not changed, so make sure to compare the current
  // and next values if you only want to handle changes.
  // this may occur when the parent component causes your component to re-render.
  // react doesn’t call componentWillReceiveProps() with initial props during mounting. It only calls this
  // method if some of component’s props may update
  // calling this.setState() generally doesn’t trigger componentWillReceiveProps()
  public componentWillReceiveProps(nextProps: IFactoryMethodProps): void 
    if(nextProps.listName !== this.props.listName) 
      this.readItemsAndSetStatus();
    
  

  //#endregion
  //#region private methods
  private _configureWebPart(): void 
    this.props.configureStartCallback();
  

  public setInitialState(): void 
    this.state = 
      hasError: false,
      status: this.listNotConfigured(this.props)
        ? "Please configure list in Web Part properties"
        : "Ready",
      columns:[],
      DetailsListItemState:
        items:[]
      ,
      DetailsNewsListItemState:
        items:[]
      ,
      DetailsDirectoryListItemState:
        items:[]
      ,
      DetailsAnnouncementListItemState:
        items:[]
      ,
    ;
  

  // reusable inline component
  private ListMarqueeSelection = (itemState: columns: IColumn[], items: IListItem[] ) => (
      <div>
          <DetailsList
            items= itemState.items 
            columns= itemState.columns 
            setKey="set"
            layoutMode= DetailsListLayoutMode.fixedColumns 
            selectionPreservedOnEmptyClick= true 
            compact= true >
          </DetailsList>
      </div>
  )

  // read items using factory method pattern and sets state accordingly
  private readItemsAndSetStatus(): void 
    this.setState(
      status: "Loading all items..."
    );

    const factory: ListItemFactory = new ListItemFactory();
    factory.getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName)
    .then((items: any[]) => 

      var myItems: any = null;
      switch(this.props.listName) 
          case "GenericList":
              myItems = items as IListItem[];
              break;
          case "News":
              myItems = items as INewsListItem[];
              break;
          case "Announcements":
              myItems = items as IAnnouncementListItem[];
              break;
          case "Directory":
              myItems = items as IDirectoryListItem[];
              break;
      

      const keyPart: string = this.props.listName === "GenericList" ? "" : this.props.listName;
        // the explicit specification of the type argument `keyof ` is bad and
        // it should not be required.
        this.setState<keyof >(
          status: `Successfully loaded $items.length items`,
          ["Details" + keyPart + "ListItemState"] : 
            myItems
          ,
          columns: buildColumns(myItems)
        );
    );
  

  private listNotConfigured(props: IFactoryMethodProps): boolean 
    return props.listName === undefined ||
      props.listName === null ||
      props.listName.length === 0;
  

  //#endregion

我认为其余的代码是不必要的

更新 SharepointDataProvider.ts

import 
    SPHttpClient,
    SPHttpClientBatch,
    SPHttpClientResponse
   from "@microsoft/sp-http";
  import  IWebPartContext  from "@microsoft/sp-webpart-base";
  import List from "../models/List";
  import IDataProvider from "./IDataProvider";

  export default class SharePointDataProvider implements IDataProvider 
      private _selectedList: List;
      private _lists: List[];
      private _listsUrl: string;
      private _listItemsUrl: string;
      private _webPartContext: IWebPartContext;

      public set selectedList(value: List) 
        this._selectedList = value;
        this._listItemsUrl = `$this._listsUrl(guid'$value.Id')/items`;
      

      public get selectedList(): List 
        return this._selectedList;
      

      public set webPartContext(value: IWebPartContext) 
        this._webPartContext = value;
        this._listsUrl = `$this._webPartContext.pageContext.web.absoluteUrl/_api/web/lists`;
      

      public get webPartContext(): IWebPartContext 
        return this._webPartContext;
      

      // get all lists, not only tasks lists
      public getLists(): Promise<List[]> 
        // const listTemplateId: string = '171';
        // const queryString: string = `?$filter=BaseTemplate eq $listTemplateId`;
        // const queryUrl: string = this._listsUrl + queryString;
        return this._webPartContext.spHttpClient.get(this._listsUrl, SPHttpClient.configurations.v1)
          .then((response: SPHttpClientResponse) => 
            return response.json();
          )
          .then((json:  value: List[] ) => 
            return this._lists = json.value;
          );
      
    

Idataprovider.ts

import  IWebPartContext  from "@microsoft/sp-webpart-base";
import List from "../models/List";
import IListItem from "../models/IListItem";

interface IDataProvider 
  selectedList: List;
  webPartContext: IWebPartContext;
  getLists(): Promise<List[]>;


export default IDataProvider;

【问题讨论】:

您的render 方法是否被正确调用?你能记录this.props.listName 并得到正确的值吗? 第一次它实际上是未定义的。 screencast.com/t/kBChpQtLIs 听起来this._dataProvider.selectedList.Title 设置不正确。您使用的是new MockDataProvider(); 还是new SharePointDataProvider();?这应该可以帮助您缩小范围。 我在sharepoint在线工作台上,所以它使用SharepointDataProvider,我实际上可以获取数据。我将在上面粘贴更多代码 让我在github上分享我的代码,也许这样更容易 【参考方案1】:

当列表名称更改时,您调用的是readItemsAndSetStatus

  public componentWillReceiveProps(nextProps: IFactoryMethodProps): void 
    if(nextProps.listName !== this.props.listName) 
      this.readItemsAndSetStatus();
    
  

但是readItemsAndSetStatus不带参数,继续使用this.props.listName,目前还没有改变。

private readItemsAndSetStatus(): void        
    ...
    const factory: ListItemFactory = new ListItemFactory();
    factory.getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName)
    ...

尝试将nextProps.listName 传递给readItemsAndSetStatus

  public componentWillReceiveProps(nextProps: IFactoryMethodProps): void 
    if(nextProps.listName !== this.props.listName) 
      this.readItemsAndSetStatus(nextProps.listName);
    
  

然后要么使用传入参数,要么默认为this.props.listName

private readItemsAndSetStatus(listName): void        
    ...
    const factory: ListItemFactory = new ListItemFactory();
    factory.getItems(this.props.spHttpClient, this.props.siteUrl, listName || this.props.listName)
    ...

【讨论】:

ComponentDIdMount 怎么样,我也在那里使用 readItemAndSetStatus 没有参数 @LuisValencia 您当然可以在 CDM 中传递 this.readItemAndSetStatus(this.props.listName),这比在未传递参数的情况下在 factory.getItems 调用中简单地默认为 this.props.listName 更明确.也就是说,如果您在该调用中确实默认为this.props.listName,那么就像您现在在CDM中所做的那样,不传递任何东西是安全的,因为无论如何它都会默认为this.props.listName .【参考方案2】:

在您的第一个“webpart 代码”中,onInit() 方法在 loadLists() 完成之前返回:

onInit() 
  this.loadLists()  // <-- Sets this._dropdownOptions
    .then(() => 
      this.setSelectedList();
    );

  return super.onInit();  // <-- Doesn't wait for the promise to resolve

这意味着getGroupFields() 可能没有_dropdownOptions 的数据。这意味着getPropertyPaneConfiguration() 可能没有正确的数据。

我不肯定这是 问题,或者唯一的问题。我没有任何使用 SharePoint 的经验,所以对这一切持保留态度。


我看到在react-todo-basic 他们正在做同样的事情。

但是,在其他地方,我看到人们在 super.onInit Promise 中执行其他操作:

react-list-form react-sp-pnp-js-property-decorator

【讨论】:

以上是关于没有正确使用反应生命周期的主要内容,如果未能解决你的问题,请参考以下文章

在调度过程之后和渲染之前在反应生命周期中调用哪个方法

Android 生命周期 - 我的想法正确吗?

指定 Rust 闭包的生命周期

vue keepalive 路由守卫 生命周期等问题

让对象监听 Activity 生命周期事件?

在 Rust 中使用带有结构的生命周期的正确方法是啥?