Prevent child state resets on props change - javascript

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

Related

Why is functional component nested function using a stale state value?

My component fetches and displays posts using infinite scrolling (via an IntersectionObserver). The API call that the component makes is dependent on the current number of fetched posts. This is passed as an offset to the API call.
The desired effect is that, if the posts array is empty, its length is 0 and my API will return the first 5 posts. When the last post in the UI intersects with the viewport, another fetch is made, but this time the API call is passed an offset of 5, so the API skips the first 5 posts in the collection and returns the next 5 instead.
Here is my code:
export default function Feed() {
const [posts, setPosts] = useState([]);
const fetchPosts = async () => {
const newPosts = await postAPI.getFeed(posts.length);
setPosts((prevPosts) => [...prevPosts, ...newPosts]);
};
useEffect(fetchPosts, []);
const observerRef = useRef(
new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
observerRef.current.unobserve(entry.target);
fetchPosts();
}
})
);
const observePost = useCallback((node) => node && observerRef.current.observe(node), []);
return (
<>
{posts.map((post) => {
const key = post._id;
const isLastPost = key === posts.at(-1)._id;
const callbackRef = !isLastPost ? undefined : observePost; //only if last post in state, watch it!
return <PostCard {...{ key, post, callbackRef }} />;
})}
</>
);
}
However, every time the fetchPosts function is called, the value it uses for posts.length is 0 - the number the function was originally created with.
Can someone explain to me why the closure over posts.length is stale here? I thought that every time a component re-renders, all nested functions within it were recreated from scratch? As such, surely the fetchPosts function should be using the latest value of posts.length every time it is called? Any help is appreciated! :)
You are creating a new IntersectionObserver on every render - not a good idea. However, only the first of all these created observers, the one you store in the ref (the ref that is never updated), is the one on which you call observe(), and that first observer uses a stale closure over the initial value of posts.
Instead, I would suggest creating the intersection observer(s) inside the observePost function:
const [posts, setPosts] = useState([]);
const fetchPosts = useCallback(async () => {
const newPosts = await postAPI.getFeed(posts.length);
setPosts((prevPosts) => [...prevPosts, ...newPosts]);
}, [posts.length]);
// ^^^^^^^^^^^^
useEffect(fetchPosts, []);
const observePost = useCallback((node) => {
if (!node) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target);
fetchPosts();
}
});
observer.observe(node);
}, [fetchPosts]);
// ^^^^^^^^^^
Important here is the dependency of the observePost callback on the fetchPosts callback which has a dependency on the length of posts, to avoid getting stale. It's probably also possible to solve this with refs, but I don't think they're necessary here.

how to prevent re-rendering components with redux

How to prevent component re-rendering, when i have an hierarchical array of objects (array of objects) using redux dispatching and memo
for example i have like this list of list of components
like
Elements =[ // an array in store.state variable
{name='a',
subEelemets:[
{name:'a-a',
components:[
{
name:'a-a-1',
component:'TextFiled',
value:'default value...'
},
]
},
]
},
]
i used redux to update components values inside my Elements array using dispatch but the performance downgrades because of many re-rendering,
i tried with memo to prevent re rendering but it also updates the entire Elements array because i used useSelector,
const ImportedComponent = memo((props) => {
const LoadableComponent = loadable(() =>
import("../lib" + props.compName))
);
return (
<LoadableComponent
{...props}
id={props.id}
/>)},(a,b)=>a.id===b.id?true:false)
here the code how i redner my compoents
const selectTodoIds = state => state.components.map((todo,index) => todo)
const updateComp = () => {
const Components = useSelector(selectCompsIds, shallowEqual)
const renderedListItems = Components.map((CompId) =>
<ImportedComponent key={CompId} elementID={CompId} />
)
return <ul className="todo-list">{renderedListItems}</ul>
}
it works when i used a flat array as presented in the tutorial https://redux.js.org/tutorials/fundamentals/part-5-ui-react but not for hierarchical one, because i should use the "name" to define which subelement i'm updating,
is there any strategy to solve this problem?
ps: the code is just for clarification,
Rather than memo you could try useMemo:
const ImportedComponent = (props) => {
const LoadableComponent = useMemo(() => {
return loadable(() => import("../lib" + props.compName));
},[props.compName]);
...
memo only helps when there's lots of re-renders with the same props, which it doesn't appear you have.
That's a complete shot in the dark, though. Can you post a link to a stackblitz, maybe? It'd be nice to see more of your code in context.

React useEffect and useState interaction

I am using React and Material UI to create a table (XGrid) with some buttons. When you click the row, it should set the row id using useState. When you click the delete button, it should delete the row. It seems that the delete click handler is not using the value from use state. This is either some kind of closure thing or some kind of React thing.
const MyTableThing: React.FC = (props) =>
{
const { data } = props;
const [filename, setFilename] = React.useState<string>("")
const [columns, setColumns] = React.useState<GridColDef[]>([])
const handleDelete = () =>
{
someFunctionThatDeletes(filename); // filename is always ""
setFilename(""); // Does not do anything.. !
}
React.useEffect(() =>
{
if (data)
{
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) =>
{
if (column.field === "delete")
{
column.renderCell = (cellParams: GridCellParams) =>
{
return <Button onClick={handleDelete}>Delete</Button>
}
}
})
setColumns(columns)
}
}, [data?.files])
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) =>
{
console.log(`set selected row to ${param.data.id}`) // This works every time
setFilename(param.data.id)
}
}
The reason for this behavior is that React does not process setState action synchronously. It is stacked up with other state changes and then executed. React does this to improve performance of the application. Read following link for more details on this.
https://linguinecode.com/post/why-react-setstate-usestate-does-not-update-immediately
you can disable your deleteRow button till the filename variable is updated. you can use useEffect or setState with callback function.
useEffect(() => {
//Enable your delete row button, fired when filename is updated
}, filename)
OR
this.setFilename(newFilename, () => {
// ... enable delete button
});
Let me know if this helps! Please mark it as answer if it helps.
The main problem I see here is that you are rendering JSX in a useEffect hook, and then saving the output JSX into columns state. I assume you are then returning that state JSX from this functional component. That is a very bizarre way of doing things, and I would not recommend that.
However, this explains the problem. The JSX being saved in state has a stale version of the handleDelete function, so that when handleDelete is called, it does not have the current value of filename.
Instead of using the useEffect hook and columns state, simply do that work in your return statement. Or assign the work to a variable and then render the variable. Or better yet, use a useMemo hook.
Notice that we add handleDelete to the useMemo dependencies. That way, it will re-render every time handleDelete changes. Which currently changes every render. So lets fix that by adding useCallback to handleDelete.
const MyTableThing: React.FC = (props) => {
const { data } = props;
const [filename, setFilename] = React.useState<string>('');
const handleDelete = React.useCallback(() => {
someFunctionThatDeletes(filename); // filename is always ""
setFilename(''); // Does not do anything.. !
}, [filename]);
const columns = React.useMemo(() => {
if (!data) {
return null;
}
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) => {
if (column.field === 'delete') {
column.renderCell = (cellParams: GridCellParams) => {
return <Button onClick={handleDelete}>Delete</Button>;
};
}
});
return columns;
}, [data?.files, handleDelete]);
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) => {
console.log(`set selected row to ${param.data.id}`); // This works every time
setFilename(param.data.id);
};
return columns;
};

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