How to prevent re-rendering of components that have not changed? - javascript

I have a component that consists of several other components such as text fields, and when an input is made to the text field, all other components are re-rendered.
I would like to prevent the re-rendering and only re-render the component that actually changes.
I have seen that useCallback is the right way to do this and I have already seen how to use it. However, I'm having some trouble getting useCallBack to work correctly for my case.
Even if I set it up in a simple manner as below, each new character entered into the text field causes the button to be rendered again.
I don't see my mistake.
See working example in sandbox.
const Button = () => {
console.log("Button Rendered!");
window.alert("Button Rendered");
return <button onClick="">Press me</button>;
};
export default function App() {
const [textInput, setTextInput] = useState("Hallo");
const onChangeInput = useCallback(
(e) => {
setTextInput(e.target.value);
},
[textInput]
);
return (
<div>
<input
type="text"
onChange={onChangeInput}
value={textInput}
/>
<Button />
</div>
);
}
I am happy for any calrification.

Personally I would avoid React.memo / React.useRef / React.useCallback.
The simplest solution to your example is just create another component, and store the state with this.
eg.
const Button = () => {
console.log("Button Rendered!");
window.alert("Button Rendered");
return <button onClick="">Press me</button>;
};
const TextInput = () => {
const [textInput, setTextInput] = useState("Hallo");
const onChangeInput = useCallback(
(e) => {
setTextInput(e.target.value);
},
[textInput]
);
return (
<input
type="text"
onChange={onChangeInput}
value={textInput}
/>
);
}
export default function App() {
return (
<div>
<TextInput/>
<Button />
</div>
);
}
In the above if you change Text, there is not State change in App, so Button doesn't get re-rendered, no need for useMemo etc..
You will find React works really well, the more you divide your components up, not only does it solve issues of re-render, but potentially makes it a lot easier to re-use components later.
IOW: keep state as close to the component as possible, and performance will follow.
Of course your example is simple, and in a real app you will have HOC's etc to cope with, but that's another question.. :)

useCallback does not prevent rerenders. React.memo is what prevents renders. It does a shallow comparison of the previous props with the new props, and if they're the same, it skips rendering:
const Button = React.memo(() => {
console.log("Button Rendered!");
window.alert("Button Rendered");
return <button onClick="">Press me</button>;
});
The only role that useCallback plays in this is that sometimes you want to pass a function as a prop to a memoized component. For the memoization to work, props need to not change, and useCallback can help the props to not change.

changing the state causes re-render component along with all his heirs, to prevent re-render some sections, you can use useMemo to prevent unwanted re-rendering...
NOTE: useMemo has some costs... so don't overuse it (In this small example, it is not recommended at all).
in this case, if you do not need to re-rendering, you can use the useRef to save the input reference to get that value whenever you need it.
e.g:
const BlahBlah = () => {
const inputRef = React.useRef(undefined);
return (
<div>
<input ref={inputRef} />
<button onClick={() => console.log(inputRef.current.value)}
</div>
);
};

I had similar problem. I wanted to avoid rendering a component which dependent on part2 of state, when only part1 has changed.
I used shouldComponentUpdate(nextProps, nextState) as described here https://reactjs.org/docs/optimizing-performance.html to avoid rendering it.

I'm using it this way:
const [state_data, set_state_data] = useState([
true, // loading.
{}, // vehicle.
{} // user.
]);
This allows rendering only 1x instead of 3x and React keeps track of the changes. For some reason it won't track the change of objects, but arrays yes.

Related

What is the point to use useCallback in a component without React.memo?

