Component rendered 8 times - reactjs

I don't understand clearly why Row component rendered 8 times. Should I use custom comparing function in React Memo for this component? I'm using the react-window package. Please explain how it works for me. Thanks a lot.
Parent component of ListView: CovidMap Component
My entire project: Github
This is my code:
import React, { useRef, memo, useEffect } from 'react';
import { FixedSizeList as FixedList, areEqual } from 'react-window';
import './ListView.scss';
const ListView = (props) => {
const listRef = useRef();
const Row = memo((props) => {
console.log('Row rendering...');
const { data, index, style } = props;
const className =
data.itemIndex === index
? 'PatienIndicator Highlight'
: 'PatientIndicator';
return (
<button
key={index}
className={className}
onClick={() => data.onClickPatient(data.patients[index])}
style={style}
>
{data.patients[index].name}
</button>
);
}, areEqual);
const patientsLength = props.patients
? Object.keys(props.patients).length
: 0;
const data = Object.assign(
{},
{ patients: props.patients },
{ onClickPatient: props.onClickPatient },
{ itemIndex: props.itemIndex }
);
console.log('List View rendering...');
useEffect(() => {
if (props.itemIndex) {
listRef.current.scrollToItem(props.itemIndex, 'smarter');
}
});
return (
<FixedList
className="List"
height={300}
itemCount={patientsLength}
itemSize={50}
width={'100%'}
ref={listRef}
itemData={data}
>
{Row}
</FixedList>
);
};
export default memo(ListView);

The itemCount prop tells FixedSizeList how many rows to render, so your list will render however many items are in your props.patients list, which is ultimately coming from the result of filtering the array returned by the fetch in the parent component. The index prop that you're using in the Row component is passed to the Row from FixedSizeList and refers to its position in the list, so it tells the first instance of the Row component that its index is 0, and Row uses that information to render the name of the first item in the data.patients prop. FixedSizeList passes its own itemData prop to each Row component as Row's data prop.
From the react-window documentation for the FixedSizeList component:
itemCount: number
Total number of items in the list. Note that only a few items will be rendered and displayed at a time.
and
children: component
React component responsible for rendering the individual item specified by an index prop
You're passing the Row component to FixedSizeList as a child, so it renders the Row component itemCount number of times.
React memo won't affect the number of instances of the component. All memo does is potentially improves performance of rendering each instance by reusing its last rendered version if the props haven't changed.

Related

Child component prop causes "Maximum update depth exceeded"

I have the following two components.
Brands.js is a parent component that contains the SearchField component and passes a few props to it. I'll explain each prop:
brands is a list of Brand entities each containing an Id and a Name,
searchableItems is a list of Brand entities converted to an array of searchable items
setFilteredItems is a useState returned function that sets the filteredItems variable used by a table component
// I'm cutting away most of the code done in this component,
// but if necessary will expand this section.
...
<SearchField
wrapperClassName="ms-auto me-2 h-100"
inputClassName={"form-control-sm"}
items={brands}
searchableItems={searchableItems}
setFilteredItems={setFilteredItems}
itemIdResolver={(item) => item.id}
placeholder="Search for brand..."
/>
...
SearchField.js is a child component that renders a search input above the table in Brands.js component.
import React, {useEffect, useState} from 'react';
import {
Input
} from 'reactstrap';
export default function SearchField({ inputClassName, wrapperClassName, items, itemIdResolver, searchableItems, setFilteredItems, style, placeholder }) {
if (!inputClassName) inputClassName = '';
if (!wrapperClassName) wrapperClassName = '';
if (!itemIdResolver) itemIdResolver = (item) => item.id;
const [filterText, setFilterText] = useState("");
useEffect(() => {
let filteredSearchableItems = searchableItems?.filter(
searchableItem =>
searchableItem.searchString.indexOf(filterText.toLowerCase()) !== -1
);
setFilteredItems(
items?.filter(
item => filteredSearchableItems?.some(fsi => fsi.id === itemIdResolver(item))
));
}, [filterText, items, searchableItems, setFilteredItems, itemIdResolver]);
return (
<div className={"search-field-wrapper " + wrapperClassName}>
<i className="ri-search-line"></i>
<Input
type="text"
className={inputClassName}
placeholder={placeholder}
value={filterText}
onChange={e => setFilterText(e.target.value)}
style={style}
/>
</div>
)
}
The problem:
The SearchField components itemIdResolver causes the Maximum update depth exceeded error since it constantly re-renders the whole page/component.
I don't understand why fitting the itemIdResolver inside the dependency array of SearchField useEffect causes this.
Any help is appreciated.
P.S.
I tried removing the resolver and that got me rid of the maximum update error, so it's definitely due to that.

