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:
Related
This is more of a best practice question than anything. But I'm wondering if any inline functions within a React component should typically be wrapped with an onCallback for performance? Under what circumstances would I not wrap a function like this with onCallback?
For example:
const ToolSearch = (props: ToolsSearchProps) => {
const handleOnClick = () => {
alert('do something');
}
return (
<NoCardOverflow>
<ToolSearchCollapse openState={filtersOpen} onClick={handleOnClick} />
</NoCardOverflow>
);
};
In this example should I be doing this:
const ToolSearch = (props: ToolsSearchProps) => {
const handleOnClick = useCallback(() => {
alert('do something');
},[]);
return (
<NoCardOverflow>
<ToolSearchCollapse openState={filtersOpen} onClick={handleOnClick} />
</NoCardOverflow>
);
};
I will try to explain the useCallBack with an example. We all know the definition of useCallBack, but when to use is the trick part here. So, let me take an example.
const RenderText = React.memo(({ text }) => {
console.log(`Render ${text}`);
return (<div>{text}</div>);
});
const App = () => {
const [count, updateCount] = React.useState(0);
const [lists, updateLists] = React.useState(["list 1"]);
const addListItem = () => {
updateLists([...lists, "random"]);
};
return (
<div>
{`Current Count is ${count}`}
<button onClick={() => updateCount((prev) => prev + 1)}>Update Count</button>
{lists.map((item: string, index: number) => (
<RenderText key={index} text={item} />
))}
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#app"));
jsFiddle: https://jsfiddle.net/enyctuLp/1/
In the above, there are two states.
count - Number
lists - Array
The RenderText just renders the text passed to it. (which is the item in the list). If you click on the Update Count button, the RenderText will not re-render because it is independent from the main component (App) and during the updateCount, only the App component will re-render, since it needs to update the count value.
Now, pass the addListItem into RenderText component and click on the Update Count button and see what happens.
jsFiddle: https://jsfiddle.net/enyctuLp/2/
You can see that the RenderText will re-render even though there is no change in the list array and this is BECAUSE :
when the count is updated, the App will re-render
which, will re-render the addListItem
which causes the RenderText to re-render.
To avoid this, we should use useCallBack hook.
jsFiddle: https://jsfiddle.net/enyctuLp/4/
Now, the addListItem function has been memoized and it will only change when the dependency are changed.
ToolSearchCollapse is your child component, and so wrapping your handleClick function makes sense. Coz every time the parent component re-renders a new reference of handleclick will be passed to ToolSearchCollapse, and your child will re-render every time a parent state changes, (from states that aren't passed to the child also). Using useCallback will not allow creating new references, and thus you can control when your child should render.
If you don't pass this fn to the child, I don't see any reason to use useCallback.
I am passing a variable data that is subjected to change from a child component. In the parent component, I am receiving the variable data like this:
<ChildComponent sendDataToParent={sendDataToParent}/>;
let passedVal;
const sendDataToParent = (val) => {
passedVal = val;
};
So I want an alert to be triggered anytime the passed variable changes.
So I bind the passed data to a state, and then use useEffect hook. But the useEffect does not trigger anytime the passedValue changes. Though if click the button it shows the changed passed data.
export default function ParentComponet() {
const [isSelected, setIsSelected] = useState(passedVal);
const showPassedData = () => { // This triggers on click of a button
setIsSelected(passedVal);
alert(passedVal)
};
useEffect(() => { // It does not trigger after passedVal changes
setIsSelected(passedVal);
alert(passedVal)
}, [passedVal])
return (
<div>
<Button
onClick={showPassedData}
>
Show Passed Data
</Button>
</div>
);
}
That does not work because React can't detect changes of a normal variable using useEffect. It can only detect state/props changes. In your case passedVal is just a variable, even if you change it, it will not trigger useEffect.
So instead of this
let passedVal;
const sendDataToParent = (val) => {
passedVal = val;
};
you can do this:
const [passedVal, setPassedVal] = useState();
<ChildComponent sendDataToParent={setPassedVal}/>;
sendDataToParent does not really send data to parent component. If you want to do that you'd need to use smth like Redux or React's context. It's a bit hard to see the complete structure of your components from the code you provided but I think an easier way would be to pass a callback from the parent component to the child, that will be invoked when value changes.
export default function ParentComponent() {
const onDataChange = (val) => {
alert(val)
}
return (
// ...
<ChildComponent onDataChange={onDataChange}/>
)
}
I have created a popup that the user can use to add stuff to the application, every field is a separate component, because I need to reuse them in several places in different configruations.
I have tried to create an innerRef that when changed (i.e. new value is typed), the useEffect of the component should be triggered to show or hide the Done button if all values are valid.
I know that all values are valid or not from the valid prop that I assign to .current
export default function AddStock() {
const selectTypeOfQuantityRef = useRef({});
const [allValid, setAllValid] = useState(false);
useEffect(() => {
const allValues = [selectTypeOfQuantityRef.current.valid];
allValues.every((value) => value) ? setAllValid(true) : setAllValid(false);
console.log(allValues.every((value) => value)); // does not get triggered
}, [selectTypeOfQuantityRef.current]);
return (
<>
<AddPopup>
<SelectTypeOfQuantity innerRef={selectTypeOfQuantityRef} />
{allValid && <DoneButton/>}
<CancelButton/>
</AddPopup>
</>
);
}
And this is the select itself (custom of course), that sets innerRef, whenever its state changes.
Everything here works, the state of this small component itself is managed correctly, but it just does not get triggered the state update of the parent component
export default function SelectTypeOfQuantity({ defaultValue = null, innerRef }) {
const [selectTypeOfQuantity, setSelectTypeOfQuantity] = useState(defaultValue);
const [valid, setValid] = useState(false);
const [errMessage, setErrMessage] = useState("Избери стойност!");
useEffect(() => {
innerRef.current.value = selectTypeOfQuantity;
handleValidation(selectTypeOfQuantity);
}, [selectTypeOfQuantity]);
const handleValidation = (value) => {
const result = validateAutocomplete(value);
if (result.valid) {
setValid(true);
setErrMessage(null);
innerRef.current.valid = result.valid;
} else {
setValid(false);
setErrMessage(result.errMessage);
}
};
const selectTypeOfQuantityOnChange = (e, val) => {
setSelectTypeOfQuantity(val ? val.value : null);
};
return (
<Select onChange={selectTypeOfQuantityOnChange}/>
);
}
useRef does not trigger rerenders, thus useEffect will not be called
Use useRef when you need information that is available regardless of component lifecycle and whose changes should NOT trigger rerenders. Use useState for information whose changes should trigger rerenders.
Solution
As React's Philosophy states, all the data must reside within React, that's why even input components are supplied with a value and onChange event. React can't track data changes that happens outside it. As I understand from your question, the changes are happending within the React App, So instead of tracking the data through the innerRef, track them within React using React's own methods.
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].
I was wondering if there's a way to persist state of a component that will unmount and possibly remount in the example of a dropdown that has a download option, which will then close the dropdown upon selection, which will cause the option to unmount and a spinner will load.
Right now, I'm keeping all of the state of the related dropdown option in its parent and passing it back down to the option via props and would rather have that logic stored in a custom hook or something cleaner.
const Parent = () => {
const [isLoading, setIsLoading] = useState(false);
const [setDataToDownload, dataToDownload] = useState();
return (
<>
{isLoading && <Spinner />}
<Dropdown>
{({ close }) =>
options.map(op => (
<ChildOption
isLoading={isLoading}
setDataToDownload={setDataToDownload}
dataToDownload={dataToDownload}
setIsLoading={setIsLoading}
close={close}
op={op}
/>
))
}
</Dropdown>
</>
);
};
const ChildOption = ({ close, isLoading, setIsLoading }) => {
return (
<div
onClick={async () => {
close();
setIsLoading(true);
const data = await fetchSomeStuff();
setDataToDownload(data);
setIsLoading(false);
}}
>
{op.name}
</div>
);
};
if there's a way to persist state of a component that will unmount.
States of components are destroyed, when they are unmounted. So, you cannot access or retrieve state of an unmounted component. Unless, you store the state somewhere else.
You can use Redux, as a global state. You can dispatch an action from componentWillUnmount lifecycle method to store your current state in Redux store. And, you can retrieve it back in componentDidMount lifecycle method.
Other than Redux, you can use the localStorage to store component's state. Make use of componentWillUnmount and componentDidMount lifecycle methods as I explained above.