in React app profiling child re-rendering with memo - reactjs

In the profiling result the component (Table) uses React.memo and it is shown that did not re-render
but underneath there is shown the same Table component and in Why did this render is mentioned that parent re-rendered
The Table uses memo this way and the querystring stays the same.
function areEqal(prevTable, nextTable) {
return prevTable.queryString === nextTable.queryString;
}
const MemoTable = memo(Table, areEqal);
...
<MemoTable queryString={queryString} />
The question is, why the Table is shown twice (once not re-rendered, then yes) and why it re-renders when memoized to re-render just when querystring changes.

memo can only prevent renders related to props. Rendering may still happen if a context value that you're subscribed to changes. As discussed in the comments, you are apparently using a couple of contexts, and those are what's causing the render.
While deleting the useContexts appears to have worked for your case, the fact that you weren't expecting this rerender may indidate that the DataContext.provider has a common mistake in which you're changing the context value on every render. If you're providing an object, you need to make sure to only create a new object when the properties of the object have actually changed. So for example, if your current code is this:
const Example = ({ children }) => {
const [userPreferences, setUserPreferences] = useState('something');
return (
<DataContext.Provider value={{ userPreferences, setUserPreferences }}>
{children}
</DataContext.Provider>
);
}
... then you should memoize the value like this:
const Example = ({ children }) => {
const [userPreferences, setUserPreferences] = useState('something');
const value = useMemo(() => {
return { userPreferences, setUserPreferences }
}, [userPreferences]);
return (
<DataContext.Provider value={value}>
{children}
</DataContext.Provider>
);
}
As for the two copies of Table in the dev tools, the first is the memoized component MemoTable. MemoTable then renders an unmemoized Table inside of it, and that's the second one. "Table (memo)" is the default name that memo gives to the component it returns. If you'd like to change that name so it shows up differently in the dev tools, you can:
const MemoTable = memo(Table, areEqal);
MemoTable.displayName = "Hello world";

Related

React - useEffect: Dependencies when component is rendered via map

I am currently stuck employing React's useEffect Hook. The project I am working on is quite complex, but I will try to break it down to the relevant parts in order to describe my issue.
A) The Child Component
import React, {useEffect, useState} from "react";
const ChildComponent = props => {
const [needsConfirmation, setNeedsConfirmation] = useState(true);
useEffect(() => {
setNeedsConfirmation(
!!props.someObject.someArrayOfObjects.find(({status}) => status === "added")
);
}, [props.someObject]);
// I SKIP THE RETURN PART
}
B) Inside The Parent Component
const ParentComponent = props => {
const renderChildComponent = someObject => {
return (
<ChildComponent
someObject={someObject}
/>
) : null;
};
return (
<>
props.someArrayOfObjectsHandledByRedux.map(renderChildComponent)
</>
)
}
C) The Problem
Inside the child component I would like the useEffect hook to take effect every time something about the property props.someObject changes. The latter one is an app-wide state handled by Redux. Of course, I made sure that the corresponding reducer function always returns a brand new object after an action has taken place.
The relevant code inside the reducer function looks something like this:
case ActionTypes.SOME_ACTION_SUCCESS:
return {
...state,
someObject: formattedResponse(data),
};
However, no matter what I did, I just cannot get the useEffect to take effect. Using the ReduxDevTools I could clearly verify that the reducer function has successfully returned a new state object. Nevertheless, the useEffect hook inside the child component did not recognize any changes in the dependencies.
For now, I have implemented a very ugly workaround by always creating a brand new object which is handed to the child component
const renderChildComponent = someObject => {
return (
<ChildComponent
someObject={{...someObject}}
/>
) : null;
};
But this is very inefficient as it automatically re-renders all child components, even though, typically, only the data of one particular child component might have changed.
Does anybody have an idea what the problem in this particular case might be?
Thanks a lot for your input in advance.

useRef inside multiple shared components is not returning the ref object

