React performance problems with increasing number of components - javascript

Turns out the problem was my computer. However, James made some good points about how to isolate the problem and utilizing useCallback and useMemo to optimize.
I am having problems with the performance of my react app. For now I'm excluding the code because I feel there might be some common sense answers.
This is the demo video
Here some pointers
I don't have unnecessary re-renders. Only individual components get rendered when they are hovered.
The animations are confined to a container div of the hovered element so no re-paints happen on the page outside of that container when hovering.
I am not using any heavy code for the hover effect or detection.
I am wondering what else could be cause for performance problems like this. As far as I understand, the number of components shouldn't matter if they are just sitting there, not rerendering.
Here is the code for the card component that is being animated. I wasn't quite sure whats important to show here. The parent component showing all the cards does not re-render.
export default function CardFile(props) {
// Input field
const input = useRef(null)
//Input state
const [inputActive, setInputActive] = useState(false);
const [title, setTitle] = useState(props.file.name)
const [menuActive, setMenuActive] = useState(false)
const [draggable, setDraggable] = useState(true)
const [isDragged, setIsDragged] = useState(false)
// counter > 0 = is hovered
const [dragCounter, setDragCounter] = useState(0)
//_________________ FUNCTIONS _________________//
// Handle file delete
const handleDelete = (e) => {
firebase.firestore().collection('users').doc(props.file.owner).collection('files').doc(props.file.id).delete().then(() => {
console.info('Deleted')
}).catch((err) => console.err(err))
}
// Prevent default if necessary
const preventDefault = (e) => {
e.preventDefault()
e.stopPropagation()
}
// Handle rename
const handleRename = (e) => {
e.stopPropagation()
setMenuActive(false)
setInputActive(true)
}
// Handle change
const handleChange = () => {
setTitle(input.current.value)
}
// Handle focus loss
const handleFocusLoss = (e) => {
e.stopPropagation()
setInputActive(false)
firebase.firestore().collection('users').doc(props.file.owner).collection('files').doc(props.file.id).update({ name: title })
.then(() => {
console.info('Updated title')
}).catch((err) => console.error(err))
}
// Handle title submit
const handleKeyPress = (e) => {
console.log('key')
if (e.code === "Enter") {
e.preventDefault();
setInputActive(false)
firebase.firestore().collection('users').doc(props.file.owner).collection('files').doc(props.file.id).update({ name: title })
.then(() => {
console.info('Submitted title')
}).catch((err) => console.error(err))
}
}
// Set input focus
useEffect(() => {
if (inputActive) {
input.current.focus()
input.current.select()
}
}, [inputActive])
//_____________________________DRAGGING___________________________//
//Handle drag start
const onDragStartFunctions = () => {
props.onDragStart(props.file.id)
setIsDragged(true)
}
// Handle drag enter
const handleDragEnter = (e) => {
// Only set as target if not equal to source
if (!isDragged) {
setDragCounter(dragCounter => dragCounter + 1)
}
}
//Handle drag end
const handleDragEnd = (e) => {
e.preventDefault()
setIsDragged(false)
}
// Handle drag exit
const handleDragLeave = () => {
// Only remove as target if not equal to source
if (!isDragged) {
setDragCounter(dragCounter => dragCounter - 1)
}
}
// Handle drag over
const handleDragOver = (e) => {
e.preventDefault()
}
// Handle drag drop
const onDragDropFunctions = (e) => {
setDragCounter(0)
// Only trigger when target if not equal to source
if (!isDragged) {
props.onDrop({
id: props.file.id,
display_type: 'file'
})
}
}
return (
<div
className={`${styles.card} ${dragCounter !== 0 && styles.is_hovered} ${isDragged && styles.is_dragged}`}
test={console.log('render')}
draggable={draggable}
onDragStart={onDragStartFunctions}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragLeave={handleDragLeave}
onDrop={onDragDropFunctions}
>
<div className={styles.cardInner}>
<div className={styles.videoContainer} onClick={() => props.handleActiveMedia(props.file, 'show')}>
{props.file.thumbnail_url && props.file.type === 'video' &&
<MdPlayCircleFilled className={styles.playButton} />
}
{!props.file.thumbnail_url && props.file.type === 'image' &&
<MdImage className={styles.processingButton} />
}
{!props.file.thumbnail_url && props.file.type === 'video' &&
<FaVideo className={styles.processingButton} />
}
<div className={styles.image} style={props.file.thumbnail_url && { backgroundImage: `url(${props.file.thumbnail_url})` }}></div>
</div>
<div className={styles.body}>
<div className={styles.main}>
{!inputActive ?
<p className={styles.title}>{title}</p>
:
<input
ref={input}
className={styles.titleInput}
type="text"
onKeyPress={handleKeyPress}
onChange={handleChange}
onBlur={handleFocusLoss}
defaultValue={title}
/>
}
</div>
<ToggleContext onClick={() => setMenuActive(prevMenuActive => !prevMenuActive)}>
{
menuActive && <div className={styles.menuBackground} />
}
<Dropdown top small active={menuActive}>
<ButtonLight title={'Rename'} icon={<MdTitle />} onClick={handleRename} />
<ButtonLight title={'Label'} icon={<MdLabel />} onClick={() => props.handleActiveMedia(props.file, 'label')} />
<ButtonLight title={'Share'} icon={<MdShare />} onClick={() => window.alert("Sharing is not yet supported. Stay put.")} />
{/*props.file.type === 'video' && <ButtonLight title={'Split'} icon={<RiScissorsFill />} />*/}
<ButtonLightConfirm
danger
title={'Delete'}
icon={<MdDelete />}
onClick={(e) => preventDefault(e)}
confirmAction={handleDelete}
preventDrag={() => setDraggable(false)}
enableDrag={() => setDraggable(true)}
/>
</Dropdown>
</ToggleContext>
</div>
</div>
</div>
);
}
And here is the css for animating it:
.is_hovered {
box-shadow: 0 0 0 3px var(--blue);
}
.is_hovered > div {
transform: scale(0.9);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.08);
transition: .1s;
}
Edit: Added code
Edit2: Updated sample video to show re-renders

