0

I have a class component like this

import { Component } from 'react';
import {
  DEFAULT_HPP,
  DEFAULT_PAGE,
  DEFAULT_QUERY,
  PARAM_HPP,
  PARAM_PAGE,
  PARAM_SEARCH,
  PATH_BASE,
  PATH_SEARCH,
} from '../../constants';
import { Button } from '../Button';
import { Loading } from '../Loading';
import { Search } from '../Search';
import { Table } from '../Table';
import './index.css';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      results: null,
      searchKey: '',
      searchTerm: DEFAULT_QUERY,
      isLoading: false,
    };

    this.needsToSearchTopStories = this.needToSearchTopStories.bind(this);
    this.setSearchTopStories = this.setSearchTopStories.bind(this);
    this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this);
    this.onSearchChange = this.onSearchChange.bind(this);
    this.onSearchSubmit = this.onSearchSubmit.bind(this);
    this.onDismiss = this.onDismiss.bind(this);
  }

  needToSearchTopStories(searchTerm) {
    return !this.state.results[searchTerm];
  }

  setSearchTopStories(result) {
    console.log('setSearchTopStories');
    const { hits, page } = result;
    const { searchKey, results } = this.state;
    console.log('searchKey in setSearchTopStories: ' + searchKey);

    const oldHits =
      results && results[searchKey] ? results[searchKey].hits : [];
    console.log('oldHits: ' + oldHits);

    const updatedHits = [...oldHits, ...hits];
    console.log('updatedHits: ' + updatedHits);

    this.setState({
      results: { ...results, [searchKey]: { hits: updatedHits, page } },
      isLoading: false,
    });
    // console.log('results: ' + JSON.stringify(results));
  }

  fetchSearchTopStories(searchTerm, page) {
    console.log('fetchSearchTopStories');
    this.setState({ isLoading: true });
    fetch(
      `${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`
    )
      .then((response) => response.json())
      .then((result) => {
        console.log('result: ' + result);
        this.setSearchTopStories(result);
      });
  }

  componentDidMount() {
    console.log('componentDidMount');
    const { searchTerm } = this.state;
    console.log('setSearchKey');
    this.setState({ searchKey: searchTerm });
    console.log('searchKey in componentDidMount: ' + this.state.searchKey);
    this.fetchSearchTopStories(searchTerm, DEFAULT_PAGE);
  }

  componentDidUpdate() {
    console.log('componentDidUpdate');
    console.log('searchKey in componentDidUpdate: ' + this.state.searchKey);
  }

  onSearchChange(event) {
    this.setState({ searchTerm: event.target.value });
  }

  onSearchSubmit(event) {
    const { searchTerm } = this.state;
    this.setState({ searchKey: searchTerm });

    if (this.needToSearchTopStories(searchTerm)) {
      this.fetchSearchTopStories(searchTerm, DEFAULT_PAGE);
    }

    console.log('submit');
    event.preventDefault();
  }

  onDismiss(id) {
    const { searchKey, results } = this.state;
    const { hits, page } = results[searchKey];

    const isNotId = (item) => item.objectID !== id;
    const updatedHits = hits.filter(isNotId);

    this.setState({
      results: { ...results, [searchKey]: { hits: updatedHits, page } },
    });
  }

  render() {
    const { searchTerm, results, searchKey, isLoading } = this.state;
    console.log('searchKey: ' + searchKey);
    // console.log('results: ' + JSON.stringify(results));
    const page =
      (results && results[searchKey] && results[searchKey].page) || 0;
    console.log('page: ' + page);
    const list =
      (results && results[searchKey] && results[searchKey].hits) || [];
    console.log('list: ' + list);
    return (
      <div className="page">
        <div className="interactions">
          <Search
            value={searchTerm}
            onChange={this.onSearchChange}
            onSubmit={this.onSearchSubmit}
          >
            Search
          </Search>
        </div>
        <Table list={list} onDismiss={this.onDismiss} />
        <div className="interactions">
          {isLoading ? (
            <Loading />
          ) : (
            <Button
              onClick={() => this.fetchSearchTopStories(searchKey, page + 1)}
            >
              More
            </Button>
          )}
        </div>
      </div>
    );
  }
}

export default App;

I want to make its functional component so I code it like this below:

