How to restrict react Component to Re-rendering, - reactjs

I am passing functions to my child component. And I am using React.memo to restrict compoenent from re-rendering. But My component rerenders when parent re-renders. I tried to check why this is happening by using useEffect on all the props and I get to this point that my functions are causing compoenent to re-renders.
// my functions
const scrollToView = (index) => {
if (scrollRef && scrollRef.current && scrollRef.current[index]) {
scrollRef.current[index].scrollIntoView({ behavior: 'smooth' });
}
};
const scrollToReportView = (reportIndex) => {
if (scrollToReportRef && scrollToReportRef.current &&
scrollToReportRef.current[reportIndex]) {
scrollToReportRef.current[reportIndex].scrollIntoView({
behavior: 'smooth' });
}
}
.......
function LeftNav({
scrollToView, //function
scrollToReportView, //function
reports, //object
}) {
useEffect(() => {
console.log('scrollToView')
}, [scrollToView])
useEffect(() => {
console.log('scrollToReportView')
}, [scrollToReportView])
useEffect(() => {
console.log('reports')
}, [reports])
return (
<div>{'My Child Component'}</div>
);
}
export default memo(LeftNav);
And this is how my left nav is being called
<LeftNav
scrollToView={(index) => scrollToView(index)}
scrollToReportView={(repIndex)=> scrollToReportView(repIndex)}
reports={reports}
/>