React Context value gets updated, but component doesn't re-render

This Codesandbox only has mobile styles as of now
I currently have a list of items being rendered based on their status.
Goal: When the user clicks on a nav button inside the modal, it updates the status type in context. Another component called SuggestionList consumes the context via useContext and renders out the items that are set to the new status.
Problem: The value in context is definitely being updated, but the SuggestionList component consuming the context is not re-rendering with a new list of items based on the status from context.
This seems to be a common problem:
Does new React Context API trigger re-renders?
React Context api - Consumer Does Not re-render after context changed
Component not re rendering when value from useContext is updated
I've tried a lot of suggestions from different posts, but I just cannot figure out why my SuggestionList component is not re-rendering upon value change in context. I'm hoping someone can give me some insight.
Context.js
// CONTEXT.JS
import { useState, createContext } from 'react';
export const RenderTypeContext = createContext();
export const RenderTypeProvider = ({ children }) => {
const [type, setType] = useState('suggestion');
const renderControls = {
type,
setType,
};
console.log(type); // logs out the new value, but does not cause a re-render in the SuggestionList component
return (
<RenderTypeContext.Provider value={renderControls}>
{children}
</RenderTypeContext.Provider>
);
};
SuggestionPage.jsx
// SuggestionPage.jsx
export const SuggestionsPage = () => {
return (
<>
<Header />
<FeedbackBar />
<RenderTypeProvider>
<SuggestionList />
</RenderTypeProvider>
</>
);
};
SuggestionList.jsx
// SuggestionList.jsx
import { RenderTypeContext } from '../../../../components/MobileModal/context';
export const SuggestionList = () => {
const retrievedRequests = useContext(RequestsContext);
const renderType = useContext(RenderTypeContext);
const { type } = renderType;
const renderedRequests = retrievedRequests.filter((req) => req.status === type);
return (
<main className={styles.container}>
{!renderedRequests.length && <EmptySuggestion />}
{renderedRequests.length &&
renderedRequests.map((request) => (
<Suggestion request={request} key={request.title} />
))}
</main>
);
};
Button.jsx
// Button.jsx
import { RenderTypeContext } from './context';
export const Button = ({ handleClick, activeButton, index, title }) => {
const tabRef = useRef();
const renderType = useContext(RenderTypeContext);
const { setType } = renderType;
useEffect(() => {
if (index === 0) {
tabRef.current.focus();
}
}, [index]);
return (
<button
className={`${styles.buttons} ${
activeButton === index && styles.activeButton
}`}
onClick={() => {
setType('planned');
handleClick(index);
}}
ref={index === 0 ? tabRef : null}
tabIndex="0"
>
{title}
</button>
);
};
Thanks
After a good night's rest, I finally solved it. It's amazing what you can miss when you're tired.
I didn't realize that I was placing the same provider as a child of itself. Once I removed the child provider, which was nested within itself, and raised the "parent" provider up the tree a little bit, everything started working.
So the issue wasn't that the component consuming the context wasn't updating, it was that my placement of providers was conflicting with each other. I lost track of my component tree. Dumb mistake.
The moral of the story, being tired can make you not see solutions. Get rest.

How should I update individual items' className onClick in a list in a React functional component?

I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?
Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

Map and render two different arrays in the same component - React

