React-window, how to prevent rerendering list on state change? - javascript

This my code sandbox example:
https://codesandbox.io/s/react-hooks-counter-demo-kevxp?file=/src/index.js
My problem is:
The list will always rerendering on every state change inside the page so the scroll will always back to the top. I want to know why this happen, and how to prevent this behaviour even the state of the list have changes then keep the last scroll position of the list

Every time App renders, you are creating a brand new definition for the Example component. It may do the same thing as the old one, but it's a new component. So react compares the element from one render with the element of the next render and sees that they have different component types. Thus, thus it is forced to unmount the old one and mount the new one, just as it would if you changed something from a <div> to a <span>. The new one begins scrolled to 0.
The solution to this is to create Example only once, outside of App.
const Example = props => (
<List
className="List"
height={80}
itemCount={props.propsAbc.length}
itemSize={20}
width={300}
itemData={{
dataAbc: props.propsAbc
}}
>
{({ index, style, data }) => (
<div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
{data.dataAbc[index]}
</div>
)}
</List>
);
function App() {
const [count, setCount] = useState(0);
let [dataArray, setDataArray] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
return (
<div className="App">
<h1>Scroll down the blue box, then click the button</h1>
<h2>You clicked {count} times!</h2>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(count + 1)}>Increment</button>
<div
style={{ maxHeight: "80px", overflow: "äuto", background: "lightblue" }}
>
<Example propsAbc={dataArray} />
</div>
</div>
);
}
https://codesandbox.io/s/react-hooks-counter-demo-qcjgj

I don't think it's a react window problem.
A react component re-renders because there's a state change. In this case, the state change is caused by setCount (when you click the increment button), which re-renders the entire component including Example.
If Example is its own component, the scroll position won't get refreshed, because it no longer depends on the count state.
A working sample here:
https://codesandbox.io/s/react-hooks-counter-demo-hbek7?file=/src/index.js

Related

React Router - How to prevent top level refresh when child component button is clicked, preventDefault does not seem to make a difference

Background:
I have a simple react router:
<BrowserRouter >
<Routes>
<Route path="test/:locationId" element={<TestComponent />}></Route>
Then I have the component really simple:
function Inner(props) {
let ele = JSON.stringify(props);
console.log(" this is inner , with " + ele);
const [value, setValue] = useState(parseInt(ele));
return (
<div onClick={(e) => e.stopPropagation()}>
<p> Inner component: {String(ele)}</p>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log("inner btn clicked for " + value);
setValue(value + 1);
}}
>
{" "}
inner btn with clicked value {value}
</button>
</div>
);
}
export default function TestComponent(props) {
console.log("TestComponent top level component " + new Date());
console.log("I will do some heavy work like a big http request.....")
const [res, setRes] = useState([1, 2, 3]);
let params = useParams();
console.dir(params);
if (res.length > 0) {
console.log("res > 0 ");
return (
<div className="container">
{res.map((ele) => {
return <div key={String(ele)}>{Inner(ele)}</div>;
})}
</div>
);
}
Problem:
Anytime I click on any of the 3 buttons, I see TestComponent refreshes from the top level, like a new line of TestComponent top level component Mon Jul 18 2022 17:35:52 GMT-0400 (Eastern Daylight Time).
Now I plan to do some heavy http request and setState() inside TestComponent. However whenever I click on the buttons (in child components) this TestComponent always refreshes and thus will do the http request over and over again which I want to prevent. Tried e.stopPropagation() and e.preventDefault() but saw no difference.
enter image description here
Issues
It doesn't seem you understand the React component lifecycle well and are using the completely wrong tool to measure React renders.
All the console logs are occurring outside the render cycle as unintentional side-effects. What this means is that anytime React calls your app (the entire app code) to rerender (for any reason) that the component body is executed during the "render phase" in order to compute a diff for what changed and actually needs to be pushed to the DOM during the "commit phase". The commit phase is React rendering the app to the DOM and this is what we often refer colloquially as the React component rendering.
Note that the "render phase" is to be considered a pure function that can be called anytime React needs to call it. This is why it's pure function, i.e. without unintentional side-effects. The entire function body of a React function component is the "render" method.
Note the "commit phase" is where the UI has been updated to the DOM and effects can now run.
The issue of using the console.log outside the component lifecycle is exacerbated usually by the fact that we render our apps into the React.StrictMode component that does some double mounting of apps/components to ensure reusable state, and intentionally double invokes certain methods and functions as a way to detect unintentional side-effects.
Inner is defined like a React component but called manually like a regular function. This is a fairly obvious no-no. In React we pass a reference to our React components to React as JSX. We don't ever directly call our React functions directly.
Solution/Suggestion
Move all the console.log statements into useEffect hooks so they are called exactly once per render (to the DOM) cycle. All intentional side-effects should be done from the useEffect hook.
Render Inner as a React component and correctly pass props to it.
Example:
function Inner({ ele }) {
useEffect(() => {
console.log("this is inner , with " + ele);
});
const [value, setValue] = useState(ele);
return (
<div onClick={(e) => e.stopPropagation()}>
<p>Inner component: {String(ele)}</p>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log("inner btn clicked for " + value);
setValue(value + 1);
}}
>
inner btn with clicked value {value}
</button>
</div>
);
}
...
function TestComponent(props) {
const [res, setRes] = useState([1, 2, 3]);
let params = useParams();
useEffect(() => {
console.log("TestComponent top level component " + new Date());
console.log("I will do some heavy work like a big http request.....");
console.dir(params);
if (res.length > 0) {
console.log("res > 0 ");
}
});
if (res.length > 0) {
return (
<div className="container">
{res.map((ele) => (
<div key={ele}>
<Inner {...{ ele }} />
</div>
))}
</div>
);
}
}
...
<Routes>
<Route path="test/:locationId" element={<TestComponent />} />
</Routes>
Note that after the initial StrictMode mounting/remount that clicking the buttons only logs a single log from each Inner component.

