0

I have created a page which has two columns:

  1. In one column the idea is to display a list of items
  2. On the other column, I should show some info related to the selected item

The code I have so far is:

import { INavLink, INavLinkGroup, INavStyles, Nav } from "@fluentui/react";
import React, { createContext, useContext, useState } from "react";

interface HistoryTtem {
  id: string;
}

interface AppState {
  selectedItem: string | undefined;
  updateSelectedItem: (value: string | undefined) => void;
  items: Array<HistoryTtem>;
}

const AppContext = createContext<AppState>({
  selectedItem: undefined,
  updateSelectedItem: (value: string | undefined) => {},
  items: []
});

const App = () => {
  const Column1 = () => {
    const rootState: AppState = useContext(AppContext);

    const getNavLinks: Array<INavLink> = rootState.items.map((item) => ({
      name: item.id,
      key: item.id,
      url: ""
    }));

    const groups: Array<INavLinkGroup> = [
      {
        links: getNavLinks
      }
    ];

    const navStyles: Partial<INavStyles> = {
      root: {
        boxSizing: "border-box",
        border: `1px solid #eee`,
        overflowY: "auto"
      }
    };

    const onItemClick = (
      e?: React.MouseEvent<HTMLElement>,
      item?: INavLink
    ) => {
      if (item && item.key) {
        rootState.updateSelectedItem(item.key);
      }
    };

    return (
      <Nav
        onLinkClick={onItemClick}
        selectedKey={rootState.selectedItem}
        ariaLabel="List of previously searched transactions"
        styles={navStyles}
        groups={groups}
      />
    );
  };

  const Column2 = () => {
    return <div>aaa</div>;
  };

  const [historyItems, setHistoryItems] = useState<Array<HistoryTtem>>([
    {
      id: "349458457"
    },
    {
      id: "438487484"
    },
    {
      id: "348348845"
    },
    {
      id: "093834845"
    }
  ]);
  const [selectedItem, setSelectedItem] = useState<string>();

  const updateSelectedItem = (value: string | undefined) => {
    setSelectedItem(value);
  };

  const state: AppState = {
    selectedItem: selectedItem,
    updateSelectedItem: updateSelectedItem,
    items: historyItems
  };

  return (
    <AppContext.Provider value={state}>
      <div>
        <Column1 />
        <Column2 />
      </div>
    </AppContext.Provider>
  );
};

export default App;

As you can see, I have a root state which will serve to drive the update of the second column triggered from inside the first one. But it is not working. When I click on an item, the whole component in the first column is re-rendering, while it should only change the selected item.

Please find here the CodeSandbox.

Andry
  • 16,172
  • 27
  • 138
  • 246

2 Answers2

1

You shouldn't nest component functions.

The identity of Column1 changes for every render of App since it's an inner function, and that makes React think it needs to reconcile everything.

Move Column1 and Column2 up to the module level.

AKX
  • 152,115
  • 15
  • 115
  • 172
  • That makes so much sense. I actually make a bad mistake every time then because I thought so far that it is good practice to have components used by only one component defined inside it, but that is actually doing just bad stuff... that pattern is probably to be used only in HOCs – Andry Jan 14 '21 at 13:40
  • Technically you could use `React.useCallback()` to hint React that the inner component doesn't change, but that's even more confusing. :) – AKX Jan 14 '21 at 13:41
0

What makes react rerender is two things:

  1. Change in State

  2. Change in Props

You have an App Component which is the root of your components and it has a selectedItem state which is changing when an item is clicked so you have a new state and the new state will cause rerender

Taghi Khavari
  • 6,272
  • 3
  • 15
  • 32