child component without props re-renders even with react mem - reactjs

there are a lot of questions related to my issue, but all deal with child components with props. I am not sending any props but the child component is still getting re-rendered which is causing useless load on the db when the getstate function runs.
When i change the sidebar state in the parent, the child re-renders. What is causing the react memo to not work? and how to memoize the chart data to avoid hitting the db everytime the sidebar changes?
function Child() {
const [state, setstate] = useState("")
useEffect(() => {
getState('addressbalance');
},[]);
const getState = async (urlLoc) => {
try {
const response = await fetch(baseURL.concat(`/${urlLoc}`));
const jsonData = await response.json();
setstate(jsonData);
} catch (err) {
console.error(err.message);
}
};
const renderChart = () => {
return <ChartApex graph='Bar' data = {state} height={'95%'} width={'100%'}/>
}
return (
<Explore>
<Card width="90%" height='550px'>
{renderChart()}
</Card>
</Explore>
);
}
export default React.memo(Child)
Parent
<PageContainer changeSidebar={changeSidebar} sidebar={sidebar}>
<Switch>
...
<Route
path="/addressbalance"
component={() => <Child/>}
/>
</Switch>
</PageContainer>

You are using setState in the memoized component, the documentation is explicit on this point :
https://en.reactjs.org/docs/react-api.html#reactmemo
React.memo only checks for prop changes.
If your function component wrapped in React.memo has a useState, useReducer or useContext Hook in its implementation, it will still rerender when state or context change.

Related

Is there a way to make context only re-render if its data changes?

As depicted below, I have a basic react context setup.
My desire is child components consuming the context should only re-render when the context data changes, but I'm finding that's not the case.
If I update call setData2, which is not associated with the context, a state update is triggered on Container which subsequently triggers a recalculation and causes the consuming child to update.
The child component in question is already using React.memo, so it would only update if its useContext hook led it to. But the context gets updated when Container updates, even though data1 is unchanged.
Is it possible to fix this?
const Container = () => {
const [data1, setData1] = useState(...);
const [data2, setData2] = useState(...);
return (
<MyContext.Provider value={{ data1, setData1 }}>
// child component hierarchy
</MyContext.Provider>
);
};
To prevent child components re-rendering when context state changes, you should pass the child components into the context provider via the children prop:
// Define the provider component
const MyContextProvider = ({ children }) => {
const [data1, setData1] = useState(...);
const [data2, setData2] = useState(...);
return (
<MyContext.Provider value={{ data1, setData1 }}>
{children}
</MyContext.Provider>
);
};
Then somewhere in your app, render the provider:
// Render the provider component
const App = () => {
return (
<MyContextProvider>
// child component hierarchy
</MyContextProviderr>
);
};
To access the provider state, a child should use the useContext(MyContext) hook.
There is no need to use React.memo. React knows that when rendering child components using the children prop, the child components do not need to be re-rendered, because it is impossible for them to be affected by local state change.
You could wrap the child component that you do not want to be rerender (triggered by other unrelated state) in a useMemo with the related dependencies - only to that child
const Container = () => {
const [data1, setData1] = useState(...);
const [data2, setData2] = useState(...);
const rerenderInCertainConditions = useMemo(() => (
<MyContext.Provider value={{ data1, setData1 }}>
// child component hierarchy
</MyContext.Provider>
), [data1])
return (
{rerenderInCertainConditions}
);
};
Codesandbox demo

How to prevent infinite loop with React's useReducer, useContext and useEffect

