Update React Context without re-rendering the component making the update - javascript

I have a Context and 2 Components: one is displaying what is in the context, the other updating it.
By having the following code in the updater Component, it will re-render upon changing the context.
const [, setArray] = React.useContext(context);
setArray(prevArray => { return [...prevArray, []] }
This means infitie re-renders. I need to avoid this. As the updater doesn't use the data in the context it should not update.
Complete example: I'm storing and displaying Profiler data about a Component.
https://codesandbox.io/s/update-react-context-without-re-rendering-the-component-making-the-update-k8ogr?file=/src/App.js
const context = React.createContext();
const Provider = props => {
const [array, setArray] = React.useState([]);
const value = React.useMemo(() => [array, setArray], [array]);
return <context.Provider value={value} {...props} />;
};
const Metrics = () => {
const [array] = React.useContext(context);
return <TextareaAutosize value={JSON.stringify(array, null, 2)} />;
};
const Component = () => {
const [, setArray] = React.useContext(context);
const onRenderCallback = (id, _phase, actualDuration) => {
setArray(prevArray => {
return [...prevArray, [id, actualDuration]];
});
};
return (
<React.Profiler id="1" onRender={onRenderCallback}>
<div />
</React.Profiler>
);
};
export default function App() {
return (
<div className="App">
<Provider>
<Metrics />
<Component />
</Provider>
</div>
);
}

This is what I came up with using the following article: https://kentcdodds.com/blog/how-to-optimize-your-context-value
Use 2 contexts, one for storing the state, one for updating it:
const stateContext = React.createContext();
const updaterContext = React.createContext();
const array = React.useContext(stateContext);
const setArray = React.useContext(updaterContext);
Complete example:
https://codesandbox.io/s/solution-update-react-context-without-re-rendering-the-component-making-the-update-yv0gf?file=/src/App.js
import React from "react";
import "./styles.css";
import TextareaAutosize from "react-textarea-autosize";
// https://kentcdodds.com/blog/how-to-optimize-your-context-value
const stateContext = React.createContext();
const updaterContext = React.createContext();
const Provider = props => {
const [array, setArray] = React.useState([]);
return (
<stateContext.Provider value={array}>
<updaterContext.Provider value={setArray}>
{props.children}
</updaterContext.Provider>
</stateContext.Provider>
);
};
const useUpdaterContext = () => {
return React.useContext(updaterContext);
};
const Metrics = () => {
const array = React.useContext(stateContext);
return <TextareaAutosize value={JSON.stringify(array, null, 2)} />;
};
const Component = () => {
const setArray = useUpdaterContext();
const onRenderCallback = (id, _phase, actualDuration) => {
setArray(prevArray => [...prevArray, [id, actualDuration]]);
};
return (
<React.Profiler id="1" onRender={onRenderCallback}>
<div />
</React.Profiler>
);
};
export default function App() {
return (
<div className="App">
<Provider>
<Metrics />
<Component />
</Provider>
</div>
);
}

Related

Why is my global State not updating across other components

So I am trying to store a global state using context to allow me to use the same state across different components.
The issue I am having is that when I set the global state in 1 component and try to access it in the other component to use the state. It appears to be null and I cannot figure out why?
The first component where I set the global state in will always be rendered before the component shown that seems to have an empty value for the global state.
GlobalStateProvider component:
import React from "react";
import { useState, useEffect } from "react";
import axios from "axios";
const defaultActivitiesState = [];
const globalStateContext = React.createContext(defaultActivitiesState);
const dispatchStateContext = React.createContext([]);
export const useGlobalState = () =>
[
React.useContext(globalStateContext),
React.useContext(dispatchStateContext)
];
const GlobalStateProvider = ({ children }) => {
const [state, dispatch] = React.useReducer((state, newValue) => (state, newValue),
defaultActivitiesState
);
return (
<globalStateContext.Provider value={state}>
<dispatchStateContext.Provider value={dispatch}>
{children}
</dispatchStateContext.Provider>
</globalStateContext.Provider>
);
}
export default GlobalStateProvider;
Component I set the global state in:
import react from "react";
import { useState, useEffect, useMemo } from "react";
import { MapContainer, TileLayer, Popup, Polyline } from "react-leaflet";
import axios from "axios";
import polyline from "#mapbox/polyline";
import MapComp from "./MapComp";
import { useGlobalState } from "./GlobalStateProvider";
function Map() {
// ------- global state
const [activities, setActivities] = useGlobalState(); // global state
//const [activities, setActivities] = useState([]);
//const [polylines, setPolylines] = useState(null); // as empty array value is still truthy
const [isLoading, setIsLoading] = useState(true);
const [mapMode, setMapMode] = useState("light");
const [mapStyle, setMapStyle] = useState(
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
);
const [showMarkers, setShowMarkers] = useState(true);
useEffect(() => {
setActivitieData();
console.log("activities after useEffect", activities)
}, []);
const polylines = useMemo(() => {
console.log("activities inside memo", activities)
console.log("activities.len =", activities.length);
if (activities.length) {
console.log("past len");
const polylineArray = [];
for (const item of activities) {
const polylineData = item.map.summary_polyline;
const activityName = item.name;
const activityType = item.type;
polylineArray.push({
positions: polyline.decode(polylineData),
name: activityName,
activityType: activityType,
});
}
setIsLoading(false);
return polylineArray;
}
return null;
}, [activities]);
const toggleMarkers = () => {
setShowMarkers((show) => !show);
};
const getActivityData = async () => {
console.log("calling")
const response = await axios.get(
"http://localhost:8800/api/"
);
return response.data;
};
const setActivitieData = async () => {
const activityData = await getActivityData();
setActivities(activityData);
console.log("Global activities state = ", activities);
};
return !isLoading && polylines ? (
<>
<div className="select-container">
<button className="toggle-markers" onClick={() => toggleMarkers()}>
Toggle Markers
</button>
</div>
<MapComp
className={`${mapMode}`}
activityData={{ polylines }}
showMarkers={showMarkers}
/>
</>
) : (
<div>
<p>Loading...</p>
</div>
);
}
export default Map;
component that has an empty value for global state:
import React from 'react';
import { useGlobalState } from './GlobalStateProvider';
function ActivityList() {
const [activities, setActivities] = useGlobalState();
let displayValues;
displayValues =
activities.map((activity) => {
return (
<div>
<p>{activity.name}</p>
<p>{activity.distance}m</p>
</div>
);
})
return (
<>
<p>Values</p>
{displayValues}
</>
);
}
export default ActivityList;
App.js:
function App() {
return (
<GlobalStateProvider>
<div className="App">
<NavBar />
<AllRoutes />
</div>
</GlobalStateProvider>
);
}
export default App;

props getting added twice to the array variable that is declared outside the component in React / Javascript

I am trying to add, new data that user enters, to the array declared outside the component named DUMMY_MEALS and then render it as a list.
The problem here is the 'data' which is an object adds twice or more to the DUMMY_MEALS and renders twice in the page. Why is this happening?
The component with issue
"use strict";
import React from "react";
let DUMMY_MEALS = [
{id: "m1", name: "Sushi"},
];
const MealList = ({data}) => {
//const data = {id: "m5", name: "pushi"}
let mealslist = [];
DUMMY_MEALS = [data, ...DUMMY_MEALS];
mealslist = DUMMY_MEALS.map((meal) => <li>{meal.name}</li>);
return <ul>{mealslist}</ul>;
};
export default MealList;
Parent component
const Header = () => {
const [data, setData] = useState({});
const sendInputData = (inputData) => {
setData(inputData);
}
return (
<>
<MealsList data={data}/>
<MealForm getInputData={sendInputData}/>
</>
);
};
export default Header;
Sibling Component
const MealForm = (props) => {
const [name, setName] =useState("");
const formSubmitHandler = (e) => {
e.preventDefault();
let inputData = {
key : Math.random(),
name : name,
}
props.getInputData(inputData);
inputData = {};
}
return (
<form onSubmit={formSubmitHandler}>
<label htmlFor="name">name</label>
<input type="text" id="name" value={name} onChange={(e)=>setName(e.target.value)}></input>
<button>Submit</button>
</form>
);
};
export default MealForm;
You should use useState hook instead of let mealslist = []; Inside your MealList component.
And don't use DUMMY_MEALS as the component state. use useEffect hook to add the new meal to the state just once.
Check out this tested code CodeSandbox
MealList component changed as follow :
const MealList = ({ data }) => {
const [mealslist, setMealList] = useState([]);
useEffect(() => {
if (data)
setMealList([data, ...DUMMY_MEALS]);
}, []);
return <ul>{
mealslist.map((meal)=>{ <ListRender meal={meal} />})
}
</ul>;
};
And here is your App component:
const data = {
id: "k123",
name: "Falafel",
description: "An Iranian food.",
price: 16.5
};
export default function App() {
return (
<MealList data={data} />
);
}

Passing hook function to a component to be used in passed component

Can we pass hook as a function to a component and use it in that component?
Like in the example below I am passing useProps to withPropConnector which returns a Connect component. This useProps is being used in the Connect component. Am I violating any hook rules?
// Landing.jsx
export const Landing = (props) => {
const { isTypeOne, hasFetchedData } = props;
if (!hasFetchedData) {
return <Loader />;
}
const renderView = () => {
if (isSomeTypeOne) {
return <TypeOneView />;
}
return <TypeTwoView />;
};
return (
<>
<Wrapper>
{renderView()}
<SomeNavigation />
</Wrapper>
<SomeModals />
</>
);
};
const useProps = () => {
const { query } = useRouter();
const { UID } = query;
const { isTypeOne, isTypeTwo } = useSelectorWithShallowEqual(getType);
const hasFetchedData = useSelectorWithShallowEqual(
getHasFetchedData(UID)
);
const props = {
isTypeOne,
isTypeTwo,
hasFetchedData
};
return props;
};
export default withPropConnector(useProps, Landing);
// withPropConnector.js
const withPropConnector = (useProps, Component) => {
const Connect = (propsFromParent = emptyObject) => {
const props = useProps(propsFromParent);
return <Component {...propsFromParent} {...props} />;
};
return Connect;
};
export default withPropConnector;

Only conditionally re-render when context value updates

I'm am working on a large React application where performance is critical and unnecessary re-renders are costly.
I have the following example:
const CounterContext = React.createContext();
const CounterProvider = ({children}) => {
const [counterA, setCounterA] = React.useState(0);
const [counterB, setCounterB] = React.useState(0);
return (
<CounterContext.Provider value={{counterA, counterB}}>
{children}
<button onClick={() => setCounterA(counterA + 1)}>Counter A ++</button>
<button onClick={() => setCounterB(counterB + 1)}>Counter B ++</button>
<button onClick={() => {setCounterA(0); setCounterB(0)}}>reset</button>
</CounterContext.Provider>
)
}
const CounterA = () => {
const value = React.useContext(CounterContext);
console.log('CounterA re-render');
return <p>Counter A: {value.counterA}</p>;
}
const CounterB = () => {
const value = React.useContext(CounterContext);
console.log('CounterB re-render');
return <p>Counter B: {value.counterB}</p>;
};
const App = () => {
return (
<CounterProvider>
<CounterA />
<CounterB />
</CounterProvider>
)
};
jsfiddle: https://jsfiddle.net/mitchkman/k3hm0vfq/1/
When clicking the CounterA button, both CounterA and CounterB components will re-render. I'd like to only re-render CounterA, if the counterA property in value changes.
I'd also like to have the ability to have some form of flexibility for conditional re-rendering. This is a pseudocode of what I am trying to do:
const MyComponent = () => {
// Only re-render MyComponent if value.property equals 42
const value = useContext(MyContext, (value) => value.property === 42);
...
};
You may have to wrap the counterProvider to your App component. And use React.memo to achieve re-rendering only on value changes for that component.
I have done something like here in this sandbox
index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { CounterProvider } from "./AppContext";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<CounterProvider>
<App />
</CounterProvider>
</StrictMode>,
rootElement
);
Moved the counterContext to a separate file so that you can import and wrap your App component.
AppContext.js
import React, { useCallback } from "react";
const CounterContext = React.createContext();
export const CounterProvider = ({ children }) => {
const [counterA, setCounterA] = React.useState(0);
const [counterB, setCounterB] = React.useState(0);
const incrementA = () => {
setCounterA(counterA + 1);
};
const incrementB = () => {
setCounterB(counterB + 1);
};
return (
<CounterContext.Provider value={{ counterA, counterB }}>
{children}
<button onClick={incrementA}>Counter A ++</button>
<button onClick={incrementB}>Counter B ++</button>
<button
onClick={useCallback(() => {
setCounterA(0);
setCounterB(0);
}, [])}
>
reset
</button>
</CounterContext.Provider>
);
};
export default CounterContext;
Pass the counterA and counterB values to the Counter components as a props, so that React.memo can check that value before re-rendering the component.
App.js
import React from "react";
import CounterContext from "./AppContext";
const CounterA = React.memo(({ value }) => {
console.log("CounterA re-render");
return <p>Counter A: {value}</p>;
});
const CounterB = React.memo(({ value }) => {
console.log("CounterB re-render");
return <p>Counter B: {value}</p>;
});
const App = () => {
const value = React.useContext(CounterContext);
const counterA = value.counterA;
const counterB = value.counterB;
console.log(counterA);
return (
<>
<CounterA value={counterA} />
<CounterB value={counterB} />
</>
);
};
export default App;