The thing I think you should try first is 'memoizing' all your functions with useCallback. Especially as you're passing some of these functions down to other components, it's possible they are causing unnecessary re-rendering deeper in the DOM.
I don't know if you're familiar with useCallback, but basically it just wraps around your function, and only updates it when specific values change. This allows React to avoid re-creating it on every render and causing components deeper in the DOM to re-render.
You can read the docs here, but the gist of it is that instead of const getA = () => a you would write getA = useCallback(() => a, [a]), and the array contains all the dependencies for the function which cause it to update if changed.
Make sure you use these in your JSX, and avoid arrow functions like onClick={(e) => preventDefault(e)}. The function you have called preventDefault can even live outside the component entirely, since it makes no reference to anything specific to the component.
Try making these updates and see if it makes a difference. Also test without the console.log, since that can also slow things down.

Related

Child components get updated properly only after the second click in react functional component

I have the following component, where review assignments (props.peerReviewAssignmentIds) are loaded for a student's own work (related event is onClick_Submission) or a peer's work to review (related event is onClick_PeerReview ). These events work fine and the related data is loaded successfully. However, there is a problem with updating the content of the child components based on the value of the props.peerReviewAssignmentIds, which I elaborate below.
const AssignmentItem = (props) => {
const assignment = props.assignments[props.currentAssignmentId];
const onClick_Submission = (e) => {
e.preventDefault();
if (!st_showSubmission) {
props.fetchPeerReviewAssignmentForStudent(currentUserId, assignment.activeReviewRoundId);
}
set_showSubmission(!st_showSubmission);
set_isPeerReview(false);
}
const onClick_PeerReview = (e) => {
e.preventDefault();
if (!st_showPeerReviews) {
if (st_submissionContiues === false)
props.fetchPeerReviewAssignmentForReviewer(currentUserId, assignment.activeReviewRoundId);
}
set_showPeerReviews(!st_showPeerReviews);
set_isPeerReview(true);
}
return (
<>
{
st_showSubmission === true && props.peerReviewAssignmentIds.length > 0 &&
<ReviewPhaseInfoForSubmission isPeerReview={false} />
}
</>
)
}
const mapStateToProps = state => ({
peerReviewAssignmentIds: state.peerReviewAssignmentReducer.peerReviewAssignmentIds,
loading_pra: state.peerReviewAssignmentReducer.loading,
error_pra: state.peerReviewAssignmentReducer.error,
})
I will try to explain the problem with an example. When the first time onClick_Submission is triggered, props.peerReviewAssignmentIds[0] is set to 2, and all the sub components are loaded properly. Next, when onClick_PeerReview is triggered, props.peerReviewAssignmentIds[0] is set to 1, which is correct. But, the child components get updated according to the previous value of props.peerReviewAssignmentIds[0], which was 2. If the onClick_PeerReview event is triggered second time, then the child components get updated correctly according to the current value of props.peerReviewAssignmentIds[0], which is 1. Any ideas why this might be happening?
I further explain my code below.
Below is the ReviewPhaseInfoForSubmission component. In this component, based on props.peerReviewAssignmentIds[0] value (which gets updated in the parent component above) etherpadLaterSubmission variable is created with props.createSession_laterSubmission method.
const ReviewPhaseInfoForSubmission = (props) => {
const [st_is_discussFeedback, set_is_discussFeedback] = useState(false);
useEffect(() => {
set_is_discussFeedback(true);
if (props.etherpadLaterSubmission === null) {
props.createSession_laterSubmission(props.peerReviewAssignmentIds[0], discussFeedback.dueDate);
}
}, [])
return (
<div className="p-1">
{
st_is_discussFeedback === true &&
<ProvideOrDiscussFeedback provide0discuss1revise2={1} isPeerReview={props.isPeerReview} />
}
</div>
);
}
const mapStateToProps = (state) => ({
etherpadLaterSubmission: state.etherpadReducer.etherpadLaterSubmission,
loading_ep: state.etherpadReducer.loading,
})
Then, in a child component, ProvideOrDiscussFeedback (see below), the props.etherpadLaterSubmission value is used for display purposes.
const ProvideOrDiscussFeedback = (props) => {
return <div className="p-3 shadow">
{
props.etherpadLaterSubmission &&
<div>
<DisplayingEtherpad etherpadSession={props.etherpadLaterSubmission } />
</div>
}
</div>
}

