How does one cache an entire functional component in React? - reactjs

I've built a tabs component. Each tab, when clicked, changes the contents of the "main screen". Assume a tab had some back-end call to retrieve data it needs to render, then it makes no sense to have it re-run these calls every time the user clicks another tab and comes back to it. I want to retrieve what was rendered before and display it.
I looked into memo, the big warning says to "not rely on it to “prevent” a render, as this can lead to bugs.", nor does it work. Every time I wrap my component in a memo, the test:
useEffect(() => {
console.log('Rendered');
}, [])
Still runs, telling me that the component re-rendered. Then I thought about memoizing the return itself, so, like:
export const MyComponent = (context) => {
const content = useMemo(() => {
return <></>
}, [context]);
return content;
};
But quickly realized that by the time I reach this useMemo, I'm already in the re-rendering cycle, because there's no way for React to know that MyComponent's useMemo existed in the past, so, again, it re-renders the whole thing. This, in turn made me think that the memoization needs to be done at the level where MyComponent is being rendered, not inside of it but I don't know how to do it.
How can I skip re-renders if my props haven't changed?
Read all the articles, tried all the things but to no avail.
Concisely, here is my component and my latest approach:
export const MyComponent = memo(({ context, className = '', ...props }) => {
..
..
});
The interesting bit here is context. This should almost never change. Its structure is a deeply nested object, however, when I play with memo's second argument, its diff function, what ends up happening if I put a console.log in there, as follows:
const MyComponent = ({ context, className = '', ...props }) => {
};
const areEqual = (prevProps, nextProps) => {
console.log('Did equality check.');
};
export default memo(MyComponent, areEqual);
I will only see "Did equality check." once. No matter what I do, I can't seem to get a memoized component out of memo. This is how MyComponent's parent looks like:
const Parent = ({}) => {
const context = useSelector(); //context comes from the store.
const [selectedTab, setSelectedTab] = useState(false);
const [content, setContent] = useState(null);
useEffect(() => {
switch (selectedTab) {
case 'components':
setContent(<MyComponent context={context} />);
break;
}
}, [selectedTab, context]);
return(<>{content}</>);
};

#daniel-james I think the problem here is we are un-mounting the whole component when we are switching tabs. Instead try memoizing the component in the parent itself. That way your component is memoized only once.

Related

React Hooks, how to prevent unnecessary rerendering

I have a hook that gets data from another hook
const useIsAdult = () => {
const data = useFormData();
return data.age > 18;
}
This hook returns true or false only, however the useFormData is constantly being updated. Everytime useFormData reruns, it will rerender my component. I only want my component to rerender if the result of useIsAdult changes.
I know this can obviously be solved by implementing some logic via react-redux and using their useSelector to prevent rendering, but I'm looking for a more basic solution if there is any.
Before you say useMemo, please read question carefully. The return value is a boolean, memo here doesnt make any sense.
Even with returned useMemo in useIsAdult parent component will be rerendered. This problem is reason why I rarely create custom hooks with other hooks dependency. It will be rerender hell.
There I tried to show that useMemo doesnt work. And using useMemo for components its wrong way. For components we have React.memo.
const MyComponent = memo(({ isAdult }: { isAdult: boolean }) => {
console.log("renderMy");
return <h1>Hello CodeSandbox</h1>;
});
And memo will help to prevent rendering of MyComponent. But parent component still render.
https://codesandbox.io/s/interesting-lumiere-xbt4gg?file=/src/App.tsx
If you can modify useFormData maybe useRef can help. You can store data in useRef but it doesnt trigger render.
Something like this but in useFormData:
onst useIsAdult = () => {
const data = useFormData();
const prevIsAdultRef = useRef();
const isAdult = useMemo(() => data.age > 18, [data]);
if (prevIsAdultRef.current === isAdult) {
return prevIsAdultRef.current;
}
prevIsAdultRef.current = isAdult;
return isAdult;
};
I dont know how useFormData works, then you can try it self with useRef.
And if useFormData gets data very often and you cant manage this, you can use debounce or throttle to reduce number of updates
Memoize hook
const useIsAdult = () => {
const data = useFormData();
return useMemo(() => data.age > 18, [data.age]);
}
Here, useMemo will let you cache the calculation or multiple renderes, by "remembering" previous computation. So, if the dependency (data.age) doesn't change then it will use simply reuse the last value it returned, the cached one.
Memoize component expression
useMemo can also be used to memoize component expressions like this: -
const renderAge = useMemo(() => <MyAge age={data.age} />, [data.age]);
return {renderAge}
Here, MyAge will only re-render if the value of data.age changes.

