Why my nested recursive modal mounting each time? - javascript

I am creating a side hierarchy for controlling all my Modal usage in the app
import ReactNativeModal, { ModalProps } from 'react-native-modal';
export const ModalsProvider = ({ children }: { children: ReactNode }) => {
const [modals, setModals] = useState<IModal[]>([]);
const [modalsMap, setModalsMap] = useState({});
const isModalsExist = useMemo<boolean>(() => !!modals.length, [!!modals.length]);
useEffect(() => {
console.log({ isModalsExist });
}, [isModalsExist]);
const MoadlComponent: ComponentType<{ idx: number }> = ({ idx }) => {
const isNextExist = useMemo<boolean>(
() => modals.length - 1 > idx,
[modals.length - 1 > idx]
);
const { name, modalProps, props } = useMemo<IModal>(() => modals[idx], [idx]);
const MyModal = useMemo<ComponentType>(() => modalsMap[name], [name]);
useEffect(() => {
console.log({ idx });
}, []);
return (
<ReactNativeModal {...modalProps}>
<MyModal {...props} />
{isNextExist && <MoadlComponent idx={idx + 1} />}
</ReactNativeModal>
);
};
return (
<modalsContext.Provider value={{ registerModals, open, close, closeAll }}>
{children}
{isModalsExist && <MoadlComponent idx={0} />}
</modalsContext.Provider>
);
};
each time I open another modal,
useEffect(() => {
console.log({ idx });
}, []);
runs inside all nested from top to bottom (e.g.
{ idx : 2 }
{ idx : 1 }
{ idx : 0 }
), and of course, inside component logs run as well, which create an unnecessary heavy calculation
The weird thing is that my first and main suspect, isModalsExist, isn't the trigger as
useEffect(() => {
console.log({ isModalsExist });
}, [isModalsExist]);
runs only if the boolean indeed change, and anyhow if it was, I expecting the logs to be from bottom to top, not the opposite.
Any help appreciated, can't figure what I totally missing

You don't want to put the MoadlComponent into the ModalsProvider's body without wrapping it into a useEffect, because it reassigns a completely new function component each time the ModalsProvider re-renders.

Related

How to check which functional React Component has been viewed in the viewport latest?

