Too many React components re-rendering, how to limit it? - javascript

I am generating a list of components on the screen like so:
const MessagesContainer = ({ messages, categories, addHandler }) => {
const options = categories.map(category => (
{ value: category.name, label: category.name }
));
return (
<div className="d-flex flex-wrap justify-content-center">
{messages.map(message =>
<div key={message.id}>
<MessageEditor
message={message}
options={options}
addHandler={addHandler}
/>
</div>
)}
</div>
);
};
const MessageEditor = ({ message, options, addHandler }) => {
const [modifedMessage, setModifiedMessage] = useState(message);
const [isAdded, setIsAdded] = useState(false);
const textClass = (charLimit - modifedMessage.text.length) > 0 ?
'text-success' : 'text-danger';
const buttonClass = isAdded ? 'danger' : 'primary';
const ref = useRef(null);
const textAreaHandler = textArea => {
const copiedMessage = { ...modifedMessage };
copiedMessage.text = textArea.target.value;
setModifiedMessage(copiedMessage);
};
const addButtonHandler = () => {
const add = !isAdded;
setIsAdded(add);
let selectedCategoires = ref.current.state.value;
// Firing this handler results in ALL the MessageEditor
// componets on the screen being re-rendered
addHandler(modifedMessage, add, selectedCategoires);
}
return (
<div className="d-flex flex-column message-view-container ml-5 mr-5 mb-5">
<div className={`message-count-container ${textClass}`}>
{charLimit - modifedMessage.text.length}
</div>
<Select
ref={ref}
placeholder="Tags"
isMulti
name="tags"
options={options}
defaultValue={[options[0]]}
className="basic-multi-select select-container"
classNamePrefix="select"
isDisabled={isAdded}
/>
<Form.Control
style={{
width:350,
height:220,
resize:'none'
}}
className="mb-1"
as="textarea"
defaultValue={message.text}
onChange={textAreaHandler}
disabled={isAdded}
/>
<Button variant={buttonClass} onClick={addButtonHandler}>
{isAdded ? 'Remove' : 'Add'}
</Button>
</div>
);
};
And the parent component that holds the addHandler:
const { useState } = require("react");
const Messages = () => {
const [messages, setMessages] = useState([]);
const [saveMessages, setSaveMessages] = useState({});
const addHandler = (modifiedMessage, add, selectedCategoires) => {
const copiedSaveMessages = { ...saveMessages };
if (add) {
if (selectedCategoires) {
selectedCategoires = selectedCategoires.map(item => item.value);
}
copiedSaveMessages[modifiedMessage.id] = {
text: modifiedMessage.text,
tags: selectedCategoires ? selectedCategoires : []
}
} else {
delete copiedSaveMessages[modifiedMessage.id];
}
// This results in every single MessageEditor component being
// re-rendered
setSaveMessages(copiedSaveMessages);
};
return (
<div>
{categories &&
<div>
<div className="ml-5 mr-5 mt-5">
<MessagesContainer
messages={messages}
categories={categories}
addHandler={addHandler}
/>
</div>
</div>
}
{Object.keys(saveMessages).length > 0 &&
<div>
<Image
className="upload-icon"
src={uploadIcon}
/>
<div className="text-primary count-container">
<h2>{Object.keys(saveMessages).length}</h2>
</div>
</div>
}
</div>
);
};
The issue is that if I hit the add button an trigger addHandler it causes all the MessageEditor components to re-render. And the performance is very slow if I have a few hundred components on the screen.
I guess this is because the saveMessages state variable belongs to the Messages component and MessageEditor is a child of Messages so it also re-renders.
Is there an approach I can take to update this state without causing all the other components to re-render?

In Messages you should wrap your addHandler in a useCallback hook (React useCallback hook) so that it is not re-created at each render.
const addHandler = useCallback((modifiedMessage, add, selectedCategoires) => {
// function body...
}, []);
Additionally, you can also memoize MessageEditor using React.memo() (React memo).
const MessageEditor = React.memo(({ message, options, addHandler }) => {
// component body...
});

Related

How to pass a value from child to parent component in reactjs

