Change React Context without triggering a re-render - javascript

I have a download dialog that you can click on to download the graphics on my website. Because the graphics are created with HTML Canvas, I cannot simply download an array of image links. I have to contact each of the components and render each of them into a data URI.
On my app component, I have defined a context to keep track of which components should be downloaded.
export const DownloadFlagsContext = React.createContext<any>(null)
const App: React.FunctionComponent = (props) => {
const [downloadFlags, setDownloadFlags] = useState([])
return (
<DownloadFlagsContext.Provider value={{downloadFlags, setDownloadFlags}}>
<DownloadDialog/>
<div className="graphics">
<Graphic id={1}>
<Graphic id={2}>
<Graphic id={3}>
(multiple graphics)
</div>
</DownloadFlagsContext.Provider>
)
}
In my DownloadDialog, I trigger the downloads by setting the downloadFlags to the id's of each of the components that needs to be rendered.
const download = () => {
const newDownloadFlags = [] as any
for (let i = start; i < end; i++) {
newDownloadFlags.push(i)
}
setDownloadFlags(newDownloadFlags)
}
Now in each of the Graphic components, I trigger the download if the downloadFlags was changed and it contains it's id.
const render = () => {
const canvas = document.createElement("canvas")
/* code to render the graphic */
return canvas.toDataURL("image/png")
}
useEffect(() => {
if (downloadFlags.includes(props.id)) {
download("filename", render())
setDownloadFlags(downloadFlags.filter((s: string) => s !== props.id))
}
}, [downloadFlags])
The problem is that this code is triggering downloads. For example, if I set the download for 6 graphics it will result in downloading 21 images because every time that I change the downloadFlags, all of the components will get re-rendered, with the difference being that it includes one less id. So in total will download 6+5+4+3+2+1 images. Obviously this is very bad if I have a lot of graphics to download.
I would like to prevent the components from re-rendering so that it only downloads the first 6 times.
Also, I do not want to use a server. I want to download the images on the client side.

I have figured out a solution. In my app component, I keep track of the download ID's as well as a boolean flag for enabling the download:
export const DownloadIDsContext = React.createContext<any>(null)
export const DownloadFlagContext = React.createContext<any>(null)
const App: React.FunctionComponent = (props) => {
const [downloadIDs, setDownloadIDs] = useState([])
const [downloadFlag, setDownloadFlag] = useState(false)
return (
<DownloadFlagContext.Provider value={{downloadFlag, setDownloadFlag}}>
<DownloadURLsContext.Provider value={{downloadURLs, setDownloadURLs}}>
<DownloadDialog/>
<div className="graphics">
<Graphic id={1}>
<Graphic id={2}>
<Graphic id={3}>
(multiple graphics)
</div>
</DownloadURLsContext.Provider>
</DownloadFlagContext.Provider>
)
}
In my DownloadDialog, I set the boolean flag to true when I start a download.
const download = () => {
const newDownloadIDs = [] as any
for (let i = start; i < end; i++) {
newDownloadIDs.push(i)
}
setDownloadIDs(newDownloadIDs)
setDownloadFlag(true)
}
Now instead of checking for the download ID array in the child components, I only check for the boolean flag. Since it is set to false immediately it effectively only causes the components to render once.
useEffect(() => {
if (downloadFlag) {
if (downloadIDs.includes(props.id)) {
download("filename", render())
setDownloadIDs(downloadIDs.filter((s: string) => s !== props.id))
setDownloadFlag(false)
}
}
}, [downloadFlag])

Related

What is the safe way and best-practice to set a state in parent hierarchy in React to stay DRY?

