React when I update state on one element all parent element and their parents functions are called, trying to understand React re-rendering? - javascript

I've created a very simplified code version of my problem to understand the REACT rendering using typescript. When I click a button which changes state in the lowest child element all parent elements are updated by the renderer and their children on other forks. How can I change the below so it doesn't do that.
import * as React from 'react';
import { connect } from 'react-redux';
import './Grid.css';
const RenderPopup = (key: number) => {
const open = () => setShowDialog(true);
const [showDialog, setShowDialog] = React.useState(false);
const close = () => setShowDialog(false);
if (!showDialog) {
return (
<div>
<button onClick={open}>do it</button>
</div>
)
}
else {
return (
<div>
<button onClick={close}>close
</button>
</div>
)
}
}
function Cell(key:number) {
return (
<div key={key}>
{key}
{RenderPopup(key)}
</div>
)
}
const Header = () => {
return (
<div className="gridRow">
{Cell(0)}
{Cell(1)}
{Cell(2)}
</div>
)
}
const Person = (rowNum: number) => {
return (
<div key={rowNum} className="gridRow">
{Cell(0)}
{Cell(1)}
{Cell(2)}
</div>
)
}
const Persons = () => {
return (
<div>
{Person(1)}
{Person(2)}
{Person(3)}
</div>
)
}
const Grid = () => {
return (
<div>
<Header />
<Persons />
</div>
);
}
export default connect()(Grid);

Related

React parent couldn't update a child component in a mapped JSX list

NOTE: You can view and edit the code in CodeSandbox.
I have the following parent file which creates a useState list of child component called ProgressBar:
import React, { useState } from 'react';
import ProgressBar from './ProgressBar';
import './App.css';
var idCounter = 0;
export default function App() {
const [barsArray, setBarsArray] = useState([]);
const [input, setInput] = useState('');
function add() {
setBarsArray((prev) => [
...prev,
<ProgressBar key={idCounter++} restart={false} />,
]);
}
function remove() {
setBarsArray((prev) => prev.filter((bar) => bar.key !== input));
}
function reset() {
setBarsArray((prev) =>
prev.map((bar) => (bar.key === input ? { ...bar, restart: true } : bar))
);
}
return (
<div className='App'>
<div className='buttons'>
<button className='button-add' onClick={add}>
Add
</button>
<button className='button-delete' onClick={remove}>
Delete
</button>
<button className='button-delete' onClick={reset}>
Reset
</button>
<input
type='number'
value={input}
onInput={(e) => setInput(e.target.value)}
/>
</div>
<div className='bars-container'>
{barsArray.map((bar) => (
<div className='bars-index' key={bar.key}>
{bar}
<p>{bar.key}</p>
</div>
))}
</div>
</div>
);
}
The file of the child ProgressBar has the following content:
import React, { useEffect, useState } from 'react';
import './ProgressBar.css';
export default function ProgressBar(props) {
const [progress, setProgress] = useState(0);
let interval;
useEffect(() => {
interval = setInterval(() => {
setProgress((prev) => prev + 1);
}, RnadInt(10, 120));
}, []);
useEffect(() => {
if (progress >= 100) clearInterval(interval);
}, [progress]);
if (props.restart === true) {
setProgress(0);
}
return (
<>
<div className='ProgressBarContainer'>
<div className='ProgressBar' style={{ width: progress + '%' }}></div>
</div>
</>
);
}
function RnadInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
My problem is that the reset button in the parent doesn't work, as far as I'm concerned, if you change the passed props to the child, the child automatically re-renders, but even though I'm updating the props in reset function in the parent, which maps the old array of child components to a new array and only changes the props of the selected child.
Thanks!
Adding the element in state via add would require to keep the ref of element instead of actual prop bind to element. Suggestion here to use the model object and while rendering use the JSX element.
Please use the below code which defines the barsArray as object state and later uses it render ProgressBar component (from map call).
Check the working codesandbox - https://codesandbox.io/s/admiring-glitter-j3b7sw?file=/src/App.js:0-1446
import React, { useState } from "react";
import ProgressBar from "./ProgressBar";
import "./App.css";
var idCounter = 0;
export default function App() {
const [barsArray, setBarsArray] = useState([]);
const [input, setInput] = useState("");
function add() {
setBarsArray((prev) => [...prev, { id: idCounter++, restart: false }]);
}
function remove() {
setBarsArray((prev) =>
prev.filter((bar) => bar.id.toString() !== input.toString())
);
}
function reset() {
setBarsArray((prev) =>
prev.map((bar) => {
return bar.id.toString() === input.toString()
? { ...bar, restart: true }
: { ...bar };
})
);
}
return (
<div className="App">
<div className="buttons">
<button className="button-add" onClick={add}>
Add
</button>
<button className="button-delete" onClick={remove}>
Delete
</button>
<button className="button-delete" onClick={reset}>
Reset
</button>
<input
type="number"
value={input}
onInput={(e) => setInput(e.target.value)}
/>
</div>
<div className="bars-container">
{barsArray.map((bar) => (
<div className="bars-index" key={bar.id}>
<ProgressBar key={bar.id} restart={bar.restart} />
<p>{bar.key}</p>
</div>
))}
</div>
</div>
);
}
This simple fix worked for me, in the file of the child, call the conditional expression in a useEffect hook. Currently, it doesn't listen to the changes in props.restart and only runs on the initial render.
check https://reactjs.org/docs/hooks-reference.html#useeffect
useEffect(() => {
if (props.restart === true) {
setProgress(0);
}
}, [props.restart])
link to working codesandbox https://codesandbox.io/s/distracted-gauss-24d0pu

