Preventing unnecessary rendering with React.js - reactjs

I have a very basic application to test how to prevent unnecessary rendering, but I'm very confused as it is not working no matter what I try. Please take a look.
App.js
import { useState, useCallback } from "react";
import User from "./User";
let lastId = 0;
function App() {
const [users, setUsers] = useState([
{ id: 0, name: "Nicole Kidman", gender: "Female" },
]);
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
const newUsers = [...users];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
setUsers(newUsers);
}
},
[users]
);
const addNewUser = useCallback(() => {
let newUser = { id: ++lastId, name: "John Doe", gender: "Male" };
setUsers((prevUsers) => [...prevUsers, newUser]);
}, []);
return (
<div className="App">
<button onClick={addNewUser}>Add user</button>
<br />
{users.map((user) => (
<User key={user.id} user={user} handleUserChange={handleUserChange} />
))}
</div>
);
}
export default App;
User.js
import { useRef, memo } from "react";
const User = memo(({ user, handleUserChange }) => {
const renderNum = useRef(0);
return (
<div className="user">
<div> Rendered: {renderNum.current++} times</div>
<div>ID: {user.id}</div>
<div>
Name:{" "}
<input
name="name"
value={user.name}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<div>
Gender:{" "}
<input
name="gender"
value={user.gender}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<br />
</div>
);
});
export default User;
Why the useCallback and memo doesn't do the job here? How can I make it work, prevent rendering of other User components if another User component is changing(typing something in Input)?
Thank you.

useCallback and useMemo take a dependency array. If any of the values inside that array changes, React will re-create the memo-ized value/callback.
With this in mind, we see that your handleUsersChange useCallback is recreated every time the array users changes. Since you update the users state inside the callback, every time you call handleUsersChange, the callback is re-created, and therefore the child is re-rendered.
Solution:
Don't put users in the dependency array. You can instead access the users value inside the handleUsersChange callback by providing a callback to setUsers functions, like so:
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
setUsers((oldUsers) => {
const newUsers = [...oldUsers];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
return newUsers;
}
return oldUsers;
})
},
[]
);

Related

Updates are affecting the states of multiple Contexts

I am trying to save 2 sets of input values using React Context, compare them using JSON.stringify(context1) !== JSON.stringify(context2) and if they are not the same, the page triggers a "save" CTA.
What I am not understanding is that the input updates are affecting both contexts even though I am only updating the state of one. Not understanding why this is happening.
Also, happy to hear more elegant ways of doing this.
Below is some code that replicates the issue and here is a CodePen of it:
const { useContext, useEffect, useState } = React
const Context1 = React.createContext()
const Context2 = React.createContext()
function Child(props) {
const { passHandleChange } = props
const { context1, setContext1 } = useContext(Context1)
const onChange = (index, key, event) => {
passHandleChange(index, key, event)
}
const formatPeople = () => {
return context1.tempPersons.map((person, index) => {
return (
<div>
{ index }
<input value={context1.tempPersons[index].name} onChange={event => onChange(index, 'name', event)} />
<input value={context1.tempPersons[index].email} onChange={event => onChange(index, 'email', event)} />
</div>
)
})
}
return (
<div>
{ formatPeople() }
</div>
)
}
function App() {
const initProfile = {
isModified: false,
tempPersons: [
{
name: "Bob",
email: "bob#email.com"
},
{
name: "Jill",
email: "jill#email.com"
},
{
name: "John",
email: "john#email.com"
}
]
}
const handleChange = (index, key, event) => {
let value = event.target.value
let tempPersons = context1.tempPersons
tempPersons[index] = context1.tempPersons[index]
tempPersons[index][key] = value
setContext1(prevState => ({
...prevState,
tempPersons: tempPersons
}))
}
const [context1, setContext1] = useState(initProfile)
const [context2, setContext2] = useState(initProfile.tempPersons)
return (
<div>
<Context1.Provider value={ { context1, setContext1 } }>
<Context2.Provider value={ { context2, setContext2 } }>
<Child passHandleChange={ handleChange } />
<hr />
<pre>
context1
{ JSON.stringify(context1) }
</pre>
<pre>
context2
{ JSON.stringify(context2) }
</pre>
</Context2.Provider>
</Context1.Provider>
</div>
)
}
ReactDOM.render( <App />, document.getElementById("root") );
It’s because objects in JavaScript are passed by reference, so the two contexts are actually referring to the same array (tempPersons). When that array is modified through the reference in one context, the other context will also appear modified, because they’re referring to the same array in memory.
You can confirm this by running the following:
const sharedArray = ['a', 'b'];
const ref1 = sharedArray;
const ref2 = sharedArray;
ref1.push('c');
console.log(ref2.length);
// 3

