Excessive rerendering when interacting with global state in React Context - reactjs

I'm building a Chat app, I'm using ContextAPI to hold the state that I'll be needing to access from different unrelated components.
A lot of rerendering is happening because of the context, everytime I type a letter in the input all the components rerender, same when I toggle the RightBar which its state also resides in the context because I need to toggle it from a button in Navbar.
I tried to use memo on every components, still all the components rerender everytime I interact with state in context from any component.
I added my whole code simplified to this sandbox link : https://codesandbox.io/s/interesting-sky-fzmc6
And this is a deployed Netlify link : https://csb-fzmc6.netlify.app/
I tried to separate my code into some custom hooks like useChatSerice, useUsersService to simplify the code and make the actual components clean, I'll also appreciate any insight about how to better structure those hooks and where to put CRUD functions while avoiding the excessive rerendering.
I found some "solutions" indicating that using multiple contexts should help, but I can't figure out how to do this in my specific case, been stuck with this problem for a week.
EDIT :
The main problem here is a full rerender with every letter typed in the input.
The second, is the RightBar toggle button which also causes a full rerender.

Splitting the navbar and chat state into two separate React contexts is actually the recommended method from React. By nesting all the state into a new object reference anytime any single state updated it necessarily triggers a rerender of all consumers.
<ChatContext.Provider
value={{ // <-- new object reference each render
rightBarValue: [rightBarIsOpen, setRightBarIsOpen],
chatState: {
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue,
},
}}
>
{children}
</ChatContext.Provider>
I suggest carving rightBarValue and state setter into its own context.
NavBar context
const NavBarContext = createContext([false, () => {}]);
const NavBarProvider = ({ children }) => {
const [rightBarIsOpen, setRightBarIsOpen] = useState(true);
return (
<NavBarContext.Provider value={[rightBarIsOpen, setRightBarIsOpen]}>
{children}
</NavBarContext.Provider>
);
};
const useNavBar = () => useContext(NavBarContext);
Chat context
const ChatContext = createContext({
editValue: "",
setEditValue: () => {},
editingId: null,
setEditingId: () => {},
inputValue: "",
setInputValue: () => {}
});
const ChatProvider = ({ children }) => {
const [inputValue, setInputValue] = useState("");
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
const chatState = useMemo(
() => ({
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue
}),
[editValue, inputValue, editingId]
);
return (
<ChatContext.Provider value={chatState}>{children}</ChatContext.Provider>
);
};
const useChat = () => {
return useContext(ChatContext);
};
MainContainer
const MainContainer = () => {
return (
<ChatProvider>
<NavBarProvider>
<Container>
<NavBar />
<ChatSection />
</Container>
</NavBarProvider>
</ChatProvider>
);
};
NavBar - use the useNavBar hook
const NavBar = () => {
const [rightBarIsOpen, setRightBarIsOpen] = useNavBar();
useEffect(() => {
console.log("NavBar rendered"); // <-- log when rendered
});
return (
<NavBarContainer>
<span>MY NAVBAR</span>
<button onClick={() => setRightBarIsOpen(!rightBarIsOpen)}>
TOGGLE RIGHT-BAR
</button>
</NavBarContainer>
);
};
Chat
const Chat = ({ chatLines }) => {
const { addMessage, updateMessage, deleteMessage } = useChatService();
const {
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue
} = useChat();
useEffect(() => {
console.log("Chat rendered"); // <-- log when rendered
});
return (
...
);
};
When running the app notice now that "NavBar rendered" only logs when toggling the navbar, and "Chat rendered" only logs when typing in the chat text area.

I recommend use jotai or other state management libraries.
Context is not suitable for high-frequency changes.
And, the RightBar's state looks can separate to other hook/context.
There is tricky one solution solve some render problems:
https://codesandbox.io/s/stoic-mclaren-x6yfv?file=/src/context/ChatContext.js
Your code needs to be refactored, and useChatService in ChatSection also depends on your useChat, so ChatSection will re-render when the text changes.

It looks like you are changing a global context on input field data change. If your global context is defined on a level of parent components (in relation to your input component), then the parent and all children will have to re-render.
You have several options to avoid this behavior:
Use context on a lower level, e.g. by extracting your input field to an external component and using useContext hook there
Save the input to local state of a component and only sync it to the global context on blur or submit

Related

How to make react components communicate each others in large component tree