React js useState&useEffect array duplicates elements after a change

I am a beginner in react js programming. I'm trying to do the todo project, which is a classic project. When I delete or add an element from the list, the newly formed list appears on the screen by combining with the previous one, I will show it with a picture below. I did not understand the source of the eror so wanted to post it here to get some advices suggestions about why it is happening.Thank you.(I am getting and storing data in firebase firestore database)
Before Adding an element initial array state
After adding an element to the array.
I am using useState for array and using useEffect to get initial data
MainPage.js that contains form and the list components.
const MainPage = () => {
const [isLoading, setLoding] = useState(true);
const [array, setArray] = useState([]);
const sub = async (email) => {
var result = [];
await onSnapshot(doc(db, "users", email), (doc) => {
var data = doc.data().todos;
data.forEach((element) => {
Object.keys(element).map(() => {
result.push(element["title"]);
});
});
setArray(result);
setLoding(false);
});
};
useEffect(() => {
sub(auth.currentUser.email);
}, []);
const onAddToDo = (todoTitle) => {
setArray((prevAray) => {
return [...prevAray, todoTitle];
});
};
const onRemove = (title) => {
setArray((prevAray) => {
return [array.pop(array.indexOf(title))];
});
};
return (
<div>
{isLoading && <h1>Loading</h1>}
{!isLoading && (
<div>
<section>
<NavBar></NavBar>
<ToDoForm passData={onAddToDo} />
</section>
<section>
<CardList removeCards={onRemove} array={array} />
</section>
</div>
)}
</div>
);
};
export default MainPage;
Firebase.js that stores the firebase update methods
export const deleteItem = (title) => {
updateDoc(doc(db, "users", auth.currentUser.email), {
todos: arrayRemove({ title: title }),
});
};
export const addnewTodo = (title) => {
updateDoc(doc(db, "users", auth.currentUser.email), {
todos: arrayUnion({ title: title }),
});
};
TodoForm.js component
const ToDoForm = (props) => {
const [todoTitle, setTitle] = useState("");
const titleChangeHandler = (event) => {
setTitle(event.target.value);
};
const newTodoAdder = (event) => {
event.preventDefault();
addnewTodo(todoTitle);
props.passData(todoTitle);
};
return (
<div className="form_holder">
<div className="form_container">
<form onSubmit={newTodoAdder}>
<h3>Add Events</h3>
<label>Title</label>
<input
onChange={titleChangeHandler}
type="text"
placeholder="Title"
id="title"
></input>
<div className="holder">
<button type="sumbit">Add</button>
</div>
</form>
</div>
</div>
);
};
export default ToDoForm;
CardList.js component
const CardList = (props) => {
const array = props.array;
if (array.length === 0) {
return (
<div className="grid_container">
<h2>Found no todos</h2>
</div>
);
}
return (
<div className="grid_container">
{array.map((element, index) => {
return (
<Card
removeSelf={() => {
props.removeCards(element);
}}
key={index}
title={element}
/>
);
})}
</div>
);
};
export default CardList;
Card.js component
const Card = (props) => {
const handleRemove = (event) => {
event.preventDefault();
deleteItem(props.title);
props.removeSelf();
};
return (
<div className="card">
<h2 className="card__title">{props.title}</h2>
<button type="button" onClick={handleRemove}>
Delete
</button>
</div>
);
};
export default Card;
EDIT ;
Index.js file
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
SOLUTION
I fixed the issue by changing the add and remove functions that were inside of MainPage.js file You can see the new versions bellow. Hope someday it will help somebody.
Use effect was called once all I had to do get the data again after a change...
New Remove and Add functions
const onAddToDo = (todoTitle) => {
console.log(todoTitle + " Added");
sub(auth.currentUser.email);
};
const onRemove = (title) => {
console.log(title + " Deleted");
sub(auth.currentUser.email);
};