I have an issue that I need to access the refs in a shared BaseTable component. For example on a parent route component:
function Route() {
<div>
<BaseTable />
<BaseTable />
</div>
}
function BaseTable() {
const ref = useRef();
console.log(ref && ref.current);
return <table ref={ref}>...</table>
}
I would expect that upon the render of each BaseTable I would see the ref node, however I am only seeing it on one of the children BaseTable and the other is only returning null.
Strangely enough (and possibly important), when I make changes to the file and save, the hot reloading then picks up both nodes when re-rendering. When I only have a single BaseTable rendering, the ref is set correctly, however this issue only happens when I render multiple BaseTable components.
Your code is correct.
I think you are confused because your are writing to the console in the render function.
The first time the function is rendered, the ref will be undefined. Instead, use the useEffect hook to write to the console after the component has mounted and the ref has been initialized. You will then see that ref.current is the table element.
function BaseTable() {
const ref = useRef();
useEffect(() => {
console.log(ref && ref.current);
}, []);
return <table ref={ref}>...</table>;
}

Apollo's useQuery and updating a parent's prop with its result

I have a wrapping parent component that accepts some layout-related props, and a child component (representing a page) that needs to run a GraphQL query via Apollo, and update that parent's prop with the result (say the page title). The parent requiring props that I need to update is not a pattern I chose, but the implementation of a component I must use.
App = () {
const [layout, setLayout] = useState({});
return (
<Wrapper layout={layout: 'foo'}>
<PageComponent setLayout={setLayout} />
</Wrapper>
);
}
PageComponent = (props) => {
const { loading, error, data } = useQuery(QUERY); // Apollo's useQuery
useEffect(() => {
props.setPageParams({ title: data.lists[0].title });
}, [data]);
if (loading) return <p>Loading</p>;
if (error) return <p>Error</p>;
return (
<p>component!</p>
);
}
I've tried too many different approaches to write them all down here, but the results are one of two things: a loop in the useEffect (which I somewhat understand, I think passing data to useEffect is causing it to set the parent's props and re-render the child every time because it is an object and isn't been deeply compared), or the following:
Cannot update a component (`App`) while rendering a different component (`PageComponent`).
The latter happens while attempting a few approaches without useEffect, such as:
if(data && data.lists && awaitingPageLayout) {
props.setLayout({ title: data.lists[0].title });
awaitingPageLayout = false;
}
I have also tried to use useQuery's onCompleted, which I believe resulted in another loop.
Hoping for a push in the right direction, I'm out of ideas here. Appreciate your time!

Does React renders Component due to props?

According to that link: http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
render() may be triggered with new props. Could someone give me a code example for that? I cannot see how props change invoke rendering! Please not by changing the props via the state; then it is setState() that invokes render()...
Look at shouldComponentUpdate() - this is it's signature - it returns a boolean. Props is there so you can compare and manually say whether the component should update.
shouldComponentUpdate(nextProps, nextState)
For function components React.memo is the replacement for shouldComponentUpdate which is used with class components.
const myComponent = React.memo(props => {
...
code of React.FunctionComponent
...
},
(prevProps, nextProps) => prevProps.propA === nextProps.propA
);
React.memo gets two arguments shown above: a React.FunctionComponent which will be wrapped around by memo and an optional function that returns a boolean.
When the function returns true, the component will not be re-rendered. If the function is omitted then its default implementation in React.memo works like the implementation of shouldComponentUpdate in React.PureComponent. E.g. it does shallow comparison of props, the difference is that only props are taken into account because state doesn’t exist for functional components.
Using hooks is neater to show. The new props data passed to ComponentB causes a re-rendering of ComponentB:
import React, { useState } from 'react'
import ComponentB from '...'
const ComponentA = props => {
const [data, setData] = useState(0) // data = 0
handleChangeProp = item => setData(item) // data = 1
return(
<div>
<button onClick{() => handleChangeProp(1)}
<ComponentB props={data} />
</div>
)
}
Yes, when you do a setState(newState) or when you pass in changed props the component will re render, this is why you can't mutate. The following will not work because you set state with mutated state.
export default function Parent() {
const [c, setC] = useState({ c: 0 });
console.log('in render:', c);
return (
<div>
<button
onClick={() =>
setC(state => {
state.c++;
console.log('state is:', state);
return state;
})
}
>
+
</button>
<Child c={c.c} />
</div>
);
}
That code "won't work" because pressing + will not cause a re render, you mutated state and then set state with the same object reference so React doesn't know you changed anything.
This is how React detects changes, you may think that comparing {c:0} to {c:1} is a change but because you mutated there actually is no change:
const a = {c:1};
a.c++;//you "changed" a but a still is a
To indicate a change in React you have to create a new reference:
const a = {c:1};
const b = {...a};//b is shallow copy of a
a===b;//this is false, even though both a and b have same internal values
This means you can also have unintended renders because you create an object prop that may have the same value but still is a different reference than the last time you created it.
Note that even <Child prop={1} will cause Child to render if Child is not a pure component (see links at the end).
What you want to avoid is doing <Child prop={{c:value}} because every time you pass prop it'll force Child to render and React to do a virtual DOM compare even if value didn't change. The virtual DOM compare will probably still detect that Child virtual DOM is the same as last time and won't do an actual DOM update.
The most expensive thing you can do is <Child onEvent={()=>someAction(value)}. This is because now the virtual DOM compare will fail even if value and someAction did't change. That's because you create a new function every time.
Usually you want to memoize creating props in a container, here is an example of doing this with react-redux hooks. Here is an example with stateful components passing handlers.