useCallback is still leading to re-renders of children when parent's states change

Simplifying my app's components, I have a NoteList component with a particular child called FileTree. There are various states on NoteList. What I'm trying to do is to stop FileTree from re-rendering any time a state that only impacts NoteList changes. I'm getting tripped up on some functions that FileTree uses.
NoteList looks roughly like this:
const [folderName, setFolderName] = React.useState("")
...
const handleCreateFolder = React.useCallback(async (e) => {
e.preventDefault();
...
await axios.post(...call to create a folder...)
}, [])
const folderNameChangeHandler = React.useCallback((folN) => {
setFolderName(folN)
}, [])
return (
...
<FileTree prop1={prop1Value} prop2='prop2HardCoded' handleCreateFolder={handleCreateFolder} setFolderName={folderNameChangeHandler} />
)
Using the profiler, I still see that FileTree is re-rendering with every keystroke into a text input that uses setFolderName (or really, it should be using folderNameChangeHandler if I understand correctly) to change folderName.
What am I doing wrong with useCallback?
Using useCallback is not going to prevent re-renders when using setFolderName because setFolderName is going to cause a re-render every time it is called.
Check out React.memo to control when <FileTree /> re-renders.
You can use it to access previous and next props to determine if <FileTree /> should re-render.
function FileTree() {...}
export default React.memo(FileTree, (prevProps, nextProps) => {
return true // this will never re-render
// or do your custom logic to determine when it should re-render
})

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].

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.

How to use React Hooks Context with multiple values for Providers

