I am dynamically adding similar components into a FlatList. Each component has a button where the default text is hard-coded, whereas when the user presses the button, a modal to pick time pops up. When i click the button that adds another of the same component, the previous component re-renders and sets back its default hard-coded text. How do i prevent re-renders of previous components?
I've tried setting another prop extraData as suggested from other SO answers to somewhat similar questions. But i can't get the expected behaviour.
The custom component is in a separate file.
Here is my FlatList:
const [setElements, elements] = useState([]);
const [dailyIndex, setDailyIndex] = useState(0);
const addElements = () => {
setElements([...elements, addElementItem()]);
setDailyIndex(prevState => prevState.dailyIndex + 1);
};
const addElementItem = index => <ElementItem elementKey={index} />;
<FlatList
extraData={dailyIndex}
data={elements}
renderItem={({ item, index }) => addElementItem(index)}
numColumns={2}
keyExtractor={(item, index) => index}
/>;
And my custom Component:
const ElementItem = props => {
return (
<Layout key={props.keyElement}>
<Layout>
<Button onPress={showTimeHandler}>{time ? button_time : 'TIME'}</Button>
<TimePickerModal
mode='time'
isVisible={isTimePickerVisible}
onConfirm={onTimeChangeHandler}
onCancel={showTimeHandler}
date={new Date(new Date().setHours(0, 0, 0, 0))}
/>
</Layout>
</Layout>
);
};
As suggested below, i used memo but it still re-renders the rest of the components in the FlatList
const ElementItem = memo(props => {
return (
<Layout key={props.keyElement}>
<Layout>
<Button onPress={showTimeHandler}>{time ? button_time : 'TIME'}</Button>
<TimePickerModal
mode='time'
isVisible={isTimePickerVisible}
onConfirm={onTimeChangeHandler}
onCancel={showTimeHandler}
date={new Date(new Date().setHours(0, 0, 0, 0))}
/>
</Layout>
</Layout>
);
}, (prevProps, nextProps) => prevProps.keyElement === nextProps.keyElement);
Photos to illustrate:
When i click on the add button and select a time
When i click on the add button again
Appreciate all the help.
Use memo with functional components:
import React, { memo } from 'react';
// 🙅‍♀️
const ComponentB = (props) => {
return <div>{props.propB}</div>
};
// 🙆‍♂️
const ComponentB = memo((props) => {
return <div>{props.propB}</div>
});
That’s it! You just need to wrap <ComponentB> with a memo() function. Now it will only re-render when propB actually changes value regardless of how many times its parent re-renders!
For more information please visit https://alligator.io/react/keep-react-fast/#use-memo-and-purecomponent
Related
I am getting the following error during sonarqube scan:
Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state. Instead, move this component definition out of the parent component “SectionTab” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.
I understand that it says that I should send the component as a prop from the parent, but I don't want to send the icon everytime that I want to use this component, is there another way to get this fixed?
import Select from "#mui/material/Select";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faAngleDown } from "#fortawesome/pro-solid-svg-icons/faAngleDown";
const AngleIcon = ({ props }: { props: any }) => {
return (
<FontAwesomeIcon
{...props}
sx={{ marginRight: "10px" }}
icon={faAngleDown}
size="xs"
/>
);
};
const SectionTab = () => {
return (
<Select
id="course_type"
readOnly={true}
IconComponent={(props) => <AngleIcon props={props} />}
variant="standard"
defaultValue="cr"
disableUnderline
/>
);
};
export default SectionTab;
What can you do:
Send the component as the prop:
IconComponent={AngleIcon}
If you need to pass anything to the component on the fly, you can wrap it with useCallback:
const SectionTab = () => {
const IconComponent = useCallback(props => <AngleIcon props={props} />, []);
return (
<Select
id="course_type"
readOnly={true}
IconComponent={IconComponent}
variant="standard"
defaultValue="cr"
disableUnderline
/>
);
};
This would generate a stable component, but it's pretty redundant unless you need to pass anything else, and not via the props. In that case, a new component would be generated every time that external value changes, which would make it unstable again. You can use refs to pass values without generating a new component, but the component's tree won't be re-rendered to reflect the change in the ref.
const SectionTab = () => {
const [value, setValue] = useState(0);
const IconComponent = useCallback(
props => <AngleIcon props={props} value={value} />
, []);
return (
<Select
id="course_type"
readOnly={true}
IconComponent={IconComponent}
variant="standard"
defaultValue="cr"
disableUnderline
/>
);
};
I have a parent component with a handler function:
const folderRef = useRef();
const handleCollapseAllFolders = () => {
folderRef.current.handleCloseAllFolders();
};
In the parent, I'm rendering multiple items (folders):
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
name={folder.name}
content={folder.content}
id={folder.id}
ref={folderRef}
/>
))}
In the child component I'm using the useImperativeHandle hook to be able to access the child function in the parent:
const [isFolderOpen, setIsFolderOpen] = useState(false);
// Collapse all
useImperativeHandle(ref, () => ({
handleCloseAllFolders: () => setIsFolderOpen(false),
}));
The problem is, when clicking the button in the parent, it only collapses the last opened folder and not all of them.
Clicking this:
<IconButton
onClick={handleCollapseAllFolders}
>
<UnfoldLessIcon />
</IconButton>
Only collapses the last opened folder.
When clicking the button, I want to set the state of ALL opened folders to false not just the last opened one.
Any way to solve this problem?
You could create a "multi-ref" - ref object that stores an array of every rendered Folder component. Then, just iterate over every element and call the closing function.
export default function App() {
const ref = useRef([]);
const content = data.map(({ id }, idx) => (
<Folder key={id} ref={(el) => (ref.current[idx] = el)} />
));
return (
<div className="App">
<button
onClick={() => {
ref.current.forEach((el) => el.handleClose());
}}
>
Close all
</button>
{content}
</div>
);
}
Codesandbox: https://codesandbox.io/s/magical-cray-9ylred?file=/src/App.js
For each map you generate new object, they do not seem to share state. Try using context
You are only updating the state in one child component. You need to lift up the state.
Additionally, using the useImperativeHandle hook is a bit unnecessary here. Instead, you can simply pass a handler function to the child component.
In the parent:
const [isAllOpen, setAllOpen] = useState(false);
return (
// ...
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
isOpen={isAllOpen}
toggleAll={setAllOpen(!isAllOpen)}
// ...
/>
))}
)
In the child component:
const Child = ({ isOpen, toggleAll }) => {
const [isFolderOpen, setIsFolderOpen] = useState(false);
useEffect(() => {
setIsFolderOpen(isOpen);
}, [isOpen]);
return (
// ...
<IconButton
onClick={toggleAll}
>
<UnfoldLessIcon />
</IconButton>
)
}
I have implemented this component:
function CardList({
data = [],
isLoading = false,
ListHeaderComponent,
ListEmptyComponent,
...props
}) {
const keyExtractor = useCallback(({ id }) => id, []);
const renderItem = useCallback(
({ item, index }) => (
<Card
data={item}
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
/>
),
[]
);
const renderFooter = useCallback(() => {
if (!isLoading) return null;
return (
<View style={globalStyles.listFooter}>
<Loading />
</View>
);
}, [isLoading]);
return (
<FlatList
{...props}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={renderFooter()}
ListEmptyComponent={ListEmptyComponent}
/>
);
}
As my CardList component is heavy, I have tried to optimize it following these tips.
But, I think that instead of using useCallback for renderFooter, I should use useMemo, in order to memoize the resulted JSX and not the method:
const ListFooterComponent = useMemo(() => {
if (!isLoading) return null;
return (
<View style={globalStyles.listFooter}>
<Loading />
</View>
);
}, [isLoading]);
Am I correct?
If you want to avoid expensive calculations use useMemo. useCallback is for memoizing a callback/function.
Official docs do have an example of using useMemo with JSX for avoiding re render:
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
Personal observation:
But to be more detailed, it seems it is not that useMemo here magically prevents re render, but the fact that you render same reference on the same spot in component hierarchy, makes react skip re render.
Here:
let Child = (props) => {
console.log('rendered', props.name);
return <div>Child</div>;
};
export default function Parent() {
let [a, setA] = React.useState(0);
let [b, setB] = React.useState(0);
// Only re-rendered if `a` changes:
const child1 = React.useMemo(() => <Child a={a} name="a" />, [a]);
// Only re-rendered if `b` changes:
const child2 = React.useMemo(() => <Child b={b} name="b" />, [b]);
return (
<div
onClick={() => {
setA(a + 1);
}}
>
{a % 2 == 0 ? child2 : child1}
</div>
);
}
If you keep clicking the div you can see it still prints "rendered b" even though we never changed the b prop. This is because react is receiving a different reference each time inside the div, and doesn't optimize the re render - like it does if you had simply rendered only child2 without that condition and child1.
Note: The fact that react skips rendering when it receives same element reference in the same spot is known, so apparently useMemo technique works because of that when it comes to rendering optimization.
"Should" is a matter of opinion, but you certainly can. React elements are fully reusable. It's not likely to make any real difference to your component, though, since creating the elements is fast and renderFooter is just used immediately in a component that's already running (unlike keyExtractor and renderItem, which you're passing to FlatList, so you want to make them stable where possible so FlatList can optimize its re-rendering). But you certainly can do that.
useMemo is perfectly sensible here as it memoizes the result (the JSX). As you say, useCallback memoizes the function not the result.
I am trying to create a system where I can easily click a given sentence on the page and have it toggle to a different sentence with a different color upon click. I am new to react native and trying to figure out the best way to handle it. So far I have been able to get a toggle working but having trouble figuring out how to change the class as everything is getting handled within a single div.
const ButtonExample = () => {
const [status, setStatus] = useState(false);
return (
<div className="textline" onClick={() => setStatus(!status)}>
{`${status ? 'state 1' : 'state 2'}`}
</div>
);
};
How can I make state 1 and state 2 into separate return statements that return separate texts + classes but toggle back and forth?
you can just create a component for it, create a state to track of toggle state and receive style of text as prop
in React code sandbox : https://codesandbox.io/s/confident-rain-e4zyd?file=/src/App.js
import React, { useState } from "react";
import "./styles.css";
export default function ToggleText({ text1, text2, className1, className2 }) {
const [state, toggle] = useState(true);
const className = `initial-style ${state ? className1 : className2}`;
return (
<div className={className} onClick={() => toggle(!state)}>
{state ? text1 : text2}
</div>
);
}
in React-Native codesandbox : https://codesandbox.io/s/eloquent-cerf-k3eb0?file=/src/ToggleText.js:0-465
import React, { useState } from "react";
import { Text, View } from "react-native";
import styles from "./style";
export default function ToggleText({ text1, text2, style1, style2 }) {
const [state, toggle] = useState(true);
return (
<View style={styles.container}>
<Text
style={[styles.initialTextStyle, state ? style1 : style2]}
onPress={() => toggle(!state)}
>
{state ? text1 : text2}
</Text>
</View>
);
}
This should be something you're looking for:
import React from "react"
const Sentence = ({ className, displayValue, setStatus }) => {
return (
<div
className={className}
onClick={() => setStatus((prevState) => !prevState)}
>
{displayValue}
</div>
);
};
const ButtonExample = () => {
const [status, setStatus] = React.useState(false);
return status ? (
<Sentence
className="textLine"
displayValue="state 1"
setStatus={setStatus}
/>
) : (
<Sentence
className="textLineTwo"
displayValue="state 2"
setStatus={setStatus}
/>
);
};
You have a Sentence component that takes in three props. One for a different className, one for a different value to be displayed and each will need access to the function that will be changing the status state. Each setter from a hook also has access to a function call, where you can get the previous (current) state value, so you don't need to pass in the current state value.
Sandbox
I have a PlayArea component with a number of Card components as children, for a card game.
The position of the cards is managed by the PlayArea, which has a state value called cardsInPlay, which is an array of CardData objects including positional coordinates among other things. PlayArea passes cardsInPlay and setCardsInPlay (from useState) into each Card child component.
Cards are draggable, and while being dragged they call setCardsInPlay to update their own position.
The result, of course, is that cardsInPlay changes and therefore every card re-renders. This may grow costly if a hundred cards make it out onto the table.
How can I avoid this? Both PlayArea and Card are functional components.
Here's a simple code representation of that description:
const PlayArea = () => {
const [cardsInPlay, setCardsInPlay] = useState([]);
return (
<>
{ cardsInPlay.map(card => (
<Card
key={card.id}
card={card}
cardsInPlay={cardsInPlay}
setCardsInPlay={setCardsInPlay} />
}
</>
);
}
const Card = React.memo({card, cardsInPlay, setCardsInPlay}) => {
const onDrag = (moveEvent) => {
setCardsInPlay(
cardsInPlay.map(cardInPlay => {
if (cardInPlay.id === card.id) {
return {
...cardInPlay,
x: moveEvent.clientX,
y: moveEvent.clientY
};
}
return cardInPlay;
}));
};
return (<div onDrag={onDrag} />);
});
It depends on how you pass cardsInPlay to each Card component. It doesn't matter if the array in state changes as long as you pass only the required information to child.
Eg:
<Card positionX={cardsInPlay[card.id].x} positionY={cardsInPlay[card.id].y} />
will not cause a re-render, because even i the parent array changes, the instance itself is not getting a new prop. But if you pass the whole data to each component :
<Card cardsInPlay={cardsInPlay} />
it will cause all to re-render because each Card would get a new prop for every render as no two arrays,objects are equal in Javascript.
P.S : Edited after seeing sample code
The problem is you're passing the entire cardsInPlay array to each Card, so React.memo() will still re-render each card because the props have changed. Only pass the element that each card needs to know about and it will only re-render the card that has changed. You can access the previous cardsInPlay using the functional update signature of setCardsInPlay():
const PlayArea = () => {
const [cardsInPlay, setCardsInPlay] = useState([]);
const cards = cardsInPlay.map(
card => (
<Card
key={card.id}
card={card}
setCardsInPlay={setCardsInPlay} />
)
);
return (<>{cards}</>);
};
const Card = React.memo(({ card, setCardsInPlay }) => {
const onDrag = (moveEvent) => {
setCardsInPlay(
cardsInPlay => cardsInPlay.map(cardInPlay => {
if (cardInPlay.id === card.id) {
return {
...cardInPlay,
x: moveEvent.clientX,
y: moveEvent.clientY
};
}
return cardInPlay;
})
);
};
return (<div onDrag={onDrag} />);
});