Consider example:
const Child = () => {
console.log("I did re-render!");
return null;
};
const App = () => {
const [_, setValue] = useState();
const fn = useCallback(() => {
// do something
}, []);
return (
<div>
<button onClick={() => setValue(Math.random)}>click</button>
<Child fn={fn} />
</div>
);
};
With every state change in App (click the button), the Child component re-renders, even if the passed prop fn is wrapped with useCallback. However, if I wrap Child with React.memo, it starts to work correctly - it does not re-render when parent re-renders.
My question: What's the point of using useCallbacks without React.memo?? Should I always use React.memo if I dont want the component to always re-render if its parent re-renders?
Should useCallbacks always be used with React.memo? Because it seems like they are senseless and useless without React.memo.
Playground: https://codesandbox.io/s/peaceful-khorana-nrojpb?file=/src/App.js
My question: What's the point of using useCallbacks without React.memo??
There is none unless you otherwise tell React to compare based on refernece down the line.
Should I always use React.memo if I dont want the component to always re-render if its parent re-renders?
Yes, or something equivalent.
Should useCallbacks always be used with React.memo? Because it seems like they are senseless and useless without React.memo.
Yes, unless you do something equivalent.
Just to elaborate - other than React.memo you can always wrap a sub-render with useMemo:
const App = () => {
const [_, setValue] = useState();
const fn = useCallback(() => {
// do something
}, []);
const child = useMemo(() => <Child fn={fn} />, [fn]);
return (
<div>
<button onClick={() => setValue(Math.random)}>click</button>
{child}
</div>
);
};
Or "roll your own" with useRef+useEffect or use class components and override shouldComponentUpdate or inherit from React.PureComponent.

React functional components in Array.map are always rerendering when getting passed a function as props

I am trying to render multiple buttons in a parent component that manages all child states centrally. This means that the parent stores e.g. the click state, the disabled state for each button in his own state using useState and passes it as props to the childs. Additionally the onClick function is also defined inside of the parent component and is passed down to each child. At the moment I am doing this like the following:
const [isSelected, setIsSelected] = useState(Array(49).fill(false));
...
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
...
(In the render function:)
return isSelected.map((isFieldSelected, key) => {
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
})
To try to prevent the child component from rerendering I am using...
... useCallback to make react see that the onClick function always stays the same
... React.Fragment to make react find a component again because otherwise a child would not have a unique id or sth similar
The child component is exported as:
export default React.memo(TheChildComponent, compareEquality) with
const compareEquality = (prev, next) => {
console.log(prev, next);
return prev.isSelected === next.isSelected;
}
Somehow the log line in compareEquality is never executed and therefore I know that compareEquality is never executed. I don't know why this is happening either.
I have checked all blogs, previous Stackoverflow questions etc. but could not yet find a way to prevent the child components from being rerendered every time that at least one component executes the onClick function and by doing that updated the isSelected state.
I would be very happy if someone could point me in the right direction or explain where my problem is coming from.
Thanks in advance!
This code will actually generate a new onClick function every render, because useCallback isn't given a deps array:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
});
The following should only create one onClick function and re-use it throughout all renders:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
Combined with vanilla React.memo, this should then prevent the children from re-rendering except when isSelected changes. (Your second argument to React.memo should have also fixed this -- I'm not sure why that didn't work.)
As a side note, you can simplify this code:
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
to the following:
<TheChildComponent key={key}
isSelected={isFieldSelected}
onClick={onClick}
/>
(assuming you indeed only need a single component in the body of the map).
Turns out the only problem was neither useCallback, useMemo or anything similar.
In the render function of the parent component I did not directly use
return isSelected.map(...)
I included that part from a seperate, very simple component like this:
const Fields = () => {
return isSelected.map((isFieldSelected, i) => (
<TheChildComponent
key={i}
isSelected={isFieldSelected}
onClick={onClick}
/>
));
};
That is where my problem was. When moving the code from the seperate component Fields into the return statement of the parent component the rerendering error vanished.
Still, thanks for the help.

How to set initial state value for useState Hook in jest and enzyme?

