React - generating a unique random key causes infinite loop - javascript

I have a componenet that wraps its children and slides them in and out based on the stage prop, which represents the active child's index.
As this uses a .map() to wrap each child in a div for styling, I need to give each child a key prop. I want to assign a random key as the children could be anything.
I thought I could just do this
key={`pageSlide-${uuid()}`}
but it causes an infinite loop/React to freeze and I can't figure out why
I have tried
Mapping the children before render and adding a uuid key there, calling it via key={child.uuid}
Creating an array of uuids and assigning them via key={uuids[i]}
Using a custom hook to store the children in a state and assign a uuid prop there
All result in the same issue
Currently I'm just using the child's index as a key key={pageSlide-${i}} which works but is not best practice and I want to learn why this is happening.
I can also assign the key directly to the child in the parent component and then use child.key but this kinda defeats the point of generating the key
(uuid is a function from react-uuid, but the same issue happens with any function including Math.random())
Here is the full component:
import {
Children,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import PropTypes from "prop-types";
import uuid from "react-uuid";
import ProgressBarWithTicks from "./ProgressBarWithTicks";
import { childrenPropType } from "../../../propTypes/childrenPropTypes";
const calculateTranslateX = (i = 0, stage = 0) => {
let translateX = stage === i ? 0 : 100;
if (i < stage) {
translateX = -100;
}
return translateX;
};
const ComponentSlider = ({ stage, children, stageCounter }) => {
const childComponents = Children.toArray(children);
const containerRef = useRef(null);
const [lastResize, setLastResize] = useState(null);
const [currentMaxHeight, setCurrentMaxHeight] = useState(
containerRef.current?.childNodes?.[stage]?.clientHeight
);
const updateMaxHeight = useCallback(
(scrollToTop = true) => {
if (scrollToTop) {
window.scrollTo(0, 0);
}
setCurrentMaxHeight(
Math.max(
containerRef.current?.childNodes?.[stage]?.clientHeight,
window.innerHeight -
(containerRef?.current?.offsetTop || 0) -
48
)
);
},
[stage]
);
useEffect(updateMaxHeight, [stage, updateMaxHeight]);
useEffect(() => updateMaxHeight(false), [lastResize, updateMaxHeight]);
const resizeListener = useMemo(
() => new MutationObserver(() => setLastResize(Date.now())),
[]
);
useEffect(() => {
if (containerRef.current) {
resizeListener.observe(containerRef.current, {
childList: true,
subtree: true,
});
}
}, [resizeListener]);
return (
<div className="w-100">
{stageCounter && (
<ProgressBarWithTicks
currentStage={stage}
stages={childComponents.length}
/>
)}
<div
className="position-relative divSlider align-items-start"
ref={containerRef}
style={{
maxHeight: currentMaxHeight || null,
}}>
{Children.map(childComponents, (child, i) => (
<div
key={`pageSlide-${uuid()}`}
className={`w-100 ${
stage === i ? "opacity-100" : "opacity-0"
} justify-content-center d-flex`}
style={{
zIndex: childComponents.length - i,
transform: `translateX(${calculateTranslateX(
i,
stage
)}%)`,
pointerEvents: stage === i ? null : "none",
cursor: stage === i ? null : "none",
}}>
{child}
</div>
))}
</div>
</div>
);
};
ComponentSlider.propTypes = {
children: childrenPropType.isRequired,
stage: PropTypes.number,
stageCounter: PropTypes.bool,
};
ComponentSlider.defaultProps = {
stage: 0,
stageCounter: false,
};
export default ComponentSlider;
It is only called in this component (twice, happens in both instances)
import { useEffect, useReducer, useState } from "react";
import { useParams } from "react-router-dom";
import {
FaCalendarCheck,
FaCalendarPlus,
FaHandHoldingHeart,
} from "react-icons/fa";
import { IoIosCart } from "react-icons/io";
import { mockMatches } from "../../../templates/mockData";
import { initialSwapFormState } from "../../../templates/initalStates";
import swapReducer from "../../../reducers/swapReducer";
import useFetch from "../../../hooks/useFetch";
import useValidateFields from "../../../hooks/useValidateFields";
import IconWrap from "../../common/IconWrap";
import ComponentSlider from "../../common/transitions/ComponentSlider";
import ConfirmNewSwap from "./ConfirmSwap";
import SwapFormWrapper from "./SwapFormWrapper";
import MatchSwap from "../Matches/MatchSwap";
import SwapOffers from "./SwapOffers";
import CreateNewSwap from "./CreateNewSwap";
import smallNumberToWord from "../../../functions/utils/numberToWord";
import ComponentFader from "../../common/transitions/ComponentFader";
const formStageHeaders = [
"What shift do you want to swap?",
"What shifts can you do instead?",
"Pick a matching shift",
"Good to go!",
];
const NewSwap = () => {
const { swapIdParam } = useParams();
const [formStage, setFormStage] = useState(0);
const [swapId, setSwapId] = useState(swapIdParam || null);
const [newSwap, dispatchNewSwap] = useReducer(swapReducer, {
...initialSwapFormState,
});
const [matches, setMatches] = useState(mockMatches);
const [selectedMatch, setSelectedMatch] = useState(null);
const [validateHook, newSwapValidationErrors] = useValidateFields(newSwap);
const fetchHook = useFetch();
const setStage = (stageIndex) => {
if (!swapId && stageIndex > 1) {
setSwapId(Math.round(Math.random() * 100));
}
if (stageIndex === "reset") {
setSwapId(null);
dispatchNewSwap({ type: "reset" });
}
setFormStage(stageIndex === "reset" ? 0 : stageIndex);
};
const saveMatch = async () => {
const matchResponse = await fetchHook({
type: "addSwap",
options: { body: newSwap },
});
if (matchResponse.success) {
setStage(3);
} else {
setMatches([]);
dispatchNewSwap({ type: "setSwapMatch" });
setStage(1);
}
};
useEffect(() => {
// set matchId of new selected swap
dispatchNewSwap({ type: "setSwapMatch", payload: selectedMatch });
}, [selectedMatch]);
return (
<div>
<div className="my-3">
<div className="d-flex justify-content-center w-100 my-3">
<ComponentSlider stage={formStage}>
<IconWrap colour="primary">
<FaCalendarPlus />
</IconWrap>
<IconWrap colour="danger">
<FaHandHoldingHeart />
</IconWrap>
<IconWrap colour="warning">
<IoIosCart />
</IconWrap>
<IconWrap colour="success">
<FaCalendarCheck />
</IconWrap>
</ComponentSlider>
</div>
<ComponentFader stage={formStage}>
{formStageHeaders.map((x) => (
<h3
key={`stageHeading-${x.id}`}
className="text-center my-3">
{x}
</h3>
))}
</ComponentFader>
</div>
<div className="mx-auto" style={{ maxWidth: "400px" }}>
<ComponentSlider stage={formStage} stageCounter>
<SwapFormWrapper heading="Shift details">
<CreateNewSwap
setSwapId={setSwapId}
newSwap={newSwap}
newSwapValidationErrors={newSwapValidationErrors}
dispatchNewSwap={dispatchNewSwap}
validateFunction={validateHook}
setStage={setStage}
/>
</SwapFormWrapper>
<SwapFormWrapper heading="Swap in return offers">
<p>
You can add up to{" "}
{smallNumberToWord(5).toLowerCase()} offers, and
must have at least one
</p>
<SwapOffers
swapId={swapId}
setStage={setStage}
newSwap={newSwap}
dispatchNewSwap={dispatchNewSwap}
setMatches={setMatches}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<MatchSwap
swapId={swapId}
setStage={setStage}
matches={matches}
selectedMatch={selectedMatch}
setSelectedMatch={setSelectedMatch}
dispatchNewSwap={dispatchNewSwap}
saveMatch={saveMatch}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<ConfirmNewSwap
swapId={swapId}
setStage={setStage}
selectedSwap={selectedMatch}
newSwap={newSwap}
/>
</SwapFormWrapper>
</ComponentSlider>
</div>
</div>
);
};
NewSwap.propTypes = {};
export default NewSwap;
One solution
#Nick Parsons has pointed out I don't even need a key if using React.Children.map(), so this is a non issue
I'd still really like to understand what was causing this problem, aas far as I can tell updateMaxHeight is involved, but I can't quite see the chain that leads to an constant re-rendering
Interstingly if I use useMemo for an array of uuids it works
const uuids = useMemo(
() => Array.from({ length: childComponents.length }).map(() => uuid()),
[childComponents.length]
);
/*...*/
key={uuids[i]}

Related

React and Javascript: how can I move const out of function App()?

How can I move one or more const out of function App?
In the simple test App below, I'm using localStorage to store a value which determines if a div is dispayed. The handleToggle dismisses the div and stores a value in localStorage. Clearing localstorage and reloading shows the div again.
In a simple test App on localhost, this works. But in my more complex production App, I'm getting the error Invalid hook call. Hooks can only be called inside of the body of a function component , which has a myriad of fixes, one of which points out the issue may be that a const needs to be a separate function.
And so I'm thinking the issue is that I need to convert the two const to a function that can be placed right under the import blocks and out of the function App() block.
As a start, in this simple App, how can I move the two const out of function App()?
import './App.css';
import * as React from 'react';
function App() {
const [isOpen, setOpen] = React.useState(
JSON.parse(localStorage.getItem('is-open')) || false
);
const handleToggle = () => {
localStorage.setItem('is-open', JSON.stringify(!isOpen));
setOpen(!isOpen);
};
return (
<div className="App">
<header className="App-header">
<div>{!isOpen && <div>Content <button onClick={handleToggle}>Toggle</button></div>}</div>
</header>
</div>
);
}
export default App;
Edit: This is the full production file with Reza Zare's fix that now throws the error 'import' and 'export' may only appear at the top level on line 65:
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
import {
Compose,
Notifications,
HomeTimeline,
CommunityTimeline,
PublicTimeline,
HashtagTimeline,
DirectTimeline,
FavouritedStatuses,
BookmarkedStatuses,
ListTimeline,
Directory,
} from '../../ui/util/async-components';
import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
const componentMap = {
'COMPOSE': Compose,
'HOME': HomeTimeline,
'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline,
'FAVOURITES': FavouritedStatuses,
'BOOKMARKS': BookmarkedStatuses,
'LIST': ListTimeline,
'DIRECTORY': Directory,
};
// Added const
const getInitialIsOpen = () => JSON.parse(localStorage.getItem('is-open')) || false;
const App = () => {
const [isOpen, setOpen] = React.useState(getInitialIsOpen());
const handleToggle = () => {
localStorage.setItem('is-open', JSON.stringify(!isOpen));
setOpen(!isOpen);
};
function getWeekNumber(d) {
d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay()||7));
var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
var weekNo = Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7);
return [d.getUTCFullYear(), weekNo];
}
var result = getWeekNumber(new Date());
// errors out here: 'import' and 'export' may only appear at the top level.
export default class ColumnsArea extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = {
columns: ImmutablePropTypes.list.isRequired,
isModalOpen: PropTypes.bool.isRequired,
singleColumn: PropTypes.bool,
children: PropTypes.node,
};
// Corresponds to (max-width: $no-gap-breakpoint + 285px - 1px) in SCSS
mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1174px)');
state = {
renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
}
componentDidMount() {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
if (this.mediaQuery) {
if (this.mediaQuery.addEventListener) {
this.mediaQuery.addEventListener('change', this.handleLayoutChange);
} else {
this.mediaQuery.addListener(this.handleLayoutChange);
}
this.setState({ renderComposePanel: !this.mediaQuery.matches });
}
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
}
componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
componentDidUpdate(prevProps) {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
}
componentWillUnmount () {
if (!this.props.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
if (this.mediaQuery) {
if (this.mediaQuery.removeEventListener) {
this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
} else {
this.mediaQuery.removeListener(this.handleLayouteChange);
}
}
}
handleChildrenContentChange() {
if (!this.props.singleColumn) {
const modifier = this.isRtlLayout ? -1 : 1;
this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
}
}
handleLayoutChange = (e) => {
this.setState({ renderComposePanel: !e.matches });
}
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
}
setRef = (node) => {
this.node = node;
}
renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
}
renderError = (props) => {
return <BundleColumnError multiColumn errorType='network' {...props} />;
}
render () {
const { columns, children, singleColumn, isModalOpen } = this.props;
const { renderComposePanel } = this.state;
if (singleColumn) {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
</div>
</div>
<div className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' />
// output of getInitialIsOpen
<div class='banner'>
{!isOpen && <div>Content <button onClick={handleToggle}>Toggle</button></div>}
</div>
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
<div className='columns-area__panels__pane__inner'>
<NavigationPanel />
</div>
</div>
</div>
);
}
return (
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
{columns.map(column => {
const params = column.get('params', null) === null ? null : column.get('params').toJS();
const other = params && params.other ? params.other : {};
return (
<BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
</BundleContainer>
);
})}
{React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
</div>
);
}
}