TypeError: list.map not a function

I'm building an app for React Practice and I'm getting an error while trying to store an array of objects to local storage. I'm a beginner at React so I'm not sure what's going on.
I get the error in the IncomeOutputList component, because I am trying to list a bunch of objects using an array.
Here is my code:
App component:
import React, { useState, useReducer } from 'react';
import BudgetInput from './components/input/BudgetInput';
import IncomeOutputList from './components/output/IncomeOutputList';
import IncomeOutput from './components/output/IncomeOutput';
const useSemiPersistentState = (key, initialState) => {
const [value, setValue] = React.useState(
localStorage.getItem(key) || initialState
);
React.useEffect(()=>{
localStorage.setItem(key, JSON.stringify(value));
}, [value, key])
return [value, setValue];
};
const App = () => {
const [incomes, setIncomes] = useSemiPersistentState('income',[{}]);
const [description, setDescription] = useState('');
const [type, setType] = useState('+');
const [value, setValue] = useState('');
const incomeObj = {
desc: description,
budgetType: type,
incomeValue: value
}
const handleIncomeObjArray = () => {
setIncomes(incomes.concat(incomeObj));
console.log(incomes + "testing");
}
const handleChange = (event) => { //this handler is called in the child component BudgetInput
setDescription(event.target.value);
}
const handleSelectChange = (event) => { //this handler is called in the child component BudgetInput
setType(event.target.value);
}
const handleValueChange = (event) => {
setValue(event.target.value);
console.log(incomeObj)
}
return (
<div className="App">
<div className="top">
</div>
<div className="bottom">
<BudgetInput
descValue={description}
onDescChange={handleChange}
onSelectChange={handleSelectChange}
type={type}
onBudgetSubmit={handleIncomeObjArray}
budgetValue={value}
onValChange={handleValueChange}
/>
{/* <IncomeOutput
obj={incomeObj}
/> */}
{/* <IncomeOutput
desc={description}
type={type}
/> */}
<IncomeOutputList
list={incomes}
/>
</div>
</div>
)
};
IncomeOutputList component:
import React from 'react';
import IncomeOutput from './IncomeOutput';
// list will be list of income objects
const IncomeOutputList = ({ list }) => {
return (
<div>
{list.map(item => <IncomeOutput
id={item.id}
value={item.incomeValue}
type={item.budgetType}
desc={item.desc}
/>)}
</div>
)
}
export default IncomeOutputList;
IncomeOutput component:
import React from 'react';
import ValueOutput from './ValueOutput';
const IncomeOutput = ({ desc, type,id, value }) => {
//id = inc-{id}
return (
<>
<div className="item clearfix" id={id}>
<div className="item__description">{desc}</div>
<ValueOutput
type={type}
value={value}
/>
</div>
</>
)
}
export default IncomeOutput;
EDIT: here is a codesandbox for this code: https://codesandbox.io/s/busy-shirley-bwhv2?file=/src/App.js:2394-2509
It's because your Custom reducer return a string and not an array
try this
const useSemiPersistentState = (key, initialState) => {
const [value, setValue] = React.useState(
localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)) : initialState
);
React.useEffect(()=>{
localStorage.setItem(key, JSON.stringify(value));
}, [value, key])
return [value, setValue];
};

Categories