0

I have a warning appearing in chrome devtools Console :

Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
    in div (at Search.jsx:37)
    in Search (at pages/index.jsx:79)
    in main (created by Basic)
    in Basic (created by Context.Consumer)
    in Content (at pages/index.jsx:78)
    in section (created by Context.Consumer)
    in BasicLayout (created by Context.Consumer)
    ...

The code works as intended. It is a React Implementation of Flexsearch, the Web's fastest and most memory-flexible full-text search library. But this warning is bugging me.

I worked so much on it without finding a proper solution.

Search.jsx :

/**
 * Vendor Import
 */
import React from 'react';
import _find from 'lodash/find';
import _map from 'lodash/map';
import _isEmpty from 'lodash/isEmpty';
import _isEqual from 'lodash/isEqual';
import NotFound from '../images/notfound.svg';
import { Col, Row } from 'antd';

/**
 * Component import
 */
import ProductList from '../components/ProductList';

/**
 * Utils import
 */
import { filterData } from '../utils/filterdata';
import ContextConsumer from '../utils/context';

/**
 * Style import
 */
import './search.css';

class Search extends React.Component {
  state = {
    query: '',
    results: this.props.groupedData,
  };

  render() {

    return (
      <div className={this.props.classNames}>
        <ContextConsumer>
          {({ data }) => {
            this.handleSearch(data.query);
          }}
        </ContextConsumer>

        <div className='search__list'>
          {!_isEmpty(this.state.results) ? (
            <ProductList products={this.state.results} />
          ) : (
            <Row>
              <Col span={24} className='no_results'>
               No results corresponding to "<b>{this.state.query}</b>"
              </Col>
              <Col xs={24} sm={12} md={8} lg={6} className='no_results'>
                <NotFound />
              </Col>
            </Row>
          )}
        </div>
      </div>
    );
  }

  /**
   * Handle search
   * @param {String} query 
   */
  handleSearch = (query) => {
    if (!_isEqual(this.state.query, query)) {
      const groupedData = this.props.groupedData;
      const results = this.getSearchResults(groupedData, query);
      this.setState({ results: results, query: query });
    }
  };

  /**
   * Get the data associated to the query
   * @param {Array} data
   * @param {String} query
   */
  getSearchResults(data, query) {
    const index = window.__FLEXSEARCH__.en.index;
    const store = window.__FLEXSEARCH__.en.store;

    if (!query || !index) {
      return data;
    } else {
      let resultingNodesID = [];
      Object.keys(index).forEach((idx) => {
        resultingNodesID.push(...index[idx].values.search(query));
      });
      resultingNodesID = Array.from(new Set(resultingNodesID));

      const resultingNodes = store
        .filter((node) => (resultingNodesID.includes(node.id) ? node : null))
        .map((node) => node.node);

      const resultingGroupedData = [];
      _map(resultingNodes, (node) => {
        resultingGroupedData.push(_find(data, { ref: node.ref }));
      });

      return resultingGroupedData;
    }
  }

  /**
   * Invoked immediately after updating occurs.
   * @param prevProps
   */
  componentDidUpdate(prevProps) {
    const { selectedMenu, groupedData } = this.props;

    if (!_isEqual(prevProps.selectedMenu, selectedMenu)) {
      const filteredData = filterData(groupedData, selectedMenu);
      const results = filteredData;
      this.setState({ results: results });
    }
  }
}

export default Search;

ContextProviderComponent :

/**
 * Vendor Import
 */
import React from 'react';

const defaultContextValue = {
  data: {
    // set your initial data shape here
    query: '',
  },
  set: () => {},
};

const { Provider, Consumer } = React.createContext(defaultContextValue);

class ContextProviderComponent extends React.Component {
  constructor() {
    super();

    this.setData = this.setData.bind(this);
    this.state = {
      ...defaultContextValue,
      set: this.setData,
    };
  }

  setData(newData) {
    this.setState((state) => ({
      data: {
        ...state.data,
        ...newData,
      },
    }));
  }

  render() {
    return <Provider value={this.state}>{this.props.children}</Provider>;
  }
}

export { Consumer as default, ContextProviderComponent };

What am I doing wrong ?

ps: If you see some improvements or useless code, i'm all ears !

Ali Lewis
  • 33
  • 5

2 Answers2

1

I found the solution.

