How to use useMemo hook to memoize children - javascript

I have a component that renders Markers on leaflet map. The markers need to change position every time the server sends a new position for one or more markers.
How can I change the position of the specific markers that changed its position without re-render all the markers?
I was thinking to use useMemo hook but I didn't succeed to use this hook on map function because hook cannot be called inside callback.
const Participants = () => {
// This pattern is showed here: https://medium.com/digio-australia/using-the-react-usecontext-hook-9f55461c4eae
const { participants, setParticipants } = useContext(ParticipantsContext);
useEffect(() => {
const socket = io('http://127.0.0.1:8000');
socket.on('location', data => {
if (data) {
const ps = [...participants];
// currently change the position of the first participant
ps[0].lat = data.dLat;
ps[0].long = data.dLong;
setParticipants(ps);
console.log(data);
}
});
}, []);
const renderParticipants = () => {
return participants.map(p => {
return (
<ParticipantIcon key={p.id} id={p.id} position={[p.lat, p.long]}>
{p.id}
</ParticipantIcon>
);
});
};
return <div>{renderParticipants()}</div>;
};
const ParticipantIcon = ({ id, position, children }) => {
// This is showing if the component rerenderd
useEffect(() => {
console.log(id);
});
return (
<MapIcon icon={droneIcon} position={position}>
{children}
</MapIcon>
);
};
The actual results were that every time the socket receives location it re-renders all the participants' icons instead of re-render only the first participant in the array.

Because you are updating the whole position array every render, the reference to the array representing the previous location and the current location will be different, although the latitude and longitude might be exactly the same. To make it work, wrap PariticpantIcon inside React.memo, then do either one of the followings:
Split position into 2 different props, namely lat and long. Then inside ParticipantIcon you can put them back together. This codesandbox explains best.
Restructure the participants array. Group lat and long together initially would prevent new reference being created at render phase. This codesandbox demonstrates this.
Bonus: Since the ParticipantIcon component just displays the id, you might as well make it cleaner like this:
const ParticipantIcon = ({ id, position, children }) => {
// This is showing if the component rerenderd
useEffect(() => {
console.log(id);
});
return (
<MapIcon icon={droneIcon} position={position}>
{id}
</MapIcon>
);
};

Related

Why does calling one `setState` function update an entirely separate state value?

I have an issue where one setState function is updating two separate state values.
I am trying to make a small React sortable array. The program should behave like so:
fetches data on mount
stores that data in both unsortedData & displayedData state hooks
when a user clicks "toggle sorting" button the array in displayedData is sorted (using .sort())
on a second click of "toggle sorting" it should set displayedData to be the same value as unsortedData - restoring the original order
However, on first click of toggle sorting, it sorts both unsortedData & displayedData, which means I lose the original data order. I understand I could store their order, but I'm wanting to know why these state values appear to be coupled.
Stackblitz working code here.
GIF showing issue here
I cannot find where this is happening. I can't see any object/array referencing going on (I'm spreading for new objects/arrays).
Code here:
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [displayedData, setDisplayedData] = useState([])
const [isSorted, setisSorted] = useState(false)
const handleSorting = () => setisSorted(!isSorted)
useEffect(() => {
if (isSorted === true) setDisplayedData([...unsortedData.sort()]) // sort data
if (isSorted === false) setDisplayedData([...unsortedData]) // restore original data order
}, [isSorted, unsortedData])
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData]) // store original data order in "unsortedData"
setDisplayedData([...mockData])
}, [])
return (
<div>
{displayedData.map(item => item)}
<br />
<button onClick={() => handleSorting()}>Toggle sorting</button>
</div>
)
}
The .sort function mutates the array, so when you do this:
setDisplayedData([...unsortedData.sort()])
You are mutating unsortedData, then making a copy of it afterwards. Since you mutated the original array, that change can display on the screen when the component rerenders.
So a minimum fix would be to copy first, and sort afterwards:
setDisplayedData([...unsortedData].sort())
Also, why do I need the useEffect? Why can't I just move the contents of the useEffect (that has the if statements in) into the handleSorting function?
Moving it into handleSorting should be possible, but actually i'd like to propose another option: have only two states, the unsorted data, and the boolean of whether to sort it. The displayed data is then a calculated value based on those two.
const [unsortedData, setUnsortedData] = useState([3, 9, 6]) // initialize with mock data
const [isSorted, setisSorted] = useState(false)
const displayedData = useMemo(() => {
if (isSorted) {
return [...unsortedData].sort();
} else {
return unsortedData
}
}, [unsortedData, isSorted]);
const handleSorting = () => setisSorted(!isSorted)
// No use useEffect to sort the data
// Also no useEffect for the mock data, since i did that when initializing the state
The benefits of this approach are that
You don't need to do a double render. The original version sets isSorted, renders, then sets displayed data, and renders again
It's impossible to have mismatched states. For example, in between that first and second render, isSorted is true, and yet the displayed data isn't actually sorted yet. But even without the double render case, having multiple states requires you to be vigilant that every time you update unsortedData or isSorted, you also remember to update displayedData. That just happens automatically if it's a calculated value instead of an independent state.
If you want to display sorted array to user only when the user presses the button, you could use this code statement (orignaly coppied from vue3 todo list)
const filters = {
none: (data) => [...data],
sorted: (data) => [...data].sort((a, b) => a - b),
}
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [currentFilter, setCurrentFilter] = useState('none')
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData])
}, [])
return (
<div>
<ul>
{/* All magic is going here! */}
{filters[currentFilter](unsortedData).map(item => <li key={item}>{item}</li>)}
</ul>
<br />
<button
onClick={() =>
setCurrentFilter(currentFilter === 'none' ? 'sorted' : 'none')}>
Toggle sorting
</button>
</div>
)
}

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 set initial state value in Gatsby based on the result from static query?