My goal is to make it so I know which video the user has seen in the viewport latest. This was working until I turned the videos into functional React components, which I can't figure out how to check the ref until after the inital render of the React parent. This is currently the top part of the component:
function App() {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);
function useIsInViewport(ref) {
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useMemo(
() =>
new IntersectionObserver(([entry]) =>
setIsIntersecting(entry.isIntersecting)
),
[]
);
useEffect(() => {
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref, observer]);
return isIntersecting;
}
var videoProxy = new Proxy(videoViewports, {
set: function (target, key, value) {
// console.log("value " + value)
// console.log("key " + key)
console.log(videoViewports);
if (value) {
setMostRecentVideo(key);
//console.log("Most Rec: " + mostRecentVideo);
}
target[key] = value;
return true;
},
});
const [isGlobalMute, setIsGlobalMute] = useState(true);
const [mostRecentVideo, setMostRecentVideo] = useState("");
videoProxy["Podcast 1"] = useIsInViewport(ref1);
videoProxy["Podcast 2"] = useIsInViewport(ref2);
videoProxy["Podcast 3"] = useIsInViewport(ref3);
And each component looks like this:
<VideoContainer
ref={ref1}
videoProxy={videoProxy}
mostRecentVideo={mostRecentVideo}
setMostRecentVideo={setMostRecentVideo}
title="Podcast 1"
isGlobalMute={isGlobalMute}
setIsGlobalMute={setIsGlobalMute}
videoSource={video1}
podcastName={podcastName}
networkName={networkName}
episodeName={episodeName}
episodeDescription={episodeDescription}
logo={takeLogo}
muteIcon={muteIcon}
unmuteIcon={unmuteIcon}
></VideoContainer>
I had moved the logic for checking if the component was in the viewport into each component, but then it was impossible to check which component was the LATEST to move into viewport. I tried looking online and I don't understand how I would forward a ref here, or how to get the useIsInViewport to only start working after the initial render since it can't be wrapped in a useEffect(() => {}, []) hook. Maybe I'm doing this completely the wrong way with the wrong React Hooks, but I've been bashing my head against this for so long...
First of all: I'm not quite sure, if a Proxy.set is the right way of accomplishing your goal (depends on your overall app architecture). Because setting data does not always mean, the user has really seen the video or is in the viewport.
I've created a simple solution that uses two components. First the a VideoList that contains all videos and manages the viewport calculations so you don't have thousands of event listeners on resize, scroll and so on (or Observers respectively).
The Video component is a forwardRef component, so we get the ref of the rendered HTML video element (or in the case of this example, the encompassing div).
import { forwardRef, useCallback, useEffect, useState, createRef } from "react";
function inViewport(el) {
if (!el) {
return false;
}
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
const Video = forwardRef((props, ref) => {
return (
<div ref={ref}>
<p>{props.source}</p>
<video {...props} />
</div>
);
});
const VideoList = ({ sources }) => {
const sourcesLength = sources.length;
const [refs, setRefs] = useState([]);
useEffect(() => {
// set refs
setRefs((r) =>
Array(sources.length)
.fill()
.map((_, i) => refs[i] || createRef())
);
}, [sourcesLength]);
const isInViewport = useCallback(() => {
// this returns only the first but you can also apply a `filter` instead of the index
const videoIndex = refs.findIndex((ref) => {
return inViewport(ref.current);
});
if (videoIndex < 0) {
return;
}
console.log(`lastSeen video is ${sources[videoIndex]}`);
}, [refs, sources]);
useEffect(() => {
// add more listeners like resize, or use observer
document.addEventListener("scroll", isInViewport);
document.addEventListener("resize", isInViewport);
return () => {
document.removeEventListener("scroll", isInViewport);
document.removeEventListener("resize", isInViewport);
};
}, [isInViewport]);
return (
<div>
{sources.map((source, i) => {
return <Video ref={refs[i]} source={source} key={i} />;
})}
</div>
);
};
export default function App() {
const sources = ["/url/to/video1.mp4", "/url/to/video1.mp4"];
return (
<div className="App">
<VideoList sources={sources} />
</div>
);
}
Working example that should lead you into the right directions: https://codesandbox.io/s/distracted-waterfall-go6g7w?file=/src/App.js:0-1918
Please go over to https://stackoverflow.com/a/54633947/1893976 to see, why I'm using a useState for the ref list.

Prevent component Rerendening UseState Array - ReactJs

Every time I add or delete a component the whole page reloads, how do I prevent it?
When I use the HandleClick or the handleRemoveItem, the page reset and its components.
export default function Home() {
const [PostArray, setPostArray] = useState<React.ReactNode[]>([]);
let component = <PostSheet />;
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
event.preventDefault();
if (PostArray.length < 8) {
setPostArray((OldArray: any) => [...OldArray, component]);
}
}
const handleRemoveItem = useCallback(
(todo: any) => {
let newPostArray = [...PostArray];
newPostArray.splice(PostArray.indexOf(todo), 1);
setPostArray(newPostArray);
},
[PostArray]
);
const MappedArray = PostArray.map((e) => (
<PostSheet key={uuid()} click={handleRemoveItem} />
));
return (
<DefaultLayout click={handleClick}>
<Wrapper>{MappedArray}</Wrapper>
</DefaultLayout>
);
}

React: Warning: Each child in a list should have a unique "key" prop

I'm experiencing this warning with different components in my application but I will take just the following one as example.
I have got the following component. It has a map function that renders multiple components from an array:
const Clipboards = () => {
const { user, addClipboard } = useAuth();
const clipboards = user?.clipboards || [];
return (
<>
{clipboards.map(clipboard =>
<Clipboard clipboard={clipboard}/>
)}
<IconButton onClick={() => addClipboard()} color={'secondary'}>
<PlusIcon/>
</IconButton>
</>
)
};
export default Clipboards;
with Clipboard being as follows. As you can see, I use key prop in the wrapping div:
const Clipboard = ({clipboard, setSelectedClipboard}) => {
const {addClipboardRubric} = useAuth();
const {enqueueSnackbar} = useSnackbar();
const selectClip = () => {
setSelectedClipboard({id: clipboard.id, name: clipboard.name})
};
const [{isActive}, drop] = useDrop(() => ({
accept: 'rubric',
collect: (monitor) => ({
isActive: monitor.canDrop() && monitor.isOver(),
}),
drop(item, monitor) {
handleDrop(item)
},
}));
const handleDrop = async (rubric) => {
try {
await addClipboardRubric({
clipboardId: clipboard.id,
rubric: rubric
})
enqueueSnackbar('Rubric added', {
variant: 'success'
});
} catch (e) {
enqueueSnackbar('Rubric already added', {
variant: 'error'
});
}
}
console.log(`clip-${clipboard.id}`)
return (
<div ref={drop} key={clipboard.id}>
<IconButton
id={`clip-${clipboard.id}`}
onClick={selectClip}
color={'secondary'}
>
<Badge badgeContent={clipboard?.rubrics?.length || 0} color={"secondary"}>
<ClipboardIcon/>
</Badge>
</IconButton>
</div>
)
}
export default connect(
({search: {repertories = {}}}) => ({repertories}),
(dispatch) => ({
setSelectedClipboard: (payload) => dispatch(SET_SELECTED_CLIPBOARD(payload)),
})
)(Clipboard);
As you can see. I'm adding the key and it is unique.
What would be the problem then?
You have to add a unique key to every item returned from the map function. For example, you can use the index as a unique key which is not recommended but just to give you an example. In your code, you are adding a key inside the component. You need to add the key to the component itself. Check the below code.
const Clipboards = () => {
const { user, addClipboard } = useAuth();
const clipboards = user?.clipboards || [];
return (
<>
{clipboards.map((clipboard,index) =>
<Clipboard clipboard={clipboard} key={index}/>
)}
<IconButton onClick={() => addClipboard()} color={'secondary'}>
<PlusIcon/>
</IconButton>
</>
)
};
export default Clipboards;
You haven't added a key to the Clipboard component which is returned by the map.
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.
Source
Try something like this (if Clipboard as an id property):
{clipboards.map(clipboard =>
<Clipboard key={clipboard.id} clipboard={clipboard}/>
)}
Read this before using the index as a key