In my react app, I have some complex component tree.
In this component tree, I have a <Footer/> component with buttons. I also have <SomeComponent/> component elsewhere in the tree. This component is actually loaded from some dynamic code and is not always the same (similar to some widget engine, where the container is handled by the app engine, and the content is dynamically loaded). It means the context has no knowledge of what are actually the components.
In order to plug everything else, I have a custom react context that holds some fields and methods, which is exposed trough a custom useMyContext hook.
This is working quite well except one remaining issue :
In my <Footer /> I have a button that should call something inside the <SomeComponent/> component. As an example I may have a 'Refresh' button that should ask the component to get latest data.
Basically I have this react tree:
App
SomeContextProvider
Footer
RefreshButton
Deep/Nested/Component/Structure
SomeComponent
(contains a refresh function)
How can I call the refresh function in my component from the footer ?
I tried to play with forwarding refs and useImperativeHandler hook, which may work, but the deep nesting of component tree leads to a big mess of forwarding refs.
I also tried to extend the context provider, but I didn't found a way to "reverse" the callback (context can react to Refresh button action, but I cannot react to this in sibling branch of the component tree).
How could I handle this ?
PS: if it matters, I'm using react 16.13.1 and typescript 4.5
I think I have the start of a clean solution.
Basically, I can handle my scenario by implementing a subscribe/unsubscribe pattern hold by by app context.
This way I can emit some kind of event from my outer context, and let components in the tree subscribe and handle the events as needed.
Some repro : https://codesandbox.io/s/infallible-chaplygin-79c3gq?file=/src/App.tsx.
Relevant parts below:
Custom react context
type Subscribe = (cb: () => void) => () => void;
type AppContextData = {
subscribe: Subscribe;
onSubmit: () => void;
};
const AppContext = createContext<AppContextData | undefined>(undefined);
const useAppContext = (): AppContextData => {
const context = useContext(AppContext);
if (!context)
throw new Error(`useAppContext must be used within a AppContextProvider`);
return context;
};
Container component
const AppContextProvider: React.FC<PropsWithChildren<{}>> = ({ children }) => {
const subscribtions: (() => void)[] = [];
const subscribe: Subscribe = (cb) => {
subscribtions.push(cb);
return () => {
subscribtions.splice(subscribtions.indexOf(cb), 1);
};
};
const emitSubmit = () => {
subscribtions.forEach((cb) => cb());
};
const appContext: AppContextData = {
subscribe,
onSubmit: emitSubmit
};
return (
<AppContext.Provider value={appContext}>{children}</AppContext.Provider>
);
};
App
export default function App() {
return (
<AppContextProvider>
<div className="App">
<Main />
<Footer />
</div>
</AppContextProvider>
);
}
And finally, subscription and submission trigger:
Component with button
const Footer: React.VFC = () => {
const { onSubmit } = useAppContext();
return <button onClick={onSubmit}>Submit</button>;
};
Component that subscribes (and unsubscribe thanks to react effect)
const Main: React.VFC = () => {
const [myString, setMyString] = useState("initial");
const context = useAppContext();
useEffect(() => {
return context.subscribe(() => setMyString("from context"));
}, [context]);
return <p>{myString}</p>;
};

How to detect route changes using React Router in React?

I have a search component that is global to my application, and displays search results right at the top. Once the user does any sort of navigation, e.g., clicking a search result or using the back button on the browser, I want to reset the search: clear the results and the search input field.
Currently, I am handling this with a Context; I have a SearchContext.Provider that broadcasts the resetSearch function, and wherever I am handling navigation, I have consumers that invoke resetSearch before processing navigation (which I do programmatically with the useHistory hook).
This doesn't work for back button presses on the browser's controls (since that is something out of my control).
Is there an intermediate step before my Routes are rendered (or any browser navigation happens) that I can hook into, to make sure my resetSearch function is invoked?
As requested, here is the code:
// App.js
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const resetSearch = () => {
setResults([]);
setQuery("");
};
<SearchContext.Provider value={{ resetSearch }}>
// all of my <Route /> components in this block
</SearchContext.Provider>
// BusRoute.js
const { resetSearch } = useContext(SearchContext);
const handleClick = stop => {
resetSearch();
history.push(`/stops/${stop}`);
};
return (
// some other JSX
<button onClick={() => handleClick(stop.code)}>{stop.name}</button>
);
You can listen to history changes:
useEffect(() => {
const unlisten = history.listen((location) => {
console.log('new location: ', location)
// do your magic things here
// reset the search: clear the results and the search input field
})
return function cleanup() {
unlisten()
}
}, [])
You can use this effect in your parent component which controls your global search's value.
You can use componentWillUnmount feature from class components with useEffect in hooks with functional component

