Does React.useMemo() have the cost to compare, like shouldComponentUpdate() in React.PureComponent ?
Is it bad to put variables changed often into useMemo() or useCallback()'s deps.
Thanks
Here is a ยต-benchmark, React.memo() seems to be the fastest, with PureComponent in the middle and useCallback() consistently the slowest:
class P extends React.PureComponent {
render() {
return <span onClick={() => this.props.n} />
}
}
function C(props) {
const c = React.useCallback(() => props.n, [props.n])
return <span onClick={c} />
}
const M = React.memo(props => <span onClick={() => props.n} />)
const target = document.getElementById('target')
function test(o) {
console.log('From fastest to slowest:')
const entries = Object.entries(o)
const results = new Map(entries.map(([name]) => [name, 0]))
for (let i = 0; i < 1e3; i++) {
for (let [name, X] of entries) {
const start = performance.now()
for (let j = 0; j < 1e2; j++) {
ReactDOM.render(<X n={i} />, target)
}
const took = performance.now() - start
results.set(name, results.get(name) + took)
}
}
const sorted = Array.from(results).sort(([, a], [, b]) => a-b)
for (let [name, t] of sorted) {
console.log(name, t)
}
}
test({PureComponent: P, useCallback: C, memo: M})
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="target" hidden></div>
Related
After I request two endpoints and store it in a new state variable I'm not being able to render the component after the state changes. When i assign the state variable to the dependency array of useEffect it renders infinitely.
I tried a few things but the only way that i've being able to do to render the component after it loads has been just adding the merge state to the dependency array.
import { ChangeEvent, FC, useEffect, useState } from "react";
import spacex from "../api/spacex";
import CardGrid from "../components/CardGrid";
import Header from "../components/Header";
import Pagination from "../components/Pagination";
import SkeletonGrid from "../components/SkeletonGrid";
type Launch = {
mission_name: string;
};
const LaunchesMain: FC = () => {
const [launches, setLaunches] = useState<any>([]);
const [rockets, setRockets] = useState<any>([]);
const [merged, setMerged] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [postsPerPage, setPostsPerPage] = useState(9);
const [searchTerm, setSearchTerm] = useState("");
const [filteredResult, setFilteredResult] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchRockets = async () => {
const responseRocket = await spacex.get("/rockets");
const responseLaunches = await spacex.get("/launches");
setRockets(responseRocket.data);
setLaunches(responseLaunches.data);
};
fetchRockets().then(() => {
const mergedApis = () => {
const launchesCopy: any = [...launches];
for (let i = 0; i < launches.length; i++) {
for (let j = 0; j < rockets.length; j++) {
if (launches[i].rocket.rocket_name === rockets[j].rocket_name) {
launchesCopy[i].rocket = rockets[j];
}
}
}
setMerged(launchesCopy);
setIsLoading(false);
};
mergedApis();
});
}, []);
console.log(merged);
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value);
if (searchTerm.length === 0) {
setFilteredResult(merged);
} else if (searchTerm.length > 0) {
const filteredData = merged.filter((launch: Launch) => {
return `${launch.mission_name}`
.toLowerCase()
.includes(searchTerm.toLowerCase());
});
setFilteredResult(filteredData);
}
};
const lastPostIndex = currentPage * postsPerPage;
const firstPostIndex = lastPostIndex - postsPerPage;
const currentPosts = merged.slice(firstPostIndex, lastPostIndex);
return (
<>
<Header />
<div className="text-white">
<div>
<input
style={{
background:
"linear-gradient(0deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.05)), #121212",
}}
onChange={(event) => handleSearchChange(event)}
placeholder="Search all launches..."
value={searchTerm}
className="md:w-[26rem] w-[16rem] h-[3rem] rounded-lg mt-10 mx-5 md:mx-24 rounded-3"
/>
</div>
<div className="mx-5 md:ml-24 mt-5 opacity-40">
Total({currentPosts.length})
</div>
{isLoading ? (
<SkeletonGrid cards={postsPerPage} />
) : (
<CardGrid
postsData={currentPosts}
filteredResult={filteredResult}
searchTerm={searchTerm}
/>
)}
<Pagination
totalPosts={merged.length}
postsPerPage={postsPerPage}
setCurrentPage={setCurrentPage}
currentPage={currentPage}
/>
</div>
</>
);
};
export default LaunchesMain;
This is the code of the component. How can i solve this issue?
Since you need rockets and launches as a dependency of the useEffect, whenever they change, the useEffect is called, which calls the api, which changes, etc... However, you don't use rockets and launches states beyond merging them, and then you use the merged state.
So you don't have to store rockets and launches in the state. Use Promise.all() to get both data arrays in to .then() block, merge them, and store only the merged state:
useEffect(() => {
const fetchRockets = () => Promise.all(
spacex.get("/rockets"),
spacex.get("/launches")
])
fetchRockets()
.then(([responseRocket, responseLaunches]) => {
const rockets = responseRocket.data;
const launches = responseLaunches.data;
for (let i = 0; i < launches.length; i++) {
for (let j = 0; j < rockets.length; j++) {
if (launches[i].rocket.rocket_name === rockets[j].rocket_name) {
launches[i].rocket = rockets[j];
}
}
}
setMerged(launches);
setIsLoading(false);
});
}, []);
Really popular project by Clement Mihailescu - https://github.com/clementmihailescu/Pathfinding-Visualizer-Tutorial/blob/master/src/PathfindingVisualizer/PathfindingVisualizer.jsx
He has done this using class component, and I want to use functional components
What I want to happen is when mouseDown occurs I want "isWall" property to be set to true.
I reckon I need to modify the useEffect but I don't know how to go about that.
function PathFindingVisualizer() {
const [Grid, setGrid] = useState([]);
const [MouseIsPressed, setMouseIsPressed] = useState(false);
useEffect(() => {
initializeGrid();
}, []);
const initializeGrid = () => {
const grid = [];
for(let i = 0; i < rows; i++){
const currentRow = [];
for(let j = 0; j < cols; j++){
currentRow.push(createNode(i, j));
}
grid.push(currentRow);
};
setGrid(grid);
};
const createNode = (i, j) => {
return {
i,
j,
isStart: i === START_NODE_ROW && j === START_NODE_COL,
isEnd: i === END_NODE_ROW && j === END_NODE_COL,
distance: Infinity,
isVisited: false,
isWall: false,
previousNode: null,
};
};
const gridWithWallToggle = (grid, i, j) => {
const newGrid = [...grid];
const node = grid[i][j];
const newNode = {
...node,
isWall : !node.isWall
};
console.log(newNode);
newGrid[i][j] = newNode;
return newGrid
}
const handleMouseDown = (i, j) => {
const newGrid = gridWithWallToggle(Grid, i, j);
setGrid(newGrid);
setMouseIsPressed(true);
}
const gridWithNodes = (
<div className="grid">
{Grid.map((row, rowIdx) => {
return (
<div key={rowIdx} className="row">
{row.map((node, nodeIdx) => {
const {i, j, isEnd, isStart, isWall} = node;
return (
<Node
key={nodeIdx}
i={i}
j={j}
isEnd={isEnd}
isStart={isStart}
isWall={isWall}
mouseIsPressed={MouseIsPressed}
onMouseDown={handleMouseDown(i, j)}
></Node>
);
})}
</div>
);
})}
</div>
)
return (
<>
<button onClick = {visualizeDijkstra}>Visualize</button>
<div className="board">
{gridWithNodes}
</div>
</>
)
}
export default PathFindingVisualizer
I am learning react and decided to try create a sorting visualizer. I started with bubble sort and pretty much succeeded creating a basic visualizer. setTimeout was the main function that I used to apply the visualization.
I felt that relaying on setTimeout does not utilize react enough and I wanted to try different approach, applying the visualization with useState hook and the rerendering that is happening when changing the state. I understand that useState hook is asynchronous, and will not immediately reflect.
Here is my code:
import React, { useContext, useState, useEffect } from 'react';
const NUMBER_OF_ELEMENTS = 10;
const DEFAULT_COLOR = 'black';
const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
const Dummy = () => {
const [arr, setArr] = useState([]);
const [numberOfElements, setNumberOfElements] = useState(NUMBER_OF_ELEMENTS);
const [doneElements, setDoneElements] = useState([]);
useEffect(() => {
resetArray();
}, []);
const resetArray = () => {
const arr1 = [];
for(let i = 0; i < numberOfElements; i++)
{
arr1[i] = randomIntFromInterval(5, 100);
}
console.log(arr1);
setArr(arr1);
}
const bubbleSort = (arr, n) => {
let i, j, temp, swapped, delay = 1;
for(i = 0; i < n - 1; i++)
{
swapped = false;
for(j = 0; j < n - i - 1; j++)
{
createColor(j, j + 1, delay++, 'darkred');
if(arr[j] > arr[j + 1])
{
// swap arr[j] and arr[j+1]
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
createAnimation(j, j + 1, delay++);
}
createColor(j, j + 1, delay++, 'black');
}
createSingleColor(n - i - 1, delay++, 'green');
// If no two elements were
// swapped by inner loop, then break
if(swapped === false) break;
}
for(let k = 0; k < n - i - 1; k++) {
createSingleColor(k, delay++, 'green');
}
}
const createAnimation = (one, two, delay) => {
const arrayBars = document.getElementsByClassName('array-bar');
setTimeout(() => {
const barOneHeight = arrayBars[one].style.height;
const barTwoHeight = arrayBars[two].style.height;
arrayBars[two].style.height = `${barOneHeight}`;
arrayBars[one].style.height = `${barTwoHeight}`;
}, 250 * delay);
}
const createColor = (one, two, delay, color) => {
const arrayBars = document.getElementsByClassName('array-bar');
setTimeout(() => {
arrayBars[two].style.backgroundColor = color;
arrayBars[one].style.backgroundColor = color;
}, 250 * delay);
}
const createSingleColor = (index, delay, color) => {
const arrayBars = document.getElementsByClassName('array-bar');
setTimeout(() => {
arrayBars[index].style.backgroundColor = color;
}, 250 * delay);
}
const handleSort = (arr) => {
bubbleSort(arr, arr.length);
}
const handlerRange = (e) => {
setNumberOfElements(e.target.value);
}
return (
<div>
<div className="array-container">
{arr.map((value, idx) => (
<div className="array-bar"
key={idx}
style={{
backgroundColor: 'black',
height: `${value}px`,
width: `${100 / arr.length}%`,
display: 'inline-block',
margin: '0 1px'
}}>
</div>
))}
</div>
<div className="buttons-container">
<button onClick={() => handleSort(arr)}>Sort!</button>
<button onClick={() => resetArray()}>Reset</button>
<button onClick={() => {
setDoneElements([...doneElements, 7]);
console.log(doneElements);}}>print</button>
</div>
<div className="slider-container">
1
<input type="range"
min="1"
max="100"
onChange={(e) => handlerRange(e)}
className="slider"
id="myRange"
/>
100
</div>
{numberOfElements}
</div>
);
}
export default Dummy;
For example when I tried using the setDoneElements in the bubblesort function I messed up the visualization.
Is there a way to use hooks to apply the visualization, and not to rely on setTimeout that much?
Found a solution:
import React, { useState, useEffect } from 'react';
const shortid = require('shortid');
const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
const Dummy2 = () => {
const [arr, setArr] = useState([]);
const [length, setLength] = useState(10);
const [doneElements, setDoneElements] = useState([]);
const [compareElements, setCompareElements] = useState([]);
useEffect(() => {
generateArray();
}, [length]);
const generateArray = () => {
setDoneElements([]);
setCompareElements([]);
const tempArr = [];
for(let i = 0; i < length; i++) {
tempArr.push(randomIntFromInterval(7, 107));
}
setArr([...tempArr]);
}
const handleLength = (e) => {
setLength(e.target.value);
}
const bubbleSort = () => {
let i, j, swapped, delay = 100;
const tempArr = [...arr];
const tempDoneElements = [...doneElements];
for(i = 0; i < length - 1; i++)
{
swapped = false;
for(j = 0; j < length - i - 1; j++)
{
createColor([j, j + 1], delay, 'COMPARE');
delay += 100;
if(tempArr[j] > tempArr[j + 1])
{
createAnimation(tempArr, j, j + 1, delay);
delay += 100;
swapped = true;
}
createColor([], delay, 'NONE');
delay += 100;
}
tempDoneElements.push(length - i - 1);
createColor(tempDoneElements, delay, 'DONE');
delay += 100;
// If no two elements were
// swapped by inner loop, then break
if(swapped === false) break;
}
for(let k = 0; k < length - i - 1; k++) {
tempDoneElements.push(k);
}
createColor(tempDoneElements, delay, 'DONE');
delay += 100;
}
const createAnimation = (tempArr, indexOne, indexTwo, delay) => {
const temp = tempArr[indexOne];
tempArr[indexOne] = tempArr[indexTwo];
tempArr[indexTwo] = temp;
const newArr = [...tempArr];
setTimeout(() => {
setArr([...newArr]);
}, delay);
}
const createColor = (tempElements, delay, action) => {
switch(action) {
case 'DONE':
const newDoneElements = [...tempElements];
setTimeout(() => {
setDoneElements([...newDoneElements]);
}, delay);
break;
case 'COMPARE':
setTimeout(() => {
setCompareElements([...tempElements]);
}, delay);
break;
default:
setTimeout(() => {
setCompareElements([]);
}, delay);
}
}
const maxVal = Math.max(...arr);
return (
<div>
<div className="array-container" style={{height: '50%'}}>
{arr.map((value, idx) => (
<div className="array-element"
key={shortid.generate()}
style={{height: `${(value * 100 / maxVal).toFixed()}%`,
width: `calc(${100 / length}% - 2px)`,
margin: '0 1px',
display: 'inline-block',
backgroundColor: compareElements.includes(idx) ? 'darkred' :
doneElements.includes(idx) ? 'green' : 'black',
color: 'white'}}
></div>))
}
</div>
<div>
<button onClick={() => generateArray()}>New array</button>
<button onClick={() => bubbleSort()}>Sort</button>
</div>
<div className="slider-container">
1
<input type="range"
min="1"
max="100"
onChange={(e) => handleLength(e)}
className="slider"
id="myRange"
/>
100
</div>
{length}
</div>
);
}
export default Dummy2;
Instead of messing with the DOM I used state to keep track of changes so react will be charge of changing things.
When manipulating the array in the sorting function we need to remember that arr is part of state therefore it is immutable. any change that we do we need to do on a duplicate and apply the changes at the right time so an animation will occur, that is what I done in the createAnimation function.
To keep track on the colors I added to the state doneElements and compareElements. Every time an element get to it's final position it's index is added to doneElements. At any given time only two elements are compared therefore compareElements will contain only two elements or none.
I have a naive pattern matching function, and I'm trying to slow down execution of each comparison so I can create a visualiser for it. However, I want to be able to access my i and j variables outside of the function. I am attempting to do this by declaring them outside of the function, passing them in, and returning them after each match. This way I can press a button to control the flow of execution. However, they are not being returned properly, and I suspect this has something to do with my use of async/await, and the need to return the values as a Promise.
https://codesandbox.io/s/staging-http-0zm04?file=/src/App.tsx:0-1072
import React, { useState } from "react";
import "./styles.css";
const delay = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));
export const naive = async (text: string, pattern: string, i: number, j: number) => {
const matches = [];
let n = text.length;
let m = pattern.length;
while (i < n){
while (j < pattern.length && pattern[j] === text[i + j]){
j += 1;
await delay(500);
}
if (j === m){
matches.push(i)
}
return [i, j, matches]
}
}
export default function App() {
const [text, setText] = useState<string>("abcdefghijklmnopqrstuvwxyzabcd")
const [pat, setPat] = useState<string>("abc")
const [i, updateI] = useState(0);
const [j, updateJ] = useState(0);
const nextMatch = () => {
let results = naive(text, pat, i, j);
updateI(results[0]);
updateJ(results[1]);
}
return (
<div>
<button style = {{width: "100px", height: "50px"}}onClick = {() => nextMatch()}/>
{i}
{j}
</div>
);
}
As navie is an async function you have to add then.This would help to return correct i and j values
const nextMatch = () => {
naive(text, pat, i, j).then((results) => {
updateI(results[0]);
updateJ(results[1]);
});
};
I am trying to implement Conway's Game of Life in React, but it is freezing whenever a new generation is called. I assume this is because there is too much overhead caused by constantly re-rendering the DOM, but I don't know how to resolve this, nor can I think of an alternative to simply posting my entire code, so I apologise in advance for the verbosity.
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import styled from "styled-components"
interface TileProps {
bool: boolean
}
const Tile: React.FC<TileProps> = ({bool}) => {
const colour = bool == true ? "#00FF7F" : "#D3D3D3"
return (
<div style = {{backgroundColor: colour}}/>
)
}
interface GridProps {
cells: boolean[][]
}
const StyledGrid = styled.div`
display: grid;
grid-template-columns: repeat(100, 1%);
height: 60vh;
width: 60vw;
margin: auto;
position: relative;
background-color: #E182A8;
`
const Grid: React.FC<GridProps> = ({cells}) => {
return (
<StyledGrid>
{cells.map(row => row.map(el => <Tile bool = {el}/>))}
</StyledGrid>
)
}
const randomBoolean = (): boolean => {
const states = [true, false];
return states[Math.floor(Math.random() * states.length)]
}
const constructCells = (rows: number, columns: number): boolean[][] => {
return constructEmptyMatrix(rows, columns).map(row => row.map(e => randomBoolean()))
}
const constructEmptyMatrix = (rows: number, columns: number): number[][] => {
return [...Array(rows)].fill(0).map(() => [...Array(columns)].fill(0));
}
const App: React.FC = () => {
const columns = 100;
const rows = 100;
const [cells, updateCells] = useState<boolean[][]>(constructCells(rows, columns));
useEffect(() => {
const interval = setInterval(() => {
newGeneration();
}, 1000);
return () => clearInterval(interval);
}, []);
const isRowInGrid = (i: number): boolean => 0 <= i && i <= rows - 1
const isColInGrid = (j : number): boolean => 0 <= j && j <= columns -1
const isCellInGrid = (i: number, j: number): boolean => {
return isRowInGrid(i) && isColInGrid(j)
}
const numberOfLiveCellNeighbours = (i: number, j: number): number => {
const neighbours = [
[i - 1, j], [i, j + 1], [i - 1, j + 1], [i - 1, j + 1],
[i + 1, j], [i, j - 1], [i + 1, j - 1], [i + 1, j + 1]
]
const neighboursInGrid = neighbours.filter(neighbour => isCellInGrid(neighbour[0], neighbour[1]))
const liveNeighbours = neighboursInGrid.filter(x => {
const i = x[0]
const j = x[1]
return cells[i][j] == true
})
return liveNeighbours.length;
}
const updateCellAtIndex = (i: number, j: number, bool: boolean) => {
updateCells(oldCells => {
oldCells = [...oldCells]
oldCells[i][j] = bool;
return oldCells;
})
}
const newGeneration = (): void => {
cells.map((row, i) => row.map((_, j) => {
const neighbours = numberOfLiveCellNeighbours(i, j);
if (cells[i][j] == true){
if (neighbours < 2){
updateCellAtIndex(i, j, false);
} else if (neighbours <= 3){
updateCellAtIndex(i, j, true);
}
else {
updateCellAtIndex(i, j, false);
}
} else {
if (neighbours === 3){
updateCellAtIndex(i, j, true);
}
}
}))
}
return (
<div>
<Grid cells = {cells}/>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));
The application freezes because React does not batch your individual state updates. More information about this can be found in this answer
You have two options here.
Use ReactDOM.unstable_batchedUpdates:
This can be done with a single line change, but note that the method is not part of the public API
useEffect(() => {
const interval = setInterval(() => {
// wrap generation function into batched updates
ReactDOM.unstable_batchedUpdates(() => newGeneration())
}, 1000);
return () => clearInterval(interval);
}, []);
Update all states in one operation.
You could refactor your code to set updated cells only once. This option does not use any unstable methods
useEffect(() => {
const interval = setInterval(() => {
// `newGeneration` function needs to be refactored to remove all `updateCells` calls. It should update the input array and return the result
const newCells = newGeneration(oldCells);
// there will be only one call to React on each interval
updateCells(newCells);
}, 1000);
return () => clearInterval(interval);
}, []);