Keep Component from Rerendering with Custom Hooks - reactjs

I have a custom hook useWindowSize that listens to changes of the window size. Once the window size is below a certain threshold, some text should disappear in the Menu. This is implemented in another custom hook useSmallWindowWidth that takes the returned value of useWindowSize and checks whether it is smaller than the threshold.
However, even though only the nested state changes, my component rerenders constantly when the window size changes. Rerendering the menu takes about 50ms on average which adds up if I want to make other components responsive as well.
So how could I keep the component from rerendering? I tried React.memo by passing return prev.smallWindow === next.smallWindow; inside a function, but it didnt work.
My attempt so far:
//this hook is taken from: https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react
function useWindowSize() {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
}
function useSmallWindowWidth() {
const [width] = useWindowSize();
const smallWindowBreakpoint = 1024;
return width < smallWindowBreakpoint;
}
function Menu() {
const smallWindow = useSmallWindowWidth();
return (
<div>
<MenuItem>
<InformationIcon/>
{smallWindow && <span>Information</span>}
</MenuItem>
<MenuItem>
<MenuIcon/>
{smallWindow && <span>Menu</span>}
</MenuItem>
</div>
);
}

You can try wrapping all your JSX inside a useMemo
function App() {
return useMemo(() => {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}, []);
}
Put in the array in the second argument of the useMemo what variable should make your jsx rerender. If an empty array is set (like in the exemple), the jsx never rerenders.

Related

React - Resetting children state when parent changes its state in functional components

I'm working with a list of notes in React Native, and I was using a bad-performant method to select/deselect the notes when I'm "edit mode". Everytime I selected a note, the application had to re-render the entire list everytime. If I do a test with 100 notes, I get input lags when I select/deselect a note, obviously.
So I decided to move the "select state" to the single Child component. By doing this, I'm having the re-render only on that component, so it's a huge improvement of performance. Until here, everything's normal.
The problem is when I'm disabling edit mode. If I select, for example, 3 notes, and I disable the "edit mode", those notes will remain selected (indeed also the style will persist). I'd like to reset the state of all the selected note, or finding a valid alternative.
I recreated the scene using React (not React Native) on CodeSandbox with a Parent and a Child: https://codesandbox.io/s/loving-field-bh0k9k
The behavior is exactly the same. I hope you can help me out. Thanks.
tl;dr:
Use-case:
Go in Edit Mode by selecting a note for .5s
Select 2/3 elements by clicking on them
Disable Edit Mode by selecting a note for .5s
Expectation: all elements get deselected (state of children resetted)
Reality: elements don't get deselected (state of children remains the same)
this is easy enough to do with a useEffect hook.
It allows you to "watch" variable changes over time.
When editMode changes the contents of the Effect hook runs, so when editMode goes from true to false, it will set the item's selected state.
Add this to your <Child /> component:
useEffect(() => {
if (!editMode) {
setSelected(false);
}
}, [editMode]);
If you use React.memo you can cache the Child components and prevent their re-renders.
const Parent = () => {
const [editMode, setEditMode] = useState(false);
const [childrenList, setChildrenList] = useState(INITIAL_LIST);
const [selected, setSelected] = useState([]);
const toggleEditMode = useCallback(() => {
if (editMode) {
setSelected([]);
}
setEditMode(!editMode);
}, [editMode]);
const deleteSelectedChildren = () => {
setChildrenList(childrenList.filter((x) => !selected.includes(x.id)));
setEditMode(false);
};
const onSelect = useCallback((id) => {
setSelected((prev) => {
if (prev.includes(id)) {
return prev.filter((x) => x !== id);
}
return [...prev, id];
});
}, []);
// Check when <Parent /> is re-rendered
console.log("Parent");
return (
<>
<h1>Long press an element to enable "Edit Mode"</h1>
<ul className="childrenWrapper">
{childrenList.map((content, index) => (
<Child
key={content.id}
index={index}
content={content}
editMode={editMode}
toggleEditMode={toggleEditMode}
onSelect={onSelect}
selected={selected.includes(content.id)}
/>
))}
</ul>
{editMode && (
<button onClick={deleteSelectedChildren}>DELETE SELECTED</button>
)}
</>
);
};
You have to wrap the functions you pass as props inside useCallback, otherwise they will be different on every Parent render, invalidating the memoization.
import { useRef, memo } from "react";
const Child = memo(
({ content, editMode, toggleEditMode, onSelect, selected }) => {
// Prevent re-rendering when setting timer thread
const timerRef = useRef();
// Toggle selection of the <Child /> and update selectedChildrenIndexes
const toggleCheckbox = () => {
if (!editMode) return;
onSelect(content.id);
};
// Toggle Edit mode after .5s of holding press on a Child component
const longPressStartHandler = () => {
timerRef.current = setTimeout(toggleEditMode, 500);
};
// Release setTimeout thread in case it's pressed less than .5s
const longPressReleaseHandler = () => {
clearTimeout(timerRef.current);
};
// Check when <Child /> is re-rendered
console.log("Child - " + content.id);
return (
<li
className={`childContent ${editMode && "editMode"} ${
selected && "selected"
}`}
onMouseDown={longPressStartHandler}
onMouseUp={longPressReleaseHandler}
onClick={toggleCheckbox}
>
<pre>
<code>{JSON.stringify(content)}</code>
</pre>
{editMode && (
<input type="checkbox" onChange={toggleCheckbox} checked={selected} />
)}
</li>
);
}
);
You can see a working example here.