import { useEffect, useState } from 'react';
import {
  DEFAULT_HPP,
  DEFAULT_PAGE,
  DEFAULT_QUERY,
  PARAM_HPP,
  PARAM_PAGE,
  PARAM_SEARCH,
  PATH_BASE,
  PATH_SEARCH,
} from '../../constants';
import { Button } from '../Button';
import { Loading } from '../Loading';
import { Search } from '../Search';
import { Table } from '../Table';
import './index.css';

function Appful() {
  const [isLoading, setIsLoading] = useState(false);
  const [searchTerm, setSearchTerm] = useState(DEFAULT_QUERY);
  const [searchKey, setSearchKey] = useState('');
  const [results, setResults] = useState(null);

  const setSearchTopStories = (result) => {
    console.log('setSearchTopStories');
    const { hits, page } = result;
    console.log('searchKey in setSearchTopStories: ' + searchKey);

    const oldHits =
      results && results[searchKey] ? results[searchKey].hits : [];
    console.log('oldHits: ' + oldHits);

    const updatedHits = [...oldHits, ...hits];
    console.log('updatedHits: ' + updatedHits);

    setResults({ ...results, [searchKey]: { hits: updatedHits, page: page } });
    // console.log('results: ' + JSON.stringify(results));
    setIsLoading(false);
  };

  const fetchSearchTopStories = async (searchTerm, page) => {
    console.log('fetchSearchTopStories');

    setIsLoading(true);
    const response = await fetch(
      `${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`
    );
    const result = await response.json();
    console.log('result: ' + result);
    setSearchTopStories(result);
  };

  useEffect(() => {
    console.log('componentDidMount');
    console.log('setSearchKey');
    setSearchKey(searchTerm);
    console.log('searchKey in componentDidMount: ' + searchKey);
    fetchSearchTopStories(searchTerm, DEFAULT_PAGE);
  }, []);

  useEffect(() => {
    console.log('componentDidUpdate');
    console.log('searchKey in componentDidUpdate: ' + searchKey);
  });

  const onSearchChange = (event) => {
    setSearchTerm(event.target.value);
  };

  const needToSearchTopStories = (searchTerm) => {
    return !results[searchTerm];
  };

  const onSearchSubmit = (event) => {
    setSearchKey(searchTerm);

    if (needToSearchTopStories(searchTerm)) {
      fetchSearchTopStories(searchTerm, DEFAULT_PAGE);
    }

    console.log('submit');
    event.preventDefault();
  };

  const onDismiss = (id) => {
    const { hits, page } = results[searchKey];

    const isNotId = (item) => item.objectID !== id;
    const updatedHits = hits.filter(isNotId);

    setResults({ ...results, [searchKey]: { hits: updatedHits, page: page } });
  };

  console.log('searchKey: ' + searchKey);
  //   console.log('results: ' + JSON.stringify(results));
  const page = (results && results[searchKey] && results[searchKey].page) || 0;
  console.log('page: ' + page);
  const list = (results && results[searchKey] && results[searchKey].hits) || [];
  console.log('list: ' + list);

  return (
    <div className="page">
      <div className="interactions">
        <Search
          value={searchTerm}
          onChange={onSearchChange}
          onSubmit={onSearchSubmit}
        >
          Search
        </Search>
      </div>
      <Table list={list} onDismiss={onDismiss} />
      <div className="interactions">
        {isLoading ? (
          <Loading />
        ) : (
          <Button onClick={() => fetchSearchTopStories(searchKey, page + 1)}>
            More
          </Button>
        )}
      </div>
    </div>
  );
}

export default Appful;

but they are not showing the same result (see console: page and list) after the first rendering (componentDidMount)

with class component shows like this below: enter image description here

with functional component shows like this below (we could ignore first componentDidUpdate because its basically is the componentDidMount): enter image description here

How to make the functional component render the same like class component? Why list and page of functional and class are not the same, please give explanation about how they render. Thank you

1 Answers1

0

I solve it after knowing how useState works in hooks and component lifecycle. There is called lazy computation when first rendering (initializing) in functional. see result below

Class Component enter image description here

Functional Component enter image description here

