How to debounce with a shared state on a MUI range slider - javascript

The initial problem was that with the range slider from MUI it kept calling the function the setter is linked to. So I've added debounce from lodash to reduce that and that kind of worked. But what I was reducing was how frequently I'm calling the setter. Which also happened to be setting the value of the slider so now the UI is slow because of my debounce. I need that setter because it's sharing state with a different component. So I was thinking having yet another useState to just take care of the value where I would "setSliderDisplayValue" and do my debounce which is in a useMemo. Maybe this is a bad way of doing this but I also couldn't figure out having an onChange handler that calls a setter and calls a useMemo who has a debounce in it. If anyone has any other idea how I can get the ui to still be super responsive and not have to call the setter a tons of time while sliding the slider that would be amazing.
I have a range slider and here is how I use the debounce:
const handleChange = (event: Event, newValue: number | number[]) => {
// This setter comes from a parent component because its value needs to be used in two places at the same time(see codesandbox).
props.setDisplayValue(newValue as number[]);
console.log(newValue);
};
const debouncedChangeHandler = React.useMemo(() => {
debounce(handleChange, 300, { leading: false, trailing: true });
}, []);
Then here is the slider:
<Slider
defaultValue={props.value}
value={props.value}
onChange={debouncedChangeHandler}
valueLabelDisplay="on"
getAriaValueText={valuetext}
/>
Here is the codesandbox, this is a smaller example(look inside demo.tsx): https://codesandbox.io/s/range-slider-gfip2n?file=/demo.tsx:794-816
Any help is greatly appreciated

Related

Is the old `setX` value from `useState` still valid after the state is updated? [duplicate]

Is useState's setter able to change during a component life ?
For instance, let's say we've got a useCallback which will update the state.
If the setter is able to change, it must be set as a dependency for the callback since the callback use it.
const [state, setState] = useState(false);
const callback = useCallback(
() => setState(true),
[setState] // <--
);
The setter function won't change during component life.
From Hooks FAQ:
(The identity of the setCount function is guaranteed to be stable so it’s safe to omit.)
The setter function (setState) returned from useState changes on component re-mount, but either way, the callback will get a new instance.
It's a good practice to add state setter in the dependency array ([setState]) when using custom-hooks. For example, useDispatch of react-redux gets new instance on every render, you may get undesired behavior without:
// Custom hook
import { useDispatch } from "react-redux";
export const CounterComponent = ({ value }) => {
// Always new instance
const dispatch = useDispatch();
// Should be in a callback
const incrementCounter = useCallback(
() => dispatch({ type: "increment-counter" }),
[dispatch]
);
return (
<div>
<span>{value}</span>
// May render unnecessarily due to the changed reference
<MyIncrementButton onIncrement={dispatch} />
// In callback, all fine
<MyIncrementButton onIncrement={incrementCounter} />
</div>
);
};
The short answer is, no, the setter of useState() is not able change, and the React docs explicitly guarantee this and even provide examples proving that the setter can be omitted.
I would suggest that you do not add anything to the dependencies list of your useCallback() unless you know its value can change. Just like you wouldn't add any functions imported from modules or module-level functions, constant expressions defined outside the component, etc. adding those things is just superfluous and makes it harder to read your handlers.
All that being said, this is all very specific to the function that is returned by useState() and there is no reason to extend that line of reasoning to every possible custom hook that may return a function. The reason is that the React docs explicitly guarantee the stable behavior of useState() and its setters, but it does not say that the same must be true for any custom hook.
React hooks are still kind of a new and experimental concept and we need to make sure we encourage each other to make them as readable as possible, and more importantly, to understand what they actually do and why. If we don't it will be seen as evidence that hooks are a "bad idea," which will prohibit adoption and wider understanding of them. That would be bad; in my experience they tend to produce much cleaner alternatives to the class-based components that React is usually associated with, not to mention the fact that they can allow organizational techniques that simply aren't possible with classes.

How does this handleClick function in React retrieve previous state value?