React Hook (useState, setState) is both true and false

So I have a React state variable const [pickingHotspot, setPickingHotspot] = useState(false);. I then have this button <button type="button" className="btn btn-outline-danger" onClick={() => setPickingHotspot(true)}> which just sets the state to true onClick. I have another handler
tmp.on('mousedown', (event) => {
if (pickingHotspot){
console.log(tmp.mouseEventToCoords(event));
} else {
console.log(pickingHotspot);
}
});
where tmp is a Pannellum 360 Image Viewer (its a third party library, but I don't think it matters what it is), and this is set in my useState(...,[]) which runs once on load. Lastly, I have an onClick div that just prints the value of pickingHotspot for debugging purposed. Here's the weird part:
When I load the page and click the debug div, the value is false. Cool, that works. Then I click the button (which should set it to true!) and then click the debug div again. The value is true! But when I click the Pannellum viewer, the value is false? I'm not sure how the value could possibly be both true and false, depending on where I click. Are there different versions/instances of these variables? I've tried linking everything to individual function handlers that are outside of the html components and outside of the useEffect in case there's some weird scope stuff happening, but nothing has worked so far.
I tried to show all of the code needed, but here's the full thing (I took most of the unrelated stuff out to simplify it, its a lot of code to look through.):
function TourCreator(props){
const [scenes, setScenes] = useState({});
const [media, setMedia] = useState({});
const [viewer, setViewer] = useState(null);
// Editor states
const [pickingHotspot, setPickingHotspot] = useState(false);
function handle(event){
if (pickingHotspot){
console.log(viewer.mouseEventToCoords(event));
} else {
console.log(pickingHotspot);
}
}
// Called once on load
useEffect(() => {
if (Object.keys(media).length == 0){
// Sends the request to the backend for "data"
sendGetRequest(window.$PROJECT, true, {
id: params["project_id"],
}).then((data) => {
data.images = reshapeArray(data.images, 3);
console.log(data)
setMedia(data);
let tmp = window.pannellum.viewer('panorama', tour)
setViewer(tmp);
// Print Pitch/Yaw on click
tmp.on('mousedown', (event) => handle(event));
});
}
}, [])
return (
<div className="p-3">
{/* MAIN CONTENT */}
<div className='tour-creator-root mx-auto p-3 row rounded'>
{/* MAIN BOX */}
<div className='main-box col-9 px-0 rounded'>
{/* Pannellum viewer */}
<div id='panorama' className="w-100 rounded-top">
<button type="button" className="save-button btn btn-outline-danger">
Save
</button>
</div>
{/* Toolbar */}
<div className="toolbar w-100 d-flex flex-column justify-content-center rounded-bottom p-3"
onClick={
() => {
console.log(pickingHotspot)
}
}>
<div className="d-flex flex-row justify-content-around">
<button type="button" className="btn btn-outline-danger" onClick={
() => setPickingHotspot(true)
}>
Add Hotspot
</button>
</div>
</div>
</div>
</div>
</div>
)
}
export default TourCreator;
Try passing pickingHotspot in your dependency array for useEffect.
Your event handler is attached to your element in the useEffect on componentDidMount because of the empty dependency array. This will only happen once and that old function will be used. That old function will close over the value of the previous state. You can attach your event handler again on every relevant state change by passing pickHotSpot in your dependency array.
It is also a recommended approach to keep all your relevant code inside the hook. You could have put your listener function inside your hook, and would have seen a missing dependency warning from one of your lint tools.
Also, if there is no specific reason for you to add event hanlder like this from javascript, then add inline usin JSX, like #MB__ suggested. That will be executed on every render so it should be correct. At any time only one eventhandler for the particular event will be attached.

Text editor value disappears in React Js

I am making a simple accordion and inside each accordion, there is a text editor.
Accordion.js
<div className="wrapper">
{accordionData.map((item, index) => (
<Accordion>
<Heading>
<div
style={{ padding: "10px", cursor: "pointer" }}
className="heading"
onClick={() => toggleHandler(index)}
>
{toggleValue !== index ? `Expand` : `Shrink`}
</div>
</Heading>
<Text> {toggleValue === index && item.content && <EditorContainer />} </Text>
</Accordion>
))}
</div>
Here accordion is made up of as a component. This line {toggleValue === index && item.content && <EditorContainer />} is made to check the accordion clicked and then it loads the content and text editor accordingly.
Complete working example:
https://codesandbox.io/s/react-accordion-forked-dcqbo
Steps to reproduce the issue:
-> Open the above link
-> There will be three accordion
-> Click on any of the accordion, that will change the text from Expand to Shrink
-> Now fill some random text inside the editor then click on the text Shrink
-> Again open the same accordion by clicking Expand
-> Now already entered value is missing
I doubt it happens because every time we expand/shrink, the text_editor.js component gets called and that has the state value like,
this.state = {
editorState: EditorState.createEmpty()
};
Here instead of EditorState.createEmpty(), Should I need to give any other thing?
Requirement:
How can I store the already entered value in the text editor. Even though user clicks expand/shrink, the entered text needs to be remain there in the editor.
Any help is much appreciated.
You are correct, the entered value is missing because you are unmounting the EditorContainer component when its shrinked — that when you expand it again it creates a new editorState which is empty.
2 Possible solutions I could think of.
Move editorState and onEditorStateChange to the Parent component and pass that to EditorContainer. This way, when we unmount the EditorContainer we won't lose the previous editorState because it's on the Parent.
We wrap our EditorContainer inside a div and we'll apply a display style when we toggle between shrink/expand. This way, we are only hiding the EditorContainer not unmounting so its states will retain.
I would choose to implement the 2nd solution because we only have to make changes to our Accordion.js file. In either ways, I would create a new component that would handle the current item. I call it NormalAccordionItem.
const NormalAccordionItem = ({ data }) => {
const [show, setShow] = useState(false);
function toggle() {
setShow((prev) => !prev);
}
return (
<Accordion>
<Heading>
<div
style={{ padding: "10px", cursor: "pointer" }}
className="heading"
onClick={toggle}
>
{show ? "Shrink" : "Expand"}
</div>
</Heading>
<Text>
<div style={{ display: show ? "block" : "none" }}> // notice this
<EditorContainer />
</div>
</Text>
</Accordion>
);
};
Then on our NormalAccordion we'll use NormalAccordionItem.
const NormalAccordion = () => {
return (
<div className="wrapper">
{accordionData.map((data) => (
<NormalAccordionItem data={data} key={data.id} />
))}
</div>
);
};
That's it, check the demo below.
Edit Updated demo to expand NormalAccordionItem one at a time.

What's the correct pattern to change parent state inside a child state in React?

Imagine I have a page "Parent" which conditionally renders a div "Child".
On the click of a button, "Child" opens. To close "Child" one has to click in a X button inside it.
This is how I would do it and in my opinion it looks clean.
const Parent = (props) => {
const [childVisible, setChildVisible] = useState(false);
return (
<>
{childVisible && <Child close={setChildVisible.bind(false)} />}
<button onClick={setChildVisible.bind(true)}>
Open Child
</button>
</>
)
}
const Child = (props) => {
return (
<div>
<p>Im Child</p>
<button onClick={props.close()}> X </button>
</div>
)
}
Since react v16.13.0 react has introduced a warning Warning: Cannot update a component from inside the function body of a different component. and it seems I can't do this anymore.
What's the correct pattern now? I would rather not have a state in both components stating the same thing.
Call back was not properly added .You could do like this onClick={props.close}
While use onClick={props.close()} like this. close() function run on child mount instead of click event
const Child = (props) => {
return (
<div>
<p>Im Child</p>
<button onClick={props.close}> X </button>
</div>
)
}

Why does my React grandchild component not update after receiving a change in props from its parent? Or: do I need to use state?

I have tried finding the answer to this on StackOverflow and there are some related posts (e.g. React Child Component Not Updating After Parent State Change) but I want to understand why this is not working...
I have a React application that will display a layout of character cards (that is, each card displays a different character). It uses a child component, CharacterBoard, that lays out the CharacterCards, which would be a grandchild component. I pass the characters down from the App to the CharacterBoard as props, and CharacterBoard in turn maps these out the CharacterCards.
The problem is that I want the state of the character to change when I click on one of them. Specifically, I want the revealed field to change. However, even though the state change is reflected in the array of characters in the App (that is, the revealed field changes correctly), and the change is reflected in the array of characters in CharacterBoard, but not in CharacterCard. In fact, my mapping does not seem to be called at all in CharacterBoard when the props change.
Do I need to use something like getDerivedStateFromProps in CharacterBoard and set the state of that component and then use the state to map the values down to CharacterCard? If so, why?
In short (tl;dr), can you pass props on down through the component chain and map them out along the way and still have all changes reflected automatically?
Thanks for any guidance.
If it helps, the render method of my App is
render() {
const {state: {characters}} = this
return (
<div>
<header>
</header>
<main>
<CharacterBoard
onCardSelected={this.onCardSelected}
rowSize={logic.ROW_SIZE}
characters={characters}
cardSize={this.CARD_SIZE}/>
</main>
</div>
);
}
that of CharacterBoard is
render() {
const {props: {characters, rowSize, cardSize,onCardSelected}} = this
const rowUnit = 12 / rowSize
const cardLayout = characters
.map((character, i) => (
<Col xs={6} sm={rowUnit} key={character.name}>
<CharacterCard
onCardSelected = {onCardSelected}
key={i + Math.random()}
character={character}
cardSize={cardSize}
/>
</Col>
)
)
return (
<div>
<Container>
<Row>
{cardLayout}
</Row>
</Container>
</div>
)
}
and finally CharacterCard has this render method
render() {
const {props: {character, cardSize}} = this
const {thumbnail, revealed} = character
const imgURL = `${thumbnail.path}/${cardSize}.${thumbnail.extension}`
const topCardClass = classNames('characterCard__card-back', {'characterCard__card-back--hidden': revealed})
console.log(revealed)
return < a href="/#" onClick={this.onCardSelected}>
<div className='characterCard__card'>
<div className={topCardClass}>
<img src="/images/card_back.png" alt=""/>
</div>
< div className='characterCard__card-front'>< img alt=''
src={imgURL}/>
</div>
</div>
</a>
}
Doh! A simple forgetting to setState in App. Knowing that it should work made me go back through the code one more time and see that, indeed, it was a stupid error on my part.

Categories