ReactJS Chat Websockets emits message twice

I have a Chat component which works fine on the happy flow but when I go on another view (meaning I exit the chat component) and come back after that in the chat component, I get duplicates messages.
I placed an console.log into the function which is triggered by the enter event but it displays only once. However, the emit from inside trigger twice I think because on the server side (nodeJs) I get the data twice.
This is my code:
function Chat() {
let [chatInput, setChatInput] = useState('');
let [chatArea, setChatArea] = useState([]);
const handleOnChange = (e) => {
setChatInput(e.target.value)
}
useEffect(() => {
const addTextMessage = (event) => {
if (event.keyCode === 13 && chatInput.value !== '') {
event.preventDefault();
socket.emit('chat', {
message: chatInput.value
});
setChatInput('');
}
}
const chatInput = document.querySelector('.chat-input input');
chatInput.addEventListener("keyup", addTextMessage, false);
socket.on('chat', (data) => {
setChatArea(prevInputState => (
[...prevInputState, <section key={prevInputState.length}>{data.sender}: {data.message}</section>]
))
})
return () => {
chatInput.removeEventListener("keyup", addTextMessage, false);
socket.off('chat');
};
}, []);
return (
<React.Fragment>
<section className="chat-room">
<div className="chat-area">
{chatArea}
</div>
<div className="chat-input">
<input value={chatInput} onChange={handleOnChange} type="text" />
</div>
</section>
</React.Fragment>
);
}
Some issues I'm spotting:
chatInput is a string, It feels weird adding a event listener to a string. It might work.
<input> has a prop called onKeyUp that is probably very useful for you.
This might be much more simple and easier to find the issue if you still get duplicates:
const [userInput, setUserInput] = useState("")
const [chatMessages, setChatMessages] = useState([])
function trySendMessage(event) {
// Send message if last key was ENTER and we got some text
if (event.keyCode === 13 && userInput !== "") {
event.preventDefault();
socket.emit("chat", {
message: chatInput,
})
setChatInput("");
}
}
// This effect only changes the state
// Notice I'm not storing the rendered result in state,
// Instead I handle rendering below in the rendering section
useEffect(() => {
socket.on('chat', (newMessage) => setChatArea([...chatMessages, newMessage])
return () => socket.off('chat')
}, [])
return (
<section>
{
chatMessages.map(message => (
<div key={message.id}>{message.name}: {message.body})</div>
)
}
<input
value={userInput}
onChange={(e) => setUserInput(e.target.value)}
onKeyUp={trySendMessage}
/>
</section>
)
I found the problem. Contrary to what I was thinking, the issue was coming from my server side.
I have placed my nsSocket.on('chat', (data) => { by mistake inside another nsSocket.on(' and after I extracted it outside, the problems were fixed.

How to auto hide and show component in react native

In my home screen I want to auto hide my header in 2 seconds, then I will have a button to show the header when pressed. I have tried with HomeStack.Screen but could not achieve it, I have to create my custom header called HeaderHomeComponent.js and imported it on my homescreen, still I could not achieve it. Please I need help on this issue.
Here is my code:
const [showHeader, setShowHeader] = useState(true);
const onRecord = async () => {
if (isRecording) {
camera.current.stopRecording();
} else {
setTimeout(() => setIsRecording && camera.current.stopRecording(), 23*1000);
const data = await camera.current.recordAsync();
}
};
const visibility = () => {
setTimeout(() => setShowHeader(false), 2000);
}
return (
<View style={styles.container}>
<RNCamera
ref={camera}
type={cameraType}
flashMode={flashMode}
onRecordingStart={() => setIsRecording(true)}
onRecordingEnd={() => setIsRecording(false)}
style={styles.preview}
/>
<HeaderHomeComponent />
You can create a function with useeffect.
Make sure you passs show and handleClose functions from Parent. (Example given below).
const MessageBox = (props) => {
useEffect(() => {
if (props.show) {
setTimeout(() => {
props.handleClose(false);
}, 3000);
}
}, [props.show]);
return (
<div className={`messageBox ${props.show ? "show" : null}`}>
{props.message}
</div>
);
};
UseEffect will be called everytime props.show state will change. And we only want our timer to kick in when the show becomes true, so that we can hide it then.
Also, now to use this, it's simple, in any component.
const [showMessageBox, setShowMessageBox] = useState(false);
return(
<MessageBox
show={showMessageBox}
handleClose={setShowMessageBox} />
);
Also, make sure to handle css, part as well for show and hide.
Simple Example below.
.messageBox {
display: none;
}
.messageBox.show {
display: block;
}
Hope this helps, :-)
You need to do something like this as Mindaugas Nakrosis mentioned in comment
const [showHeader, setShowHeader] = useState(true);
useEffect(() => {
setTimeout(() => setShowHeader(false), 2000);
}, []);
In return where your header is present
{
showHeader && <HeaderHomeComponent/>;
}
I think the approach gonna fit "auto hide and show in 2 seconds", is using Animetad opacity, and giving fix height or/and z-index (as fit you) to the element
// HeaderHomeComponent.js
const animOpacity = useRef(new Animated.Value(1)).current // start with showing elem
//change main view to
<Animated.View
style={{ ...yourStyle... ,
opacity: animOpacity,
}}
>
and then for creating the animation somewhere
() => {
Animated.timing(animOpacity, {
toValue: +(!animOpacity), // the numeric value of not current
duration: 2000, // 2 secs
}).start();
}}
The hieraric location of the declaration of the ref should control usage as calling the effect. maybe you can create useEffect inside the header that can determine if it should be visible or not depends navigation or some other props.
hope its helpful!

Where to put javascript functionality when certain Redux actions are thrown?

I have the following Redux Reducer to handle the offset of an infinite scroll component:
const offset = handleActions(
{
[questionListTypes.ON_QUESTIONS_SCROLL]: state => state + QuestionsLoadChunkTotal,
[combineActions(questionListTypes.RESET_QUESTIONS_OFFSET)]: () => {
document.getElementById('question-list-infinite-scroll').scrollTop = 0;
return 0;
},
},
0,
);
When the offset of the component resets I want to scroll the HTML element to the top. I have added the following line in the reducer to handle this:
document.getElementById('question-list-infinite-scroll').scrollTop = 0;
This doesn't feel right to me to put it here because it has nothing to do with my state. Is there a better way to handle this situation?
You may use a Redux middleware, which purpose is to handle side effects.
It receives every action that goes through and enables us to have any side effect.
const scrollReseter = store => next => action => {
next(action);
if (action.type === combineActions(questionListTypes.RESET_QUESTIONS_OFFSET)) {
document.getElementById('question-list-infinite-scroll').scrollTop = 0;
}
}
See https://redux.js.org/advanced/middleware/
You can use a ref to get a reference to the DOM element and use an effect to manipulate that element when a certain value in the state changes.
Here is an example using local state:
const App = () => {
//this would be data that comes from state
// maybe with useSelector or with connect
const [scrollToTop, setScrollToTop] = React.useState(0);
//create a ref to the element you want to scroll
const scrollRef = React.useRef();
//this would be an action that would set scrollToTop with a new
// value
const goToTop = () => setScrollToTop((val) => val + 1);
//this is an effect that runs every time scrollToTop changes
// it will run on mount as well so when scrollToTop is 0 it
// does nothing
React.useEffect(() => {
if (scrollToTop) {
scrollRef.current.scrollTop = 0;
}
}, [scrollToTop]);
return (
<div
ref={scrollRef}
style={{ maxHeight: '250px', overflow: 'scroll' }}
>
{[...new Array(10)].map((_, key) => (
<h1
key={key}
onClick={goToTop}
style={{ cursor: 'pointer' }}
>
click to scroll to top
</h1>
))}
</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>

Click Down/Up keys for scrolling inside ul element- React

I got a problem trying to make my own Autocomplete component in react..
I created a component which render the results and set max-height on their wrapper,
I used an onKeyDown event on the input element to track down/up key press. Right now I use it to mark the active item... but when the max-height I set is too small and there is a scroll in the side when the "active-item" go off the div's height limit the scroll doesn't go down with it... How can I fix it?
import React, { useState, useEffect } from "react"
const Autocomplete = ({ options }) => {
const [activeOption, setActiveOption] = useState(4)
const [filteredOptions, setFilteredOptions] = useState([])
const [showOptions, setShowOptions] = useState(false)
const [userInput, setUserInput] = useState("")
useEffect(() => {
setShowOptions(userInput)
setFilteredOptions([
...options.filter(
option => option.toLowerCase().indexOf(userInput.toLowerCase()) > -1
)
])
setActiveOption(0)
}, [userInput])
const handleKeyDown = e => {
if (e.key === "ArrowDown") {
if (activeOption === filteredOptions.length - 1) return
setActiveOption(activeOption + 1)
}
if (e.key === "ArrowUp") {
if (activeOption === 0) return
setActiveOption(activeOption - 1)
}
}
return (
<>
<div className="search">
<input
type="text"
className="search-box"
value={userInput}
onChange={e => setUserInput(e.target.value)}
onKeyDown={handleKeyDown}
/>
<ul className="options">
{showOptions &&
filteredOptions.map((option, i) => (
<li className={activeOption === i ? `option-active` : ``} key={i}>
{option}
</li>
))}
</ul>
</div>
</>
)
}
export default Autocomplete
function App() {
return (
<div className="App">
<Autocomplete
options={[
"Alligator",
"Bask",
"Crocodilian",
"Death Roll",
"Eggs",
"Jaws",
"Reptile",
"Solitary",
"Tail",
"Wetlands"
]}
/>
</div>
)
}
.option-active {
font-weight: bold;
background: cornflowerblue;
}
.options {
height: 100px;
overflow: overlay;
}
Here's a picture to explain better my problem:
when second item is active:
when the sixth is active:
As you can see the scroll stays the same and doesn't go down with the li element...
Thanks by heart!
I think you could maintain an array of element refs, in order to call scrollIntoView method of that element, when active option changes.
Untested code:
// Use a ref container (we won't use `current`)
const elmRefs = useRef()
// Instead we build a custom ref object in each key of the ref for each option
useEffect(() => {
options.forEach(opt => elmRefs[opt] = {current: null}
}, [options])
// Effect that scrolls active element when it changes
useLayoutEffect(() => {
// This is what makes element visible
elmRefs[options[activeOption]].current.scrollIntoView()
}, [options, activeOption])
// In the "render" section, connect each option to elmRefs
<li className={activeOption === i ? `option-active` : ``} ref={elmRefs[option]} key={i}>
{option}
</li>
Let me know what you think!
Edit:
If the elmRefs need to be initialized imediately, you could do:
// Outside component
const initRefs = options => Object.fromEntries(options.map(o => [o, {current: null}]))
// Then in the component, replace useRef by:
const elmRefs = useRef(initRef(options))
And replace elmRefs by elmRefs.current in the rest of the code...

Categories