Refactoring a React PureComponent to a hooks based functional component - javascript

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

Related

How does the splice work in Function Component(hooks) in React? [duplicate]

I understand that React tutorials and documentation warn in no uncertain terms that state should not be directly mutated and that everything should go through setState.
I would like to understand why, exactly, I can't just directly change state and then (in the same function) call this.setState({}) just to trigger the render.
E.g.: The below code seems to work just fine:
const React = require('react');
const App = React.createClass({
getInitialState: function() {
return {
some: {
rather: {
deeply: {
embedded: {
stuff: 1,
},
},
},
},
},
};
updateCounter: function () {
this.state.some.rather.deeply.embedded.stuff++;
this.setState({}); // just to trigger the render ...
},
render: function() {
return (
<div>
Counter value: {this.state.some.rather.deeply.embedded.stuff}
<br></br>
<button onClick={this.updateCounter}>Increment</button>
</div>
);
},
});
export default App;
I am all for following conventions but I would like to enhance my further understanding of how ReactJS actually works and what can go wrong or is it sub-optimal with the above code.
The notes under the this.setState documentation basically identify two gotchas:
That if you mutate state directly and then subsequently call this.setState this may replace (overwrite?) the mutation you made. I don't see how this can happen in the above code.
That setState may mutate this.state effectively in an asynchronous / deferred way and so when accessing this.state right after calling this.setState you are not guaranteed to access the final mutated state. I get that, by this is not an issue if this.setState is the last call of the update function.
This answer is to provide enough information to not change/mutate the state directly in React.
React follows Unidirectional Data Flow. Meaning, the data flow inside react should and will be expected to be in a circular path.
React's Data flow without flux
To make React work like this, developers made React similar to functional programming. The rule of thumb of functional programming is immutability. Let me explain it loud and clear.
How does the unidirectional flow works?
states are a data store which contains the data of a component.
The view of a component renders based on the state.
When the view needs to change something on the screen, that value should be supplied from the store.
To make this happen, React provides setState() function which takes in an object of new states and does a compare and merge(similar to object.assign()) over the previous state and adds the new state to the state data store.
Whenever the data in the state store changes, react will trigger an re-render with the new state which the view consumes and shows it on the screen.
This cycle will continue throughout the component's lifetime.
If you see the above steps, it clearly shows a lot of things are happening behind when you change the state. So, when you mutate the state directly and call setState() with an empty object. The previous state will be polluted with your mutation. Due to which, the shallow compare and merge of two states will be disturbed or won't happen, because you'll have only one state now. This will disrupt all the React's Lifecycle Methods.
As a result, your app will behave abnormal or even crash. Most of the times, it won't affect your app because all the apps which we use for testing this are pretty small.
And another downside of mutation of Objects and Arrays in JavaScript is, when you assign an object or an array, you're just making a reference of that object or that array. When you mutate them, all the reference to that object or that array will be affected. React handles this in a intelligent way in the background and simply give us an API to make it work.
Most common errors done when handling states in React
// original state
this.state = {
a: [1,2,3,4,5]
}
// changing the state in react
// need to add '6' in the array
// bad approach
const b = this.state.a.push(6)
this.setState({
a: b
})
In the above example, this.state.a.push(6) will mutate the state directly. Assigning it to another variable and calling setState is same as what's shown below. As we mutated the state anyway, there's no point assigning it to another variable and calling setState with that variable.
// same as
this.state.a.push(6)
this.setState({})
Many people do this. This is so wrong. This breaks the beauty of React and is bad programming practice.
So, what's the best way to handle states in React? Let me explain.
When you need to change 'something' in the existing state, first get a copy of that 'something' from the current state.
// original state
this.state = {
a: [1,2,3,4,5]
}
// changing the state in react
// need to add '6' in the array
// create a copy of this.state.a
// you can use ES6's destructuring or loadash's _.clone()
const currentStateCopy = [...this.state.a]
Now, mutating currentStateCopy won't mutate the original state. Do operations over currentStateCopy and set it as the new state using setState().
currentStateCopy.push(6)
this.setState({
a: currentStateCopy
})
This is beautiful, right?
By doing this, all the references of this.state.a won't get affected until we use setState. This gives you control over your code and this'll help you write elegant test and make you confident about the performance of the code in production.
To answer your question,
Why can't I directly modify a component's state?
Well, you can. But, you need to face the following consequences.
When you scale, you'll be writing unmanageable code.
You'll lose control of state across components.
Instead of using React, you'll be writing custom codes over React.
Immutability is not a necessity because JavaScript is single threaded, but it's a good to follow practices which will help you in the long run.
PS. I've written about 10000 lines of mutable React JS code. If it breaks now, I don't know where to look into because all the values are mutated somewhere. When I realized this, I started writing immutable code. Trust me! That's the best thing you can do it to a product or an app.
The React docs for setState have this to say:
NEVER mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.
setState() will always trigger a re-render unless conditional rendering logic is implemented in shouldComponentUpdate(). If mutable objects are being used and the logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.
Basically, if you modify this.state directly, you create a situation where those modifications might get overwritten.
Related to your extended questions 1) and 2), setState() is not immediate. It queues a state transition based on what it thinks is going on which may not include the direct changes to this.state. Since it's queued rather than applied immediately, it's entirely possible that something is modified in between such that your direct changes get overwritten.
If nothing else, you might be better off just considering that not directly modifying this.state can be seen as good practice. You may know personally that your code interacts with React in such a way that these over-writes or other issues can't happen but you're creating a situation where other developers or future updates can suddenly find themselves with weird or subtle issues.
the simplest answer to "
Why can't I directly modify a component's state:
is all about Updating phase.
when we update the state of a component all it's children are going to be rendered as well. or our entire component tree rendered.
but when i say our entire component tree is rendered that doesn’t mean that the entire DOM is updated.
when a component is rendered we basically get a react element, so that is updating our virtual dom.
React will then look at the virtual DOM, it also has a copy of the old virtual DOM, that is why we shouldn’t update the state directly, so we can have two different object references in memory, we have the old virtual DOM as well as the new virtual DOM.
then react will figure out what is changed and based on that it will update the real DOM accordingly .
hope it helps.
It surprises me that non of the current answers talk about pure/memo components (React.PureComponent or React.memo). These components only re-render when a change in one of the props is detected.
Say you mutate state directly and pass, not the value, but the over coupling object to the component below. This object still has the same reference as the previous object, meaning that pure/memo components won't re-render, even though you mutated one of the properties.
Since you don't always know what type of component you are working with when importing them from libraries, this is yet another reason to stick to the non-mutating rule.
Here is an example of this behaviour in action (using R.evolve to simplify creating a copy and updating nested content):
class App extends React.Component {
state = { some: { rather: { deeply: { nested: { stuff: 1 } } } } };
mutatingIncrement = () => {
this.state.some.rather.deeply.nested.stuff++;
this.setState({});
}
nonMutatingIncrement = () => {
this.setState(R.evolve(
{ some: { rather: { deeply: { nested: { stuff: n => n + 1 } } } } }
));
}
render() {
return (
<div>
Normal Component: <CounterDisplay {...this.state} />
<br />
Pure Component: <PureCounterDisplay {...this.state} />
<br />
<button onClick={this.mutatingIncrement}>mutating increment</button>
<button onClick={this.nonMutatingIncrement}>non-mutating increment</button>
</div>
);
}
}
const CounterDisplay = (props) => (
<React.Fragment>
Counter value: {props.some.rather.deeply.nested.stuff}
</React.Fragment>
);
const PureCounterDisplay = React.memo(CounterDisplay);
ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/ramda#0/dist/ramda.min.js"></script>
<div id="root"></div>
To avoid every time to create a copy of this.state.element you can use update with $set or $push or many others from immutability-helper
e.g.:
import update from 'immutability-helper';
const newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});
setState trigger re rendering of the components.when we want to update state again and again we must need to setState otherwise it doesn't work correctly.
My current understanding is based on this and this answers:
IF you do not use shouldComponentUpdate or any other lifecycle methods (like componentWillReceiveProps, componentWillUpdate, and componentDidUpdate) where you compare the old and new props/state
THEN
It is fine to mutate state and then call setState(), otherwise it is not fine.

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
);

