React Context API with multiple values performance - javascript

I'm using the React Context API to store many global state values (around 10 and probably more will be needed) and many components are using them. Unfortunately whenever any of the values change, all components using the useContext hook have to rerender. My current solution is to use useMemo for the return value of the components and useCallback for any complex functions and inside custom hooks I have. This addresses most of my performance concerns, but having to use the useMemo and useCallback all the time is quite annoying and missing one is quite easy. Is there a more professional way to do it?
Here's an example based on my code:
GlobalStateContext.js
import React, { useState } from 'react'
const GlobalStateContext = React.createContext({ })
export const GlobalStateProvider = ({ children }) => {
const [config, setConfig] = useState({
projectInfo: ''
})
const [projectFile, setProjectFile] = useState('./test.cpp')
const [executionState, setExecutionState] = useState("NoProject")
return (
<GlobalStateContext.Provider
value={{
executionState,
config,
projectFile,
setExecutionState,
setConfig,
setProjectFile,
}}
>
{children}
</GlobalStateContext.Provider>
)
}
export default GlobalStateContext
Example.jsx
import React, { useContext } from 'react'
import GlobalStateContext from '../utils/GlobalStateContext.js'
export default Example = () => {
const {
executionState,
setExecutionState,
} = useContext(GlobalStateContext)
return useMemo(
() => (
<div>
The current execution state is: {executionState}
<br />
<button onClick={() => setExecutionState('Running')}>Running</button>
<button onClick={() => setExecutionState('Stopped')}>Stopped</button>
<button onClick={() => setExecutionState('Crashed')}>Crashed</button>
</div>
),
[
executionState,
setExecutionState,
]
)
}

Currently, this problem is unavoidable with context. There is an open RFC for context selectors to solve this, but in the meantime, some workarounds are useContextSelector and Redux, both of which prevent a subscribing component from rendering if the data it's reading did not change.

Related

React Hooks - Preventing child components from rendering