React useState hook (and useEffect) not working in callback function

Having tried useState, it's functiional variation and attempt with useEffect (not allowed in a callback function).
I am so stuck.
I have a parent 'Table' component, which renders a TableHead child and a TableBody child.
The TableHead child has a checkbox, which when clicked executes a callback function on the parent.
At this point the boolean selectAll (from a useState setter and value), is supposed to toggle (change value).
But it remains in it's initial state.
the result is that the first time the header checkbox for selectall, does fire and the re-render does show all the rows in the body as checked, but then unchecking the 'selectAll' does fire the callback, but the 'selectAll' remains false and all the rows remain checked.
Parent component Code:
function OTable(props) {
const [selectAll, setSelectAll] = useState(false);
const onAllRowsSelected = useCallback(() => {
if (selectAll === false)
{
setSelectAll(selectAll => selectAll = true);
}
else
{
setSelectAll(selectAll => selectAll = false);
}
}
return (
<TableContainer>
<Table>
<OTableHead
onSelectAllClick={onAllRowsSelected}
setSelectAll={setSelectAll}
/>
<OTableBody
selectAll={selectAll}
/>
</Table>
</TableContainer>
How to do it?
Thanks
If I make a couple of reasonable assumptions (for instance, that you have the closing ) on the useCallback call), your code for toggling selectAll works, though from your use of useCallback I suspect it doesn't quite work the way you want it to. Here's your code with those assumptions:
const {useState, useCallback} = React;
const TableContainer = ({children}) => {
return <div>{children}</div>;
};
const Table = ({children}) => {
return <div>{children}</div>;
};
const OTableHead = ({onSelectAllClick, children}) => {
console.log(`OTableHead is rendering`);
return <div>
<input type="button" onClick={onSelectAllClick} value="Select All" />
<div>{children}</div>
</div>;
};
const OTableBody = ({selectAll}) => {
return <div>selectAll = {String(selectAll)}</div>;
};
function OTable(props) {
const [selectAll, setSelectAll] = useState(false);
const onAllRowsSelected = useCallback(() => {
if (selectAll === false)
{
setSelectAll(selectAll => selectAll = true);
}
else
{
setSelectAll(selectAll => selectAll = false);
}
});
return (
<TableContainer>
<Table>
<OTableHead
onSelectAllClick={onAllRowsSelected}
setSelectAll={setSelectAll}
/>
<OTableBody
selectAll={selectAll}
/>
</Table>
</TableContainer>
);
}
ReactDOM.render(
<OTable />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
But some things stand out:
You're not passing any dependency array to useCallback. That doesn't do anything useful, because useCallback will always return the new function you pass it. I suspect you meant to have an empty dependency array on it so that it always reused the first function (to avoid unnecessary re-rendering of OTableHead).
You're using the callback form of setSelectAll, but you're using hardcoded values (true and false). This code:
const onAllRowsSelected = useCallback(() => {
if (selectAll === false)
{
setSelectAll(selectAll => selectAll = true);
}
else
{
setSelectAll(selectAll => selectAll = false);
}
});
does exactly what this code would do (given that we know that selectAll is a boolean to start with, it would be very subtly different if we didn't know that):
const onAllRowsSelected = useCallback(() => {
setSelectAll(!selectAll);
});
because the if uses the version of selectAll that the function closes over, not the parameter the callback received. (setSelectAll(selectAll => selectAll = false); is functionally identical to setSelectAll(() => false), assigning to the parameter doesn't have any effect.) And in turn, that code is the same as this:
const onAllRowsSelected = () => {
setSelectAll(!selectAll);
};
But I suspect you used the callback version for the same reason you used useCallback.
The code doesn't succeed in avoiding having the re-rendering, as you can see from the console.log I added to OTableHead above.
useCallback is useful for avoiding making child elements re-render if the callback hasn't really changed, by memoizing the callback. Here's how you'd use it correctly in that code
Pass an empty dependencies array to useCallback so it only ever returns the first callback you define.
Use the parameter value that the function version of setSelectAll passes your callback.
Ensure that the component you want to have not re-render if the callback didn't change implements checks on its properties and doesn't re-render when they haven't changed. With a function component like OTableHead you can do that just by passing it through React.memo.
Here's the example above with those changes:
const {useState, useCallback} = React;
const TableContainer = ({children}) => {
return <div>{children}</div>;
};
const Table = ({children}) => {
return <div>{children}</div>;
};
// *** Use `React.memo`:
const OTableHead = React.memo(({onSelectAllClick, children}) => {
console.log(`OTableHead is rendering`);
return <div>
<input type="button" onClick={onSelectAllClick} value="Select All" />
<div>{children}</div>
</div>;
});
const OTableBody = ({selectAll}) => {
return <div>selectAll = {String(selectAll)}</div>;
};
function OTable(props) {
const [selectAll, setSelectAll] = useState(false);
const onAllRowsSelected = useCallback(() => {
// Callback version, using the parameter value
setSelectAll(selectAll => !selectAll);
}, []); // <=== Empty dependency array
return (
<TableContainer>
<Table>
<OTableHead
onSelectAllClick={onAllRowsSelected}
setSelectAll={setSelectAll}
/>
<OTableBody
selectAll={selectAll}
/>
</Table>
</TableContainer>
);
}
ReactDOM.render(
<OTable />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
If you aren't worried about unnecessary re-rendering, then you can get rid of useCallback entirely and just do this:
const onAllRowsSelected = () => {
setSelectAll(!selectAll);
};
You don't need useCalback in this case. Just update setState like this:
const onAllRowsSelected = () => {
setSelectAll((preState) => !preState);
};
Yes, keeps the inital value because useCallback is a memoization and, if you don't add the state dependencies, it keeps the initial value (due to the memoization itself). To solve, just put selectAll as useCallback dependencies:
const onAllRowsSelected = useCallback(() => {
setSelectAll((prev) => !prev)
}, [selectAll])
There is no need to memoize the state updater function since React guarantees it to be a stable reference.
useState
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
You can simplify your callback to just update the state using the functional update.
const onAllRowsSelected = () => setSelectAll(all => !all);
If OTableHead requires that onSelectAllClick prop be a stable reference, then the useCallback can be used with an empty dependency array in order to provide a stable onAllRowsSelected callback reference. Note: this doesn't effect the ability for onAllRowsSelected to correctly toggle from the previous state value, it's only to provide a stable callback reference to children components.
useCallback
useCallback will return a memoized version of the callback that only
changes if one of the dependencies has changed. This is useful when
passing callbacks to optimized child components that rely on reference
equality to prevent unnecessary renders (e.g.
shouldComponentUpdate).
const onAllRowsSelected = useCallback(
() => setSelectAll(all => !all),
[],
);

How to control the order of rendering components in React?

There is a page contains a booking list and a popup window(A modal with survey questions). To reduce the impact on booking list loading time, I want to render the modal component after the booking list be completely loaded.
ps.there are network data request in both <BookingList/> and <Modal/>.
How should I do with React?
Thanks for help.
export default function Body() {
return (
<>
<BookingList .../>
<Modal .../>
</>
);
}
You can conditionally render the Modal component after the BookingList fetches the results.
For that, you'd need a state variable in the parent component of BookingList and Modal. Code sandbox https://codesandbox.io/s/romantic-cookies-zrpit
import React, { useEffect, useState, useCallback } from "react";
const BookingList = ({ setLoadedBookingList }) => {
useEffect(() => {
setTimeout(() => {
setLoadedBookingList();
}, 1000);
}, [setLoadedBookingList]);
return <h2>BookingList</h2>;
};
const Modal = () => <h1>Modal</h1>;
export default function App() {
const [loadBookingList, setLoadBookingList] = useState(false);
const setLoadedBookingList = useCallback(() => {
setLoadBookingList(true);
}, []);
return (
<div className="App">
<BookingList setLoadedBookingList={setLoadedBookingList} />
{loadBookingList && <Modal />}
</div>
);
}
You can use react hooks useState() and useEffect():
export default function Body() {
// `isLoaded` is a variable with false as initial value
// `setLoaded` is a method to modify the value of `isLoaded`
const [isLoaded, setLoaded] = React.useState(false)
React.useEffect(() => {
// some method used in loading the resource,
// after completion you can set the isLoaded to true
loadBookingList().then(loaded => setLoaded(true));
},
// second argument to `useEffect` is the dependency which is out `setLoaded` function
[setLoaded]);
return (
<>
// conditional rendering based on `isLoaded`
{isLoaded && <BookingList .../>}
<Modal .../>
</>
);
}

Why useState in React Hook not update state

When i try example from React Hook, i get a problem about useState.
In code below, when click button, i add event for document and check value of count.
My expect is get count in console.log and view as the same. But actual, i got old value (init value) in console & new value in view . I can not understand why count in view changed and count in callback of event not change.
One more thing, when i use setCount(10); (fix a number). And click button many time (>2), then click outside, i got only 2 log from checkCount. Is it React watch count not change then don't addEventListener in next time.
import React, { useState } from "react";
function Example() {
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1);
console.log("value set is ", count);
document.addEventListener("click", checkCount);
};
const checkCount = () => {
console.log(count);
};
return (
<div>
<p>You clicked {count} times</p>
<p>Click button first then click outside button and see console</p>
<button onClick={() => add()}>Click me</button>
</div>
);
}
export default Example;
If you want to capture events outside of your component using document.addEventListener, you will want to use the useEffect hook to add the event, you can then use the useState to determine if your capturing or not.
Notice in the useEffect I'm passing [capture], this will make it so the useEffect will get called when this changes, a simple check for this capture boolean determines if we add the event or not.
By using useEffect, we also avoid any memory leaks, this also copes with when your unmount the component, it knows to remove the event too.
const {useState, useEffect} = React;
function Test() {
const [capture, setCapture] = useState(false);
const [clickInfo, setClickInfo] = useState("Not yet");
function outsideClick() {
setClickInfo(Date.now().toString());
}
useEffect(() => {
if (capture) {
document.addEventListener("click", outsideClick);
return () => {
document.removeEventListener("click", outsideClick);
}
}
}, [capture]);
return <div>
<p>
Click start capture, then click anywhere, and then click stop capture, and click anywhere.</p>
<p>{capture ? "Capturing" : "Not capturing"}</p>
<p>Clicked outside: {clickInfo}</p>
<button onClick={() => setCapture(true)}>
Start Capture
</button>
<button onClick={() => setCapture(false)}>
Stop Capture
</button>
</div>
}
ReactDOM.render(<React.Fragment>
<Test/>
</React.Fragment>, document.querySelector('#mount'));
p { user-select: none }
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="mount"></div>
#Keith i understand your example but when i apply get some confuse. In origin, i always call function is handleClick and still call it after run handleClickOutside but now i don't know how to apply that with hook.
This is my code that i want insted of Hook
class ClickOutSide extends Component {
constructor(props) {
super(props)
this.wrapperRef = React.createRef();
this.state = {
active: false
}
}
handleClick = () => {
if(!this.state.active) {
document.addEventListener("click", this.handleClickOut);
document.addEventListener("contextmenu", this.handleClickOut);
this.props.clickInside();
} else {
document.removeEventListener("click", this.handleClickOut);
document.removeEventListener("contextmenu", this.handleClickOut);
}
this.setState(prevState => ({
active: !prevState.active,
}));
};
handleClickOut = event => {
const { target } = event;
if (!this.wrapperRef.current.contains(target)) {
this.props.clickOutside();
}
this.handleClick()
}
render(){
return (
<div
onDoubleClick={this.props.onDoubleClick}
onContextMenu={this.handleClick}
onClick={this.handleClick}
ref={this.wrapperRef}
>
{this.props.children}
</div>
)
}
}
export default ClickOutSide

react initial state calculated from DOM

I am using https://www.npmjs.com/package/react-slick and I'm trying to make it est "slidesToShow" dynamically based on available width.
I've managed to get it working, but only after window resize. I need to supply it with an initial state calculated from an element width. The problem is that the element doesn't exist at that point.
This is my code (the interesting parts):
const Carousel = (props: ICarouselProps) => {
const slider = useRef(null);
const recalculateItemsShown = () => {
const maxItems = Math.round(
slider.current.innerSlider.list.parentElement.clientWidth / 136, // content width is just hardcoded for now.
);
updateSettings({...props.settings, slidesToShow: maxItems});
};
useEffect(() => {
window.addEventListener('resize', recalculateItemsShown);
return () => {
window.removeEventListener('resize', recalculateItemsShown);
};
});
const [settings, updateSettings] = useState({
...props.settings,
slidesToShow: //How do I set this properly? slider is null here
});
return (
<div id="carousel" className="carousel">
<Slider {...settings} {...arrows} ref={slider}>
<div className="test-content/>
<div className="test-content/>
/* etc. */
<div className="test-content/>
<div className="test-content/>
<div className="test-content/>
</Slider>
</div>
);
};
export default Carousel;
if I call updateSettings in my useEffect I get an infinite loop.
So I would have to either:
Somehow get the width of an element that hasn't yet been created
or
call updateSettings once after the very first render.
You could have a function that returns maxItems and use that everywhere, so:
const getMaxItems = () => Math.round(slider.current.innerSlider.list.parentElement.clientWidth / 136)
You use its return result within recalculateItemsShown to update the settings.
const recalculateItemsShown = () => {
updateSettings({...props.settings, slidesToShow: getMaxItems()});
};
And you also use its return value to set the state initially.
const [settings, updateSettings] = useState({
...props.settings,
slidesToShow: getMaxItems()
});
If the element doesn't exist initially, you could use useEffect with an empty array as the second argument. This tells useEffect to watch changes to that array and call it everytime it changes, but since its an empty array that never changes, it will only run once - on initial render.
useEffect(() => {
updateSettings({...props.settings, slidesToShow: getMaxItems()});
}, []);
You can read more about skipping applying events here: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
I believe the uselayouteffect hook is designed for this exact use case.
https://reactjs.org/docs/hooks-reference.html#uselayouteffect
It works exactly the same way as useEffect except that it fires after the dom loads so that you can calculate the element width as needed.

Resources