@Scotty Jamison is right about the origin of the issue. His answer of helped me to rewrite my code.

Search.jsx

/**
 * Vendor Import
 */
import React from 'react';
import _isEmpty from 'lodash/isEmpty';
import _isEqual from 'lodash/isEqual';
import NotFound from '../images/notfound.svg';
import { Col, Row } from 'antd';

/**
 * Component import
 */
import ProductList from './ProductList';

/**
 * Utils import
 */
import { filterData } from '../utils/filterdata';
import { SearchContext } from '../utils/searchcontext';
import { getSearchResults } from '../utils/getsearchresults';

/**
 * Style import
 */
import './search.css';

class Search extends React.Component {
  constructor(props) {
    super(props);
    this.state = { results: this.props.groupedData, query: '' };
  }

  previousContext = '';

  /**
   * Invoked immediately after a component is mounted.
   */
  componentDidMount() {
    //console.log('--- componentDidMount ---');
    this.previousContext = this.context;
  }

  /**
   * Invoked immediately after updating occurs.
   * @param prevProps
   */
  componentDidUpdate(prevProps) {
    //console.log('--- componentDidUpdate ---');

    const { selectedMenu, groupedData } = this.props;

    if (!_isEqual(prevProps.selectedMenu, selectedMenu)) {
      this.setState({ results: filterData(groupedData, selectedMenu) });
    }

    if (!_isEqual(this.previousContext, this.context)) {
      let searchQuery = this.context;
      this.setState({ results: getSearchResults(groupedData, searchQuery) });
    }

    this.previousContext = this.context;
  }

  render() {
    let searchQuery = this.context;
    return (
      <div className={this.props.classNames}>
        <div className='search__list'>
          {!_isEmpty(this.state.results) ? (
            <ProductList products={this.state.results} />
          ) : (
            <Row>
              <Col span={24} className='no_results'>
                Pas de résultats correspondants à "<b>{searchQuery}</b>"
              </Col>
              <Col xs={24} sm={12} md={8} lg={6} className='no_results'>
                <NotFound />
              </Col>
            </Row>
          )}
        </div>
      </div>
    );
  }
}

Search.contextType = SearchContext;

export default Search;

getseatchresults.js

/**
 * Vendor Import
 */
import _find from 'lodash/find';
import _map from 'lodash/map';

/**
 * Get the results from search
 * @param {Array} data
 * @param {String} query
 */
export const getSearchResults = (data, query) => {
  const index = window.__FLEXSEARCH__.en.index;
  const store = window.__FLEXSEARCH__.en.store;

  if (!query || !index) {
    return data;
  } else {
    let resultingNodesID = [];
    Object.keys(index).forEach((idx) => {
      resultingNodesID.push(...index[idx].values.search(query));
    });
    resultingNodesID = Array.from(new Set(resultingNodesID));

    const resultingNodes = store
      .filter((node) => (resultingNodesID.includes(node.id) ? node : null))
      .map((node) => node.node);

    const resultingGroupedData = [];
    _map(resultingNodes, (node) => {
      resultingGroupedData.push(_find(data, { ref: node.ref }));
    });

    return resultingGroupedData;
  }
};

searchcontext.js

/**
 * Vendor Import
 */
import React from 'react';

/**
 * This context is for the Search query. It provides a query from the search bar in MyLayout.jsx to the Search.jsx component.
 * Due to the impossibility to pass props from the Layout to other components, a context has to be used.
 */
export const SearchContext = React.createContext('');

Here is what I did:
The former context component wasn't mine. It was a generic boilerplate from the Gatsby flexsearch plugin integration. I didn't understand the code intent. So I checked React Doc and read all the context section. I then simplified the code, exported the search logic outside the Search.jsx component and simplified the last one.

Ali Lewis
  • 33
  • 5
0

When its asking for your render function to be pure, its wanting it to not update state. It shouldn't call anything that updates state either.

In search.jsx you're calling this.handleSearch() inside render. handleSearch() calls this.setState(). You either need to handle this searching logic before passing data in through the context provider. (So, move the search handling logic into ContextProviderComponent and putting the search results into the context), or you need to listen to context changes outside of the render function. This answer gives a number of ways to do this.

As for code quality, in my quick lookover of your code I didn't see any obvious red flags, so good job! You seem to have the essentials of React down.

Scotty Jamison
  • 2,983
  • 1
  • 12
  • 14