With
<LeftNav
scrollToView={(index) => scrollToView(index)}
scrollToReportView={(repIndex)=> scrollToReportView(repIndex)}
reports={reports}
/>
you're creating new anonymous functions every time you render the LeftNav component, so memoization does absolutely nothing.
Just
<LeftNav
scrollToView={scrollToView}
scrollToReportView={scrollToReportView}
reports={reports}
/>
instead (assuming those functions are stable by identity (e.g. are declared outside the component or are properly React.useCallbacked or React.useMemoed).
In other words, if your component is currently
function Component() {
// ...
const scrollToView = (index) => {
if (scrollRef && scrollRef.current && scrollRef.current[index]) {
scrollRef.current[index].scrollIntoView({ behavior: "smooth" });
}
};
const scrollToReportView = (reportIndex) => {
if (scrollToReportRef && scrollToReportRef.current && scrollToReportRef.current[reportIndex]) {
scrollToReportRef.current[reportIndex].scrollIntoView({
behavior: "smooth",
});
}
};
return (
<LeftNav
scrollToView={(index) => scrollToView(index)}
scrollToReportView={(repIndex) => scrollToReportView(repIndex)}
reports={reports}
/>,
);
}
it needs to be something like
function Component() {
// ...
const scrollToView = React.useCallback((index) => {
if (scrollRef?.current?.[index]) {
scrollRef.current[index].scrollIntoView({ behavior: "smooth" });
}
}, []);
const scrollToReportView = React.useCallback((reportIndex) => {
if (scrollToReportRef?.current?.[reportIndex]) {
scrollToReportRef.current[reportIndex].scrollIntoView({
behavior: "smooth",
});
}
}, []);
return (<LeftNav scrollToView={scrollToView} scrollToReportView={scrollToReportView} reports={reports} />);
}
so the scrollToView and scrollToReportView functions have stable identities.

Related

Test DOM nodes on mount using react-teting-library

const DomNodeData = () => {
useEffect(() => {
const domNode = document.getElementById('visitDate')
if (domNode) {
//do something
// this needs to be tested
}
}, [])
return (
<div id="visitDate">Data</div>
)
}
describe('DemoData', () => {
it('Render dom node', () => {
render(<DomNodeData />)
})
})
After rendering the component in the test case, I cannot get the dom node, it's null. How can this be implemented in test?

Detect If Function Runs on Another Component in React

I need to detect if handleSelectProduct is being called in another component.
My problem is that if I want the child component(ProductDetailsComponent) to rerender, it still outputs the console.log('HELO'). I only want to output the console.log('HELO') IF handleSelectProduct is being click only.
const ProductComponent = () => {
const [triggered, setTriggered] = React.useState(0);
const handleSelectProduct = (event) => {
setTriggered(c => c + 1);
};
return (
<div>
Parent
<button type="button" onClick={handleSelectProduct}>
Trigger?
</button>
<ProductDetailsComponent triggered={triggered} />
</div>
);
};
const ProductDetailsComponent = ({ triggered }) => {
React.useEffect(() => {
if (triggered) {
console.log('HELO');
}
}, [triggered]);
return <div>Child</div>;
};
ReactDOM.render(
<ProductComponent />,
document.getElementById("root")
);
The simplest solution sounds to me by using an useRef to keep the old value, thus consider the console.log only when the triggered value changes.
const ProductDetailsComponent = ({ triggered }) => {
const oldTriggerRef = React.useRef(0);
React.useEffect(() => {
if (triggered !== oldTriggerRef.current) {
oldTriggerRef.current = triggered;
console.log('HELO');
}
}, [triggered]);
return <div>Child</div>;
};

How to create a dynamic array of React hooks for an array of components

const AnimatedText = Animated.createAnimatedComponent(Text);
function Component({ texts }) {
const [visitIndex, setVisitIndex] = React.useState(0);
// can't create an array of shared value for each text
// since useSharedValue is a hook, and that throws a warning
const textScalesShared = texts.map((_) => useSharedValue(1));
// can't create an array of animated style for each text
// since useAnimatedStyle is a hook, and that throws a warning
const animatedTextStyle = textScalesShared.map((shared) =>
useAnimatedStyle(() => ({
transform: [{ scale: shared.value }],
}))
);
useEffect(() => {
// code to reduce text scale one after another
// it will loop over the array of textScaleShared values
// passed to each component and update it
if (visitIndex === texts.length) {
return;
}
textScalesShared[visitIndex].value = withDelay(
1000,
withTiming(0.5, {
duration: 1000,
})
);
const timerId = setTimeout(() => {
setVisitIndex((idx) => idx + 1);
}, 1000);
return () => {
clearTimeout(timerId);
};
}, [visitIndex]);
return texts.map((text, index) => {
if (index <= visitIndex) {
return (
<AnimatedRevealingText
key={index}
fontSize={fontSize}
revealDuration={revealDuration}
style={animatedStylesShared[index]}
{...props}
>
{text}
</AnimatedRevealingText>
);
} else {
return null;
}
});
}
I want to apply animated styles to an array of components, but since useSharedValue and useAnimatedStyle are both hooks, I am unable to loop over the prop and create a shared value and the corresponding style for each of the component.
How can I achieve the same?
EDIT: updated to add the full code.
You can create a component to handle the useSharedValue and useAnimatedStyle hooks for every item using the visitIndex value:
AnimatedTextItem.js
const AnimatedText = Animated.createAnimatedComponent(Text);
const AnimatedTextItem = ({text, visited}) => {
const textScaleShared = useSharedValue(1);
const style = useAnimatedStyle(() => ({
transform: [{ textScaleShared.value }],
}));
useEffect(()=> {
if(visited) {
textScaleShared.value = withDelay(
1000,
withTiming(0.5, {
duration: 1000,
});
);
}
}, [visited]);
return (<AnimatedText style={style}>{text}</AnimatedText>)
}
Component.js
function Component({texts}) {
const [visitIndex, setVisitIndex] = React.useState(0);
useEffect(() => {
// code to reduce text scale one after another
// it will loop over the array of textScaleShared values
// passed to each component and update it
if (visitIndex === texts.length) {
return;
}
const timerId = setTimeout(() => {
setVisitIndex((idx) => idx + 1);
}, revealDuration);
return () => {
clearTimeout(timerId);
};
}, []);
return texts.map((text, index) => (<AnimatedTextItem text={text} visited={visitIndex === index}/>))
}
You can compose a component to handle it for you, but you need to pass the index of the text you're mapping through.
Like this
const AnimatedText = ({styleIndex}) => {
const textScaleShared = useSharedValue(styleIndex + 1);
const animatedTextStyle = useAnimatedStyle(() => ({
transform: [{ scale: textScaleShared.value }],
}));
const Animated = Animated.createAnimatedComponent(Text);
return <Animated style={animatedTextStyle}>{text}</Animated>;
};
function Component({ texts }) {
useEffect(() => {
// code to reduce text scale one after another
}, []);
return texts.map((text, index) => (
<AnimatedText key={index} styleIndex={index}>
{text}
</AnimatedText>
));
}
Interesting problem :) Let me see if i can come up a solution.
You already notice hook can't be in a dynamic array since the length of array is unknown.
Multiple components
You can have as many as components as you want, each one can have a hook, ex.
const Text = ({ text }) => {
// useSharedValue(1)
// useAnimatedStyle
}
const Components = ({ texts }) => {
return texts.map(text => <Text text={text} />)
}
Single hook
You can also see if you can find a className that can apply to all components at the same time. It's css i assume.