React-spring not animating as expected

So I'm using react-visibility-sensor with react-spring to animate text sliding char by char from any side I want.
In my home page the animation is running smoothly, I use it twice one from the right side and another from the top side.
When I switch routes and go to another page the animation does not work.
I have my code divided in an "Title" component "Char" component and a custom hook "useAnimatedText".
Title component:
import React from "react";
import VisibilitySensor from "react-visibility-sensor";
import useAnimatedText from "../../hooks/useAnimatedText";
import Char from './Char'
const Title = ({title, side}) => {
// HERE I CALL A CUSTOM HOOK THAT WILL DIFINE IF THE ELEMENT IS VISIBLE OR NOT
// AND HANDLE THE ANIMATION WHEN NECESSARY
const [isVisible, onChange, objArray] = useAnimatedText(title)
let elements = objArray.map((item, i) => {
return(
<Char
key={i}
isVisible={isVisible}
item={item}
delay={400 + (i * 40)}
side={side}
/>
)
})
console.log(isVisible)
return(
<VisibilitySensor onChange={onChange} >
<span className="title-box">
<h1 className="my-heading divided-heading">
{elements}
</h1>
<hr className="title-ruller"></hr>
</span>
</VisibilitySensor>
)
}
export default Title
Char component:
import { useSpring, animated } from "react-spring"
const Char = (props) => {
const { isVisible, item, delay, isBouncy, side} = props
const [ref, addBounce] = useBounce()
let springConfig = {}
if (side === 'right') {
springConfig = {
to: {
opacity: isVisible ? 1 : 0,
translateX : isVisible ? '0px' : '1000px'
},
config: { mass:2, tension: 200, friction: 30},
delay: delay
}
}
else if (side === 'top') {
springConfig = {
to: {
opacity: isVisible ? 1 : 0,
translateY: isVisible ? '0px' : '-500px'
},
config:{ mass:2, tension: 250, friction: 35},
delay: delay
}
}
const spring = useSpring({...springConfig})
return(
<animated.span
style={ spring }
className={isVisible ? 'is-visible' : 'is-not-visible'}
>
{item.char === ' ' ? <span> </span> : item.char}
</animated.span>
)
}
export default Char
This is the custom Hook:
import { useState } from "react";
import { stringToArray } from '../helpers'
// HOOK THAT HANDLES THE TEXT ANIMATION BY SETTING A STATE OF VISIBILITY
function useAnimatedText(string) {
const [isVisible, setVisibility] = useState(false);
const onChange = visiblity => {
visiblity && setVisibility(visiblity);
};
let objArray = stringToArray(string)
return [isVisible, onChange, objArray]
}
export default useAnimatedText
I did a console.log(isVisible) and the value was true but it was rendering in the page the spring values as if it was false(not visible).
I really canĀ“t understand where I'm going wrong here, the only problem I have is when I'm not at my main route, could it be because of react-router-dom?
If someone has any clue, let me know.