This codepen toggles a button value from true to false.
I understand this apart from how this handleClick function is working:
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
Can someone please break this down and explain in simple terms how the function retrieves the isToggleOn bool value?
I know we can't directly use !this.state.isToggleOn in setState but can someone kindly explain in simple terms why this handleClick function is more reliable in this scenario for a React newbie?
This is where the handleClick function is called:
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
From the setState docs:
The first argument is an updater function with the signature:
(state, props) => stateChange
state is a reference to the component state at the time the change is being applied.
In your example prevState is state renamed, and it contains all of the assignments in state, including isToggleOn.
You could alternately just read this.state in the setState function. The prevState pattern is used to communicate that this is the old/current state, the state that is being changed.
setState docs
In this scenario you can actually use both, because it's really simple and you call the setState from the render function. In class components is a bit hard to find a place where this this.setState({oldState:this.state.oldState}) can be problematic.
On the contrary, if you use functional components or hooks, you can see this problem quite often.
JS is not Object oriented!
To understand this you have to think first that js is not an object oriented language. React tries just to simulate it. Basically when you change a state, the class containing it gets called again trying to run only the code which changes. In your case, when handleClick is called, the render function get triggered because the state has changed!
Said this, handleClick is not just a function! It's also a property of the class. React tries to understand whether it should "re-declare" those functions depending on the values they use.
Good Practice
In the case you wrote, isToggleOn: !prevState.isToggleOn, the function is indipendent. It doesn't need any other property of the class (apart of setState, which works with a different pattern), so after the state changes, the react engine doesn't have to set the function again.
Bad Practice (sometimes much much easyer to write)
On the contrary if you use isToggleOn: !this.state.isToggleOn the handleClick function is dependent of this.state so every time it changes, the react engine has to "re-declare" the function by sort of substituting this.state with {isToggleOn:...}
How to recreate that pattern
function myRandom(property){
//Kind of private Variable
let hiddenVar=Math.random();
return property(hiddenVar);
}
//Let's say here the Math class is unreachable while myRandom is.
myRandom((num)=>{
console.log(num);//This ac
})
Maybe just check the docs on this?
The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered. Generally we recommend using componentDidUpdate() for such logic instead.
In essence, you are getting the previous instance of the state in the function, then making a change to the state and returning it. This new state will get saved and cause a re-render of your component.
If you use functional components, you can negate having to pass a callback here and could do something like:
function MyComponent() {
const [on, setOn] = React.useState(false)
return (
<button onClick={() => setOn(!on)}>
{on? 'ON' : 'OFF'}
</button>
);
}

What counts as mutating state in React?