Consider this scenario:
An admin panel that has a sidebar and a content.
Sidebar contains menu and each item that user clicks, a component would be shown in the content area.
The content is a Router that changes the component based on URL.
Let's say we have these components:
Page1.jsx
Page2.jsx
Page3.jsx
Each of the does some operation when gets loaded. Each does it inside useEffect.
useEffect(() => {
// some operation inside PageN.jsx that takes some time
// Here we should show a progress
}, [])
We can easily define a state in each PageN.jsx and based on that return a progress or the actual component:
const [progress, setProgress] = useState()
return progress
?
<div>Loading...</div>
:
<div>Actual component</div>
This however is inefficient for an application with 100 pages and kills DRY and also introduces a lot of inconsistencies. Each page might show a different loading component, etc.
One way is to lift-up the progress state to the parent element Router or event the App itself. Then pass it down the hierarchy.
const PageN = ({ setContentProgress }) => {
// now I can call parent's setContentProgress
But it soon gets into the famous Cannot update a component from inside the function body of a different component.
So, what is the safe approach here? What is the best practice?
I would definitely suggest you to use context API, to simply build some layout context on top of your sidebar and content area.
Recently I implemented something similar for my personal project where I decided that I want to implement linear progress bar like YouTube has (red line progressing below the header), so the only option for me was to create layout context which will handle progress and then in its children I would simply use exposed actions from context itself, in order to update progress.
I did it in this way:
interface IProps {
children: React.ReactNode;
}
const initialState = {
displayProgressBar: false,
progressPercentage: 0
};
const PROGRES_BAR_TRANSITION = 300; // ms
let timeout: undefined | NodeJS.Timeout;
const Layout = ({ children }: IProps) => {
const { pathname } = useLocation();
const [context, setContext] = useState(initialState);
const navigate = useNavigate();
useEffect(() => {
setContext(initialState);
return () => {
if (timeout) clearTimeout(timeout);
};
}, [pathname]);
const setLayoutProgressVisiblity = useCallback((isVisible: boolean) => {
if (!isVisible)
timeout = setTimeout(() => {
setContext((p) => ({ ...p, displayProgressBar: isVisible }));
}, PROGRES_BAR_TRANSITION);
else setContext((p) => ({ ...p, displayProgressBar: isVisible }));
}, []);
const setLayoutProgressPercentage = useCallback(
(percentage: number) =>
setContext((p) => ({ ...p, progressPercentage: percentage })),
[]
);
const contextActions =
useMemo<ILayoutContext>(() => ({ setLayoutProgressPercentage, setLayoutProgressVisiblity }),
[setLayoutProgressPercentage, setLayoutProgressVisiblity]);
const goToHomepage = useCallback(() => {
navigate("/", { replace: true });
}, [navigate]);
return (
<LayoutContext.Provider value={contextActions}>
<div className="app-container p-relative overflow-auto">
<div>{/** HERE YOU CAN PUT HEADER */}</div>
{context.displayProgressBar && (
<HeaderProgressBar progress={context.progressPercentage} />
)}
<div className=" py-4 flex-fill overflow-auto">
{<div className="container h-100">{children}</div>}
</div>
</div>
</LayoutContext.Provider>
);
};
export default Layout;
Beside this, I created one custom hook in order to use it whenever I want to access layout actions:
export const useLayoutContext = () => {
const context = useContext(LayoutContext);
if (!context)
throw new Error(
"No LayoutContext.Provider found when calling useLayoutContext."
);
return context;
};
And finally it is very easy to update progress within any element which is inside LayoutContext. Simple as this:
const { setLayoutProgressPercentage, setLayoutProgressVisiblity } =
useLayoutContext();

Prevent child state resets on props change

I am currently making a Graph component that fetches data from an API, parses the data to be used with a graph library, and then renders the graph. I have all of that working right now, but the issue I am having is with adding the ability to filter. The filtering I am currently doing is done by the parent of the Graph component, which will set the filters prop in the component which is then processed by a useEffect. But this seems causes some portions to re-render and I am trying to prevent. Below is what I have roughly speaking.
Rough example of Parent:
const Parent = (props) => {
const [filters, setFilters] = useState({});
//there are more state values than just this one also cause
//the same problem when their setState is called.
return (
<Graph filters={filters} />
<FilterComponent
onChange={(value) => setFilters(value)}
/>
)
}
export default Parent
Rough example of Child:
const Graph = (props) => {
const [nodes, setNodes] = useState({});
const [links, setLinks] = useState({});
const [velocity, setVelocity] = useState(0.08);
const createGraph = async () => {
//fetches the data, processes it and then returns it.
//not including this code as it isn't the problem
return {
nodes: nodes,
links: links,
};
}
//loads the graph data on mount
useEffect(() => {
const loadGraph = async () => {
const data = await createGraph();
setNodes(data.nodes);
setLinks(data.links);
};
loadGraph();
}, []);
//filters the graph on props change
useEffect(() => {
//this function uses setNodes/setLinks to update the graph data
filterGraph(props.filter);
}, [props.filters]);
return (
<ForceGraph2D
graphData={{
nodes: nodes,
links: links,
}}
d3VelocityDecay={velocity}
cooldownTicks={300}
onEngineStop={() => setVelocity(1)}
/>
);
}
export default Graph
My main issue is that whenever the FilterComponent updates, while I want it to update the graph data, this seems to re-render the Graph component. This causes the graph to start moving. This graph library creates a graph which kinda explodes out and then settles. The graph has a cooldown of 300, and after which it isn't supposed to move, which is where onEngineStop's function is called. But changing the filter state in Parent causes the graph to regain it's starting velocity and explode out again. I want to be able to change the filter state, update the graph data, without re-rendering it. I've looked into useMemo, but don't know if that's what I should do.
I'm fairly new to React having just started two weeks ago, so any help is greatly appreciated! Also, this is my first post on stackOverflow, so I apologize if I didn't follow some community standards.
Edit
I was asked to include the filterGraph function. The function actually was designed to handle different attributes to filter by. Each node/link has attributes attached to them like "weight" or "size". The filterComponent would then pass the attr and the value range to filter by. If a component falls outside that range it becomes transparent.
const Graph = (props) => {
...
//attr could be something like "weight"
//val could be something like [5,10]
const filterGraph = ({ attr, val }) => {
for (const [id, node] of Object.entries(nodes)) {
const value = nodes[id][attr];
if (val.length == 2) {
if (val[0] > value || val[1] < value) {
const color = nodes[id]["color"] || "#2d94adff";
nodes[id]["color"] = setOpacity(color, 0)
);
} else {
const color = nodes[id]["color"] || "#2d94adff";
nodes[id]["color"] = setOpacity(color, 1)
);
}
}
}
setNodes(Object.values(this.nodes));
}
...
}
In your example you mention that filterGraph uses setNodes/setLinks. So everytime the filter changes (props.filters) you will do 2 setState and 2 rerenders will be triggered. It can be that React will batch them so it will only be 1 rerender.
Depending on what filterGraph does exactly you could consider let it return filteredNodes en filteredLinks without putting the filterGraph in a useEffect.
Then pass the filteredNodes en filteredLinks to the graphData like graphData={{
nodes: filteredNodes,
links: filteredLinks,
}}
This way you won't trigger extra rerenders and the data will be filtered on every render. which is already triggered when the props.filters change. This is an interesting article about deriving state https://kentcdodds.com/blog/dont-sync-state-derive-it
Since you also mention that there are more state values in the parent you could make the component a pure component, which means it won't get rerendered when the parent renders but the props that are being passed don't change
https://reactjs.org/docs/react-api.html#reactmemo
Also it's better to include createGraph in the useEffect it's being used or wrap it in a useCallback so it won't be recreated every render.
const Graph = React.memo((props) => {
const [nodes, setNodes] = useState({});
const [links, setLinks] = useState({});
const [velocity, setVelocity] = useState(0.08);
//loads the graph data on mount
useEffect(() => {
const createGraph = async () => {
//fetches the data, processes it and then returns it.
//not including this code as it isn't the problem
return {
nodes: nodes,
links: links,
};
}
const loadGraph = async () => {
const data = await createGraph();
setNodes(data.nodes);
setLinks(data.links);
};
loadGraph();
}, []);
const { filteredNodes, filteredLinks } = filterGraph(props.filter)
return (
<ForceGraph2D
graphData={{
nodes: filteredNodes,
links: filteredLinks,
}}
d3VelocityDecay={velocity}
cooldownTicks={300}
onEngineStop={() => setVelocity(1)}
/>
);
})
export default Graph