High memory usage in React Native FlatList

I am developing an app which allows the user to search for books and then display it in the search results. For displaying the results, I am using a FlatList with 3 columns and displaying the book cover and some basic information about the book.
I am storing the results from the API response in state without the comoponent. As more results are added, the memory consumption increases but the data is in JSON format, no images are store in state.
I have tried, using removeClippedSubviews and few other options that allow setting the window size but that has little to no difference on the memory usage.
Am I missing something here or is there a way to optimise this? Sample code is uploaded to this github repo
Here is the code snippet I am using:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* #format
* #flow strict-local
*/
import type { Node } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
FlatList,
Platform,
SafeAreaView,
StatusBar,
StyleSheet,
useColorScheme,
View,
} from 'react-native';
import { Button, SearchBar, useTheme } from 'react-native-elements';
import { searchBooks } from './api/GoogleBooksService';
import HttpClient from './network/HttpClient';
import BookCard from './components/BookCard';
const searchParamsInitialState = {
startIndex: 1,
maxResults: 12,
totalItems: null,
};
let debounceTimer;
const debounce = (callback, time) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(callback, time);
};
const isEndOfList = searchParams => {
const { startIndex, maxResults, totalItems } = searchParams;
if (totalItems == null) {
return false;
}
console.log('isEndOfList', totalItems - (startIndex - 1 + maxResults) < 0);
return totalItems - (startIndex - 1 + maxResults) < 0;
};
const App: () => Node = () => {
const isDarkMode = useColorScheme() === 'dark';
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [globalSearchResults, setGlobalSearchResults] = useState([]);
const [searchParams, setSearchParams] = useState(searchParamsInitialState);
let searchCancelToken;
let searchCancelTokenSource;
// This ref will be used to track if the search Term has changed when tab is switched
const searchRef = useRef();
const clearSearch = () => {
console.log('Clear everything!');
searchRef.current = null;
setGlobalSearchResults([]);
setSearchParams(searchParamsInitialState);
setIsLoading(false);
searchCancelTokenSource?.cancel();
searchCancelToken = null;
searchCancelTokenSource = null;
};
useEffect(() => {
debounce(async () => {
setIsLoading(true);
await searchGlobal(searchTerm);
setIsLoading(false);
}, 1000);
}, [searchTerm]);
/**
* Search method
*/
const searchGlobal = async text => {
if (!text) {
// Clear everything
clearSearch();
return;
}
setIsLoading(true);
try {
// Use the initial state values if the search term has changed
let params = searchParams;
if (searchRef.current !== searchTerm) {
params = searchParamsInitialState;
}
const { items, totalItems } = await searchBooks(
text,
params.startIndex,
params.maxResults,
searchCancelTokenSource?.token,
);
if (searchRef.current === searchTerm) {
console.log('Search term has not changed. Appending data');
setGlobalSearchResults(prevState => prevState.concat(items));
setSearchParams(prevState => ({
...prevState,
startIndex: prevState.startIndex + prevState.maxResults,
totalItems,
}));
} else {
console.log(
'Search term has changed. Updating data',
searchTerm,
);
if (!searchTerm) {
console.log('!searchTerm', searchTerm);
clearSearch();
return;
}
setGlobalSearchResults(items);
setSearchParams({
...searchParamsInitialState,
startIndex:
searchParamsInitialState.startIndex +
searchParamsInitialState.maxResults,
totalItems,
});
}
searchRef.current = text;
} catch (err) {
if (HttpClient.isCancel(err)) {
console.error('Cancelled', err.message);
}
console.error(`Error searching for "${text}"`, err);
}
setIsLoading(false);
};
const renderGlobalItems = ({ item }) => {
return <BookCard book={item} />;
};
const { theme } = useTheme();
return (
<SafeAreaView style={styles.backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
/>
<View style={styles.container}>
<SearchBar
showLoading={isLoading}
placeholder="Enter search term here"
onChangeText={text => {
setSearchTerm(text);
}}
value={searchTerm}
platform={Platform.OS}
/>
{isLoading && globalSearchResults.length <= 0 && (
<ActivityIndicator animating style={styles.loader} />
)}
{globalSearchResults.length > 0 && (
<FlatList
removeClippedSubviews
columnWrapperStyle={styles.columnWrapper}
data={globalSearchResults}
numColumns={3}
showsHorizontalScrollIndicator={false}
keyExtractor={item => item + item.id}
renderItem={renderGlobalItems}
ListFooterComponent={
<>
{!isLoading &&
!isEndOfList(searchParams) &&
searchParams.totalItems > 0 && (
<Button
type="clear"
title="Load more..."
onPress={async () => {
await searchGlobal(searchTerm);
}}
/>
)}
{isLoading && searchParams.totalItems != null && (
<ActivityIndicator
size="large"
style={{
justifyContent: 'center',
}}
color={theme.colors.primary}
/>
)}
</>
}
/>
)}
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
backgroundStyle: 'white',
container: {
height: '100%',
width: '100%',
},
columnWrapper: {
flex: 1,
},
loader: {
flex: 1,
justifyContent: 'center',
},
});
export default App;
There is something called PureComponent in react native. If you create FlatList as PureComponent, you can see lot of improvement.
It will not rerender items until data has been changed.
for example:
class MyList extends React.PureComponent {
}
For more reference check this
Can you try to chuck your array of list items into small sub-arrays, this package uses this mechanism https://github.com/bolan9999/react-native-largelist
The package has been praised by complex app teams including the Discord Mobile Team - https://discord.com/blog/how-discord-achieves-native-ios-performance-with-react-native

Does changing the props of a child always re-render the parent even with React.memo?

I'm trying to prevent a modal element to re-render when it's supposed to be invisible.
The course I'm following does this by converting the component to a class based component and using shouldComponentUpdate() but I wanted to check if using React.memo() did the same thing.
I tried, and it doesn't, but I'm not sure why.
The component that should not render is this:
import React , {useEffect} from 'react'
import Aux from '../../../hoc/Aux';
import Backdrop from '../Backdrop/Backdrop';
import classes from './Modal.module.css';
const Modal = (props) => {
useEffect(() => {
console.log('[Modal.js] useEffect')
});
return (
<Aux>
<Backdrop show={props.show} clicked={props.modalClosed} />
<div
className={classes.Modal}
style={{
transform: props.show ? 'translateY(0)' : 'translateY(-100vh)',
opacity: props.show ? '1': '0'
}}>
{props.children}
</div>
</Aux>)
};
export default React.memo(Modal);
Which is managed by
import React, {Component} from 'react'
import Aux from '../../hoc/Aux';
import Burger from '../../components/Burger/Burger'
import BuildControls from '../../components/Burger/BuildControls/BuildControls'
import Modal from '../../components/UI/Modal/Modal'
import OrderSummary from '../../components/Burger/OrderSummary/OrderSummary'
const INGREDIENT_PRICES = {
salad: 0.5,
cheese: 0.4,
meat: 1.3,
bacon: 0.7
}
class BurgerBuilder extends Component {
state = {
ingredients: {
salad: 0,
bacon: 0,
cheese: 0,
meat: 0
},
totalPrice: 4,
purchasable: false,
purchasing: false
}
purchaseHandler = () => {
this.setState({purchasing: true});
};
purchaseCancelHandler = () => {
this.setState({purchasing:false});
};
purchaseContinueHandler = () => {
alert('You Continue!')
};
updatePurchaseState = (ingredients) => {
let purchasable = false;
for (let ingredient in ingredients){
if (ingredients[ingredient]>0){
purchasable = true;
break;
}
}
this.setState({purchasable:purchasable})
}
addIngredientHandler = (type) => {
const oldCount = this.state.ingredients[type];
const updatedCount = oldCount +1;
const updatedIngredients = {
...this.state.ingredients
};
updatedIngredients[type] = updatedCount;
const priceAddition = INGREDIENT_PRICES[type];
const oldPrice = this.state.totalPrice;
const newPrice = oldPrice + priceAddition;
this.setState({totalPrice: newPrice, ingredients: updatedIngredients});
this.updatePurchaseState(updatedIngredients);
};
removeIngredientHandler = (type) => {
const oldCount = this.state.ingredients[type];
if (oldCount <= 0)
return;
const updatedCount =oldCount -1;
const updatedIngredients = {
...this.state.ingredients
};
updatedIngredients[type] = updatedCount;
const priceAddition =INGREDIENT_PRICES[type];
const oldPrice = this.state.totalPrice;
const newPrice = oldPrice - priceAddition;
this.setState({totalPrice: newPrice, ingredients: updatedIngredients});
this.updatePurchaseState(updatedIngredients);
};
render () {
const disabledInfo = {
...this.state.ingredients
};
for (let key in disabledInfo){
disabledInfo[key] = disabledInfo[key] <= 0;
}
return (
<Aux>
<Modal show={this.state.purchasing} modalClosed={this.purchaseCancelHandler}>
<OrderSummary
ingredients={this.state.ingredients}
purchaseCancelled={this.purchaseCancelHandler}
purchaseContinued={this.purchaseContinueHandler}
price={this.state.totalPrice} />
</Modal>
<Burger ingredients={this.state.ingredients}/>
<BuildControls
ingredientAdded={this.addIngredientHandler}
ingredientRemoved={this.removeIngredientHandler}
disabled={disabledInfo}
price={this.state.totalPrice}
purchasable={this.state.purchasable}
ordered={this.purchaseHandler}/>
</Aux>
);
}
}
export default BurgerBuilder;
With BuildControls I change the Ingredients state; but not the props I pass to modal, purchasing and purchaseHandler
Does changing the ingredients prop I pass to it's child always prompt a re-render even when Modal itself is under React.memo() ?
You are changing one of the props you pass to Modal - the children prop. It is passed implicitly by adding children to a react element. And since you are changing the child element it will cause a re-render.

React UseContext change not re-rendering component

I am trying to make a simple 'Nonogram'/'Picross' game using React to learn UseContext and UseReducer, but am puzzled as to why my top component (App) is not re-rendering when a value it uses changes. Perhaps I am missing something basic, but I've read through documentation and examples online and can't see why it is not re-rendering.
Expectation: User goes on the application, clicks on the squares to change their value (draw a cross by clicking on the squares), and the text underneath the board reads "Congratulations!", as it is based on the value of 'isComplete'
Problem: As above, but 'Keep trying' remains.
I added a button to see the boardState as defined in the UseReducer function, too.
Code is as follows:
App.js
import './App.css';
import { useReducer } from 'react';
import Table from './Table';
import BoardContext from './BoardContext';
import boardReducer from './BoardReducer';
function App() {
//Puzzle layout
const puzzleArray = [
[true, false, true],
[false, true, false],
[true, false, true]
];
//Creating a set of blank arrays to start the game as the userSelection
const generateUserSelection = () => {
const userSelection = [];
puzzleArray.forEach(row => {
let blankRow = [];
row.forEach(square => {
blankRow.push(false)
});
userSelection.push(blankRow);
})
return userSelection;
};
//Initial Context value
const boardInfo = {
puzzleName: "My Puzzle",
puzzleArray: puzzleArray,
userSelection: generateUserSelection(),
isComplete: false
};
const [ boardState, dispatch ] = useReducer(boardReducer, boardInfo)
return (
<BoardContext.Provider value={{board: boardState, dispatch}}>
<div className="App">
<header className="App-header">
<p>
Picross
</p>
<Table />
</header>
<div>
{boardState.isComplete ?
<div>Congratulations!</div>
: <div>Keep trying</div>
}
</div>
<button onClick={() => console.log(boardState)}>boardState</button>
</div>
</BoardContext.Provider>
);
}
export default App;
Table.jsx:
import { useContext, useEffect } from 'react';
import './App.css';
import Square from './Square';
import BoardContext from './BoardContext';
function Table() {
useEffect(() => {console.log('table useEffect')})
const { board } = useContext(BoardContext);
const generateTable = solution => {
const squareLayout = []
for (let i = 0; i < solution.length; i++) {
const squares = []
for (let j = 0; j < solution[i].length; j++) {
squares.push(
<Square
position={{row: i, column: j}}
/>
);
};
squareLayout.push(
<div className="table-row">
{squares}
</div>
);
};
return squareLayout;
};
return (
<div className="grid-container">
{generateTable(board.puzzleArray)}
</div>
);
}
export default Table;
Square.jsx
import { useContext, useState } from 'react';
import './App.css';
import BoardContext from './BoardContext';
function Square(props) {
const { board, dispatch } = useContext(BoardContext)
const [ isSelected, setIsSelected ] = useState(false);
const { position } = props;
const handleToggle = () => {
console.log(board)
board.userSelection[position.row][position.column] = !board.userSelection[position.row][position.column]
dispatch(board);
setIsSelected(!isSelected);
}
return (
<div className={`square ${isSelected ? " selected" : ""}`}
onClick={handleToggle}
>
{position.row}, {position.column}
</div>
);
}
export default Square;
Thanks
Edit: I know for a simple application like this it would be very easy to pass down state through props, but the idea is to practice other hooks, so wanting to avoid it. The ideas I am practicing in this would ideally be extensible to bigger projects in the future.
Edit 2: As requested, here's my BoardReducer.js file:
const boardReducer = (state, updateInfo) => {
let isComplete = false;
if (JSON.stringify(updateInfo.userSelection) === JSON.stringify(state.puzzleArray)) {
isComplete = true;
}
updateInfo.isComplete = isComplete;
return updateInfo;
}
export default boardReducer;
(using JSON.stringify as a cheap way to check matching arrays as it's only a small one for now!)
Issue
You are mutating your state object in a couple places:
const handleToggle = () => {
console.log(board);
board.userSelection[position.row][position.column] = !board.userSelection[position.row][position.column]; // <-- mutation!
dispatch(board);
setIsSelected(!isSelected);
}
And in reducer
const boardReducer = (state, updateInfo) => {
let isComplete = false;
if (JSON.stringify(updateInfo.userSelection) === JSON.stringify(state.puzzleArray)) {
isComplete = true;
}
updateInfo.isComplete = isComplete; // <-- mutation!
return updateInfo; // <-- returning mutated state object
}
Since no new state object is created React doesn't see a state change and doesn't rerender your UI.
Solution
useReducer will typically employ a "redux" pattern where the reducer function consumes the current state and an action to operate on that state, and returns a new state object.
You should dispatch an action that toggles the user selection and checks for a complete board.
Board Reducer
When updating state you should shallow copy any state objects that you are updating into new object references, starting with the entire state object.
const boardReducer = (state, action) => {
if (action.type === "TOGGLE") {
const { position } = action;
const nextState = {
...state,
userSelection: state.userSelection.map((rowEl, row) =>
row === position.row
? rowEl.map((colEl, col) =>
col === position.column ? !colEl : colEl
)
: rowEl
)
};
nextState.isComplete =
JSON.stringify(nextState.userSelection) ===
JSON.stringify(state.puzzleArray);
return nextState;
}
return state;
};
Create an action creator, which is really just a function that returns an action object.
const togglePosition = position => ({
type: "TOGGLE",
position
});
Then the handleToggle should consume/pass the row and column position in a dispatched action.
const handleToggle = () => dispatch(togglePosition(position));
Simple Demo
Demo Code:
const puzzleArray = [
[true, false, true],
[false, true, false],
[true, false, true]
];
const userSelection = Array(3).fill(Array(3).fill(false));
const togglePosition = (row, column) => ({
type: "TOGGLE",
position: { row, column }
});
const boardReducer = (state, action) => {
if (action.type === "TOGGLE") {
const { position } = action;
const nextState = {
...state,
userSelection: state.userSelection.map((rowEl, row) =>
row === position.row
? rowEl.map((colEl, col) =>
col === position.column ? !colEl : colEl
)
: rowEl
)
};
nextState.isComplete =
JSON.stringify(nextState.userSelection) ===
JSON.stringify(state.puzzleArray);
return nextState;
}
return state;
};
export default function App() {
const [boardState, dispatch] = React.useReducer(boardReducer, {
puzzleArray,
userSelection,
isComplete: false
});
const handleClick = (row, column) => () =>
dispatch(togglePosition(row, column));
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>{boardState.isComplete ? "Congratulations!" : "Keep Trying"}</div>
<div>
{boardState.userSelection.map((row, r) => (
<div key={r}>
{row.map((col, c) => (
<span
key={c}
className={classnames("square", { active: col })}
onClick={handleClick(r, c)}
/>
))}
</div>
))}
</div>
</div>
);
}

Categories

Resources