Background is at the top, my actual question is simple enough, at the bottom, but I provided the context in case I'm going about this totally wrong and my question turns out to not even be relevant. I have only been using react for about two weeks.
What I'm trying to do is create a singleton, re-usable backdrop that can be closed either by clicking it, or by clicking a control on the elements that use a backdrop. This is to avoid rendering multiple backdrops in multiple places in the DOM (e.g. grouping a backdrop with each different type of modal, side drawer or content preview) or have multiple sources of truth for the state of the backdrop.
What I've done is create the Backdrop itself, which is not exported
const Backdrop = props => (
props.show ? <div onClick={props.onClose} className={classes.Backdrop}></div> : null
);
I've also created a backdrop context, managed by a WithBackdrop higher order class component which manages the state of the backdrop and updates the context accordingly
class WithBackdrop extends Component {
state = {
show: true,
closeListeners: []
}
show() {
this.setState({ show: true });
}
hide() {
this.state.closeListeners.map(f => f());
this.setState({ show: false, closeListeners: [] });
}
registerCloseListener(cb) {
// this.setState({ closeListeners: [...this.state.closeListeners, cb]});
// Does this count as mutating state?
this.state.closeListeners.push(cb);
}
render() {
const contextData = {
isShown: this.state.show,
show: this.show.bind(this),
hide: this.hide.bind(this),
registerCloseListener: this.registerCloseListener.bind(this)
};
return (
<BackdropContext.Provider value={contextData}>
<Backdrop show={this.state.show} onClose={this.hide.bind(this)} />
{this.props.children}
</BackdropContext.Provider>
);
}
}
export default WithBackdrop;
I've also exported a 'backdropable' HOC which wraps a component with the context consumer
export const backdropable = Component => (props) => (
<BackdropContext.Consumer>
{value => <Component {...props} backdropContext={value}/>}
</BackdropContext.Consumer>
);
The usage of this API would be as follows: Wrap the part of your Layout/App that you want to potentially have a backdrop, and provide the context to any component that would activate a backdrop. 'Backdropable' is a just a lazy word I used for 'can trigger a backdrop' (not shown here, but I'm using TypeScript and that makes a little more sense as an interface name). Backdropable components can call show() or hide() and not have to worry about other components which may have triggered the backdrop, or about multiple sources of truth about the backdrop's state.
The last problem I had, however, was how to trigger a backdropable components close handler? I decided the WithBackdrop HOC would maintain a list of listeners so that components that need to react when the backdrop is closed by clicking the backdrop (rather than by that backdropable component's close button or something). Here is the modal component I'm using to test this
const modal = (props) => {
props.backdropContext.registerCloseListener(props.onClose);
return (
<div
className={[
classes.Modal,
(props.show ? '' : classes.hidden)
].join(' ')}>
{props.children}
<button onClick={() => {
props.onClose();
props.backdropContext.hide()
}}>Cancel</button>
<button onClick={props.onContinue}>Continue</button>
</div>
)
}
export default backdropable(modal);
As far as I understand, it is best practice to never mutate state. My question is, does pushing to an array maintained in state count as mutating state, and what potentially bad consequences should I expect from this? Should I copy the array into a new array with the new element every single time, or will I only get undefined React behaviour if I try to change the reference of a state member. As far as I understand react only shallowly compares previous and next state to determine re-renders and provides utilities for more complicated comparisons, and so this should be fine right? The reason is that the array copying method triggers a re-render, then the modal tries to re-register the closeListener, then WithBackdrop tries to add it again...and I get an infinite state update loop.
Even if there is nothing wrong with simply pushing to the same array, do you think there is a better way to go about doing this?
Thanks, I sincerely appreciate the efforts anyone who tries to answer this long question.
EDIT: this.setState({ closeListeners: [...this.state.closeListeners, cb]}); results in an infinite state-update loop.
Mutating state in React is when you change any value or referenced object in state without using setState.
As far as I understand, it is best practice to never mutate state. My
question is, does pushing to an array maintained in state count as
mutating state,
Yes
and what potentially bad consequences should I expect from this?
You can expect to change the value of state and not see the ui update.
Should I copy the array into a new array with the new element every
single time,
Yes:
const things = [...this.state.things]
// change things
this.setState({ things })
or will I only get undefined React behaviour if I try to
change the reference of a state member. As far as I understand react
only shallowly compares previous and next state to determine
re-renders and provides utilities for more complicated comparisons,
and so this should be fine right?
It will compare if you call setState and update if necessary. If you do not use setState, it won't even check.
Any changes directly to the state (without setState()) = mutating the state. In your case it is this line:
this.state.closeListeners.push(cb);
As #twharmon mentioned, you change the values in the memory but this does not trigger the render() of your component, but your component will eventually updated from the parent components leading to ugly and hard to debug side effects.
The solution for your problem using destructuring assignment syntax:
this.setState({
closeListeners: [...this.state.closeListeners, cb]
});
PS: Destructuring also helps to keep your code cleaner:
const Backdrop = ({ show, onClose }) => (
show ? <div onClick={onClose} className={classes.Backdrop}></div> : null
);

Calling React setState hook from outside react (DOM/Google Maps)

I'm building a function React component which uses various useState() hooks. Each of them providing a state and a function which can be called to update it. This works beautifully from within react itself. But in my scenario I have to deal with other (DOM/Google Maps) environments as well.
As a callback from a DOM element put on the Google map, I want to call setState(!state) to flip a boolean. However, this works only once.
I think that the problem is that the setState hook fails to fetch the latest state but uses the initial state instead. Flipping a bool 1 will invert it, but the flipping it again without taking the former change into account does not update anything.
I've managed to solve this by implementing a state that sets the boolean on a data attribute in the DOM (and then flip that bool) but I think that's a rather ugly solution.
How should I update the state in functional React component from a callback function provided by something not React?
You'll want to use functional updates.
const [bool, setBool] = React.useState(false);
// Flip bool depending on latest state.
setBool((prevBool) => !prevBool);
As opposed to using the latest state in the component itself, which can use the wrong state depending on the memoization / state life cycle:
const [bool, setBool] = React.useState(false);
// bool could be behind here. DON'T do this.
setBool(!bool);
If the problem is setState using the wrong state, you can pass a function to setState instead:
setState(state => !state);
This will use the latest state instead of the state which occurred at the React render. Not sure how this will play with the weird outside-of-React situation here, but it may help out. If this page isn't even using React (the component with the state you want to edit isn't even rendered) then HTML LocalStorage might be your best bet for persisting information.

Refactoring a React PureComponent to a hooks based functional component

I have a working class based implementation of an Accordion component which I'm trying to refactor to use the new hooks api.
My main challenge is to find a way to re-render only the toggled <AccordionSection /> while preventing all the other <AccordionSection/> components from re-rendering every time the state of the parent <Accordion/> (which keeps track of the open sections on its state) is updated.
On the class-based implementation I've managed to achieve this by making the <AccordionSection /> a PureComponent, passing the isOpen and onClick callbacks to it via a higher-order component which utilizes the context API, and by saving these callbacks on the parent <Accordion/>'s component's state as follows:
this.state = {
/.../
onClick: this.onClick,
isOpen: this.isOpen
};
which, to my understanding, keeps the reference to them and thus prevents them from being created as new instances on each <Accordion /> update.
However, I can't seem to get this to work with the hooks-based implementation.
Some of the things I've already tried to no success:
Wrapping the Accordion section with memo - including various render conditions on the second callback argument.
wrapping the onClick and isOpen callbacks with useCallback (doesn't seem to work since they have dependencies which update on each <Accordion/> render)
saving the onClick and isOpen to the state like this: const [callbacks] = useState({onClick, isOpen}) and then passing the callbacks object as the ContextProvider value. (seems wrong, and didn't work)
Here are the references to my working class-based implementation:
https://codesandbox.io/s/4pyqoxoz9
and my hooks refactor attempt:
https://codesandbox.io/s/lxp8xz80z7
I kept the logs on the <AccordionSection/> render in order to demonstrate which re-renders I'm trying to prevent.
Any inputs will be very appreciated.
so I ended up adding this little nugget after chasing too many rabbits..
const cache = {};
const AccordionSection = memo(({ children, sectionSlug, onClick, isOpen }) => {
if (cache[sectionSlug]) {
console.log({
children: children === cache[sectionSlug].children,
sectionSlug: sectionSlug === cache[sectionSlug].sectionSlug,
onClick: onClick === cache[sectionSlug].onClick,
isOpen: isOpen === cache[sectionSlug].isOpen
});
}
cache[sectionSlug] = { children, sectionSlug, onClick, isOpen };
This showed that it was onClick that was changing. Which then seems obvious as the Accordion component is rendering and creating a new onClick.
wrapping he onClick creation with useCallback rectifies the issue.
const onClick = useCallback(
sectionSlug =>
setOpenSections({
...(exclusive ? {} : openSections),
[sectionSlug]: !openSections[sectionSlug]
}),
[]
);
though I do seem to have broken exclusive in the process as it's always enabled now..
https://codesandbox.io/s/1o08p08m27
oh, I did move a few other pieces around in there that might have contributed to the fix..
Update
refactored to use useReducer and moved all the logic there so we can deliver a stable onClick
Update
they say sleep is good, but for me it's just trying to get to sleep..
I knew there was something I was missing.. realised last night we don't need the reducer, just the function form of setState which allows us to access the up-to-date state from within the useCallback memoed function. Converted #itaydafna's optimisation here https://codesandbox.io/s/8490v55029

Categories