Avoiding rerender using callback in child component whenever state in parent changes - reactjs

I am having some issues with components being rerendered to often whenever the parent component has updated its state.
I have a simple Usercomponent which displays a name. The App component uses useState to store multiple users. Whenever the provided callback onClick is being run all components are rerendered, this decreases performance. I have tried using useCallback in order to prevent this from happening. The code can be run here. How can I prevent the other components being rerendered whenever the parent updates the users state?
const App = () => {
const [users, setUsers] = useState([
{ id: 1, name: "Cory" },
{ id: 2, name: "Meg" },
{ id: 3, name: "Bob" }
]);
const deleteUser = useCallback(id => {
setUsers(users => users.filter(user => user.id !== id));
}, []);
const renderUser = user => {
return <User key={user.id} user={user} onClick={deleteUser} />;
};
return (
<div>
<h1>Users</h1>
<ul>{users.map(renderUser)}</ul>
</div>
);
};
User
const User = props => {
const onDeleteClick = () => {
props.onClick(props.user.id);
};
console.log(`${props.user.name} just rendered`);
return (
<li>
<input type="button" value="Delete" onClick={onDeleteClick} />
{props.user.name}
</li>
);
};

use React.memo in your children component
export default React.memo(User);

you can use React.memo in User Component to stop re-rendering of the Component.
Working Link: https://codesandbox.io/s/no-arrow-func-render-eziql
Hope this helps!

Related

Is there a way to refresh a childs state from a parent component (Both are function components)

function clearChildState(){
//Stuff to clear child state from parent components
}
I want an input from the user (Who sees the parent component) to click a button to clear child components states. How would one go about this?
You can pass the items as prop (parent->child).
<Child items={items} />
The child continues to have an items state, which is initialized from the items prop.
When the parent passes an empty array to the child, the child's items state would be reset to [].
This can be achieved using getDerivedStateFromProps in class based child component.
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {items: []};
}
static getDerivedStateFromProps(props,state) {
return {items: props.items};
}
// render
}
If you must use function components, you could use refs and effects to do it like this:
const ChildComponent = (props) => {
// Store your initial state
const initialState = {
name: 'Default Name',
title: 'Default Title'
}
const [{name, title} , setState] = React.useState(initialState)
const setNewState = () => {
setState({ name: 'New Name', title: 'New Title' })
}
// Use a ref to store the previous prop value
const prevClearChildStateCount = React.useRef(0)
// Use effects to run a check whenever the prop is updated
React.useEffect(() => {
if (props.clearChildStateCount > prevClearChildStateCount.current) {
setState(initialState)
}
prevClearChildStateCount.current = props.clearChildStateCount
}, [props.clearChildStateCount])
return (
<div>
<p>Name: {name}</p>
<p>Title: {title}</p>
<button onClick={setNewState}>Set new state</button>
</div>
)
}
const ParentComponent = () => {
const [clearChildStateCount, updateClearChildState] = React.useState(0)
const clearChildState = () => {
updateClearChildState(clearChildStateCount + 1)
}
return (
<div>
<ChildComponent clearChildStateCount={clearChildStateCount} />
<button onClick={clearChildState}>Clear Child State</button>
</div>
)
}
ReactDOM.render(<ParentComponent />, document.getElementById('container'))
Link to Fiddle

using createselector still rerenders component

I learn about redux and its features. One thing that I get trouble with is createselector in reduxtoolkit. I have a slice:
const titlesSlice = createSlice({
name: "title",
initialState: {
titles: [],
title: "",
},
reducers: {
addTitle: (state, action) => {
state.titles.push({
id: Math.trunc(Math.random() * 10000).toString(),
title: state.title,
});
},
titleChange: (state, action) => {
state.title = action.payload;
},
},
});
and a selectors like:
const getTitles = (state) => (state.titles.titles);
export const selectTitlesLengthWithReselect = createSelector(
[getTitles],
(titles) => titles.filter(elem => elem.title.length > 5))
In App.js I added input for adding title:
function App(props) {
const dispatch = useDispatch();
const title = useSelector((state) => state.titles.title);
return (
<div className="App">
<div>
<input type="text"
onChange={(e) => dispatch(titleChange(e.target.value))}
value={title} />
<button onClick={() => dispatch(addTitle())}>Save</button>
<Titlelist />
</div>
</div>
);
}
TitleList component:
const Titlelist = () => {
const allTitles = useSelector(selectTitlesLengthWithReselect);
console.log("RENDERED");
return (
<div>
{allTitles.map((elem) => (
<li key={elem.id}>{elem.title}</li>
))}
</div>
)
}
Problem is every time input value(title in the titleReducer) changes the TitleList component rerenders. But the data that comes from selector is memoized(I checked for the prev and current value equality and they are same). Is there something that I'm doing wrong or why does component rerenders?
Nothing wrong with your selector, it is memoized, the selector did not cause the re render (re creating of jsx).
Dispatching titleChange will re render App so it will re render Titlelist because it is not a pure component, you can make it a pure component with React.memo:
const Titlelist = React.memo(function TitleList() {
const allTitles = useSelector(
selectTitlesLengthWithReselect
);
console.log('RENDERED');
return (
<div>
{allTitles.map((elem) => (
<li key={elem.id}>{elem.title}</li>
))}
</div>
);
});
As phry commented; it is usually fine to let your component re create jsx as React will do a virtual DOM compare and not re render DOM if the jsx is the same, if you have handlers that are re created (like: onClick={()=>new ref}) then that will fail virtual DOM compare. You can use useCallback to prevent that.
A pure component will generate the same jsx reference if no props changed so will have a quicker virtual dom compare but will also take up more memory and setup processing so it may not always benefit your application.