I had a child component UploadImage.js and parent component Parent.js. I am uploading an image and want to pass the value of file name to the Parent.js component. How can I do so?
UploadImage.js
import React, { useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
.
.
.
const UploadImage = () => {
const [files, setFiles] = useState([]);
const { getRootProps, getInputProps } = useDropzone({
accept: {
'image/*': []
},
onDrop: acceptedFiles => {
setFiles(acceptedFiles.map(file => Object.assign(file, {
preview: URL.createObjectURL(file)
})));
}
});
//preview component
const thumbs = files.map(file => (
<div style={thumb} className="d-flex flex-row mt-1 col-12 mx-auto" key={file.name}>
<div style={thumbInner}>
<img
src={file.preview}
style={img}
// Revoke data uri after image is loaded
onLoad={() => { URL.revokeObjectURL(file.preview) }}
/>
</div>
</div>
)
);
//wanted to pass file[0].name to Parent Component
console.log(files.length > 0 ? files[0].name : "")
useEffect(() => {
// Make sure to revoke the data uris to avoid memory leaks, will run on unmount
return () => files.forEach(file => URL.revokeObjectURL(file.preview));
}, []);
return (
<section className="container">
<div {...getRootProps({ className: 'dropzone mx-3 text-center mt-4 mb-2 p-3 bg-light border border-primary border-1 rounded-4 ' })}>
<input {...getInputProps()} />
<p className='fw-bold text-primary'>Drag 'n' drop some files here, or click to select files</p>
</div>
<aside style={thumbsContainer} className="d-flex flex-row">
{thumbs}
</aside>
</section>
);
}
export default UploadImage;
And my Parent component is like this
import React, { useState} from "react";
import UploadImage from "../components/uploadImage";
const Parent = () => {
const [uploadFileName, setUploadFileName] = useState("");
return (
<div className="mx-3 mt-4 mb-2">
<UploadImage />
<h3 className="m-3">{uploadFileName} </h3>
</div>
);
};
export default UploadButton;
How can I display the file name from UploadImage.js to Parent.js in the uploadFileName state ???
you create a function in your parent element like:
const NameSetter = imageName => {
setUploadFileName(imageName);
}
and then send the NameSetter as a prop to your child element like:
<UploadImage nameHandler={NameSetter} />
and then in your child element you call the nameHandler prop like:
(call this when you get the name, for ex: on the callback of your backend )
props.nameHandler('name of your image');
you can use call back props to update the children to parent.
import React, { useState} from "react";
import UploadImage from "../components/uploadImage";
const Parent = () => {
const [uploadFileName, setUploadFileName] = useState("");
return (
<div className="mx-3 mt-4 mb-2">
<UploadImage setUploadFileName={setUploadFileName}/>
<h3 className="m-3">{uploadFileName} </h3>
</div>
);
};
export default UploadButton;
Then you can set whereever you want to call in child it will update in parent component. You can check through by adding consoling on the parent component.
Hey MagnusEffect you're almost correct, just make these changes-
In UploadImage.js-
const UploadImage = ({setUploadFileName}) => {
<input {...getInputProps()} onChange=
{(e)=>setUploadFileName(e.target.files[0].name)} />
}
While in Parent Component just pass setvalues-
const Parent = () => {
const [uploadFileName, setUploadFileName] = useState("");
return (
<div className="mx-3 mt-4 mb-2">
<UploadImage setUploadFileName={setUploadFileName} />
<h3 className="m-3">{uploadFileName} </h3>
</div>
);
}
Hope this code will help to solve your query if you still facing issue, just lemme know i will help you more. Thanks
You should move const [files, setFiles] = useState([]); to Parents.js and then pass them by Props for UploadImage.js.
// UploadImage Component
const UploadImage = (props) => {
const {files, onUpdateFiles} = props;
const { getRootProps, getInputProps } = useDropzone({
accept: {
'image/*': []
},
onDrop: acceptedFiles => {
onUpdateFiles(acceptedFiles.map(file => Object.assign(file, {
preview: URL.createObjectURL(file)
})));
}
});
...
}
// Parents component
const Parent = () => {
const [files, setFiles] = useState([]);
return (
<div className="mx-3 mt-4 mb-2">
<UploadImage files={files} onUpdateFiles={setFiles} />
{files.length > 0 && <h3 className="m-3">{files[0].name}</h3>}
</div>
);
};

react update state from children broke component at the same level

I am new to react. I'm trying to update the parent state from the child but i have an error on another component at the the same level of the child one.
that's my code.
RedirectPage.js (parent)
const RedirectPage = (props) => {
const [status, setStatus] = useState("Loading");
const [weather, setWeather] = useState(null);
const [location, setLocation] = useState(null)
const [showLoader, setShowLoader] = useState(true)
const [userId, setUserId] = useState(false)
const [isPlaylistCreated, setIsPlaylistCreated] = useState(false)
const headers = getParamValues(props.location.hash)
const getWeather = () =>{
//fetch data..
//...
//...
.then(response => {
var res = response.json();
return res;
})
.then(result => {
setWeather(result)
setShowLoader(false)
setStatus(null)
setLocation(result.name)
});
})
}
const changeStateFromChild = (value) => {
setIsPlaylistCreated(value)
}
useEffect(() => {
getWeather()
},[]);
return (
<div className="containerRedirectPage">
{showLoader ? (
<div className="wrapperLogo">
<img src={loader}className="" alt="logo" />
</div>)
: (
<div className="wrapperColonne">
<div className="firstRow">
<WeatherCard weatherConditions={weather}/>
</div>
{isPlaylistCreated ? (
<div className="secondRow">
<PlaylistCard />
</div>
) : (
<PlaylistButton userId={userId} headers={headers} weatherInfo={weather} playlistCreated={changeStateFromChild} />
)}
</div>
)}
</div>
)
};
export default RedirectPage;
PlaylistButton.js:
export default function PlaylistButton({userId, headers, weatherInfo, playlistCreated}) {
const buttonClicked = async () => {
// ...some code...
playlistCreated(true)
}
return (
<div className="button-container-1">
<span className="mas">CREA PLAYLIST</span>
<button onClick={buttonClicked} id='work' type="button" name="Hover">CREA PLAYLIST</button>
</div>
)
}
and that's the other component i'm getting the error when i click on button.
WeatherCard.js:
const WeatherCard = ({weatherConditions}) => {
const [weather, setWeather] = useState(null);
const [icon, setIcon] = useState(null);
const getTheIcon = () => {
// code to get the right icon
}
setIcon(x)
}
useEffect(() => {
getTheIcon()
},[]);
return (
<div className="weatherCard">
<div className="headerCard">
<h2>{weatherConditions.name}</h2>
<h3>{Math.floor(weatherConditions.main.temp)}°C</h3>
</div>
<div className="bodyCard">
<h5>{weatherConditions.weather[0].description}</h5>
<img className="weatherIcon" src={icon} alt="aa" />
</div>
</div>
)
};
export default WeatherCard;
the first time i load the redirect page WeatherCard component is right. When i click the button i get this error:
error
Can someone explain me why ?
What is the effect of the setting playlistCreated(true) ?
Does it affects the weatherCondition object ?
If weatherCondition could be undefined at some point you need to check it before using its properties (name, main.temp, and weather)
Update:
The error clearly state that it cannot read name from weather because it's undefined. You have to check it before using the weather object properties.
if (!weatherConditions) {
return <div>Loading...</div> // or something appropriate.
}
return (
<div className="weatherCard">
<div className="headerCard">
<h2>{weatherConditions.name}</h2>
{weatherConditions.main && <h3>{Math.floor(weatherConditions.main.temp)}°C</h3>}
</div>
<div className="bodyCard">
{weatherConditions.weather &&
{weatherConditions.weather.length > 0 &&
<h5>{weatherConditions.weather[0].description}</h5>}
....
)

React "is not a function" problem in my todo list program

I'm new to react and doing a todo list tutorial. I decided to add an "updateTask" function myself.
When you click the edit button, it turns the task text into an input field and it grabs the value of that input field along with the ID and runs "updateTask" function in App.js
It's basically the same code as "handleToggler" in App.Js (right under updateTask) which updates the state of the checkbox being checked or not.
However, I am getting an error that says "const tasksLeft = tadoState.filter(tado => !tado.completed) is not a function. This is right above the return statement in app.js and it is unrelated to the change I made so I've kind of been stuck.
I console.logged tadoState and a bunch of other stuff I changed to try and see where i messed up but it seems like the state is indeed changing just like I want it to. So I don't know what is causing the error.
APP.js
import Tasklist from './Tasklist.js';
import './index.css';
import { useState, useRef, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
function App() {
// State of Task List
const [tadoState, setTadostate] = useState([]);
const LOCAL_STORAGE_KEY = 'tadoApp.tados'
// GET Local Storage and setState
useEffect(() => {
const tadoStateLS = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if (tadoStateLS) setTadostate(tadoStateLS)
},[])
// SAVE to Local Storage when there is a change to tadoState
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(tadoState))
}, [tadoState])
// Reference to Input Value
const addTodoRef = useRef();
// Add new Task
const addNewTadoHandler = (ref) => {
// Grab value from input field
const tadoValue = ref.current.value;
// Check for user input
if (tadoValue === '') return
// setState
setTadostate(prevTadoState => {
return [...prevTadoState, { id: uuidv4(), name: tadoValue, completed: false }]
})
ref.current.value = null;
}
// Update Existing Task
const updateTask = (id, value) => {
const newTadoState = [...tadoState]
const updatedTask = newTadoState.find(tado => tado.id === id)
updatedTask.name = value
setTadostate(updatedTask)
}
// Enter to Add
const enterToAddHandler = (e) => {
if (e.key === "Enter") {
addNewTadoHandler(addTodoRef);
}
}
// Checkbox Toggler Function
const handleToggler = (id) => {
// Make sure to clone state and then modify. Spread original state. Find item, toggle, and then setStates
const newTadoState = [...tadoState];
const tadoToggle = newTadoState.find(tado => tado.id === id)
tadoToggle.completed = !tadoToggle.completed;
setTadostate(newTadoState);
}
// Clear Completed Tasks
const clearTasksHandler = () => {
const newTadoState = tadoState.filter(tado => tado.completed === false);
setTadostate(newTadoState);
}
const clearAllTasksHandler = () => {
setTadostate([]);
}
const clearTaskHandler = (id) => {
const newTadoState = tadoState.filter(tado => tado.id !== id);
setTadostate(newTadoState);
}
// # of tasks left
const tasksLeft = tadoState.filter(tado => !tado.completed);
return (
<>
<div className="container-sm" style={{textAlign:'center', marginTop:"100px"}}>
<div className="row">
<div className="col-md-6 m-auto">
<h1 className="fw-bolder">TADOSKY</h1>
<p className="fw-light">All Tasks</p>
<div className="container">
<div className="row align-items-center">
{/* INPUT FIELD */}
<div className="col-10">
<input onKeyDown={enterToAddHandler} ref={addTodoRef} type="text" className="form-control" style={{width:"107%"}} />
</div>
{/* ADD BUTTON */}
<div className="col-2">
<button onClick={() => addNewTadoHandler(addTodoRef)} className="btn bg-black text-white fw-bold m-2">+</button>
</div>
</div>
</div>
{/* CLEAR COMPLETED BUTTON */}
<button onClick={clearTasksHandler} className="btn btn-outline-dark fw-lighter m-2 px-3">Clear Completed Tasks</button>
{/* CLEAR ALL */}
<button onClick={clearAllTasksHandler} className="btn btn-purple text-white m-2">Clear All</button>
{/* # of Tasks left */}
<p className="mt-3 ">{tasksLeft.length} Tasks tado!</p>
{/* TASKLIST */}
<Tasklist tadoState={tadoState} updateTask={updateTask} handleToggler={handleToggler} clearTaskHandler={clearTaskHandler} />
</div>
</div>
</div>
</>
);
}
export default App;
TadoInput.js
import React, { useState, useEffect, useRef } from 'react'
export default function TadoInput({tado, editableState, updateTask}) {
const editRef = useRef()
const newInputHandler = (e) => {
const value = editRef.current.value
if (e.key === 'Enter') {
updateTask(tado.id, value)
}
}
if (editableState === false) {
return (
<div className="card-title">
{tado.name}
</div>
)
} else {
return (
<div className="card-title">
<input onKeyDown={newInputHandler} ref={editRef} type="text" style={{width: "300px"}} />
</div>
)
}
}
Is there anything wrong in the code you can spot that I'm missing?