I'm currently trying to figure out how to avoid creating an infinite loop when wrapping my application in a Context provider (taking in values from useReducer) and then updating via child component with a useEffect hook.
There's an example of the problem here on CodeSandbox.
Obviously it's difficult to talk about the problem without reposting all the code here, but key points:
Root:
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const value = { state, dispatch };
return (
<Context.Provider value={value}>
...
</Context.Provider>
Child:
export const Page1: FC = () => {
const { dispatch, state } = useContext(Context);
const { isLoading } = state;
useEffect(() => {
dispatch({
type: "loading",
payload: false
});
}, [dispatch]);
return (...)
I'm probably missing something obvious, but any pointers might help others who run into the same problem.
Full example on CodeSandbox.
The root of the problem is here
<Route path="/page1" component={() => <Page1 />} />
When you pass inlined arrow function as component you basically creating new component for every render and forcing Route to completely re-mount this part. When it happens useEffect gets called again and so on, and so on.
You need to change it like that:
<Route path="/page1"><Page1 /></Route>
// or
<Route path="/page1" component={Page1} />
Citation from react-router docs:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).
Source: https://reactrouter.com/web/api/Route/route-render-methods

React access state after render with functional components

I'm a bit of a newbie with React functional components, I have a child and parent components with some state that gets updated with useEffect, which state apparently resets back to its initial values after render.
Parent has a list of users it passes to its child:
Parent:
const Parent = () => {
const [users, setUsers] = useState([])
const getUsers = () => {
setUsers(["pedro", "juan"])
}
useEffect(() => {
getUsers()
}, []);
return <div>
<Child users={users} />
}
Child:
const Child = () => {
const [users, setUsers] = useState([])
useEffect(() => {
setUsers(props.users)
}, [[...props.users]]);
}
If I for any reason try to access state (users) from either my child or parent components I get my initial value, which is an empty array, not my updated value from getUsers(), generally with a Parent Class component I'd have no trouble accessing that info, but it seems like functional components behave diffently? or is it caused by the useEffect? generally I'd use a class component for the parent but some libraries I use rely on Hooks, so I'm kind of forced to use functional components.
There are a couple of mistakes the way you are trying to access data and passing that data.
You should adapt to the concept of lifting up state, which means that if you have users being passed to your Child component, make sure that all the logic regarding adding or removing or updating the users stays inside the Parent function and the Child component is responsible only for displaying the list of users.
Here is a code sandbox inspired by the code you have shared above. I hope this answers your question, do let me know if otherwise.
Also sharing the code below.
import React, { useState } from "react";
export default function Parent() {
const [users, setUsers] = useState([]);
let [userNumber, setUserNumber] = useState(1); // only for distinctive users,
//can be ignored for regular implementation
const setRandomUsers = () => {
let newUser = {};
newUser.name = `user ${userNumber}`;
setUsers([...users, newUser]);
setUserNumber(++userNumber);
};
return (
<div className="App">
<button onClick={setRandomUsers}>Add New User</button>
<Child users={users} />
</div>
);
}
const Child = props => {
return (
props.users &&
props.users.map((user, index) => <div key={index}>{user.name}</div>)
);
};
it doesnt make sense to me that at Child you do const [users, setUsers] = useState([]). why dont you pass down users and setUsers through props? your child's setUser will update only its local users' state value, not parent's. overall, duplicating parent state all around its children is not good, you better consume it and updating it through props.
also, once you do [[...props.users]], you are creating a new array reference every update, so your function at useEffect will run on every update no matter what. useEffect doesnt do deep compare for arrays/objects. you better do [props.users].

How to avoiding "options" props from being initialised inline?

Since the reference of options changes when ParentComp re-renders, what are some good methods to prevent SubComp rerendering when its props haven't changed? Example situation:
const ParentComp = ({ uri, someOtherProp }) => {
[...some other state hooks here]
return <SubComp options={{ uri }} someOtherProp={someOtherProp}/>
}
React.memo
If your function component renders the same result given the same
props, you can wrap it in a call to React.memo for a performance boost
in some cases by memoizing the result. This means that React will skip
rendering the component, and reuse the last rendered result.
const ParentComp = ({ uri, someOtherProp }) => {
[...some other state hooks here]
return <SubComp options={{ uri }} someOtherProp={someOtherProp}/>
}
const equalProps = (prevProps, nextProps) => {
/*
return true if passing next props would/should
result in the same output, false otherwise
*/
};
export default memo(ParentComp, equalProps);
NOTE:
This method only exists as a performance optimization. Do not rely on
it to “prevent” a render, as this can lead to bugs.
Another way of doing this to prevent the parent from passing a new reference for the options prop every time it renders. Now you are doing options={{ uri }} but { uri }!=={ uri }. You are creating a new object for options even if uri didn't change.
You could use useMemo for this
const ParentComp = ({ uri, someOtherProp }) => {
const options = React.useMemo(() => ({ uri }), [uri]);
return (
<SubComp
options={options}
someOtherProp={someOtherProp}
/>
);
};
If you have lots of props and just want SubComponent to re render when something changes you can do the following:
const ParentComp = ({ uri, someOtherProp }) => {
const memoProps = React.useMemo(
() => ({
options: { uri },
someOtherProp,
}),
[someOtherProp, uri]
);
return <SubComp {...memoProps} />;
};
This is assuming that SubComp is a either a class that inherits from PureComponent, component wrapped with React Redux connect or a functional component wrapped with React.memo (no need for the compare function). Normal functional components will re render even if props have same reference as previous render.

Prevent Unnecessary Re-rendering of Child Elements

I am creating a global notifications component in react that provides a createNotification handle to its children using Context. The notifications are rendered along with props.children. Is there anyway to prevent the re-rendering of the props.children if they haven't changed?
I have tried using React.memo and useMemo(props.children, [props.children]) to no prevail.
const App = () => {
return (
<Notifications>
<OtherComponent/>
</Notifications/>
);
}
const Notifications = (props) => {
const [notifications, setNotifications] = useState([]);
const createNotification = (newNotification) => {
setNotifications([...notifications, ...newNotification]);
}
const NotificationElems = notifications.map((notification) => <Notification {...notification}/>);
return (
<NotificationContext.Provider value={createNotification}>
<React.Fragment>
{NotificationElems}
{props.children}
</React.Fragment>
</NotificationContext.Provider>
);
};
const OtherComponent = () => {
console.log('Re-rendered');
return <button onClick={() => useContext(NotificationContext)(notification)}>foo</button>
}
Every time a new notification is created, props.children is re-rendered even though nothing actually changes within it. It just adds elements along side it. This can be quite expensive if you have a big app and everything re-renders for each notification that shows up. If there is no way to prevent this, how can I split it up so I can do this:
<div>
<OtherComponent/>
<Notifications/>
</div>
and share with OtherComponent the createNotification handle?
You need to use the useCallback hook to create your createNotification imperative handler. Otherwise you will create a new function on every render of the Notifications component which will lead to all components consuming your context to re-render because you always pass a new handler whenever you add a notification.
Also you likely didn't mean to spread the newNotification into the array of notifications.
The next thing you need to do is to provide the updater callback version of setState inside setNotifications. It gets passed the current list of notifications that you can use the append the new one. This makes your callback independent of the current value of the notification state. It is usually an error to update the state based on the current state without using an updater function because react batches multiple updates.
const Notifications = props => {
const [notifications, setNotifications] = useState([]);
// use the useCallback hook to create a memorized handler
const createNotification = useCallback(
newNotification =>
setNotifications(
// use the callback version of setState
notifications => [...notifications, newNotification],
),
[],
);
const NotificationElems = notifications.map((notification, index) => <Notification key={index} {...notification} />);
return (
<NotificationContext.Provider value={createNotification}>
<React.Fragment>
{NotificationElems}
{props.children}
</React.Fragment>
</NotificationContext.Provider>
);
};
Another issue is that you conditionally call the useContext hook which is not allowed. Hooks must be called unconditionally:
const OtherComponent = () => {
// unconditiopnally subscribe to context
const createNotification = useContext(NotificationContext);
console.log('Re-rendered');
return <button onClick={() => createNotification({text: 'foo'})}>foo</button>;
};
Fully working example:

Resources