How to mix useCallback with useRef in functional components

I'm newish to React and am working on an infinite scroll component. Multiple components will use infinite scroll and need to be synchronized together (i.e., scrolling one element programmatically scrolls other components as well).
So I've created ScrollProvider which maintains scroll state among components (even if they're rerendered), and a lower level hook useScrollSync. useScrollState returns a ref and a handleScroll callback which modify the state in the scroll provider. That all works just fine. However, I separately want to measure a component's size. The example provided in by the React team shows a callback since that for sure will be executed once the component is mounted, and the element would not be null. The problem is that the div already has a ref from the useScrollSync hook.
The core question
If I wanted to measure my div in addition to using scroll sync on it, how do I assign both a callback ref AND other ref to it? Is there a pattern around this, given that an element can only have one div?
Some (simplified) code:
ScrollProvider
const ScrollContext = React.createCreateContext();
const ScrollProvider = ({initialScrollTop, initialScrollLeft}) => {
const controlledElements = useRef(new Map());
const scrollPositions = useRef({
scrollTop: initialScrollTop,
scrollLeft: initialScrollLeft,
controllingElementKey: null
});
const register = (key, controlledElementRef) => {
controlledElements.current.set(key, controlledElementRef);
}
const handleScrollHOF = (key) => {
return () => {
scrollPositions.controllingElementKey = key;
//some scrolling logic
}
}
return {register, scrollPositions, handleScrollHOF};
}
useScrollSync
const useScrollSync = () => {
const scrollContext = useContext(ScrollContext);
const elementRef = useRef(null);
const keyRef = useRef({key: Symbol()}); // this probably could also be useState
useEffect(() => {
scrollContext.register(keyRef, elementRef);
}, []);
return {ref: elementRef, handleScroll: handleScrollHOF(keyRef.current)};
}
SomeComponent (Round 1)
const SomeComponent = () => {
// this would be within the provider tree
const {ref, handleScroll} = useScrollSync();
return (
<div onScroll={handleScroll} ref={ref}>some stuff</div>
)
}
Now the challenge is adding in a measurements hook...
useMeasurements
const useMeasurements = () => {
// something like this, per the React team's Hooks FAQ
const [measurements, setMeasurements] = useState(null);
const measurementRef = useCallback((element) => {
if(element !== null) {
setMeasurements(element.getBoundingClientRect());
}
});
return {measurementRef, measurements};
}
In order to add this to SomeComponent...
SomeComponent (Round 2)
const SomeComponent = () => {
// this would be within the provider tree
const {ref, handleScroll} = useScrollSync();
const {measurementRef, measurements} = useMeasurements();
// I cannot assign measurementRef to this same div,
// and changing useMeasurements to just measure an injected ref winds up
// with that ref being null and it never being recalculated
return (
<div onScroll={handleScroll} ref={ref}>some stuff</div>
)
}
I've sort of hit a wall here, or maybe I'm just overtired. Any thoughts on how to get beyond this?
The main issue I see is that you aren't referencing a viable ref in useMeasurement. In addition, useCallback executes synchronously as part of rendering, before the DOM is created. You'll need to reference your element in another useEffect hook.