React performance and passing down arrow functions as props

I've recently learned that passing down object literals or functions as props can cause unnecessary re-renders. As I am doing an audit on my app, I am finding some cases where I have common components that have callbacks on events that do different things. It's unclear to me what the most elegant way to handle this would be.
So for example in my app I have a component called SharedBtn that is used all over the app multiple places and in large loops. This button has an onClick listener. But in every instance this onClick is used we are passing down a different function to do a different thing every time.
Example:
https://codesandbox.io/s/k31120vnyo
I read this related article with examples. But their solution is to move the onClick logic to the shared component. This would be ugly for me as it is used in many different spots with many different handlers. How could I have the multiple click handlers without moving the click handling logic to the SharedBtn component itself?
You could create a small wrapper component for each special instance.
class IndexSharedButton extends React.PureComponent {
handleClick = e => {
this.props.onClick(e, this.props.index);
};
render() {
return <SharedBtn copy={this.props.copy} onClick={this.handleClick} />;
}
}
class AnimalSharedButton extends React.PureComponent {
handleClick = e => {
this.props.onClick(this.props.animal, this.props.type);
};
render() {
return (
<SharedBtn copy={this.props.animal} onClick={this.handleClick} />
);
}
}
You could also manually manage caching the bound handlers, but IMO that's pretty ugly and much less idiomatic.
Also, I think it goes without saying that you shouldn't worry about this much at all if you haven't measured it to be an actual issue. Re-renders don't happen that often, and with shouldComponentUpdate should happen even less. Optimizations like this add more code for little benefit.
Most of the time the performance hit is negligible. But the correct way to mitigate this in the age of hooks is via useCallback.
import { useCallback } from "react"
const MyComp = () => {
const func = useCallback(() => doSomething(), []);
return <OtherComp func={func}/>
};