I just started learning React and I'm having issues understanding how to map and and render two different arrays in the same component. In my CMS I have 3 entries containing a title and a description each. I am calling them from GraphQL with the var data. The component SectionLeft has a grid container with threeFeatures as className. In my homepage I am passing the content to the grid container with the prop bottomcontent. I am passing the component SingleFeature in array which is my grid column and will contain an icon, a title and a description, it will repeat 3 times and compose my 3 column x 1 row grid. The component SingleFeature has the prop children which should contain an array map of the constant iconList containing 3 different svg icon components. I can't understand how to write a multiple array map for the component SingleFeature which will display the same data array displayed below for the props feature and details, but will render the 3 components for children in array. Any explanation would be really appreciated. Thank you.
homepage.js
import IconOne from "../components/icons/icon-one"
import IconTwo from "../components/icons/icon-two"
import IconThree from "../components/icons/icon-three"
export const iconList = [IconOne, IconTwo, IconThree]
export default function Home({data}) {
return (
<div>
<SectionLeft bottomcontent =
{data.allContentfulHomepageWordpressFeatures.edges.map(edge => (
<SingleFeature
children = {
...iconList array...
}
feature = {
edge.node.feature
}
details = {
edge.node.description
}
/>
))
}
/>
);
}
section-left.js
export default function SectionLeft(props) {
return (
<div className="threeFeatures">{props.bottomcontent}</div>
);
}
single-feature.js
export default function SingleFeature(props) {
return(
<div>
{props.children}
<h3>{props.feature}</h3>
<p>{props.details}</p>
</div>
);
}
GraphQL query
export const query = graphql`
allContentfulHomepageSpeedFeatures {
edges {
node {
feature
description
}
}
}
}
`
I would do it this way:
data.allContentfulHomepageWordpressFeatures.edges.map((edge, index) => (
<SingleFeature
children = {
iconList[index]
}
feature = {
edge.node.feature
}
details = {
edge.node.description
}
/>
))
map passes the index of the element in the mapped array as a second parameter to your arrow function.

Trigger re-render of subcomponent (react-table) using hooks