Using a High Order Component does not send data to the form in React

I want to adjust a demo provided by some tutorial about React Design Patterns, subject: Higher Order Component, and want to use an external data source from the url:
https://jsonplaceholder.typicode.com/users/1
to display the data within my form.
I guess since it's an async call, my Form always displays the "Loading part". What's the best way to solve this issue to ultimately receive the data? I can clearly see response.data not being empty when I log it, but the State variables are when I log them inside of the useEffect Hook
This is what I got so far.
Any help, tips, additional sources to learn this would be highly appreciated.
This is my HOC which I just copied:
import React, { useState, useEffect } from "react";
import axios from "axios";
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
export const withEditableResource = (Component, resourcePath, resourceName) => {
return (props) => {
const [originalData, setOriginalData] = useState(null);
const [editedData, setEditedData] = useState(null);
useEffect(() => {
(async () => {
const response = await axios.get(resourcePath);
setOriginalData(response.data);
setEditedData(response.data);
})();
}, []);
const onChange = (changes) => {
setEditedData({ ...editedData, ...changes });
};
const onSave = async () => {
const response = await axios.post(resourcePath, {
[resourceName]: editedData,
});
setOriginalData(response.data);
setEditedData(response.data);
};
const onReset = () => {
setEditedData(originalData);
};
const resourceProps = {
[resourceName]: editedData,
[`onChange${capitalize(resourceName)}`]: onChange,
[`onSave${capitalize(resourceName)}`]: onSave,
[`onReset${capitalize(resourceName)}`]: onReset,
};
return <Component {...props} {...resourceProps} />;
};
};
That's my form, I want to use - in the last lines you can find the hard-coded URL path, I want to swap for a parameter once this problem is done:
import { withEditableResource } from "./withEditableResource";
export const UserInfoFormImproved = withEditableResource(
({ user, onChangeUser, onSaveUser, onResetUser }) => {
const { name, email, username } = user || {};
return user ? (
<>
<label>
Name:
<input
value={name}
onChange={(e) => onChangeUser({ name: e.target.value })}
/>
</label>
<label>
Email:
<input
value={email}
onChange={(e) => onChangeUser({ email: e.target.value })}
/>
</label>
<label>
Username:
<input
value={username}
onChange={(e) => onChangeUser({ username: e.target.value })}
/>
</label>
<button onClick={onResetUser}>Reset</button>
<button onClick={onSaveUser}>Save Changes</button>
</>
) : (
<p>Loading...</p>
);
},
`https://jsonplaceholder.typicode.com/users/3`,
"User"
);
And that's the actual use of this two components within my App - I've added my idea on how to solve the parameter argument here:
import { UserInfoFormImproved } from "./HigherOrderComponents/UserInfoFormImproved";
function App() {
return (
<UserInfoFormImproved userId={1} />
);
}
export default App;

React TypeError is not a function with Onboarding implementation