Is it faster to use React.memo if the areEqualFunction performs complex/a large number of comparisons?

Say I have the following code:
import React, { memo } from 'react';
const MyComponent = ({ arrayOfStuff }) => (
<div>
{arrayOfStuff.map(element => (
<p key={element.foo}>element.foo</p>
))}
</div>
);
const areEqual = (prevProps, nextProps) => {
const prevArrayOfStuff = prevProps.arrayOfStuff;
const nextArrayOfStuff = nextProps.arrayOfStuff;
if (prevArrayOfStuff.length !== nextArrayOfStuff.length)
return false;
for (let i; i < prevArrayOfStuff.length && i < nextArrayOfStuff.length; ++i) {
if (prevArrayOfStuff[i].foo !== nextArrayOfStuff[i].foo)
return false;
}
return true;
};
export default memo(MyComponent, areEqual);
And suppose arrayOfStuff is pretty large, maybe hundreds of elements. Am I really saving much time memoizing the component? I would think that in the event the props are the same, it would iterate over all the elements regardless of the memo since both areEqual and the render function do so.
The best answer to this is: Profile it and see. :-)
But although your array may have hundreds of entries in it, the checks you're doing aren't complex, they're quite straightforward and quick. (I'd add an if (prevArrayOfStuff === nextArrayOfStuff) { return true; } at the beginning.)
Some pros and cons:
Pros:
Your check is quite straightforward and fast, even for hundreds of elements.
If you find no changes, you save:
Creating a bunch of objects (the React "element"s returned by the component).
React having to compare the keys of the previous elements with the new ones to see if it needs to update the DOM.
Remember that your component will be called to re-render any time anything in its parent changes, even if those changes don't relate to your component.
Cons:
If there are change in the array often, you're just adding more work for no reward because areEqual is going to return false anyway.
There's ongoing maintenance cost to areEqual, and it presents opportunities for bugs.
So it really comes down to what changes in your overall app, and in particular the parents of your component. If those parents have state or props that change often but not related to your component, your component doing its check could save a lot of time.
Here's something demonstrating how your component will get called to re-render when something in its parent changed even though nothing in its props did:
Without memoizing it (React won't actually update DOM elements if nothing changes, but your function is called and creates the React elements that React compares to the rendered ones):
const {useState, useEffect} = React;
// A stand-in for your component
const Example = ({items}) => {
console.log("Example rendering");
return <div>
{items.map(item => <span key={item}>{item}</span>)}
</div>;
};
// Some other component
const Other = ({counter}) => {
console.log("Other rendering");
return <div>{counter}</div>;
};
// A parent component
const App = () => {
// This changes every tick of our interval timer
const [counter, setCounter] = useState(0);
// This changes only every three ticks
const [items, setItems] = useState([1, 2, 3]);
useEffect(() => {
const timer = setInterval(() => {
setCounter(c => {
c = c + 1;
if (c % 3 === 0) {
// Third tick, change `items`
setItems(items => [...items, items.length + 1]);
}
// Stop after 6 ticks
if (c === 6) {
setTimeout(() => {
console.log("Done");
}, 0);
clearInterval(timer);
}
return c;
});
}, 500);
return () => clearInterval(timer);
}, []);
return <div>
<Example items={items} />
<Other counter={counter} />
</div>;
};
ReactDOM.render(<App/>, document.getElementById("root"));
.as-console-wrapper {
max-height: 80% !important;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
With memoizing it:
const {useState, useEffect} = React;
// A stand-in for your component
const Example = ({items}) => {
console.log("Example rendering");
return <div>
{items.map(item => <span key={item}>{item}</span>)}
</div>;
};
const examplePropsAreEqual = ({items: prevItems}, {items: nextItems}) => {
const areEqual = (
prevItems === nextItems ||
(
prevItems.length === nextItems.length &&
prevItems.every((item, index) => item === nextItems[index])
)
);
if (areEqual) {
console.log("(skipped Example)");
}
return areEqual;
}
const ExampleMemoized = React.memo(Example, examplePropsAreEqual);
// Some other component
const Other = ({counter}) => {
console.log("Other rendering");
return <div>{counter}</div>;
};
// A parent component
const App = () => {
// This changes every tick of our interval timer
const [counter, setCounter] = useState(0);
// This changes only every three ticks
const [items, setItems] = useState([1, 2, 3]);
useEffect(() => {
const timer = setInterval(() => {
setCounter(c => {
c = c + 1;
if (c % 3 === 0) {
// Third tick, change `items`
setItems(items => [...items, items.length + 1]);
}
// Stop after 6 ticks
if (c === 6) {
setTimeout(() => {
console.log("Done");
}, 0);
clearInterval(timer);
}
return c;
});
}, 500);
return () => clearInterval(timer);
}, []);
return <div>
<ExampleMemoized items={items} />
<Other counter={counter} />
</div>;
};
ReactDOM.render(<App/>, document.getElementById("root"));
.as-console-wrapper {
max-height: 80% !important;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

How to I wrap a useState variable in a if statment, but still have it's value be available outside the if reactjs

I have the following code I have a cards state variable using useState, I have atttempted to add my array above to it, but it just adds an empty array, I wasn't able to put the state inside of the if becuase then my variable was undefined. I tried wrapping everything beflow the state and the state in the if , but the then I get some return issues. So the focus is passing into the useState(stateReplace)
Any help would be great
import React, { useState, useCallback, useEffect, useMemo } from "react";
import { Card } from "./Card";
import update from "immutability-helper";
import { LeadsBuilderCollection } from "../../api/LeadsCollection";
import { useTracker } from "meteor/react-meteor-data";
const style = {
width: 400,
};
export const Container = ({ params }) => {
const { leadsBuilder, isLoading } = useTracker(() => {
const noDataAvailable = { leadsBuilder: [] };
if (!Meteor.user()) {
return noDataAvailable;
}
const handler = Meteor.subscribe("leadsBuilder");
if (!handler.ready()) {
return { ...noDataAvailable, isLoading: true };
}
const leadsBuilder = LeadsBuilderCollection.findOne({ _id: params._id });
return { leadsBuilder };
});
const [cards, setCards] = useState([]);
let stateReplace = useMemo(() => {
if (!isLoading && leadsBuilder?.inputs?.length) {
leadsBuilder.inputs.map((leadInput, i) => {
({ id: i, text: leadInput.name });
});
}
return [];
}, [isLoading, leadsBuilder]);
useEffect(() => {
setCards(stateReplace);
}, [setCards, stateReplace]);
const moveCard = useCallback(
(dragIndex, hoverIndex) => {
const dragCard = cards[dragIndex];
setCards(
update(cards, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard],
],
})
);
},
[cards]
);
const renderCard = (card, index) => {
return (
<>
{isLoading ? (
<div className="loading">loading...</div>
) : (
<>
<Card
key={card.id}
index={index}
id={card.id}
text={card.text}
moveCard={moveCard}
/>
</>
)}
</>
);
};
return (
<>
{isLoading ? (
<div className="loading">loading...</div>
) : (
<>
<div style={style}>{cards.map((card, i) => renderCard(card, i))}</div>
</>
)}
</>
);
};
Update: I can get it to run if I place a setState in a useEffect but then I get a warning and the drag and drop doesnt work
useEffect(() => {
setCards(stateReplace);
});
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Update #2
const [cards, setCards] = useState([]);
let stateReplace = useMemo(() => {
console.log("memo");
if (!isLoading && leadsBuilder?.inputs?.length) {
return leadsBuilder.inputs.map((leadInput, i) => {
({ id: i, text: leadInput.name });
});
}
return [];
}, [isLoading]);
console.log(stateReplace);
useEffect(() => {
setCards(stateReplace);
console.log(setCards);
}, [setCards, stateReplace]);
current output
(4) [undefined, undefined, undefined, undefined]
memo
cannot read propery `id`
i would do it like that
//for preveting updates of memo if ledsBulder will changes on each render
const leadsBuilderRef = useRef(leadsBuilder)
let stateReplace = useMemo(()=>{
if (!isLoading && leadsBuilder.current?.inputs?.length) {
return leadsBuilder.current.inputs.map((leadInput, i) => {
return { id: i, text: leadInput.name };
});
}
return []
}, [isLoading, leadsBuilderRef]);
and then
useEffect(() => {
setCards(stateReplace);
}, [setCards, stateRepalce]);

Categories

Resources