React onclick update to state is not a function

I have a component A, which displays contents of a component B conditionally. Component B is contains a list of items, and when one clicks one of the items in the list, a new layout is supposed to be fired up showing details of the item. When i try to pass the props to switch to a new layout on a component B list item, i get an error toggleSearchType is not a function . Any assistance or recommendation on what i might be doing wrong will be appreciated.
My index file looks like this :
const PatientSearch: React.FC<PatientSearchProps> = ({ closePanel }) => {
const { t } = useTranslation();
const [searchType, setSearchType] = useState<SearchTypes>(SearchTypes.BASIC);
const toggleSearchType = (searchType: SearchTypes) => {
setSearchType(searchType);
};
return (
<>
<Overlay header={t('addPatientToList', 'Add patient to list')} closePanel={closePanel}>
<div className="omrs-main-content">
{searchType === SearchTypes.BASIC ? (
<BasicSearch toggleSearchType={toggleSearchType} />
) : searchType === SearchTypes.ADVANCED ? (
<PatientScheduledVisits toggleSearchType={toggleSearchType} />
) : searchType === SearchTypes.SCHEDULED_VISITS ? (
<AdvancedSearch toggleSearchType={toggleSearchType} />
) : null}
</div>
</Overlay>
</>
);
};
The searchtypes are as below :
export enum SearchTypes {
BASIC = 'basic',
ADVANCED = 'advanced',
SCHEDULED_VISITS = 'scheduled-visits'
}
My component A looks like this :
import React, { useEffect, useMemo, useState } from 'react';
interface BasicSearchProps {
toggleSearchType: (searchMode: SearchTypes) => void;
}
const BasicSearch: React.FC<BasicSearchProps> = ({ toggleSearchType }) => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<any>(null);
const customRepresentation = '';
return (
<div className={searchResults?.length ? styles.lightBackground : styles.resultsContainer}>
{searchResults?.length ? (
<div className={styles.resultsContainer}>{<SearchResults toggleSearchType={searchResults} patients={searchResults} />}</div>
) : (
<div>
<div className={styles['text-divider']}>{t('or', 'Or')}</div>
<div className={styles.buttonContainer}>
<Button
kind="ghost"
iconDescription="Advanced search"
renderIcon={Search16}
onClick={() => toggleSearchType(SearchTypes.ADVANCED)}>
{t('advancedSearch', 'Advanced search')}
</Button>
</div>
</div>
)}
</div>
);
};
export default BasicSearch;
Component B looks like this :
interface SearchResultsProps {
patients: Array<any>;
hidePanel?: any;
toggleSearchType: (searchMode: SearchTypes) => void;
}
function SearchResults({ patients, toggleSearchType }: SearchResultsProps ) {
const fhirPatients = useMemo(() => {
return patients.map((patient) => {
const preferredAddress = patient.person.addresses?.find((address) => address.preferred);
});
}, [patients]);
return (
<>
{fhirPatients.map((patient) => (
<div key={patient.id} className={styles.patientChart} onClick={() => toggleSearchType(SearchTypes.SCHEDULED_VISITS)} >
<div className={styles.container}>
<ExtensionSlot
extensionSlotName="patient-header-slot"
state={{
patient,
patientUuid: patient.id,
// onClick: onClickSearchResult,
}}
/>
</div>
</div>
))}
</>
);
}
}

React toggle view functionality in the parent via child component