Best practice for emitting an event to another component in the same container?

So I have a button that lives in a container and uses a callback on it's onClick event to update state in the container, fairly basic stuff. But now I want that container to let a different child know that the button was clicked so it can trigger the appropriate response. (and it should only let the child know once so the response isn't being triggered a million times)
The way I solved this looks and feels and quacks like a code smell, so I thought I'd ask you guys if there is a better way to do it. Here is what I did:
class myContainer extend Component {
constructor(){
super()
state= { triggered: false }
}
componentWillUpdate(nextProps, nextState){
this.hasTriggered = this.state.triggered !== nextState.triggered
}
triggerResponse = () => this.setState({...this.state, !this.state.triggered})
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.hasTriggered}/>
)
}
}
Now this seems to work perfectly fine, and maybe this is what I should do, but it just feels like there has to be a neater way of sending a simple message of "I have been clicked" to a component in the same container.
One major red flag for me is that "triggered" is a boolean, but it doesn't matter if it is true or false, so if triggered is false, it means nothing, all that matters if it was the other boolean last round. This seems like a violation of good practices to me.
*Summary: What I'm looking for is a snappy way to give state a value for just one update cycle and then go back to null or false without having to update it again. Or a different way to get the same result.
I came up with 2 different yet unsatisfying answers:
class myContainer extend Component {
constructor(){
super()
state= { hasTriggered: false }
}
shouldComponentUpdate(nextProps, nextState){
return (!nextState.hasTriggered && this.state.hasTriggered)
}
componentDidUpdate(nextProps, nextState){
if(nextState.hasTriggered)this.setState({hasTriggered: false})
}
triggerResponse = () => this.setState({hasTriggered: true})
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.state.hasTriggered}/>
)
}
}
This is unsatisfying because it is a lot of code for a very simple button click. I am setting state, sending it down, resetting state and then ignoring the next render call all to accommodate a lousy button click. The good news is that I'm no longer misusing a boolean, but this is definitely too much code.
class myContainer extend Component {
componentDidUpdate(){
this.hasTriggered = false
}
triggerResponse = () => {
this.hasTriggered = true
this.forceUpdate()
}
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.hasTriggered}/>
)
}
}
I find this method unsatisfying because I no longer have the shared state in state. Before I also did this by comparing new and old state and making a variable carry the result over to my component, but at least then I could look at my state and see that there is a state variable that has to do with this button. Now there is local state in my component that is in no way linked to the actual state, making it harder to keep track of.
After thinking about this all day I've come to the conclusion that #ShubhamKhatri was on the right track in his comment, I thought I was pulling up state to my container by using a callback and passing state down, but clearly there is too much logic being executed in my component if it's handling a click event. So my new rule is that in this kind of scenario you should just pull up whatever state you need to execute the onClick inside the container. If your dumb components are executing anything other than a callback it is a mistake.
The reason I was tempted to do the onClick logic in my presentational component is because I was using a third party library(d3) to handle the graphics and so I didn't consider that the state I wasn't pulling up was the d3 state, if I move that up, change it when the button is clicked and then pass it down to my component it works beautifully.
Now this means I need to import d3 in two places and I did have to write a bit more code than before, but I think the separation of concerns and the overall cleanliness of my code is well worth it. Also it made my child component a lot easier to maintain, so that's nice.