I'm still new to React, and functional programming, and Javascript, and JSX, so go easy if this is a stupid question.
I'm modifying one of the example material-ui tables from react-table v7. The original code can be found here. The example is completely functional and is using React Hooks as opposed to classes, as do all of the components of the template I'm using (shout out to creative-tim.com!)
My parent function (representative of a page in my dashboard application), for instance Users.js or Stations.js fetches data from a backend api inside a useEffect hook. That data is then passed as a prop to my subcomponent ReactTables.js
For some reason ReactTables.js does not receive changes to the "data" prop after the parent page's useEffect finishes. However, once I modify the data from a subcomponent of ReactTables (in this case AddAlarmDialog.js) then the table re-renders and all of my data suddenly appears.
How can I trigger the re-render of my subcomponent when data is returned from the parent component's useEffect? I noticed that in older versions of React there was a lifecycle function called componentWillReceiveProps(). Is this the behavior I need to emulate here?
Example Parent Component (Alarms.js):
import React, { useEffect, useState } from "react";
// #material-ui/core components
// components and whatnot
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";
import ReactTables from "../../components/Table/ReactTables";
import { server } from "../../variables/sitevars.js";
export default function Alarms() {
const [columns] = useState([
{
Header: "Alarm Name",
accessor: "aName"
},
{
Header: "Location",
accessor: "aLocation"
},
{
Header: "Time",
accessor: "aTime"
},
{
Header: "Acknowledged",
accessor: "aAcked"
},
{
Header: "Active",
accessor: "aActive"
}
]);
const [data, setData] = useState([]);
const [tableType] = useState("");
const [tableLabel] = useState("Alarms");
useEffect(() => {
async function fetchData() {
const url = `${server}/admin/alarms/data`;
const response = await fetch(url);
var parsedJSON = JSON.parse(await response.json());
var tableElement = [];
parsedJSON.events.forEach(function(alarm) {
tableElement = [];
parsedJSON.tags.forEach(function(tag) {
if (alarm.TagID === tag.IDX) {
tableElement.aName = tag.Name;
}
});
tableElement.aTime = alarm.AlarmRcvdTime;
parsedJSON.sites.forEach(function(site) {
if (site.IDX === alarm.SiteID) {
tableElement.aLocation = site.Name;
}
});
if (alarm.Active) {
tableElement.aActive = true;
} else {
tableElement.aActive = false;
}
if (!alarm.AckedBy && !alarm.AckedTime) {
tableElement.aAcked = false;
} else {
tableElement.aAcked = true;
}
//const newData = data.concat([tableElement]);
//setData(newData);
data.push(tableElement);
});
}
fetchData().then(function() {
setData(data);
});
}, [data]);
return (
<div>
<GridContainer>
<GridItem xs={12} sm={12} md={12} lg={12}>
<ReactTables
data={data}
columns={columns}
tableType={tableType}
tableLabel={tableLabel}
></ReactTables>
</GridItem>
</GridContainer>
</div>
);
}
Universal Table Subcomponent (ReactTables.js):
import React, { useState } from "react";
// #material-ui/core components
import { makeStyles } from "#material-ui/core/styles";
// #material-ui/icons
import Assignment from "#material-ui/icons/Assignment";
// core components
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";
import Card from "components/Card/Card.js";
import CardBody from "components/Card/CardBody.js";
import CardIcon from "components/Card/CardIcon.js";
import CardHeader from "components/Card/CardHeader.js";
import { cardTitle } from "assets/jss/material-dashboard-pro-react.js";
import PropTypes from "prop-types";
import EnhancedTable from "./subcomponents/EnhancedTable";
const styles = {
cardIconTitle: {
...cardTitle,
marginTop: "15px",
marginBottom: "0px"
}
};
const useStyles = makeStyles(styles);
export default function ReactTables(props) {
const [data, setData] = useState(props.data);
const [columns] = useState(props.columns);
const [tableType] = useState(props.tableType);
const [skipPageReset, setSkipPageReset] = useState(false)
const updateMyData = (rowIndex, columnId, value) => {
// We also turn on the flag to not reset the page
setData(old =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value
};
}
return row;
})
);
};
const classes = useStyles();
return (
<GridContainer>
<GridItem xs={12}>
<Card>
<CardHeader color="primary" icon>
<CardIcon color="primary">
<Assignment />
</CardIcon>
<h4 className={classes.cardIconTitle}>{props.tableLabel}</h4>
</CardHeader>
<CardBody>
<EnhancedTable
data={data}
columns={columns}
tableType={tableType}
setData={setData}
updateMyData={updateMyData}
skipPageReset={skipPageReset}
filterable
defaultPageSize={10}
showPaginationTop
useGlobalFilter
showPaginationBottom={false}
className="-striped -highlight"
/>
</CardBody>
</Card>
</GridItem>
</GridContainer>
);
}
ReactTables.propTypes = {
columns: PropTypes.array.isRequired,
data: PropTypes.array.isRequired,
tableType: PropTypes.string.isRequired,
tableLabel: PropTypes.string.isRequired,
updateMyData: PropTypes.func,
setData: PropTypes.func,
skipPageReset: PropTypes.bool
};
**For the record: if you notice superfluous code in the useEffect it's because I was messing around and trying to see if I could trigger a re-render.
I dont know exactly how the reactTable is handling its rendering, but if its a pure functional component, then the props you pass to it need to change before it will re-evaluate them. When checking if props have changed, react will just do a simple === comparison, which means that if your props are objects whos properties are being modified, then it will still evaluate as the same object. To solve this, you need to treat all props as immutable
In your example, you are pushing to the data array, and then calling setData(data) which means that you are passing the same instance of the array. When react compares the previous version of data, to the new version that you are setting in the call to setDate, it will think data hasnt changed because it is the same reference.
To solve this, you can just make a new array from the old array by spreading the existing array into a new one. So, instead of doing
data.push(tableElement);
You should do
const newInstance = [...data, tableElement];
Your code will need some tweaking because it looks like you are adding in lots of tableElements. But the short version of the lesson here is that you should never try and mutate your props. Always make a new instance
EDIT: So, after looking again, I think the problem is the way you are using the default param in the useState hook. It looks like you are expecting that to set the state from any prop changes, but in reality, that param is simply the default value that you will put in the component when it is first created. Changing the incoming data prop doesn't alter your state in any way.
If you want to update state in response to changes in props, you will need to use the useEffect hook, and set the prop in question as a dependancy.
But personally, I would try and not have what is essentially the same data duplicated in state in two places. I think the best bet would be to store your data in your alarm component, and add a dataChanged callback or something which will take your new data prop, and pass it back up to alarm via a parameter in the callback

Resources