As a newbie in React, it seems that re-rendering of components is the thing not to do.
Therefore, for example, if I want to create a menu following this architecture :
App is parent of Menu, which have a map function which creates the MenuItem components
menu items come from a data source (here it's const data)
when I click on a MenuItem, it updates the state with the selected MenuItem value
for now it's fine, except that all the components are re-rendered (seen in the various console.log)
Here's the code :
App
import React, { useState} from "react"
import Menu from "./menu";
function App() {
const data = ["MenuItem1", "MenuItem2", "MenuItem3", "MenuItem4", "MenuItem5", "MenuItem6"]
const [selectedItem, setMenuItem] = useState(null)
const handleMenuItem = (menuItem) => {
setMenuItem(menuItem)
}
return (
<div className="App">
<Menu items = {data} handleMenuItem = {handleMenuItem}></Menu>
<div>{selectedItem}</div>
</div>
);
}
export default App;
Menu
import React from "react";
import MenuItem from "./menuItem";
const Menu = (props) => {
return (
<>
{props.items.map((item, index) => {
return <MenuItem key = {index} handleMenuItem = {props.handleMenuItem} value = {item}></MenuItem>
})
}
{console.log("menuItem")}
</>
)
};
export default React.memo(Menu);
MenuItem
import React from "react";
const MenuItem = (props) => {
return (
<>
<div onClick={() => props.handleMenuItem(props.value)}>
<p>{props.value}</p>
</div>
{console.log("render du MenuItem")}
</>
)
};
export default React.memo(MenuItem);
as you might see, I've used the React.memo in the end of MenuItem but it does not work, as well as the PureComponent
If someone has an idea, that'd be great to have some advice.
Have a great day
Wrap your handleMenuItem function with useCallback to avoid rerendering when the function changes. This will create a single function reference that will be used in the MenuItem as props and will avoid rereading since it's the same function instance always.
I have used an empty dependency array in this case which is correct for your use case. If your function has any state references then they should be added to the array.
const handleMenuItem = useCallback((menuItem) => {
setMenuItem(menuItem);
}, []);
There's a lot to unpack here so let's get started.
The way hooks are designed to prevent re-rendering components unnecessarily is by making sure you use the same instance of any unchanged variables, most specifically for object, functions, and arrays. I say that because string, number, and boolean equality is simple 'abc' === 'abc' resolves to true, but [] === [] would be false, as those are two DIFFERENT empty arrays being compared, and equality in JS for objects and functions and arrays only returns true when the two sides being compared are the exact same item.
That said, react provides ways to cache values and only update them (by creating new instances) when they need to be updated (because their dependencies change). Let's start with your app.js
import React, {useState, useCallback} from "react"
import Menu from "./menu";
// move this out of the function so that a new copy isn't created every time
// the App component re-renders
const data = ["MenuItem1", "MenuItem2", "MenuItem3", "MenuItem4", "MenuItem5", "MenuItem6"]
function App() {
const [selectedItem, setMenuItem] = useState(null);
// cache this with useCallback. The second parameter (the dependency
// array) is an empty array because there are no items that, should they
// change, we should create a new copy. That is to say we should never
// need to make a new copy because we have no dependencies that could
// change. This will now be the same instance of the same function each
// re-render.
const handleMenuItem = useCallback((menuItem) => setMenuItem(menuItem), []);
return (
<div className="App">
<Menu items={data} handleMenuItem={handleMenuItem}></Menu>
<div>{selectedItem}</div>
</div>
);
}
export default App;
Previously, handleMenuItem was set to a new copy of that function every time the App component was re-rendered, and data was also set to a new array (with the same entries) on each re-render. This would cause the child component (Menu) to re-render each time App was re-rendered. We don't want that. We only want child components to re-render if ABSOLUTELY necessary.
Next is the Menu component. There are pretty much no changes here, although I would urge you not to put spaces around your = within your JSX (key={index} not key = {index}.
import React from "react";
import MenuItem from "./menuItem";
const Menu = (props) => {
return (
<>
{props.items.map((item, index) => {
return <MenuItem key={index} handleMenuItem={props.handleMenuItem} value={item}/>
})
}
{console.log("menuItem")}
</>
)
};
export default React.memo(Menu);
For MenuItem, let's cache that click handler.
import React from "react";
const MenuItem = (props) => {
// cache this function
const handleClick = useCallback(() => props.handleMenuItem(props.value), [props.value]);
return (
<>
<div onClick={handleClick}>
<p>{props.value}</p>
</div>
{console.log("render du MenuItem")}
</>
)
};
export default React.memo(MenuItem);

React - Passing callbacks from React Context consumers to providers

I have the following context
import React, { createContext, useRef } from "react";
const ExampleContext = createContext(null);
export default ExampleContext;
export function ExampleProvider({ children }) {
const myMethod = () => {
};
return (
<ExampleContext.Provider
value={{
myMethod,
}}
>
{children}
<SomeCustomComponent
/* callback={callbackPassedFromConsumer} */
/>
</ExampleContext.Provider>
);
}
As you can see, it renders a custom component which receive a method as prop. This method is defined in a specific screen, which consumes this context.
How can I pass it from the screen to the provider?
This is how I consume the context (with a HOC):
import React from "react";
import ExampleContext from "../../../contexts/ExampleContext";
const withExample = (Component) => (props) =>
(
<ExampleContext.Consumer>
{(example) => (
<Component {...props} example={example} />
)}
</ExampleContext.Consumer>
);
export default withExample;
And this is the screen where I have the method which I need to pass to the context provider
function MyScreen({example}) {
const [data, setData] = useState([]);
const myMethodThatINeedToPass = () => {
...
setData([]);
...
}
return (<View>
...
</View>);
}
export default withExample(MyScreen);
Update:
I am trying to do this because in my real provider I have a BottomSheet component which renders two buttons "Delete" and "Report". This component is reusable, so, in order to avoid repeating myself, I am using a context provider.
See: https://github.com/gorhom/react-native-bottom-sheet/issues/259
Then, as the bottom sheet component which is rendered in the provider can receive optional props "onReportButtonPress" or "onDeleteButtonPress", I need a way to pass the method which manipulates my stateful data inside the screen (the consumer) to the provider.
You can't, in React the data only flows down.
This is commonly called a “top-down” or “unidirectional” data flow. Any state is always owned by some specific component, and any data or UI derived from that state can only affect components “below” them in the tree.
Your callbacks ("onReportButtonPress", "onDeleteButtonPress") must be available at provider's scope.
<ExampleContext.Provider
value={{
onReportButtonPress,
onDeleteButtonPress,
}}
>
{children}
</ExampleContext.Provider>;
Render SomeCustomComponent in Consumer component. This is the React way of doing things :)

React Hooks - Updating state using props without event handlers

I'm relatively new to React and this is what I'm trying to accomplish:
User selects an item from the sidebar.
The elementID is lifted up to the parent(app.js).
app.js sends it to its child, Graphs.
Graphs will create a Graph component and append to its graph array.
Is there a better way than this? P.S I'll have more than 1x useEffect in this component.
App.js
- Sidebar
- Title-bar
- Graphs
function Graphs(props) {
const [graphs, addGraphs] = useState([]);
useEffect(() => {
if (props.graphID) {
addGraphs(graphs.concat(props.graphID));
}
}, [props.graphID]);
return (
<div>{graphs}</div>
);
}
Thank you!
I believe it is a good approach, but you should use an functional state update. The "setter" functions of React.useState hook has a callback with previous state, so you shall update it like this:
import React from "react";
function MyComponent({id}) {
const [list, setList] = React.useState([]);
React.useEffect(() => {
if (id) {
setList(previousState => {
return [
...previousState,
id
];
});
}
}, [id]);
return (
<div>
{
list.map((_id, index) => {
<p key={index}>
{_id.toString()}
</p>
)
}
</div>
);
}
There's nothing wrong with sending props as you have done Jonathan.
If you are looking for other state management options you can look into the native React useContext, useReducer hooks to create your own system. Otherwise there are frameworks like Redux Saga, and more recently Recoil is worth checking out.
If you just simply want to select an ID and send it to another component, using props or useContext is probably your best bet.

Using a global object in React Context that is not related to state

I want to have a global object that is available to my app where I can retrieve the value anywhere and also set a new value anywhere. Currently I have only used Context for values that are related to state i.e something needs to render again when the value changes. For example:
import React from 'react';
const TokenContext = React.createContext({
token: null,
setToken: () => {}
});
export default TokenContext;
import React, { useState } from 'react';
import './App.css';
import Title from './Title';
import TokenContext from './TokenContext';
function App() {
const [token, setToken] = useState(null);
return(
<TokenContext.Provider value={{ token, setToken }}>
<Title />
</TokenContext.Provider>
);
}
export default App;
How would I approach this if I just want to store a JS object in context (not a state) and also change the value anywhere?
The global context concept in React world was born to resolve problem with passing down props via multiple component layer. And when working with React, we want to re-render whenever "data source" changes. One way data binding in React makes this flow easier to code, debug and maintain as well.
So what is your specific purpose of store a global object and for nothing happen when that object got changes? If nothing re-render whenever it changes, so what is the main use of it?
Prevent re-render in React has multiple ways like useEffect or old shouldComponentUpdate method. I think they can help if your main idea is just prevent re-render in some very specific cases.
Use it as state management libraries like Redux.
You have a global object (store) and you query the value through context, but you also need to add forceUpdate() because mutating the object won't trigger a render as its not part of React API:
const globalObject = { counter: 0 };
const Context = React.createContext(globalObject);
const Consumer = () => {
const [, render] = useReducer(p => !p, false);
const store = useContext(Context);
const onClick = () => {
store.counter = store.counter + 1;
render();
};
return (
<>
<button onClick={onClick}>Render</button>
<div>{globalObject.counter}</div>
</>
);
};
const App = () => {
return (
<Context.Provider value={globalObject}>
<Consumer />
</Context.Provider>
);
};

How to use multiple global states using the same Context in React Native?

I'm developing a Mobile Application using React Native. I came up with a thought of, is it possible to use multiple global states using the same Context.
So, I implemented a context like this.
// GlobalStateContext.js
import React, { createContext, useContext, useEffect } from 'react';
const GlobalStateContext = createContext({ globalStateValue: null, setGlobalStateValue: () => {} });
export function useGlobalState (initialValue = null) {
const { globalStateValue, setGlobalStateValue } = useContext(GlobalStateContext);
useEffect(() => { setGlobalStateValue(initialValue) }, []);
return [globalStateValue, setGlobalStateValue];
}
export default function GlobalStateContextProvider (props) {
const { value, children } = props;
return (
<GlobalStateContext.Provider value={value} >
{children}
</GlobalStateContext.Provider>
)
}
In my App.js, I've wrapped my App with the GlobalStateContextProvider.
// App.js
import GlobalStateContextProvider from './path/to/GlobalStateContext';
export default function App() {
const [globalStateValue, setGlobalStateValue] = useState(null);
return (
<GlobalStateContextProvider value={{ globalStateValue, setGlobalStateValue }} >
// <MyAppContent />
</GlobalStateContextProvider>
)
}
Then, I tried to do something like this on some of my screen files.
import { useGlobalState } from '../path/to/GlobalStateContext';
const [myState1, setMyState1] = useGlobalState('State 01');
const [myState2, setMyState2] = useGlobalState('State 02');
But, there are NO TWO SEPARATE global states. I know there should do something more in the implementation if it is possible to achieve what I want.
So, is this thing is possible? When using useState hook, we can do like,
const [myState1, setMyState1] = useState('State 01');
const [myState2, setMyState2] = useState('State 02');
In this case, we have TWO SEPARATE local states.
Like this, I want to know if it is possible to define some GLOBAL STATES and use them.

Categories

Resources