componentWillReceiveProps containing to many ifs

I'm still pretty new on React development, but I've already work on 3 big project using React+Redux and I see a pattern that I dislike a lot:
componentWillReceiveProps(nextProps) {
if (nextProps.params.type === TYPE_NEW_USER) {
this.modalUsername = this.props.showPopup( < NewUsernamePopup onClose = {::this.closeUsernamePopup
}
/>, USERNAME_POPUP_ID, true);
}
if (this.state.kind !== nextProps.kind || this.state.filter !== nextProps.filter || this.state.hashtags !== nextProps.hashtags) {
this.setState({
results: [],
loading: true,
kind: nextProps.kind,
filter: nextProps.filter,
hashtags: nextProps.hashtags
}, () => this.manageResults(nextProps.results, false));
} else {
this.manageResults(nextProps.results, true);
}
this.managePages(nextProps.paging);
}
I would like to avoid the ifs inside the componentWillReceiveProps. How do you handle it? We've analysed another project using Flux and callback registration. It looks like:
componentWillMount() {
EntityStore.on(EntityActions.ENTITIES_LOADED, this.getData.bind(this));
EntityActions.entitiesLoaded();
}
The first event is emitted by the component, but afterwards the store emits the event and the component updates. Additionally a single store keeps its state and do not duplicate async calls if it already has the content. I personally like to avoid the ifs, but I do NOT want to lose Redux (its community and tools).
How would you add the current logic (ifs) inside the componentWillReceiveProps outside the component? I would like to handle the logic in a service layer and not inside the component.
I would definitely appreciate to read your opinion around this, because I've been struggling to find a solutions that fits.
The redux approach is to put the logic into the actions/reducers.
So i don't know what your manageResults method does, but it is probably the piece of logic you want to move into a reducer so you won't need to call it from your component anymore.
So the kind,filter and hashtagsvariables should be updated from redux actions only.
tl;dr properly following redux best practices would eliminate some of these conditions, but I'd be more concerned about the overall design this snippet is revealing.
To address the individual lines:
if (nextProps.params.type === TYPE_NEW_USER) {
This looks like a redux action was passed to the component? If so, that's not great, only the reducers should care about action types.
this.modalUsername = this.props.showPopup(
The lifecycle hook componentWillReceiveProps is not the right place to initiate things like that, the resulting React component in an instance var also looks quite weird.
if (this.state.kind !== nextProps.kind || this.state.filter (etc.) ) {
If you have UI state in this component that is somehow dependant on the props coming from redux, these types of ifs are somewhat necessary, since you can't do it outside the component.
You are right to dislike this "pattern", which seems to reflect bad overall design. This component seems to be involved with "pages", "results", a username, and some ajax fetching with a loading flag. Can only speculate of course, but it seems like it's doing too much. The ajax request lifecycle should definitely be modelled in a reducer.
That said, the lifecycle hooks do often contain a bunch of ifs, since the reducers don't see routing and which components get mounted/unmounted, so that's where you have to react to changing props sometimes.

Categories