I want to expand a demo provided by some tutorial about React Design Patterns, subject: Controlled Onboarding Flows, to implement multiple forms on several steps via Onboarding. But unfortunately the tutor did stop at the exciting part when it comes to having two-directional flows.
So I'm stuck and don't understand how to select the resp. function (marked with "// HOW TO DECIDE?!" in the 2nd code segment here).
So, every time I hit the prev. button, I receive the "Uncaught TypeError: goToPrevious is not a function" message, because both are defined.
Any suggestions on how to handle this?
This is what I got so far.
The idea behind this is to get the data from each form within the respo. Step Component and manage it witihin the parent component - which atm happens to be the App.js file.
Any help, tips, additional sources to learn this would be highly appreciated.
This is my template for the resp. controlled form components I want to use:
export const ControlledGenericForm = ({ formData, onChange }) => {
return (
<form>
{Object.keys(formData).map((formElementKey) => (
<input
key={formElementKey}
value={formData[formElementKey]}
type="text"
id={formElementKey}
onInput={(event) => onChange(event.target.id, event.target.value)}
/>
))}
</form>
);
};
That's my controlled Onboarding component, I want to use:
import React from "react";
export const ControlledOnboardingFlow = ({
children,
currentIndex,
onPrevious,
onNext,
onFinish,
}) => {
const goToNext = (stepData) => {
onNext(stepData);
};
const goToPrevious = (stepData) => {
onPrevious(stepData);
};
const goToFinish = (stepData) => {
onFinish(stepData);
};
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
// HOW TO DECIDE?!
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
if (currentChild && onPrevious)
return React.cloneElement(currentChild, { goToPrevious });
return currentChild;
};
And that's the actual use of this two components within my App:
import { useState } from "react";
import { ControlledOnboardingFlow } from "./ControlledComponents/ControlledOnboardingFlow";
import { ControlledGenericForm } from "./ControlledComponents/ControlledGenericForm";
function App() {
const [onboardingData, setOnboardingData] = useState({
name: "Juh",
age: 22,
hair: "green",
street: "Main Street",
streetNo: 42,
city: "NYC",
});
const [currentIndex, setCurrentIndex] = useState(0);
const formDataPartOne = (({ name, age, hair }) => ({ name, age, hair }))(
onboardingData
);
const formDataPartTwo = (({ street, streetNo, city }) => ({
street,
streetNo,
city,
}))(onboardingData);
const onNext = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex + 1);
};
const onPrevious = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex - 1);
};
const onFinish = () => {
console.log("Finished");
console.log(onboardingData);
};
const handleFormUpdate = (id, value) => {
setOnboardingData({ ...onboardingData, [id]: value });
};
const StepOne = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 1</h1>
<ControlledGenericForm
formData={formDataPartOne}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)} >
Prev
</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepTwo = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 2</h1>
<ControlledGenericForm
formData={formDataPartTwo}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)}>Prev</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepThree = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 3</h1>
<h3>
Congrats {onboardingData.name} for being from, {onboardingData.city}
</h3>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
return (
<ControlledOnboardingFlow
currentIndex={currentIndex}
onPrevious={onPrevious}
onNext={onNext}
onFinish={onFinish}
>
<StepOne />
<StepTwo />
{onboardingData.city === "NYC" && <StepThree />}
</ControlledOnboardingFlow>
);
}
export default App;
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
Since onNext exists, this is the code that will run. It clones the element and gives it a goToNext prop, but it does not give it a goToPrevious prop. So when you press the previous button and run code like onClick={() => goToPrevious(onboardingData)}, the exception is thrown.
It looks like you want to pass both functions into the child, which can be done like:
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
if (currentChild) {
return React.cloneElement(currentChild, { goToNext, goToPrevious });
}
return currentChild;
If one or both of them happens to be undefined, then the child will get undefined, but that's what you would do anyway with the if/else.

Component lose its state when re-renders

