0

I am learning React and this is my first message to Stack Overflow. Could you help me with the following problem which I have been trying to solve for days and through many tutorials?

I am doing a React project where App is the parent component and in a Bootstrap layout are its children Material-UI nested Sidebar and Container. So I should be able to raise onClick events from the Sidebar menu (return product_id of a clicked product ListItem) to the App component to update/ change product data in the Container.

May be because of my App.js is not class but function App() my code does not accept this and props. So when I have been following many tutorials I often get an error: this (or props) in not defined. In addition to that in the Material UI sidebar code the onClick of ListItem is already in use of its onClick function: <ListItem onClick={onClick} button dense>. So when I should pass a reference onSelection={handleSelection} from the parent I can't replace {onClick} with {onSelection} in the child. I tried also reference to both onClick function and onSelection in the Sidebar component: <ListItem onClick={onClick; onSelection} button dense> but it is not working. Right now in the Sidebar there is an attempt to react to both events:

function combo(e) {
    onClick(e);
    onSelection(e);
} 

triggered by <ListItem onClick={combo} ...> which does not work (because onSelection is not a function).

How should I solve this? Here are my pieces of code:

App.js (Parent)

import "./App.css";
import { Container } from "./components/Container";
import Sidebar from "./components/Sidebar";

function App(props) {
  const [files, setFiles] = useState([]);
  const [items, setItems] = useState([]);
  let product_id = 13; /* Hard coded test value */

  /* Get sidebar */
  useEffect(() => {
    fetch("/sidebar")
      .then((res) => res.json())
      .then((data) => {
        console.log(data);
        setItems(data);
      });
  }, []);

  /* Get product data */
  useEffect(() => {
    fetch(`/data/${product_id}`)
      .then((res) => res.json())
      .then((data) => {
        setFiles(data);
      });
  }, []);

  function handleSelection() {
    console.log("Hello");
  }

  return (
    <React.Fragment>
      <header className="header">
        <h5>Header</h5>
      </header>
      <div className="container-fluid">
        <div className="row">
          <div className="col-3">
            <Sidebar
              className="sidebar"
              items={items}
              product_id={product_id}
              onSelection={handleSelection}
            />
          </div>
          <div className="col-6">
            <Container files={files} />
          </div>
        </div>
      </div>
    </React.Fragment>
  );
}

export default App;

Sidebar.js (Child)

import { Collapse, List, ListItem } from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import ExpandLessIcon from "@material-ui/icons/ExpandLess";

function SidebarItem({ item, product_id }) {
  const [collapsed, setCollapsed] = useState(true);
  const { title, items, id } = item;

  function toggleCollapse() {
    setCollapsed((prevValue) => !prevValue);
  }

  function onClick() {
    if (Array.isArray(items)) {
      toggleCollapse();
    }
    product_id = id;
    console.log(product_id);  /* This is working well */
  }

  let expandIcon;
  if (Array.isArray(items) && items.length) {
    expandIcon = !collapsed ? <ExpandLessIcon /> : <ExpandMoreIcon />;
  }

  function combo(e) {
    onClick(e);
    onSelection(e);
  }

  return (
    <>
      <ListItem onClick={combo} button dense>
        <div>{title}</div>
        {expandIcon}
      </ListItem>
      <Collapse
        className="sidebar-subitem-text"
        in={!collapsed}
        timeout="auto"
        unmountOnExit
      >
        {Array.isArray(items) ? (
          <List disablePadding dense>
            {items.map((subItem, index) => (
              <SidebarItem key={`${subItem.id}${index}`} item={subItem} />
            ))}
          </List>
        ) : null}
      </Collapse>
    </>
  );
}

function Sidebar({ items }) {
  return (
    <>
      <List disablePadding dense>
        {items.map((sidebarItem, index) => (
          <SidebarItem
            key={`${sidebarItem.title}${index}`}
            item={sidebarItem}
          />
        ))}
      </List>
    </>
  );
}

export default Sidebar;
goduno
  • 5
  • 2

2 Answers2

0

This is because you did not define all the props on the Sidebar and SidebarItem component. You would want to define it like so:

function Sidebar({ items, product_id, onSelection }) 

and

function SidebarItem({ item, product_id, onSelection }) 

You probably will have to pass the onSelection to the SidebarItem inside the Sidebar as well. Similarly like you do in the App with Sidebar component

<SidebarItem
    key={`${sidebarItem.title}${index}`}
    item={sidebarItem}
    onSelection={onSelection}
/>
szczocik
  • 1,293
  • 1
  • 8
  • 18
0

Like I commented szczocik's answer helped me a lot! I added the missing props. I made the changes proposed to make parent's handleSelection() to work. I also added passData={true} to SidebarItem. That way I could control data passing so that data is passed only when subitems of the sidebar are clicked like this:

function combo(e) {
    onClick(e);
    if (passData) {
      product_id = id;
      e = id;
      onSelection(e);
    }
  }

And in the parent handleSelection() looks like this:

const [product_id, setProduct_id] = useState(13); /* (13) is default data of product #13 */

  function HandleSelection(e) {
    setProduct_id(e);
    console.log("product_id: " + product_id);
    console.log(e);
  }

What happened after these changes is that parent stopped fetching data to the container. Before this there was fetched product #13 data by useEffect. Why is that? Also what is weird that when I click any submenu item console.log(e) prints the correct product_id but console.log("product_id: " + product_id) prints the correct product_id after the second click in the same subitem. For instance if I click subitem #7 product_id is 13 (the hardcoded default) and when I click subitem #7 again console prints 7. When after that I click #4 (e) prints 4 right a way but (product_id) prints still 7 and so on. What could be reason for that?

goduno
  • 5
  • 2
  • When you set state with useState hooks, the changes are not instantaneous. It batches the changes and run the together. That's why you don't see the product id after first click. You can use the use Effect to listen to changes of product id and console it then. Also for to fetching the product when it loads, you should use use Effect with an empty array as dependency https://stackoverflow.com/questions/53048495/does-react-batch-state-update-functions-when-using-hooks – szczocik Sep 24 '20 at 07:10
  • Thank you. I could not connect my HandleSelection(e) function with useEffect if I didn't put HandleSelection inside the array of the useEffect. Only that way sidebar started to work but I got some warnings and the app slowed very much in development. Also when I clicked between products (in sidebar) the new one came to container but changed back to the former one and then back the new one. Is this flickering? Anyway I just read more about useEffect and found out that maybe I have to put 'product_id' to the array of uE. After that flickering, warnings and slowness were gone. Is this right way? – goduno Sep 24 '20 at 16:21