What is the 'proper' way to update a react component after an interval with hooks?

I'm using the alpha version of react supporting hooks, and want to validate my approach to updating the text in a component after an interval without rendering the component more times than needed when a prop changes.
EDIT: For clarity - this component is calling moment(timepoint).fromNow() within the formatTimeString function (docs here), so the update isn't totally unneccessary, I promise!
I previously had:
const FromNowString = ({ timePoint, ...rest }) => {
const [text, setText] = useState(formatTimeString(timePoint));
useEffect(() => {
setText(formatTimeString(timePoint));
let updateInterval = setInterval(
() => setText(formatTimeString(timePoint)),
30000
);
return () => {
clearInterval(updateInterval);
};
}, [timePoint]);
// Note the console log here is so we can see when renders occur
return (
<StyledText tagName="span" {...rest}>
{console.log('render') || text}
</StyledText>
);
};
This "works" - the component correctly updates if the props change, and the component updates at each interval, however on mounting, and when a prop changes, the component will render twice.
This is because useEffect runs after the render that results when the value of timePoint changes, and inside my useEffect callback I'm immediately calling a setState method which triggers an additional render.
Obviously if I remove that call to setText, the component doesn't appear to change when the prop changes (until the interval runs) because text is still the same.
I finally realised I could trigger a render by setting a state variable that I didn't actually need, like so:
const FromNowString = ({ timePoint, ...rest }) => {
// We never actually use this state value
const [, triggerRender] = useState(null);
useEffect(() => {
let updateInterval = setInterval(() => triggerRender(), 30000);
return () => {
clearInterval(updateInterval);
};
}, [timePoint]);
return (
<StyledText tagName="span" {...rest}>
{console.log("render") || formatTimeString(timePoint)}
</StyledText>
);
};
This works perfectly, the component only renders once when it mounts, and once whenever the timePoint prop changes, but it feels hacky. Is this the right way of going about things, or is there something I'm missing?
I think this approach seems fine. The main change I would make is to actually change the value each time, so that it is instead:
const FromNowString = ({ timePoint, ...rest }) => {
const [, triggerRender] = useState(0);
useEffect(() => {
const updateInterval = setInterval(() => triggerRender(prevTriggerIndex => prevTriggerIndex + 1), 30000);
return () => {
clearInterval(updateInterval);
};
}, [timePoint]);
return (
<StyledText tagName="span" {...rest}>
{console.log("render") || formatTimeString(timePoint)}
</StyledText>
);
};
I have two reasons for suggesting this change:
I think it will help when debugging and/or verifying the exact behavior that is occurring. You can then look at this state in dev tools and see exactly how many times you have triggered the re-render in this manner.
The other reason is just to give people looking at this code more confidence that it will actually do what it is intended to do. Even though setState reliably triggers a re-render (and React is unlikely to change this since it would break too much), it would be reasonable for someone looking at this code to wonder "Does React guarantee a re-render if a setState call doesn't result in any change to the state?" The main reason setState always triggers a re-render even if unchanged is because of the possibility of calling setState after having done mutations to the existing state, but if the existing state is null and nothing is passed in to the setter, that would be a case where React could know that state has not changed since the last render and optimize for it. Rather than force someone to dig into React's exact behavior or worry about whether that behavior could change in the future, I would do an actual change to the state.

Resources