According to the result pictures above, we know that searchKey in componentDidMount and componentDidUpdate in both class and functional component is perfect the same but not in the setSearchTopStories. In functional component, we declare the fetchSearchTopStories in useEffect with null dependency. Thus, the state still have value of componentDidMount. In contrast, the state in class component does not work like that, searchKey in setSearchTopStories already follow the componentDidUpdate searchKey value.

So, my suggestion is that in functional, avoid setting a state with another state during the componentDidMount (useEffect with null dependency) due to lazy computation the state would not update immediately unless rendering process occur. So, in my code I avoid using searchKey and using searchTerm instead during setSearchTopStories. Like this code below:

import { useEffect, useState } from 'react';
import {
  DEFAULT_HPP,
  DEFAULT_PAGE,
  DEFAULT_QUERY,
  PARAM_HPP,
  PARAM_PAGE,
  PARAM_SEARCH,
  PATH_BASE,
  PATH_SEARCH,
} from '../../constants';
import { Button } from '../Button';
import { Loading } from '../Loading';
import { Search } from '../Search';
import { Table } from '../Table';
import './index.css';

function Appful() {
  const [isLoading, setIsLoading] = useState(false);
  const [searchTerm, setSearchTerm] = useState(DEFAULT_QUERY);
  const [searchKey, setSearchKey] = useState('');
  const [results, setResults] = useState(null);

  const setSearchTopStories = (result) => {
    console.log('setSearchTopStories');
    const { hits, page } = result;
    console.log('searchKey in setSearchTopStories: ' + searchTerm);

    const oldHits =
      results && results[searchTerm] ? results[searchTerm].hits : [];
    console.log('oldHits: ' + oldHits);

    const updatedHits = [...oldHits, ...hits];
    console.log('updatedHits: ' + updatedHits);

    setResults({ ...results, [searchTerm]: { hits: updatedHits, page: page } });
    // console.log('results: ' + JSON.stringify(results));
    setIsLoading(false);
  };

  const fetchSearchTopStories = async (searchTerm, page) => {
    console.log('fetchSearchTopStories');

    setIsLoading(true);
    const response = await fetch(
      `${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`
    );
    const result = await response.json();
    console.log('result: ' + result);
    setSearchTopStories(result);
  };

  useEffect(() => {
    console.log('componentDidMount');
    console.log('setSearchKey');
    setSearchKey(searchTerm);
    console.log('searchKey in componentDidMount: ' + searchKey);
    fetchSearchTopStories(searchTerm, DEFAULT_PAGE);
  }, []);

  useEffect(() => {
    console.log('componentDidUpdate');
    console.log('searchKey in componentDidUpdate: ' + searchKey);
  });

  const onSearchChange = (event) => {
    setSearchTerm(event.target.value);
  };

  const needToSearchTopStories = (searchTerm) => {
    return !results[searchTerm];
  };

  const onSearchSubmit = (event) => {
    setSearchKey(searchTerm);

    if (needToSearchTopStories(searchTerm)) {
      fetchSearchTopStories(searchTerm, DEFAULT_PAGE);
    }

    console.log('submit');
    event.preventDefault();
  };

  const onDismiss = (id) => {
    const { hits, page } = results[searchKey];

    const isNotId = (item) => item.objectID !== id;
    const updatedHits = hits.filter(isNotId);

    setResults({ ...results, [searchKey]: { hits: updatedHits, page: page } });
  };

  console.log('searchKey: ' + searchKey);
  //   console.log('results: ' + JSON.stringify(results));
  const page = (results && results[searchKey] && results[searchKey].page) || 0;
  console.log('page: ' + page);
  const list = (results && results[searchKey] && results[searchKey].hits) || [];
  console.log('list: ' + list);

  return (
    <div className="page">
      <div className="interactions">
        <Search
          value={searchTerm}
          onChange={onSearchChange}
          onSubmit={onSearchSubmit}
        >
          Search
        </Search>
      </div>
      <Table list={list} onDismiss={onDismiss} />
      <div className="interactions">
        {isLoading ? (
          <Loading />
        ) : (
          <Button onClick={() => fetchSearchTopStories(searchKey, page + 1)}>
            More
          </Button>
        )}
      </div>
    </div>
  );
}

export default Appful;

Thus, we get this result its perfect the same as class component (ignore the first componentDidUpdate because basically it's the same as componentDidMount): enter image description here

please visit my github for full code, hope this could be helpful :)