How to show an image when Chrome is offline

I'm trying to show an image when the Chrome browser is offline, and when it's online show the webpage.
I transferred the image to base64 data and tried to load it in the img tag, however the base64 data is too large.
Is there a way to show an image when the browser is offline?
import imageToBase64 from "image-to-base64";
const Home = () => {
const [isOnline, setIsOnline] = useState(true);
// Checks to see if the browser has internet connection or not
window.addEventListener("online", () => setIsOnline(true));
window.addEventListener("offline", () => setIsOnline(false));
//Link to the image
const idleImgUrl = `${window.location.href}${coffeeMachine}`;
//convert image to base64 and save to local storage
imageToBase64(idleImgUrl)
.then(res => {
window.localStorage.setItem("idleImgData", res);
})
.catch(err => {
console.log(err);
});
return (
isOnline
? (<div>The web page to show</div>)
:
// <p> tag shows
<p>The browser is offline now</p>
// img tag does not show
(<img src={window.localStorage.getItem("idleImgData"} />)
);
};
Any help would be appreciated...
The trick is to load the image while the user agent still has an internet connection. The image won't be downloaded until you render the <img> tag. The cached image can then be displayed without issue later.
I wrote a short create-react-app example to illustrate.
import React, { useState, useEffect, useCallback } from 'react';
const App = () => {
const [online, setOnline] = useState(true);
const onlineListener = useCallback(() => setOnline(true), [setOnline]);
const offlineListener = useCallback(() => setOnline(false), [setOnline]);
useEffect(() => {
window.addEventListener('online', onlineListener);
window.addEventListener('offline', offlineListener);
return () => {
window.removeEventListener('online', onlineListener);
window.removeEventListener('offline', offlineListener);
};
}, [onlineListener, offlineListener]);
return (
<div className="App">
<img
style={online ? { display: 'none' } : undefined}
src="TODO"
alt="no internet"
/>
</div>
);
};
export default App;
It displays an image when the user agent loses connection and hides it again when connection is restored. It obviously won't work if connection is cut to begin with, but in such a case how did the user load your application 🤔?
If you are offline, you might not even be able to load your react bundle.js file in the first place and there is nothing you can do about it.
Also, I don't see the advantage of keeping it in your localStorage in this case. Browsers are probably going to cache it anyway, if size matters here.
If the user was able to load your bundle, you can just store the b64 hardcoded as a variable directly in your bundle or initiate an ajax on componentDidMount (using useEffect since you use hooks).
const Home = () => {
const [base64Img, setBase64Img] = useState();
const [isOnline, setIsOnline] = useState(true);
const setOnline = () => setIsOnline(true);
const setOffline = () => setIsOnline(false);
useEffect(() => {
initiateB64Retrieval().then((b64) => setBase64Img(b64));
window.addEventListener('online', setOnline);
window.addEventListener('offline', setOffline);
return () => {
window.removeEventListener('online', setOnline);
window.removeEventListener('offline', setOffline);
}
}, [])
...
}
Always good practice to remove your event listeners. Note that you cannot remove event listeners if passed with anonymous functions or when using .bind() as it creates another reference for the function.

How to optimize code in React Hooks using memo

I have this code.
and here is the code snippet
const [indicators, setIndicators] = useState([]);
const [curText, setCurText] = useState('');
const refIndicator = useRef()
useEffect(() => {
console.log(indicators)
}, [indicators]);
const onSubmit = (e) => {
e.preventDefault();
setIndicators([...indicators, curText]);
setCurText('')
}
const onChange = (e) => {
setCurText(e.target.value);
}
const MemoInput = memo((props)=>{
console.log(props)
return(
<ShowIndicator name={props.name}/>
)
},(prev, next) => console.log('prev',prev, next)
);
It shows every indicator every time I add in the form.
The problem is that ShowIndicator updates every time I add something.
Is there a way for me to limit the the time my App renders because for example I created 3 ShowIndicators, then it will also render 3 times which I think very costly in the long run.
I'm also thinking of using useRef just to not make my App renders every time I input new text, but I'm not sure if it's the right implementation because most documentations recommend using controlled components by using state as handler of current value.
Observing the given sandbox app behaviour, it seems like the whole app renders for n times when there are n indicators.
I forked the sandbox and moved the list to another functional component (and memo'ed it based on prev and next props.
This will ensure my 'List' is rendered every time a new indicator is added.
The whole app will render only when a new indicator is added to the list.
Checkout this sandbox forked from yours - https://codesandbox.io/embed/avoid-re-renders-react-l4rm2
React.memo will stop your child component rendering if the parent rerenders (and if the props are the same), but it isn't helping in your case because you have defined the component inside your App component. Each time App renders, you're creating a new reference of MemoInput.
Updated example: https://codesandbox.io/s/currying-tdd-mikck
Link to Sandbox:
https://codesandbox.io/s/musing-kapitsa-n8gtj
App.js
// const MemoInput = memo(
// props => {
// console.log(props);
// return <ShowIndicator name={props.name} />;
// },
// (prev, next) => console.log("prev", prev, next)
// );
const renderList = () => {
return indicators.map((data,index) => {
return <ShowIndicator key={index} name={data} />;
});
};
ShowIndicator.js
import React from "react";
const ShowIndicator = ({ name }) => {
console.log("rendering showIndicator");
const renderDatas = () => {
return <div key={name}>{name}</div>;
};
return <>{renderDatas()}</>;
};
export default React.memo(ShowIndicator); // EXPORT React.memo

Categories

Resources