How to call ReactJS function with event (event.ctrlKey)?

I have a code similar to this one:
function Component1(...) {
...
function checkIfCtrlKey(event) {
return event.ctrlKey;
}
return (
{ checkIfCtrlKey() && (<Component2 .../>) }
);
}
The sense behind this is that the Component2 is just rendered if the Ctrl-key is being pressed. When running this code, I get following error message: TypeError: Cannot read property 'ctrlKey' of undefined
What is my mistake? Is there a solution or other possibility to implement my need?
You need to put an event listener on the window object and within that hander you can set some state to switch between a true and false
Something like this.
function Component1() {
const [ctrlActive, setCtrlActive] = useState(false)
useEffect(() => {
window.addEventListener('keydown', (e => {
if("Do a check here to see if it's CTRL key") {
setCtrlActive(!ctrlActive)
}
}), [])
})
return ctrlActive ? <Component2 /> : null
}
You can use Vanilla JS for that like this:
import React, { useEffect, useState } from "react";
export default function App() {
const [ctrlPressed, setCtrlPressed] = useState(false);
const handleKey = (e) => setCtrlPressed(e.ctrlKey);
useEffect(() => {
window.addEventListener("keydown", handleKey);
window.addEventListener("keyup", handleKey);
return function cleanup() {
window.removeEventListener("keydown", handleKey);
window.removeEventListener("keyup", handleKey);
};
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{ctrlPressed ? <h2>You're pressing CTRL Key</h2> : null}
</div>
);
}
Working example over here
Your making mistake here,
{ checkIfCtrlKey() && (<Component2 .../>) }
refer
function checkIfCtrlKey(event) {
return event.ctrlKey;
}
How u suppose that checkIfCtrlKey will be passed with event arg when your calling like this checkIfCtrlKey() ??
You might wanted to attach it to window,
function Component1() {
const [ctrlKeyPressed, setCKP] = useState(false)
const handleKey = ev => {
setCKP(ev.ctrlKey)
}
useEffect(() => {
window.addEventListener('keydown', handleKey);
window.addEventListener('keyup', handleKey);
return () => {
window.removeEventListener('keydown', handleKey);
window.removeEventListener('keyup', handleKey);
}
}, [])
return (
<>
...
{ctrlKeyPressed && <Component2 />}
...
</>
)
}
Shows Component2 as long as ctrlKey is pressed

How to use useState hook with array state for children that call the setter function without infinite rendering loop?

I have a parent component that has an array in state. It maps over the array and passes items to child components.
import React, { useState, useEffect } from "react";
import { Channel } from "../../../constants";
import { CommandLineArguments } from "../../../main/ipcHandlers";
import { Conversion, Converter } from "../Converter/Converter";
export function App() {
const [commandLineArguments, setCommandLineArguments] = useState<null | CommandLineArguments>(null);
const [conversions, setConversions] = useState<Conversion[]>([]);
function setConversion(filepath: string, partial: Partial<Conversion>) {
const updated = conversions
.filter((conversion) => conversion.filepath === filepath)
.map((conversion) => ({ ...conversion, ...partial }));
const rest = conversions.filter((conversion) => conversion.filepath !== filepath);
setConversions([...rest, ...updated]);
}
useEffect(function getCommandLineArgumentsEffect() {
async function asyncReadSvgFile() {
const args = await window.bridgeToMainProcessApi.invoke(Channel.GetCommandLineArguments);
const s = (args.input || []).map((path) => {
return { filepath: path };
});
setConversions(s);
}
asyncReadSvgFile();
}, []);
return (
<div>
{conversions.map((c) => (
<Converter
proxy=""
setConversion={setConversion}
key={c.filepath}
filepath={c.filepath}
svg={c.svg}
processedSvg={c.processedSvg}
tgml={c.tgml}
/>
))}
</div>
);
}
The children invoke the callback to update the conversions.
import React, { useEffect } from "react";
import compose from "lodash/fp/compose";
import { XmlView, XmlType, ViewType } from "../XmlView";
import { Channel, defaultProxy } from "../../../constants";
import { prepareSvg, convertSvg } from "../../../process";
import { filenameWithoutExtension, filenameFromPath } from "../App/files";
export type Conversion = {
filepath: string;
svg?: string;
processedSvg?: string;
tgml?: string;
};
type Props = Conversion & {
proxy: string;
setConversion(filepath: string, conversion: Partial<Conversion>): void;
};
export function Converter(props: Props) {
const { filepath, svg, processedSvg, tgml, proxy, setConversion } = props;
useEffect(
function readSvgFileEffect() {
console.log("read1");
async function asyncReadSvgFile() {
console.log("read2");
const files = await window.bridgeToMainProcessApi.invoke(Channel.ReadFiles, [filepath]);
const svg = files[0].content;
setConversion(filepath, { svg });
}
asyncReadSvgFile();
},
[filepath]
);
useEffect(
function prepareSvgEffect() {
async function asyncprepareSvg() {
if (!svg) {
return;
}
const processedSvg = await prepareSvg(svg, defaultProxy ? defaultProxy : proxy);
setConversion(filepath, { processedSvg });
}
asyncprepareSvg();
},
[svg]
);
useEffect(
function convertSvgEffect() {
async function asyncConvertSvg() {
if (!processedSvg) {
return;
}
const tgml = await convertSvg(processedSvg, compose(filenameWithoutExtension, filenameFromPath)(filepath));
setConversion(filepath, { tgml });
}
asyncConvertSvg();
},
[processedSvg]
);
return (
<div>
{svg && <XmlView serialized={svg} xmlType={XmlType.Svg} viewType={ViewType.Image} />}
{processedSvg && <XmlView serialized={processedSvg} xmlType={XmlType.ProcessedSvg} viewType={ViewType.Image} />}
{tgml && <XmlView serialized={tgml} xmlType={XmlType.Tgml} viewType={ViewType.Image} />}
{svg && <XmlView serialized={svg} xmlType={XmlType.Svg} viewType={ViewType.Code} />}
{processedSvg && <XmlView serialized={processedSvg} xmlType={XmlType.ProcessedSvg} viewType={ViewType.Code} />}
{tgml && <XmlView serialized={tgml} xmlType={XmlType.Tgml} viewType={ViewType.Code} />}
</div>
);
}
I don't understand why this causes an infinite rendering loop. I understand that calling setConversions causes the parent to re-render and pass new props to the children. I guess that might cause all the children to be recreated from scratch. Feel free to provide a better explanation of what is happening.
Regardless, my main question is: how do I get around the infinite re-rendering?
I tried to reproduce the error but was not able to. Even re ordering the conversions after async did not infinitely re render but did put the conversions in random order.
I changed some of your code to optimize like not randomizing conversions and making Conversion a pure component because it will render whenever other conversions change which will make it render more times the larger conversions array get (maybe up to the point where it errors but didn't try).
The comments are where I made the changes.
const later = (value) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), Math.random() * 100)
);
//using memo so it will only re render if props change
const Converter = React.memo(function Converter(props) {
const {
filepath,
setConversion,
svg,
processedSvg,
} = props;
React.useEffect(
function readSvgFileEffect() {
later({ svg: { val: 'svg' } }).then((resolve) =>
setConversion(filepath, resolve)
);
},
//added dependencies
[filepath, setConversion]
);
React.useEffect(
function prepareSvgEffect() {
if (!svg) {
return;
}
later({
processedSvg: { val: 'processed' },
}).then((resolve) =>
setConversion(filepath, resolve)
);
},
//added dependencies
[filepath, setConversion, svg]
);
React.useEffect(
function convertSvgEffect() {
if (!processedSvg) {
return;
}
later({
tgml: { val: 'tgml' },
}).then((resolve) =>
setConversion(filepath, resolve)
);
},
//added dependencies
[filepath, processedSvg, setConversion]
);
return <pre>{JSON.stringify(props, null, 2)}</pre>;
});
function App() {
const [conversions, setConversions] = React.useState([]);
React.useEffect(function getCommandLineArgumentsEffect() {
later().then(() =>
setConversions([
{ filepath: '1' },
{ filepath: '2' },
{ filepath: '3' },
])
);
}, []);
//use useCallback so setConversion doesn't change
const setConversion = React.useCallback(
function setConversion(filepath, partial) {
//pass callback to set state function so conversions
// is not a dependency of useCallback
setConversions((conversions) =>
//do not re order conversions
conversions.map((conversion) =>
conversion.filepath === filepath
? {
...conversion,
...partial,
}
: conversion
)
);
},
[]
);
return (
<div>
{conversions.map((c) => (
<Converter
setConversion={setConversion}
key={c.filepath}
filepath={c.filepath}
svg={c.svg}
processedSvg={c.processedSvg}
tgml={c.tgml}
/>
))}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Resources