What is the best way to share some global values and functions in react?
Now i have one ContextProvider with all of them inside:
<AllContext.Provider
value={{
setProfile, // second function that changes profile object using useState to false or updated value
profileReload, // function that triggers fetch profile object from server
deviceTheme, // object
setDeviceTheme, // second function that changes theme object using useState to false or updated value
clickEvent, // click event
usePopup, // second function of useState that trigers some popup
popup, // Just pass this to usePopup component
windowSize, // manyUpdates on resize (like 30 a sec, but maybe can debounce)
windowScroll // manyUpdates on resize (like 30 a sec, but maybe can debounce)
}}
>
But like sad in docs:
Because context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders. For example, the code below will re-render all consumers every time the Provider re-renders because a new object is always created for value:
This is bad:
<Provider value={{something: 'something'}}>
This is ok:
this.state = {
value: {something: 'something'},
};
<Provider value={this.state.value}>
I imagine that in future i will have maybe up to 30 context providers and it's not very friendly :/
So how can i pass this global values and functions to components? I can just
Create separate contextProvider for everything.
Group something that used together like profile and it's functions,
theme and it's functions (what about reference identity than?)
Maybe group only functions because thay dont change itself? what
about reference identity than?)
Other simpliest way?
Examples of what i use in Provider:
// Resize
const [windowSize, windowSizeSet] = useState({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight
})
// profileReload
const profileReload = async () => {
let profileData = await fetch('/profile')
profileData = await profileData.json()
if (profileData.error)
return usePopup({ type: 'error', message: profileData.error })
if (localStorage.getItem('deviceTheme')) {
setDeviceTheme(JSON.parse(localStorage.getItem('deviceTheme')))
} else if (profileData.theme) {
setDeviceTheme(JSON.parse(JSON.stringify(profileData.theme)))
} else {
setDeviceTheme(settings.defaultTheme)
}
setProfile(profileData)
}
// Click event for menu close if clicked outside somewhere and other
const [clickEvent, setClickEvent] = useState(false)
const handleClick = event => {
setClickEvent(event)
}
// Or in some component user can change theme just like that
setDeviceTheme({color: red})
The main consideration (from a performance standpoint) for what to group together is less about which ones are used together and more about which ones change together. For things that are mostly set into context once (or at least very infrequently), you can probably keep them all together without any issue. But if there are some things mixed in that change much more frequently, it may be worth separating them out.
For instance, I would expect deviceTheme to be fairly static for a given user and probably used by a large number of components. I would guess that popup might be managing something about whether you currently have a popup window open, so it probably changes with every action related to opening/closing popups. If popup and deviceTheme are bundled in the same context, then every time popup changes it will cause all the components dependent on deviceTheme to also re-render. So I would probably have a separate PopupContext. windowSize and windowScroll would likely have similar issues. What exact approach to use gets deeper into opinion-land, but you could have an AppContext for the infrequently changing pieces and then more specific contexts for things that change more often.
The following CodeSandbox provides a demonstration of the interaction between useState and useContext with context divided a few different ways and some buttons to update the state that is held in context.
You can go to this URL to view the result in a full browser window. I encourage you to first get a handle for how the result works and then look at the code and experiment with it if there are other scenarios you want to understand.
This answer already does a good job at explaining how the context can be structured to be more efficient. But the final goal is to make context consumers be updated only when needed. It depends on specific case whether it's preferable to have single or multiple contexts.
At this point the problem is common for most global state React implementations, e.g. Redux. And a common solution is to make consumer components update only when needed with React.PureComponent, React.memo or shouldComponentUpdate hook:
const SomeComponent = memo(({ theme }) => <div>{theme}</div>);
...
<AllContext>
{({ deviceTheme }) => <SomeComponent theme={deviceTheme}/>
</AllContext>
SomeComponent will be re-rendered only on deviceTheme updates, even if the context or parent component is updated. This may or may not be desirable.
The answer by Ryan is fantastic and you should consider that while designing how to structure the context provider hierarchy.
I've come up with a solution which you can use to update multiple values in provider with having many useStates
Example :
const TestingContext = createContext()
const TestingComponent = () => {
const {data, setData} = useContext(TestingContext)
const {value1} = data
return (
<div>
{value1} is here
<button onClick={() => setData('value1', 'newline value')}>
Change value 1
</button>
</div>
)
}
const App = () => {
const values = {
value1: 'testing1',
value2: 'testing1',
value3: 'testing1',
value4: 'testing1',
value5: 'testing1',
}
const [data, setData] = useState(values)
const changeValues = (property, value) => {
setData({
...data,
[property]: value
})
}
return (
<TestingContext.Provider value={{data, setData: changeValues}}>
<TestingComponent/>
{/* more components here which want to have access to these values and want to change them*/}
</TestingContext.Provider>
)
}
You can still combine them! If you are concerned about performance, you can create the object earlier. I don't know if the values you use change, if they do not it is quite easy:
state = {
allContextValue: {
setProfile,
profileReload,
deviceTheme,
setDeviceTheme,
clickEvent,
usePopup,
popup,
windowSize
}
}
render() {
return <AllContext.Provider value={this.state.allContextValue}>...</AllContext>;
}
Whenever you then want to update any of the values you need to do I like this, though:
this.setState({
allContextValue: {
...this.state.allContextValue,
usePopup: true,
},
});
This will be both performant, and relatively easy as well :)
Splitting those up might speed up a little bit, but I would only do that as soon as you find it is actually slow, and only for parts of your context that would have a lot of consumers.
Still, if your value does not change a lot, there is really nothing to worry about.
Based on Koushik's answer I made my own typescipt version.
import React from "react"
type TestingContextType = {
value1?: string,
value2?: string,
value3?: string,
value4?: string,
value5?: string,
}
const contextDefaultValues = {
data: {
value1: 'testing1',
value2: 'testing1',
value3: 'testing1',
value4: 'testing1',
value5: 'testing1'
} as TestingContextType,
setData: (state: TestingContextType) => {}
};
const TestingContext = React.createContext(contextDefaultValues);
const TestingComponent = () => {
const {data, setData} = React.useContext(TestingContext);
const {value1} = data
return (
<div>
{value1} is here
<button onClick={() => setData({ value1 : 'newline value' })}>
Change value 1
</button>
</div>
)
}
const App = () => {
const [data, setData] = React.useState(contextDefaultValues.data)
const changeValues = (value : TestingContextType) => setData(data && value);
return (
<TestingContext.Provider value={{data, setData: changeValues}}>
<TestingComponent/>
{/* more components here which want to have access to these values and want to change them*/}
</TestingContext.Provider>
)
}

Resources