React Multiple Checkboxes are not visible as selected after change

I have a list of checkboxes.
Checkbox is not visible as selected even after the value has been changed.
Below is my code: -
import React, { useState } from "react";
import { render } from "react-dom";
const CheckboxComponent = () => {
const [checkedList, setCheckedList] = useState([
{ id: 1, label: "First", isCheck: false },
{ id: 2, label: "Second", isCheck: true }
]);
const handleCheck = (e, index) => {
checkedList[index]["isCheck"] = e.target.checked;
setCheckedList(checkedList);
console.log(checkedList);
};
return (
<div className="container">
{checkedList.map((c, index) => (
<div>
<input
id={c.id}
type="checkbox"
checked={c.isCheck}
onChange={e => handleCheck(e, index)}
/>
<label htmlFor={c.id}>{c.label}</label>
</div>
))}
</div>
);
};
render(<CheckboxComponent />, document.getElementById("root"));
I was working fine for a simple checkbox outside the loop.
I am not sure where is the problem.
Here is the link - https://codesandbox.io/s/react-multiple-checkboxes-sczhy?file=/src/index.js:0-848
Cause you pass an array to the state, so if you want your react component re-render, you must let the react know that your state change. On your handleCheck, you only change property of an value in that array so the reference is not changed.
The handleCheck function should be look like this
const handleCheck = (e, index) => {
const newCheckList = [...checkedList];
newCheckList[index]["isCheck"] = e.target.checked;
setCheckedList(newCheckList);
};
Do this instead:
const handleCheck = (e, index) => {
setCheckedList(prevState => {
const nextState = prevState.slice()
nextState[index]["isCheck"] = e.target.checked;
return nextState
});
};
Since checkedList is an array, (considered as object and handled as such), changing a property won't change the array itself. So React can know that something changed.

How to avoid unnecesary update in a stateless component with a React class-based component as a parent

I´m learning React and i found that React.memo() "is not working", because my component again re-render on every update that i do on the parent class-based component. But the problem is that props on component don´t change, at least it make sense for me
I used useEffect hook to print on my screen that re-render, although i use React.memo(Men)
const Men = props => {
useEffect(() => {
console.log("rendered");
});
return <p onClick={props.add}>{props.adder}</p>;
};
React.memo(Men);
class App extends React.Component {
state = {
counter: 0,
adder: "press"
};
add = () => {
this.setState(prevState => {
return {
counter: prevState.counter + 1
};
});
};
render() {
return (
<div className="App">
<p>{this.state.counter}</p>
<Men adder={this.state.adder} add={this.add} />
</div>
);
}
}
I expect that in my console the message 'rendered' inside the useEffect hook appears only once time.
This is happening due to how you are using memo - you need to use the return value that React.memo(Men) gives you.
Like this:
This CodePen will cause a re-render
This CodePen will NOT cause a re-render
Correct:
const MenBefore = props => {
React.useEffect(() => {
console.log("rendered");
});
return <p onClick={props.add}>{props.adder}</p>;
};
////////////////////////////////////////
const Men = React.memo(MenBefore); // <--- THIS LINE
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
class App extends React.Component {
state = {
counter: 0,
adder: "Click Me - I will -NOT- cause a re-render"
};
add = _ => {
this.setState(prevState => {
return {
counter: prevState.counter + 1
};
});
};
render() {
return (
<div className="App">
<p>{this.state.counter}</p>
<Men adder={this.state.adder} add={this.add} />
</div>
);
}
}
ReactDOM.render(<App />, document.body);
Incorrect:
const Men = props => {
React.useEffect(() => {
console.log("rendered");
});
return <p onClick={props.add}>{props.adder}</p>;
};
/////////////////////////////
React.memo(Men); // <<<--------- WRONG
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
class App extends React.Component {
state = {
counter: 0,
adder: "Click Me - I will cause a re-render"
};
add = _ => {
this.setState(prevState => {
return {
counter: prevState.counter + 1
};
});
};
render() {
return (
<div className="App">
<p>{this.state.counter}</p>
<Men adder={this.state.adder} add={this.add} />
</div>
);
}
}
ReactDOM.render(<App />, document.body);
React hooks also take a dependency array, without it the useEffect hook will fire its effect on every render. react hooks reference
useEffect(() => {
console.log("rendered");
}); // every render
useEffect(() => {
console.log("rendered");
}, []); // on mount, once
useEffect(() => {
console.log("rendered");
}, [propValue]); // every time propValue is a new reference
The memo HOC function can also take an equality comparison function to further test when re-renders should happen, but note that react still controls when they do happen. HOCs (Higher Order Components) wrap a component and return a new component to be rendered. You wrap your component but do not save the returned value to render later.
const MemoizedComponent = React.memo(Component, [optional prop compare function]);
...
render() {
return (
<MemoizedComponent />
);
};