I'm trying to make a video filtering UI. It will have a grid of videos and some filters in the sidebar. My videos are an array of items/objects that I get from a Gatsby static query.
One of my filters (from a material-ui Autocomplete component) generates an an array of tags, e.g ["data science", "javascript"]. I'm trying to filter the videos list down according to whether the tags in the filters match any of the video tags.
I'm not sure how to make this gatsby component stateful in the way I want to.
I want to be able to give my VideosGrid component all of the videos
on initial load (as there are no filters set)
I have the multichip component able to call setFilterTags
I'm try to use useEffect to listen for changes on the filterTags
and then trigger a state change and update the videos that get passed
to the videos grid component.
But I can't figure out how to make the gatsby data stateful. When I
tried, const [filteredItems, setfilteredItems] = useState([data.allItem.edges]); it breaks because my video grid
component doesn't have anything to load.
export default function LandingPage() {
const classes = useStyles();
const data = useStaticQuery(graphql`
query MainIndexQuery {
allItem(sort: {fields: view_count, order: DESC}) {
edges {
node {
title
tags
stuff
}
}
}
}
`);
const [filterTags, setFilterTags] = useState([]);
const [filteredItems, setfilteredItems] = useState([data.allItem.edges]);
// ```this doesn't seem to work.```;
// let filteredItems = data.allItem.edges; // this works
useEffect(() => {
filteredItems = filteredItems.filter(function (el) {
return el.node.tags.some((tag) => filterTags.includes(tag));
});
// I want to call setState here
}, [filterTags]);
// setFilterTags gets invoked by my chip filtering UI component...
return (
<>
<MultiChip setFilterTags={setFilterTags} />
<VideosGrid locations={filteredItems} />
</>
);
}
I think you want to use:
const [filteredItems, setfilteredItems] = useState(data.allItem.edges);
The edges (from data.allItem.edges) is an array of images itself so you don't need to wrap it.
In addition, to avoid mutation caveats, I will suggest cloning the state array of data. Something like this will create a shallow clone:
useEffect(() => {
let clonedArray= [...filteredItems];
filteredItems = clonedArray.filter(function (el) {
return el.node.tags.some((tag) => filterTags.includes(tag));
});
// I want to call setState here
}, [filterTags]);

useEffect break array of useState at first time

I am learning react hooks. I am having mock data js call "MockFireBase.js" as below:
const userIngredientsList = [];
export const Get = () => {
return userIngredientsList;
}
export const Post = (ingredient) => {
ingredient.id = userIngredientsList.length + 1;
userIngredientsList.push(ingredient);
return ingredient;
}
Then my react hooks component "Ingredients.js" will call this mock utilities as following details:
const Ingredients = () => {
const [userIngredients, setUserIngredients] = useState([]);
// only load one time
useEffect(() => { setUserIngredients(Get()); }, []);
const addIngredienHandler = ingredient => {
let responsData = Post(ingredient);
setUserIngredients(preIngredients => {
return [...preIngredients, responsData]
});
}
return (
<div className="App">
<IngredientForm onAddIngredient={addIngredienHandler} />
<section>
<IngredientList ingredients={userIngredients} />
</section>
</div>
);
)
}
When I added first ingredient, it added two (of course I get same key issue in console.log). Then I added second ingredient is fine.
If I remove the useEffect code as below, it will work good.
// only load one time
useEffect(() => { setUserIngredients(loadedIngredients); }, []);
I am wondering what I did anything wrong above, if I use useEffect
The problem is not in useEffect. It's about mutating a global userIngredientsList array.
from useEffect you set initial component state to be userIngredientsList.
Then inside addIngredienHandler you call Post(). This function does two things:
2a. pushes the new ingredient to the global userIngredientsList array`. Since it's the same instance as you saved in your state in step 1, your state now contains this ingredient already.
2a. Returns this ingredient
Then, addIngredienHandler adds this ingredient to the state again - so you end up having it in the state twice.
Fix 1
Remove userIngredientsList.push(ingredient); line from your Post function.
Fix 2
Or, if you need this global list of ingredients for further usage, you should make sure you don't store it in your component state directly, and instead create a shallow copy in your state:
useEffect(() => { setUserIngredients([...Get()]); }, []);

How to sort React components based on specific value?

I have React component. This components take 'units' - (array of objects) prop. Based on that I render component for each of item. I want to sort my components based on 'price' value, which is one of state items property. But when i trigger the sorting - state changes correctly but my components order not changing.
const SearchBoxes = ({units}) => {
const [unitsState, setUnitsState] = useState([])
useEffect(() => {
setUnitsState(units)
}, [units])
const sortByPrice = () => {
const sortedUnits = sort(unitsState).desc(u => u.price); // sorting is correct
setUnitsState(sortedUnits) // state is changing correctly
}
return (
<div>
{unitsState.map((u,i) => {
return <UnitBox key={u.price} unit={u} />
})}
</div>
)
}
Can somebody help me, please ?
Why my components order do not changing when the state is changing after sort triggering ?
You aren't calling sortByPrice anywhere--all you've done is to define the function. I haven't tried it, but what if you changed useEffect to:
useEffect(() => {
setUnitsState(sort(unitsState).desc(u => u.price));
}, [units])
Then you don't need the sort method at all.

Categories

Resources