I have a sample where I have a list of answers and a paragraph that has gaps.
I can drag the answers from the list to the paragraph gaps. After the answer fills the gap, the answer will be removed from the list. 3 answers and 3 gaps, the answer list should be empty when i drag all of them to the gaps.
But whenever I filter the listData, the component re-renders and the listData gets reset. The list always remained 2 items no matter how hard I tried. What was wrong here?
My code as below, I also attached the code sandbox link, please have a look
App.js
import GapDropper from "./gapDropper";
import "./styles.css";
const config = {
id: "4",
sort: 3,
type: "gapDropper",
options: [
{
id: "from:drop_gap_1",
value: "hello"
},
{
id: "from:drop_gap_2",
value: "person"
},
{
id: "from:drop_gap_3",
value: "universe"
}
],
content: `<p>This is a paragraph. It is editable. Try to change this text. <input id="drop_gap_1" type="text"/> . The girl is beautiful <input id="drop_gap_2" type="text"/> I can resist her charm. Girl, tell me how <input id="drop_gap_3" type="text"/></p>`
};
export default function App() {
return (
<div className="App">
<GapDropper data={config} />
</div>
);
}
gapDropper.js
import { useEffect, useState } from "react";
import * as _ from "lodash";
import styles from "./gapDropper.module.css";
const DATA_KEY = "answerItem";
function HtmlViewer({ rawHtml }) {
return (
<div>
<div dangerouslySetInnerHTML={{ __html: rawHtml }} />
</div>
);
}
function AnwserList({ data }) {
function onDragStart(event, data) {
event.dataTransfer.setData(DATA_KEY, JSON.stringify(data));
}
return (
<div className={styles.dragOptionsWrapper}>
{data.map((item) => {
return (
<div
key={item.id}
className={styles.dragOption}
draggable
onDragStart={(event) => onDragStart(event, item)}
>
{item.value}
</div>
);
})}
</div>
);
}
function Content({ data, onAfterGapFilled }) {
const onDragOver = (event) => {
event.preventDefault();
};
const onDrop = (event) => {
const draggedData = event.dataTransfer.getData(DATA_KEY);
const gapElement = document.getElementById(event.target.id);
const objData = JSON.parse(draggedData);
gapElement.value = objData.value;
onAfterGapFilled(objData.id);
};
function attachOnChangeEventToGapElements() {
document.querySelectorAll("[id*='drop_gap']").forEach((element) => {
element.ondragover = onDragOver;
element.ondrop = onDrop;
});
}
useEffect(() => {
attachOnChangeEventToGapElements();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<HtmlViewer rawHtml={data} />
</div>
);
}
const GapDropper = ({ data }) => {
const [gaps, setGaps] = useState(() => {
return data.options;
});
function onAfterGapFilled(id) {
let clonedGaps = _.cloneDeep(gaps);
clonedGaps = clonedGaps.filter((g) => g.id !== id);
setGaps(clonedGaps);
}
return (
<div>
<div>
<AnwserList data={gaps} />
<Content
data={data.content}
onAfterGapFilled={(e) => onAfterGapFilled(e)}
/>
</div>
</div>
);
};
export default GapDropper;
Code sandbox
the problem is that you are not keeping on track which ids you already selected, so thats why the first time it goes right, and then the second one, the values just replace the last id.
Without changing a lot of your code, we can accomplish by tracking the ids inside a ref.
const GapDropper = ({ data }) => {
const [gaps, setGaps] = useState(() => {
return data.options;
});
const ids = useRef([])
function onAfterGapFilled(id) {
let clonedGaps = _.cloneDeep(gaps);
ids.current = [...ids.current, id]
clonedGaps = clonedGaps.filter((g) => !ids.current.includes(g.id));
setGaps(clonedGaps);
}
return (
<div>
<div>
<AnwserList data={gaps} />
<Content
data={data.content}
onAfterGapFilled={(e) => onAfterGapFilled(e)}
/>
</div>
</div>
);
};
Maybe there is a better solution but this one does the job

react memoize using React.memo

I Try not to Rerender Persons Component When ShowCockpit State Changes In MainAssignment Component. Like when i do in Cockpit Component, it doesn't rerender When Persons state change.
In This Case We Have 3 Components MainAssignment Component [parnt] , Cockpit Component [child] , Persons Component [child].
/********************************************************/
/*** MainAssignment Component ***/
import React, { useCallback, useState } from 'react';
import Persons from './persons';
import Coockpit from './cockpit';
const MainAssignment = () => {
// All State
const [persons, setPersons] = useState([
{ id: '1', name: 'mustafa', age: 24 },
{ id: '2', name: 'ahmed', age: 25 },
{ id: '3', name: 'saad', age: 26 },
]);
const [showPersons, setShowPersons] = useState(true);
const [showCoockpit, setShowCoockpit] = useState(true);
const togglePersonHandler = useCallback(() => {
setShowPersons(!showPersons);
}, [showPersons]);
// change name in specific object in persons state
const nameChangeHandler = (e, id, personIndex) => {
let newPersons = [...persons];
let person = { ...newPersons[personIndex] };
person.name = e.target.value;
newPersons[personIndex] = person;
setPersons(newPersons);
};
// delete object from persons state
const deletePersonHandler = (personIndex) => {
let newPersons = [...persons];
newPersons.splice(personIndex, 1);
setPersons(newPersons);
};
// Main Render
return (
<>
<button
onClick={() => {
setShowCoockpit((prev) => !prev);
}}
>
remove Coockpit
</button>
{showCoockpit ? (
<div style={{ border: '1px solid' }}>
<Coockpit clicked={togglePersonHandler} personsLength={persons.length} showPersons={showPersons} />
</div>
) : null}
{showPersons ? <Persons persons={persons} clicked={deletePersonHandler} changed={nameChangeHandler} /> : null}
</>
);
};
export default MainAssignment;
/********************************************************/
/*** Cockpit Component ***/
/********************************************************/
/*** Cockpit Component ***/
import React, { useRef } from 'react';
const Cockpit = ({ clicked }) => {
let toggleBtnRef = useRef(null);
console.log('render => Cockpit');
return (
<div>
<h1>hi i'm a main assin from cockpit</h1>
<button className="toggle-persons" onClick={clicked} ref={toggleBtnRef}>
toggle persons
</button>
</div>
);
};
// in Cockpit i use React.memo and it work
export default React.memo(Cockpit);
/********************************************************/
/*** Persons Component ***/
import React, { useEffect, useRef } from 'react';
import Person from './person';
const Persons = ({ persons, clicked, changed }) => {
console.log('render => personssss');
const mainRef = {
allInputPersonRef: useRef([]),
};
return (
<>
{persons?.map((person, idx) => (
<Person
key={idx}
name={person.name}
age={person.age}
position={idx}
index={idx}
ref={mainRef}
click={() => {
clicked(idx);
}}
changed={(e) => {
changed(e, person.id, idx);
}}
/>
))}
</>
);
};
// in Persons i use React.memo and it doesn't work
export default React.memo(Persons);
/********************************************************/
/*** Person Component ***/
import React from 'react';
const Person = React.forwardRef((props, ref) => {
const { allInputPersonRef } = ref;
// value of props
const { name, age, click, changed, children, index } = props;
return (
<div>
<p onClick={click}>
i'm {name} and i'm {age} years old
</p>
<p> i'am props children: {children}</p>
<input type="text" onChange={changed} value={name} ref={(el) => (allInputPersonRef.current[index] = el)} />
<button onClick={click}>delete this person</button>
</div>
);
});
export default Person;
React.memo can prevent children from rerendering when the parent component rerenders.
It compares (by reference) each previous and next prop. When one of them is different React will rerender the child normally.
In your case you are always passing new function to changed prop
const nameChangeHandler = (e, personIndex) => {
let newPersons = [...persons];
let person = { ...newPersons[personIndex] };
person.name = e.target.value;
newPersons[personIndex] = person;
setPersons(newPersons);
};
How to avoid this?
Make sure that nameChangeHandler is the same function each time you need to rerender and you don't want to rerender the Person component. https://reactjs.org/docs/hooks-reference.html#usecallback
const nameChangeHandler = useCallback((e, personIndex) => {
setPersons((persons) => {
let newPersons = [...persons];
let person = { ...newPersons[personIndex] };
person.name = e.target.value;
newPersons[personIndex] = person;
return newPersons
});
}, []);
Similarly you should memorize deletePersonHandler function
const deletePersonHandler = useCallback((personIndex) => {
setPersons((persons)=>{
let newPersons = [...persons];
newPersons.splice(personIndex, 1);
return newPersons
});
}, []);
using useCallback with togglePersonHandler and deletePersonHandler
const nameChangeHandler = useCallback((e, id, personIndex) => {
let newPersons = [...persons];
let person = { ...newPersons[personIndex] };
person.name = e.target.value;
newPersons[personIndex] = person;
setPersons(newPersons);
}, []);
const deletePersonHandler = useCallback((personIndex) => {
let newPersons = [...persons];
newPersons.splice(personIndex, 1);
setPersons(newPersons);
}, []);

Resources