React Hooks: Trying to access state before unmount returns initial state

In general i'm trying to save global state updates when my component unmounts because react-apollo gives me a hard time with unnecessary refetches.
I'm adding all the deleted comment ids to deletedCommentsQueue and when the Comments component unmounts i want to updated my global state but when the component about to unmount deletedCommentsQueue changes to an empty array even though we can see all the comment ids before we try to do our update.
I've made a simple SandBox for you guys.
And this is my code for anyone who's interested
import React, { useState, useEffect, useContext, createContext } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const UserContext = createContext();
const comments = [
{ _id: 1, body: "first" },
{ _id: 2, body: "second" },
{ _id: 3, body: "third" }
];
const Comments = ({ commentIds }) => {
const [deletedCommentsQueue, setDeletedCommentsQueue] = useState([]);
const addToQueue = commentId => {
setDeletedCommentsQueue([...deletedCommentsQueue, commentId]);
};
const { loggedUser, setLoggedUser } = useContext(UserContext);
useEffect(
() => () => {
console.log("cleaning");
console.log("deletedCommentsQueue", deletedCommentsQueue);
const updatedComments = loggedUser.comments.filter(
commentId => !deletedCommentsQueue.includes(commentId)
);
console.log(updatedComments);
setLoggedUser({
...loggedUser,
comments: updatedComments,
likes: {
...loggedUser.likes,
comments: loggedUser.likes.comments.filter(
commentId => !deletedCommentsQueue.includes(commentId)
)
}
});
},
[]
);
return (
<div>
{deletedCommentsQueue.length > 0 && (
<h1>Comment ids for deletion {deletedCommentsQueue.join(" ")}</h1>
)}
{commentIds.map(commentId => (
<Comment
deleted={deletedCommentsQueue.includes(commentId)}
key={commentId}
comment={comments.find(c => c._id === commentId)}
deleteCommentFromCache={() => addToQueue(commentId)}
/>
))}
</div>
);
};
const Comment = ({ comment, deleted, deleteCommentFromCache }) => (
<div>
{deleted && <h2>Deleted</h2>}
<p>{comment.body}</p>
<button disabled={deleted} onClick={deleteCommentFromCache}>
Delete
</button>
</div>
);
const App = () => {
const [loggedUser, setLoggedUser] = useState({
username: "asafaviv",
comments: [1, 2, 3],
likes: {
comments: [1, 2]
}
});
const [mounted, setMounted] = useState(true);
return (
<div className="App">
<UserContext.Provider value={{ loggedUser, setLoggedUser }}>
{mounted && <Comments commentIds={loggedUser.comments} />}
</UserContext.Provider>
<br />
<button onClick={() => setMounted(!mounted)}>
{mounted ? "Unmount" : "Mount"}
</button>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I guess it's because of the empty dependency ([]) of the effect that you declare. That way, the effect is run once when the component is mounted.
Therefore, I guess in the context of its closure, deletedCommentQueues has the value of when the component was first mounted => [].
I tried your codesandbox and if you remove that [] (which means the effect is called on each update), you get the correct value when you unmount the component but...the function is called on each update which does not solve your caching problem.
IMHO, I would suggest that you set your state (const [deletedCommentsQueue, setDeletedCommentsQueue] = useState([]);) in the parent component and save wherever you want the data as soon as the mounted value turns to false instead of watching from inside the component.

Resources