How to test prop function that changes other prop Jest Enzyme

I have a component that receives value 'openDrawer' (bool) and function 'toggleDrawerHandler' in props, the function 'toggleDrawerHandler' changes the value of 'openDrawer' prop.
I would like to test it by simulating a click on div that triggers this function, and check if the component change when the value of 'openDrawer' changes.
The component
const NavigationMobile = (props) => {
const { openDrawer, toggleDrawerHandler } = props;
let navClass = ["Nav-Mobile"];
if (!openDrawer) navClass.push("Close");
return (
<div className="Mobile">
<div className="Menubar" onClick={toggleDrawerHandler}>
{openDrawer ? <FaTimes size="1.5rem" /> : <FaBars size="1.5rem" />}
</div>
<nav className={navClass.join(" ")} onClick={toggleDrawerHandler}>
<Navigation />
</nav>
</div>
);
};
The component that sends these props
const Header = (props) => {
const [openDrawer, setOpenDrawer] = useState(false);
const toggleDrawerHandler = () => {
setOpenDrawer((prevState) => !prevState);
};
return (
<header className="Header">
<NavigationMobile openDrawer={openDrawer} toggleDrawerHandler={toggleDrawerHandler} />
</header>
);
};
my test, but doesn't work
it("changes prop openDrawer when click", () => {
const wrapper = shallow(<NavigationMobile />);
expect(wrapper.find("FaBars")).toHaveLength(1);
expect(wrapper.find("nav").hasClass("Nav-Mobile")).toBeTruthy();
wrapper.find(".Menubar").simulate("click", true); // doesnt work
expect(wrapper.find("FaTimes")).toHaveLength(1);
expect(wrapper.find("nav").hasClass("Nav-Mobile Close")).toBeTruthy();
});