I know this question is already asked here: How to set initial state for useState Hook in jest and enzyme?
const [state, setState] = useState([]);
And I totally agree with Jimmy's Answer to mock the useState function from test file but I have some extended version of this question, "What if I have multiple useState statements into the hooks, How can I test them and assign the respective custom values to them?"
I have some JSX rendering with the condition of hook's state values and depending on the values of that state the JSX is rendering.
How Can I test those JSX by getting them into the wrapper of my test case code?
Upon the answer you linked, you can return different values for each call of a mock function:
let myMock = jest.fn();
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
In my opinion this is still brittle. You may modify the component and add another state later, and you would get confusing results.
Another way to test a React component is to test it like a user would by clicking things and setting values on inputs. This would fire the event handlers of the component, and React would update the state just as in real configuration. You may not be able to do shallow rendering though, or you may need to mock the child components.
If you prefer shallow rendering, maybe read initial state values from props like this:
function FooComponent({initialStateValue}) {
const [state, setState] = useState(initialStateValue ?? []);
}
If you don't really need to test state but the effects state has on children, you should (as some comments mention) just then test the side effect and treat the state as an opaque implementation detail. This will work if your are not using shallow rendering or not doing some kind of async initialization like fetching data in an effect or something otherwise it could require more work.
If you cant do the above, you might consider removing state completely out of the component and make it completely functional. That way any state you need, you can just inject it into the component. You could wrap all of the hooks into a 'controller' type hook that encapsulates the behavior of a component or domain. Here is an example of how you might do that with a <Todos />. This code is 0% tested, its just written to show a concept.
const useTodos = (state = {}) => {
const [todos, setTodos] = useState(state.todos);
const id = useRef(Date.now());
const addTodo = useCallback((task) => {
setTodos((current) => [...current, { id: id.current++, completed: false, task }]);
}, []);
const removeTodo = useCallback((id) => {
setTodos((current) => current.filter((t) => t.id !== id));
}, []);
const completeTodo = useCallback((id) => {
setTodos((current) => current.map((t) => {
let next = t;
if (t.id === id) {
next = { ...t, completed: true };
}
return next;
}))
}, []);
return { todos, addTodo, removeTodo, completeTodo };
};
const Todos = (props) => {
const { todos, onAdd, onRemove, onComplete } = props;
const onSubmit = (e) => {
e.preventDefault();
onAdd({ task: e.currentTarget.elements['todo'].value });
}
return (
<div classname="todos">
<ul className="todos-list">
{todos.map((todo) => (
<Todo key={todo.id} onRemove={onRemove} onComplete={onComplete} />
)}
</ul>
<form className="todos-form" onSubmit={onSubmit}>
<input name="todo" />
<button>Add</button>
</form>
</div>
);
};
So now the parent component injects <Todos /> with todos and callbacks. This is useful for testing, SSR, ect. Your component can just take a list of todos and some handlers, and more importantly you can trivially test both. For <Todos /> you can pass mocks and known state and for useTodos you would just call the methods and make sure the state reflects what is expected.
You might be thinking This moves the problem up a level and it does, but you can now test the component/logic and increase test coverage. The glue layer would require minimal if any testing around this component, unless you really wanted to make sure props are passed into the component.

Use dynamically created react components and fill with state values

Below is a proof of concept pen. I'm trying to show a lot of input fields and try to collect their inputs when they change in one big object. As you can see, the input's won't change their value, which is what I expect, since they're created once with the useEffect() and filled that in that instance.
I think that the only way to solve this is to use React.cloneElement when values change and inject the new value into a cloned element. This is why I created 2000 elements in this pen, it would be a major performance hog because every element is rerendered when the state changes. I tried to use React.memo to only make the inputs with the changed value rerender, but I think cloneElement simply rerenders it anyways, which sounds like it should since it's cloned.
How can I achieve a performant update for a single field in this setup?
https://codepen.io/10uur/pen/LYPrZdg
Edit: a working pen with the cloneElement solution that I mentioned before, the noticeable performance problems and that all inputs rerender.
https://codepen.io/10uur/pen/OJLEJqM
Here is one way to achieve the desired behavior :
https://codesandbox.io/s/elastic-glade-73ivx
Some tips :
I would not recommend putting React elements in the state, prefer putting plain data (array, objects, ...) in the state that will be mapped to React elements in the return/render method.
Don't forget to use a key prop when rendering an array of elements
Use React.memo to avoid re-rendering components when the props are the same
Use React.useCallback to memoize callback (this will help when using React.memo on children)
Use the functional form of the state setter to access the old state and update it (this also helps when using React.useCallback and avoid recreating the callback when the state change)
Here is the complete code :
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const INPUTS_COUNT = 2000;
const getInitialState = () => {
const state = [];
for (var i = 0; i < INPUTS_COUNT; i++) {
// Only put plain data in the state
state.push({
value: Math.random(),
id: "valueContainer" + i
});
}
return state;
};
const Root = () => {
const [state, setState] = React.useState([]);
useEffect(() => {
setState(getInitialState());
}, []);
// Use React.useCallback to memoize the onChangeValue callback, notice the empty array as second parameter
const onChangeValue = React.useCallback((id, value) => {
// Use the functional form of the state setter, to update the old state
// if we don't use the functional form, we will be forced to put [state] in the second parameter of React.useCallback
// in that case React.useCallback will not be very useful, because it will recreate the callback whenever the state changes
setState(state => {
return state.map(item => {
if (item.id === id) {
return { ...item, value };
}
return item;
});
});
}, []);
return (
<>
{state.map(({ id, value }) => {
// Use a key for performance boost
return (
<ValueContainer
id={id}
key={id}
onChangeValue={onChangeValue}
value={value}
/>
);
})}
</>
);
};
// Use React.memo to avoid re-rendering the component when the props are the same
const ValueContainer = React.memo(({ id, onChangeValue, value }) => {
const onChange = e => {
onChangeValue(id, e.target.value);
};
return (
<>
<br />
Rerendered: {Math.random()}
<br />
<input type="text" value={value} onChange={onChange} />
<br />
</>
);
});
ReactDOM.render(<Root />, document.getElementById("root"));

Parent component unnecessarily re-rendering child on parent state change

I am creating a simple Magic The Gathering search engine. The vision is to have a list of search results, and when a search result is clicked the main display renders extended information about the card selected.
You can see it here
The top level App component contains the state of what card is to be displayed and the ScrollView component maintains the state of the card selected for only the highlighting of the selected card in the list. I propagate down the setDisplayCard handler so that when a card is clicked in the list, I can set the display card as a callback.
function App(props) {
const [displayCard, setDisplayCard] = useState(null)
return (
<div className="App">
<SearchDisplay handleCardSelect={setDisplayCard}/>
<CardDisplay card={displayCard} />
</div>
);
}
function SearchDisplay({handleCardSelect}) {
const [cards, setCards] = useState([]);
useEffect(() => {
(async () => {
const cards = await testCardSearch();
setCards(cards);
})();
}, []);
async function handleSearch(searchTerm) {
const searchCards = await cardSearch({name: searchTerm});
setCards(searchCards)
};
return (
<StyledDiv>
<SearchBar
handleSubmit={handleSearch}
/>
<ScrollView
handleCardSelect={handleCardSelect}
cards={cards}
/>
</StyledDiv>
);
}
function ScrollView({cards, handleCardSelect}) {
const [selected, setSelected] = useState(null);
return (
<ViewContainer>
{cards.map((card, idx) =>
<li
key={idx}
style={selected === idx ? {backgroundColor: "red"} : {backgroundColor: "blue"}}
onClick={() => {
setSelected(idx);
handleCardSelect(card);
}}
>
<Card card={card} />
</li>
)}
</ViewContainer>
);
}
The issue I am having is that calling setDisplayCard re-renders my ScrollView and eliminates its local state of the card that was selected so I am unable to highlight the active card in the list. Based on my understanding of react, I don't see why ScrollView re-renders as it does not depend on the state of displayCard. And I am not sure what approach to take to fix it. When I click on a card in the list, I expect it to highlight red.
A child component's render method will always be called, once its parent's render method is invoked. The same goes for if its props or state change.
Since you're using functional components, you could use the React.memo HOC to prevent unnecessary component re-renders.
React.memo acts similar to a PureComponent and will shallowly compare ScrollView's old props to the new props and only trigger a re-render if they're unequal:
export default React.memo(ScrollView);
React.memo also has a second argument, which gives you control over the comparison:
function areEqual(prevProps, nextProps) {
// only update if a card was added or removed
return prevProps.cards.length === nextProps.cards.length;
}
export default React.memo(ScrollView, areEqual);
If you were to use class-based components, you could use the shouldComponentUpdate life cycle method as well.
By default (stateless) components re-render under 3 conditions
It's props have changed
It's state has changed
It's parent re-renders
This behavior can be changed using either shouldComponentUpdate for components or memo for stateless-components.
// If this function returns true, the component won't rerender
areEqual((prevProps, nextProps) => prevProps.cards === nextProps.card)
export default React.memo(ScrollView, areEqual);
However I don't think this is your problem. You are using an array Index idx as your element key which can often lead to unexpected behavior.
Try to remove key={idx} and check if this fixes your issue.
So your App component is supposed to hold the state of the card the user clicked? Right now your App component is stateless. It's a functional component. Try converting it to a class component with an initial, and maintained, state.
What is the logic of your setDisplayCard()?
I've heard that in React 16? there is something like 'useState()' and 'hooks', but I'm not familiar with it.
This person seemed to be having a similar problem,
React functional component using state

Categories

Resources