I am trying to toggle view between list of meals and meal details. I have placed a button in the child component Meal.js to the Meals.js which is meant to be the list and the details view.
Can you please help me fix this issue. Seems like its not working even with the conditional rendering method I've used in the code below.
Meal.js
import { useState } from 'react'
import './Meal.css'
const Meal = (props) => {
const [isToggled, setIsToggled] = useState(false);
const sendIdHandler = () => {
if (isToggled === true) {
setIsToggled(false);
}
else {
setIsToggled(true);
}
props.onSaveIdHandler(props.id, isToggled)
}
return (
<div
className='meal'
onClick={sendIdHandler}
>
{props.label}
</div>
);
}
export default Meal;
Meals.js
import Meal from './Meal/Meal'
const Meals = (props) => {
let toggleCondition = false;
const saveIdHandler = (data, isToggled) => {
toggleCondition = isToggled;
const mealDetails = props.mealsMenuData.findIndex(i =>
i.id === data
)
console.log(mealDetails, toggleCondition)
}
return (
<div>
{toggleCondition === false &&
props.mealsMenuData.map(item =>
<Meal
key={item.id}
id={item.id}
label={item.label}
onSaveIdHandler={saveIdHandler}
/>
)
}
{toggleCondition === true &&
<div>Horray!</div>
}
</div>
);
}
export default Meals;
UPDATE
Finally figured how to do this properly. I put the condition true/false useState in the parent instead and have Meal.js only send the id I need to view the item
Code is below..
Meals.js
import { useState } from 'react'
import Meal from './Meal/Meal'
import MealDetails from './MealDetails/MealDetails'
const Meals = (props) => {
const [show, setShow] = useState(false);
const [mealId, setMealId] = useState(0);
const saveIdHandler = (data) => {
setShow(true);
setMealId(props.mealsMenuData.findIndex(i =>
i.id === data)
)
console.log(props.mealsMenuData[mealId].ingridients)
}
const backHandler = () => {
setShow(false)
}
return (
<div>
{show === false &&
props.mealsMenuData.map(item =>
<Meal
key={item.id}
id={item.id}
label={item.label}
onSaveIdHandler={saveIdHandler}
/>
)
}
{show === true &&
<div>
<MealDetails data={props.mealsMenuData[mealId]} />
<button onClick={backHandler}>Back</button>
</div>
}
</div>
);
}
export default Meals;
Meal.js
import './Meal.css'
const Meal = (props) => {
const sendIdHandler = () => {
props.onSaveIdHandler(props.id)
}
return (
<div
className='meal'
onClick={sendIdHandler}
>
{props.label}
</div>
);
}
export default Meal;
Your problem in sendIdHandler: You can update like this:
const sendIdHandler = () => {
const newIsToggled = !isToggled;
setIsToggled(newIsToggled)
props.onSaveIdHandler(props.id, newIsToggled)
}

How to pass HTML attributes to child component in React?

I have a parent and a child component, child component has a button, which I'd like to disable it after the first click. This answer works for me in child component. However the function executed on click now exists in parent component, how could I pass the attribute down to the child component? I tried the following and it didn't work.
Parent:
const Home = () => {
let btnRef = useRef();
const handleBtnClick = () => {
if (btnRef.current) {
btnRef.current.setAttribute("disabled", "disabled");
}
}
return (
<>
<Card btnRef={btnRef} handleBtnClick={handleBtnClick} />
</>
)
}
Child:
const Card = ({btnRef, handleBtnClick}) => {
return (
<div>
<button ref={btnRef} onClick={handleBtnClick}>Click me</button>
</div>
)
}
In general, refs should be used only as a last resort in React. React is declarative by nature, so instead of the parent "making" the child disabled (which is what you are doing with the ref) it should just "say" that the child should be disabled (example below):
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = ({isDisabled, onButtonClick}) => {
return (
<div>
<button disabled={isDisabled} onClick={onButtonClick}>Click me</button>
</div>
)
}
Actually it works if you fix the typo in prop of Card component. Just rename hadnlBtnClick to handleBtnClick
You don't need to mention each prop/attribute by name as you can use javascript Object Destructuring here.
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = (props) => {
return (
<div>
<button {...props}>Click me</button>
</div>
)
}
You can also select a few props and use them differently in the child components. for example, see the text prop below.
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card text="I'm a Card" isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = ({text, ...restProps}) => {
return (
<div>
<button {...restProps}>{text}</button>
</div>
)
}

Categories

Resources