React Hooks Wrapper not getting updated after state change in useEffect

Current behaviour
I'm using a functional component with a setState hook in useEffect. The state variable that is set inside useEffect is wrapped over the return statement to render the JSX for the component.
When I debug into it, the component renders with the correct state variable but my wrapper in my test does Not show the correct information.
wrapper.update() isn't fixing this issue.
Below is a snippet of what I am trying to achieve:
const DummyComponent= ({}) => {
const [selected, setSelected] = React.useState(false);
useEffect(() => {
setSelected(true)
}, [someDependency])
return (
{
selected && (
<div id= 'container'>
{childComponents}
</div>)
}
);
})
it('test', () => {
const wrapper= mount( <DummyComponent /> );
wrapper = wrapper.update(); // this doesn't fix my problem
wrapper.find('#container')first().props().onClick();
expect(wrapper.toMatchSnapshot());
});
I am getting the below error:
Method “props” is meant to be run on 1 node. 0 found instead.
Expected Behaviour
After state update in useEffect re-render should be triggered in test case and element with id="container" should be found.
Note: This is not same as https://github.com/enzymejs/enzyme/issues/2305
 
It seems to me there's some other problem with your real code (maybe some promise-based code invoked in the effect?). Here's a working example based on your snippet:
const DummyComponent = ({}) => {
const [selected, setSelected] = React.useState(false);
const [result, setResult] = React.useState("");
React.useEffect(() => {
setSelected(true);
}, []);
return selected && <div id='container' onClick={() => setResult("test")}>
<label>{result}</label>
</div>;
};
it('test', () => {
const wrapper = mount(<DummyComponent/>);
act(() => {
wrapper.find('#container').first().props().onClick();
});
expect(wrapper.find("label").text()).toEqual("test");
});
The act is actually needed only for interaction with the component itself, not for after-render effect.
The problem is that when you first mount the component, it does not render anything, because selected is false. So, when you search for '#container', you don't get anything.
If the update is enough, then it should probably be executed before the wrapper.find(), so that the component is rendered with selected true. But React is asynchronous and I suspect that this will not be enough…
I fixed my problem, actually I need to assign my component to a different wrapper and then update the wrapper and then check for updates on the wrapper instead of the component. Below is the snippet:
it('test', () => {
const component= mount( <DummyComponent /> );
const wrapper = component.update();
wrapper.find('#container')first().props().onClick();
expect(wrapper.toMatchSnapshot());
});
This will have the updated component

how to handle useEffect with render props in react