react not rerendering after state change

I know there have been similar questions, but I have a weird issue.
This is what I'm doing
import React, {useState} from 'react';
import './App.css';
import {Table, Button, InputGroup, FormControl} from 'react-bootstrap';
function App() {
const [pons, setPons] = useState();
const [translations, setTranslations] = useState([]);
const [isInEditMode, setIsInEditMode] = useState(false);
const [inputValue, setInputValue] = useState('samochod');
const [errors, setErrors] = useState([]);
const [translationsToSave, setTranslationsToSave] = useState([]);
const changeIsInEditMode = () => setIsInEditMode(!isInEditMode);
const handleEditButtonClick = (id) => console.log('Edit', id);
const handleDeleteButtonClick = (id) => console.log('Delete', id);
const handleInputChange = (e) => setInputValue(e.target.value);
const handleFetchOnButtonClick = async () => {
const resp = await fetch(`http://localhost:8080/pons/findTranslation/${inputValue}`).then(r => r.json()).catch(e => console.log(e));
if (resp.ok === true) {
setTranslations(resp.resp[0].hits);
setErrors([]);
} else {
setErrors(resp.errors ? resp.errors : ['Something went wrong. check the input']);
}
};
const handleSaveTranslations = async () => {
const resp = await fetch('localhost:8080/pons/', {method: 'POST', body: {content: translationsToSave}});
if (resp.ok === true) {
setInputValue('');
setTranslations(null);
}
};
return (
<div className="App">
{errors.length > 0 ? errors.map(e => <div key={e}>{e}</div>) : null}
<InputGroup className="mb-3">
<FormControl
value={inputValue}
onChange={handleInputChange}
placeholder={inputValue}
/>
</InputGroup>
<div className="mb-3">
<Button onClick={handleFetchOnButtonClick} disabled={inputValue === '' || errors.length > 0}>Translate</Button>
<Button onClick={changeIsInEditMode}>
{isInEditMode ? 'Exit edit mode' : 'Enter edit mode'}
</Button>
<Button disabled={translationsToSave.length === 0} onClick={handleSaveTranslations}>Save translations</Button>
</div>
<Table striped bordered hover>
<thead>
<tr>
<th>Original</th>
<th>Translation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{translations ? translations.map(pon => pon.roms.map(rom => rom.arabs.map(arab => arab.translations.map(translation => {
const {source, target} = translation;
return (
<tr>
<td><span dangerouslySetInnerHTML={{__html: source}}/></td>
<td><span dangerouslySetInnerHTML={{__html: target}}/></td>
<td>
{
!translationsToSave.includes(target) ?
<Button onClick={() => {
const tmp = translationsToSave;
tmp.push(target);
setTranslationsToSave(tmp);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
const tmp = translationsToSave;
tmp.splice(tmp.findIndex(elem => elem === target));
setTranslationsToSave(tmp);
}}>
Remove translation
</Button>
}
</td>
</tr>
)
})))) : (
<div>No translations</div>
)}
</tbody>
</Table>
</div>
);
}
export default App;
So it's a basic app, it right now just adds and removes from an array wit setTranslationsToSave. After I click the Add translation button the view stays the same. But it refreshes when I click Enter edit mode. Same with Remove translation. I need to click Enter/Exit edit mode.
Hitting Translate also reloads the view. So the Add/Remove translation buttons are the only ones which do not refresh the page. Why? What am I missing?
The issue is that you are mutating the satte in Add/Remove translation button, so when react check before re-rendering if the state updater was called with the same state it feels that nothing has changed as it does a reference check and ehnce doesn't trigger re-render
Also while updating current state based on previous state use functional callback approach for state updater.
Update your state like below
<Button onClick={() => {
setTranslationsToSave(prev => [...prev, target]);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
setTranslationsToSave((prev) => {
const index = prev.findIndex(elem => elem === target)); return [...prev.slice(0, index), ...prev.slice(index + 1)]
});
}}>
Remove translation
</Button>
In your Add translation click handler, you're mutating the state:
<Button onClick={() => {
// tmp is just a reference to state
const tmp = translationsToSave;
// You are mutating state, this will be lost
tmp.push(target);
setTranslationsToSave(tmp);
}}>
You should duplicate the state and add the new element:
<Button onClick={() => {
setTranslationsToSave([...translationsToSave, target]);
}}>

Categories

Resources