Entire list re-renders due to the click handler - reactjs

I am using Material UI to render a grid of cards with a button on it. The problem I have is that every time I change a Select component on the parent, it re-renders the entire grid and it is really slow.
He is a simplified version of the set up.
Whenever I change the Select component, the entire grid re-renders.
// Page.js
export default function Page() {
const [target, setTarget] = useState('
');
const handleButtonClick = (cardName) => {
console.log(`${target} was the target of ${cardName}`}
}
return(
<div
<TargetSelect setTarget={setTarget} />
<Grid>
<GridCard cardName="Card 1" handleButtonClick={handleButtonClick} />
<GridCard cardName="Card 2" handleButtonClick={handleButtonClick} />
<GridCard cardName="Card 3" handleButtonClick={handleButtonClick} />
<GridCard cardName="Card 4" handleButtonClick={handleButtonClick} />
</Grid>
</div
)
}
What am I doing wrong that this is happening?

Related

Adding updated state to code editor only works once

I have an issue with my code below. When you click the add code button, it adds the code to the monaco code editor which is great. However, if you type some more code in the editor or erase whats currently there and then press the 'Add code' button, nothing is added. It's just blank.
Is there a way to whenever that 'Add code' button is clicked it clears everything in the editor and just adds the setAddCode state when the button is clicked?
And here is the code:
import { useState, useRef, useEffect } from "react";
import Editor from "#monaco-editor/react";
export default function IndexPage() {
const [input, setInput] = useState("");
const [addCode, setAddCode] = useState("# code goes here");
const editorRef = useRef(null);
function handleEditorDidMount(editor, monaco) {
setInput((editorRef.current = editor));
editorRef.current = editor;
}
return (
<div>
<div className="code-editor">
<Editor
width="100vh"
height="40vh"
theme="vs-dark"
fontSize="14"
defaultLanguage="python"
defaultValue=""
value={addCode}
onMount={handleEditorDidMount}
/>
</div>
<br />
<button onClick={() => setAddCode("print('Hello World!')")}>
Add code
</button>
</div>
);
}
The way the Editor component is set up, it will only change the value in the editor if you pass a different value prop. It's probably doing something similar to the following:
const Editor = ({ value }) => {
const [codeToDisplay, setCodeToDisplay] = useState(value);
useEffect(() => {
setCodeToDisplay(value);
}, [value]);
// etc
In your parent component, when you call setAddCode("print('Hello World!')") the first time, that'll result in the child component seeing a difference in how it was called - the value prop changed - so it'll know that there's something different to display, and update its own internal state appropriately. When you press the button again, the addCode value will stay the same - the Editor component doesn't see any difference, so it won't update.
To fix it, you can listen for changes from inside Editor by using its onchange prop to update the state in the parent component when the code inside the editor changes - that way, when you click the button later, the prop will be different, and the editor will know to update its internal state.
export default function IndexPage() {
const [input, setInput] = useState("");
const [codeValue, setCodeValue] = useState("# code goes here");
const editorRef = useRef(null);
function handleEditorDidMount(editor, monaco) {
setInput((editorRef.current = editor));
editorRef.current = editor;
}
return (
<div>
<div className="code-editor">
<Editor
width="100vh"
height="40vh"
theme="vs-dark"
fontSize="14"
defaultLanguage="python"
defaultValue=""
value={codeValue}
onChange={(newValue) => { setCodeValue(newValue); }}
onMount={handleEditorDidMount}
/>
</div>
<br />
<button onClick={() => setCodeValue("print('Hello World!')")}>
Add code
</button>
</div>
);
}

How to access elements in props.children to change how/position they are rendered in all scenarios?

Scenario
Imagine I am importing a library that provides the <CommonQuestion /> component that is rendered as defined by its sub-component children which I have wrapped here. L58 Example
I want to add a <RepeatableQuestionWithCancel /> component to the child row for the <input /> and as I cannot access the supplied <CommonQuestion /> at the source I navigate and assign individual elements of children to render them where I need them
with the cancel button. L71 Example this works
Here's the problem I can't solve. When I wrap the <CommonQuestion /> it continues to work for scenario 1 but fails in scenario 2 with TypeError Cannot read properties of undefined (reading '0') at L43 Elements.tsx. I know that the object has changed but no matter what I try to access the new shape of children I can not locate them in the object... but I know they're there because they render for scenario 1. This can be seen on L71 App.tsx by changing <QuestionWrapperControl /> to <QuestionWrapperControlTest />
Cleaned up for display - example has more helper text
export default function App() {
console.clear();
return (
<div>
<Section>
<QuestionNoWrapper />
</Section>
<hr />
<Section>
<QuestionWrapperControl />
</Section>
</div>
);
}
This is the imported component
const CommonQuestion = () => (
<QuestionTypeText>
<Label>This is a label</Label>
<Question>
<input />
</Question>
<Error>Required.</Error>
</QuestionTypeText>
);
This works for scenario 1
Here the child component <CommonQuestion /> is rendered without any manipulation.
const WrapperNoControl = (props: any) => {
const { children } = props;
return <div className="Wrapper">{children}</div>;
};
const QuestionNoWrapper = () => (
<WrapperNoControl>
<CommonQuestion />
</WrapperNoControl>
);
This works in scenario 2
Here the individual elements of the <CommonQuestion /> component is extracted and assigned to new variables. They are then rendered to add a cancel button component on the same row as the <input />
export const WrapperControl = (props: any) => {
const { children } = props;
const parent = children.props;
const label = parent.children[0];
const question = parent.children[1];
const error = parent.children[2];
return (
<>
<div className="Wrapper">
<Row>{label}</Row>
<Row>
<RepeatableQuestionWithCancel>
{question}
</RepeatableQuestionWithCancel>
</Row>
<Row>{error}</Row>
</div>
</>
);
};
export const QuestionWrapperControl = () => (
<WrapperControl>
<QuestionTypeText>
<Label>This is a label</Label>
<Question>
<input />
</Question>
<Error>Required.</Error>
</QuestionTypeText>
</WrapperControl>
);
This fails in scenario 2 When I change the <QuestionTypeText>...</QuestionTypeText> to <CommonQuestion /> it fails... and as mentioned in bullet 3 above I can't figure out the correct way to access the children.
const QuestionWrapperControlTest = () => (
<WrapperControl>
<CommonQuestion />
</WrapperControl>
);
I have read many tutorials and examined various libraries but have not found a solution. How can I correctly access the elements in the children when I add the wrapper?
Edit: When I examine the shape of the props object between scenario 2 and scenario 3 the props object no longer has any children. I would have expected to be able continue object navigation to work out how to access that element.

React fullcalendar with tooltip

I am trying to add a tooltip on hover when hovering over the event in fullcalendar. The alert works but the tooltip doesen't appear. Any tips to get me going?
const events = [
{
title: "Event 1",
start: "2021-10-04",
end: "2021-10-06",
},
{
title: "Event 2",
start: "2021-10-04",
}
];
export default function Calendar() {
return (
<div>
<FullCalendar
events={events}
eventMouseEnter={
(arg) => {
<ReactTooltip id="registerTip" place="top" effect="solid">
{arg.event.title}
</ReactTooltip>
// alert(arg.event.title);
}
}
plugins={[dayGridPlugin]}
/>
</div>
);
}
Example (Working example):
https://codesandbox.io/s/0m03n?file=/src/App.js:136-165
TL.DR: To add a tooltip to the whole calendar:
return (
<ReactTooltip id="registerTip" place="top" effect="solid">
<FullCalendar events={events} plugins={[dayGridPlugin]} />
<ReactTooltip />
);
To add a tooltip only to the title, you must use custom views components where your wrap the view with the tooltip: https://fullcalendar.io/docs/content-injection
For any component to show on the screen, it has to be rendered. On a very high level, that generally means that one component has to do return (<ComponentToRender />).
In your example, you are simply executing the code for the <ReactTooltip /> when hovering the calendar, not actually rendering the tooltip.
Pay attention that returning the <ReactTooltip /> on the onMouseEnter wouldn't work either. In that case you would be returning it on the callback, not on the component itself.
For your understanding, the <ReactTooltip /> probably has some internal logic that does something (on a very pseudo code level) like:
const [showTooltip, setShowTooltip] = useState();
onMouseEnter = setShowTooltip(true);
onMouseLeave = setShowTooltip(false);
...
return (
<>
{showTooltip && <Tooltip>}
{children}
</>

How to send unique props to similar Parents?

I currently made a custom component that acts similar to a carousel called Scrollbar. When I select a specific item it dynamically loads the information and displays a content div that has information on the item such as id, name, thumbnail-Image, etc..
The problem I am trying to solve is that I want to display multiple Scrollbars and only have one content div active at a time. The issue is that each Scrollbar has their own content div. I need a way to communicate which one is active.
I'm thinking possibly I need another component lets call it Home that displays multiple Scrollbars and onClick, it sets the current one Active and the rest inActive. All the Scrollbars should still be displaying however, one dynamic content div will appear below the active Scrollbar.
This sounds like I need to send one Scrollbar's prop isActive = true and the others isActive = false. I am unsure how to achieve this.
Here are short versions of my components:
Scrollbar.js
const Scrollbar = ({category, isActive }) => {
return (
<div className="scrollbar">
<h2>{category}</h2>
<div className="item-wrapper" style={{maxWidth: `${scrollbarWidth}px`}}>
.
.
.
.
//Handles Navigating the Scrollbar Left and Right
<SlideButton type="prev" onClick={handlePrev}/>
<SlideButton type="next" onClick={handleNext}/>
</div>
//Dynamic Content gets shown here when user clicks on an item & Scrollbar (tbd)
{isActive && currentCard && <Content activeCard={currentCard} clickFunc={handleCardChange} /> }
</div>
);
}
export default Scrollbar;
Home.js
const Home = () => {
return (
<div className="home">
<Scrollbar key="0" category="Scroll 1" activeBar={null} />,
<Scrollbar key="1" category="Scroll 2" activeBar={null} />
</div>
);
}
export default Home;
The Home component should hold the id of who is the activeBar with useState. It should pass setActiveBar to each Scrollbar.
const Home = () => {
const [activeBar, setActiveBar] = useState(0)
return (
<div className="home">
<Scrollbar
key="0"
id="0"
category="Scroll 1"
activeBar={activeBar}
setActiveBar={setActiveBar}
/>,
<Scrollbar
key="1"
id="1"
category="Scroll 2"
activeBar={activeBar}
setActiveBar={setActiveBar}
/>
</div>
);
}
The Scrollbar component should check if it's id is identical to activeBar, and set isActive accordingly. It should also set itself to be the activeBar when clicked.
const Scrollbar = ({category, id, activeBar, onClick }) => {
const isActive = activeBar === id;
return (
<div
onClick={() => setActiveBar(id)}
className="scrollbar"
>

React useState hooks - Text input loses focus when typing if passing form through props

I have a re-usable component <Layout /> that allows me to pass in custom components into a content and sidebar property.
In one instance of this, I'm putting an input field into the content which is a controlled component and uses useState() to handle the data.
However, I'm finding that my keyboard is losing focus whenever I type into the input. It appears the entire form is re-rendering and I can't stop it from doing so.
It was fine when my code was all inline, but since refactoring it to use the <Layout /> component, it is having some strange effects.
I do not wish to use any extras libraries other than core React 16.x
I have tried using useCallback(), keys, names, ids, refs - all to no avail
I've moved the callback functions out of scope and passed values through as per this answer but the problem persists
import React, { useState } from 'react';
function Layout({ content: Content, sidebar: Sidebar }) {
return (
<>
<div>
<Content />
</div>
<div>
<Sidebar />
</div>
</>
);
}
function Page() {
const [value, setValue] = useState('');
return (
<>
<Layout
content={() => (
<input
value={value}
onChange={(event) => setValue(event.target.value)}
/>
)}
sidebar={() => (
<p>Lorem ipsum...</p>
)}
/>
</>
);
}
export default Page;
Your problem is that you are defining anonymous functions and treating them as React components, which is fine for many cases, but here is causing React to lose its understanding of the underlying content within those functions. This means that even adding refs or keys to the input will do nothing, as the outer scope just sees a brand new function each time.
Consider for a second: instead of writing <Content />, you instead write <>{Content()}</>
This will get the same outcome, but shows that each time Layout is rendered it has an entirely different view of what its properties are, and thus renders the whole thing afresh each time. Because the input is being replaced quickly by a new but different input, the browser loses focus in the box. But it can also cause a tonne of other issues.
Instead, replace <Content /> with {content} and don't pass an anonymous function through- instead just pass the raw JSX as it is and it will no longer re-render the children.
import React, { useState } from 'react';
function Layout({ content, sidebar }) {
return (
<>
<div>
{content}
</div>
<div>
{sidebar}
</div>
</>
);
}
function Page() {
const [value, setValue] = useState('');
return (
<>
<Layout
content={(
<input
value={value}
onChange={(event) => setValue(event.target.value)}
/>
)}
sidebar={(
<p>Lorem ipsum...</p>
)}
/>
</>
);
}
export default Page;
The problem is that when Content is rendered as a react component inside Layout ,every-time Page re-renders, a new instance of content and sidebar functions are created, due to this react remounts the component as the function references are changed, because functions are recreated on every render inside react component and therefore the input focus is lost.
You can use content and sidebar components as render prop. A render prop is a function prop that a component uses to know what to render. Call the functions inside Layout to render Content and Sidebar.
function Layout({ content: Content, sidebar: Sidebar }) {
return (
<>
<div>{Content()}</div>
<div>{Sidebar()}</div>
</>
);
}
function Page() {
const [value, setValue] = useState("");
return (
<>
<Layout
content={() => (
<input
value={value}
onChange={event => setValue(event.target.value)}
/>
)}
sidebar={() => <p>Lorem ipsum...</p>}
/>
</>
);
}

Resources