I want to use a render props function with hooks but I am not entirely sure if this is possible.
I have a FetcherComponent that takes a renderprop but I want to use a setState from
export const HierarchyGraph: React.FunctionComponent = () => {
const [rootNode, setRootNode] = useState<HierarchyNode<GraphicalNode> | null>(null);
return (
<Fetcher
url="/hierarchy"
initialData={{}}
render={({ data }: { data: TreeData }) => {
// this will cause infinite recursion
setRootNode(getHierarchy(data));
Should I not use render props in this situation?
You can use a render prop but you have to branch inside the render function of HierarchyGraph to detect whether or not you have to make the call. Otherwise the request is triggered multiple times. Here is a quick example:
const HierarchyGraph = () => {
const [rootNode, setRootNode] = useState(null);
if (!rootNode) {
return (
<Fetcher
url="/hierarchy"
initialData={{}}
render={({ data }) => {
setRootNode(getHierarchy(data));
}}
/>
);
}
return <div>render the data related to rootNode</div>;
};
An alternative solution is to inline the call inside the render function and perform the operation on each render. It depends on the use case but if the operation is cheap it might be simpler. The last alternative is to leverage useEffect rather than the Fetcher component. Its usage would be more suited than the render prop pattern since you can explicitly trigger the call to the API only once.
it causes infinite recursion because of setRootNode causing re-render of HierarchyGraph and this again triggers setRootNode. You need to find a way to stop this state updates when it's not necessary e.g:
export const HierarchyGraph = () => {
const [data, setData] = useState({});
return (
<Fetcher
url="/hierarchy"
initialData={data}
render={({ data: newData }) => {
if(data !== newData) {
setData(newData);
}
}}
/>
);
}

getSnapshotBeforeUpdate using react hooks

How can I implement the same logic that getSnapshotBeforeUpdate gives me using react hooks?
As per the React Hooks FAQ, there isn't a way to implement getSnapshotBeforeUpdate and ComponentDidCatch lifecycle method with hooks yet
Do Hooks cover all use cases for classes?
Our goal is for Hooks to cover all use cases for classes as soon as
possible. There are no Hook equivalents to the uncommon
getSnapshotBeforeUpdate and componentDidCatch lifecycles yet, but we
plan to add them soon.
It is a very early time for Hooks, so some integrations like DevTools
support or Flow/TypeScript typings may not be ready yet. Some
third-party libraries might also not be compatible with Hooks at the
moment.
We cannot get the snapshot data in any of the hooks (useLayoutEffect or useEffect) as both will give the updated DOM values by the time they are triggered, the best place to capture the data is just before the setting the state. for example here I am capturing the scroll position before setting the state.
function ChatBox(props){
const [state, setState] = useState({chatFetched:[],isFetching:false});
const listRef = useRef();
const previousScrollDiff = useRef(0);
// on mount
useEffect(()=>{
getSomeMessagesApi().then(resp=>{
const chatFetched = [...state.chatFetched,...resp];
setState({chatFetched});
})
},[]);
useLayoutEffect(()=>{
// use the captured snapshot here
listRef.current.scrollTop = listRef.current.scrollHeight - previousScrollDiff.current;
},[state.chatFetched])
useEffect(()=>{
// don't use captured snapshot here ,will cause jerk effect in scroll
},[state.chatFetched]);
const onScroll = (event) => {
const topReached = (event.target.scrollTop === 0);
if(topReached && !state.isFetching){
setState({...state, isFetching:true});
getSomeMessagesApi().then(resp=>{
const chatFetched = [...resp,...state.chatFetched];
// here I am capturing the data ie.., scroll position
previousScrollDiff.current = listRef.current.scrollHeight -listRef.current.scrollTop;
setState({chatFetched, isFetching:false});
})
}
}
return (
<div className="ui container">
<div
className="ui container chat list"
style={{height:'420px', width:'500px',overflow:'auto'}}
ref={listRef}
onScroll={onScroll}
>
{state.chatFetched.map((message)=>{
return <ChatLi data ={message} key ={message.key}></ChatLi>
})}
</div>
</div>
);
};
we can also useMemo to capture the data before dom update happens,
function ChatBox(props){
const [state, setState] = useState({chatFetched:[],isFetching:false});
const listRef = useRef();
const previousScrollDiff = useRef(0);
// on mount
useEffect(()=>{
getSomeMessagesApi().then(resp=>{
const chatFetched = [...state.chatFetched,...resp];
setState({chatFetched});
})
},[]);
useLayoutEffect(()=>{
// use the captured snapshot here
listRef.current.scrollTop = listRef.current.scrollHeight - previousScrollDiff.current;
},[state.chatFetched])
useEffect(()=>{
// don't use captured snapshot here ,will cause jerk effect in scroll
},[state.chatFetched]);
useMemo(() => {
// caputure dom info in use effect
if(scrollUl.current){
previousScrollDiff.current = scrollUl.current.scrollHeight - scrollUl.current.scrollTop;
}
}, [state.chatFetched]);
const onScroll = (event) => {
const topReached = (event.target.scrollTop === 0);
if(topReached && !state.isFetching){
setState({...state, isFetching:true});
getSomeMessagesApi().then(resp=>{
const chatFetched = [...resp,...state.chatFetched];
setState({chatFetched, isFetching:false});
})
}
}
return (
<div className="ui container">
<div
className="ui container chat list"
style={{height:'420px', width:'500px',overflow:'auto'}}
ref={listRef}
onScroll={onScroll}
>
{state.chatFetched.map((message)=>{
return <ChatLi data ={message} key ={message.key}></ChatLi>
})}
</div>
</div>
);
};
In the above examples I am trying to do the same that is shown here in the getSnapshotBeforeUpdate react doc
You can use useMemo() instead of getSnapshotBeforeUpdate(). Read more here about how to memoize calculations with React Hooks.
Here a simple example:
It always that the user types (onChange) an irrelevant state in point of view of the List Component is changed and because of this it re-rendering and can re-rendering more than 50 times it depends on the user typing, so it's used useMemo() to memoize the List Component and it stated that just todoList listens.
import List from './List'
const todo = (props) => {
const [inputIsValid, setInputIsValid] = useState(false)
const inputValidationHandler = (event) => {
if(event.target.value.trim() === '') {
setInputIsValid(false)
} else {
setInputIsValid(true)
}
}
return <React.Fragment>
<input
type="text"
placeholder="Todo"
onChange={inputValidationHandler}
/>
{
useMemo(() => (
<List items={todoList} onClick={todoRemoveHandler} />
), [todoList])
}
</React.Fragment>
}
export default todo
Short answer: There isn't a react hook for it! But we can create a custom one!
That's using useEffect() and useLayoutEffect()! As they are the key elements!
The final example is all last! So make sure to check it (Our custom hooks equivalent).
useEffect() and useLayoutEffect()
useEffect => useEffect runs asynchronously and after a render is painted to the screen.
You cause a render somehow (change state, or the parent re-renders)
React renders your component (calls it)
The screen is visually updated
THEN useEffect runs
useEffect() => render() => dom mutation => repaint => useEffect() [access dom new state] (changing dom directly) => repaint
==> Meaning useEffect() is like comonentDidUpdate()!
useLayoutEffect => useLayoutEffect, on the other hand, runs synchronously after a render but before the screen is updated. That goes:
You cause a render somehow (change state, or the parent re-renders)
React renders your component (calls it)
useLayoutEffect runs, and React waits for it to finish.
The screen is visually updated
useLayoutEffect() => render => dom mutation [detached] => useLayoutEffec() [access dom new state] (mutate dom) => repaint (commit, attach)
===> Meaning useLayoutEffect() run like getSnapshotBeforeUpdate()
Knowing this! We can create our custom hooks that allow us to do things like with getSnapshotBeforeUpdate() and didComponentUpdate().
Such an example would be updating the scrolling for auto update in chat applications!
usePreviousPropsAndState()
Similar to the usePrevious() hook mentionned in "how to get previous prop and state"
Here an hook implementation for saving and getting previous props and state!
const usePrevPropsAndState = (props, state) => {
const prevPropsAndStateRef = useRef({ props: null, state: null })
const prevProps = prevPropsAndStateRef.current.props
const prevState = prevPropsAndStateRef.current.state
useEffect(() => {
prevPropsAndStateRef.current = { props, state }
})
return { prevProps, prevState }
}
We can see how we need to pass the props and state object!
What you pass is what you get! So it's easy to work with! An object will do well!
useGetSnapshotBeforeUpdate & useComponentDidUpdate
Here the total solution or implementation
const useGetSnapshotBeforeUpdate = (cb, props, state) => {
// get prev props and state
const { prevProps, prevState } = usePrevPropsAndState(props, state)
const snapshot = useRef(null)
// getSnapshotBeforeUpdate (execute before the changes are comitted for painting! Before anythingg show on screen) - not run on mount + run on every update
const componentJustMounted = useRef(true)
useLayoutEffect(() => {
if (!componentJustMounted.current) { // skip first run at mount
snapshot.current = cb(prevProps, prevState)
}
componentJustMounted.current = false
})
// ________ a hook construction within a hook with closure __________
const useComponentDidUpdate = cb => {
// run after the changes are applied (commited) and apparent on screen
useEffect(() => {
if (!componentJustMounted.current) { // skip first run at mount
cb(prevProps, prevState, snapshot.current)
}
})
}
// returning the ComponentDidUpdate hook!
return useComponentDidUpdate
}
You can notice how we constructed the hook within the other hook! Make use of closure! And accessing elements directly! And linking the two hooks!
pre-commit phase and commit phase (and effects hooks)
I used those terms! What does it actually means ?
class example
From the doc
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
Our custom hooks equivalent
const App = props => {
// other stuff ...
const useComponentDidUpdate = useGetSnapshotBeforeUpdate(
(prevProps, prevState) => {
if (prevProps.list.length < props.list.length) {
const list = listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
},
props,
state
)
useComponentDidUpdate((prevProps, prevState, snapshot) => {
if (snapshot !== null) {
const list = listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
})
// rest ...
}
useEffectLayout() in useGetSnapshotBeforeUpdate hook will execute first!
useEffect() in useComponentDidUpdate will execute after!
As just was shown in the lifecyle schema!

Resources