How awful is that :
useLayoutEffect(() => {
setWidth(ganttContainerRef.current.offsetWidth);
setHeight(ganttContainerRef.current.offsetHeight);
}, [])
useLayoutEffect(() => {
if (width > 0) {
setGanttReady(true);
}
}, [width])
useLayoutEffect(() => {
if (ganttReady) {
ganttRef.current.scrollTo({ left: 80 * 80 / 7 + 40 });
}
}, [ganttReady]);
i.e. rendering a component in 3 separate steps...
One to render the container (this container is built with %age, and
I need the real width for step 2 and step 3)
One to render the text
displayed in this container (need the text height for step 3)
One to
render my gantt (all with a lot of absolute positions)
If I render 1 and 2 at the same time, the container width will be 0 (first render) because 2 is not displayed yet.
No matter what I do, if I do const top = refs.current[initiative.id].offsetTop;, offsetTop will have a value as if width was still 0. offsetTop is not updated with a rerender.
With the posted code above it works fine, but how awful is it?
Is it doable to render a component in multiple phases or am I really hacking normal React behaviour?
With the posted code above it works fine, but how awful is it? Is it
doable to render a component in multiple phases or am I really hacking
normal React behaviour?
I wouldn't say it is awful, you could say it is not very efficient, but I would not worry about it if it isn't noticeable.
You can refactor it to something like this:
useLayoutEffect(() => {
let offsetWidth = ganttContainerRef.current.offsetWidth;
setWidth(offsetWidth);
setHeight(ganttContainerRef.current.offsetHeight);
if (offsetWidth > 0) {
setGanttReady(true);
ganttRef.current.scrollTo({ left: (80 * 80) / 7 + 40 });
}
}, []);
But the problem with this approach is as you can see it runs only on mount; which is Ok if that's what you want. So with your previous approach here:
useLayoutEffect(() => {
if (width > 0) {
setGanttReady(true);
}
}, [width])
you had the benefit that if width changed, then you could act on it, you don't get it with my version.
So it depends.
Related
I've got what seems like a very simple problem but I'm having some problems fixing it. I'm trying to create an interactive cat object on my page with Javascript that basically performs the following expected behavior:
When the page first loads, the cat is laying sleeping in the middle of the screen (cat-sleeping.gif)
When you move the pointer (customized to look like a mouse) a little bit closer to the cat, it sits up and starts grooming itself (cat-sitting.gif)
When you move the pointer even closer, the cat starts chasing after the cursor (cat-chasing.gif)
To handle detecting the mouse proximity and the chase behaviour, I'm using a loop that's being called from window.requestAnimationFrame after an initial setup call. The chase + mouse proximity are working as expected, but the changing animation states are not. Although the image changes as expected when the cat is activated, the animated gif becomes a static image. If it matters, I'm using Google Chrome to test things out.
Here's a snippet of the loop (note this function isn't completely finished, but everything that should be working so far works, except the animation)
function loop() {
const { pointerEvent, pointerStatic, catActive } = state;
const { cat, cursor } = settings;
// Calculate distance between cat and pointer
const distance = Math.sqrt(
Math.pow(pointerEvent.x - cat.element.offsetLeft, 2) +
Math.pow(pointerEvent.y - cat.element.offsetTop, 2)
);
// Normalize the distance
const distanceNorm = scale(distance, 0, 1000);
// Activate the cat after the pointer gets close enough
if (distanceNorm < 0.1) {
updateState({ catActive: true });
}
// Flip the cursor left or right depending on the direction of movement
if (pointerEvent.movementX < 0) {
cursor.element.style.transform = "scaleX(-1)";
} else {
cursor.element.style.transform = "scaleX(1)";
}
// Make the cat turn from left to right when the pointer is moving
if (pointerEvent.x < cat.element.offsetLeft + cat.width / 2) {
cat.element.style.transform = "scaleX(-1)";
} else {
cat.element.style.transform = "scaleX(1)";
}
// If the cat is active, chase the pointer
if (catActive) {
cat.element.src = "cat-walk.gif";
cat.element.style.left = `${toAbsolute(
distanceNorm,
cat.element.offsetLeft,
pointerEvent.x - 80
)}px`;
cat.element.style.top = `${toAbsolute(
distanceNorm,
cat.element.offsetTop,
pointerEvent.y - 35
)}px`;
}
window.requestAnimationFrame(loop);
}
Changing my state function fixed this - I made a separate change state function specifically for the cat, since I realized that state was otherwise being updated every frame as the pointer moved due to sharing data with the PointerElement event. However, I'm not 100% sure why this actually worked - to make it function, I needed to directly access catActive from my catState object instead of using its destructured variant. I'm posting this as an answer so that I can share the code, but I'd like to understand why this actually works so that I learn instead of surviving by trial and error!
function updateCatState(newState) {
let { catActive, cat, catForm } = catState;
catState = { ...catState, ...newState };
if (catState.catActive && cat.element.src !== catForm[1]) {
console.log("catActive");
cat.element.src = catForm[1];
} else {
cat.element.src = catForm[0];
}
}
I am trying to implement a stats table into my game.
Currently it's very buggy: the streak works when the if statement is true, but if I get the words correct once, the streak goes up and if I get the words correct again the streak goes up. When the statement is false the streak goes back to 0 (which I want to happen).
The problem arises when I get the words correct again, the streak does not increment anymore.
Also the win percentage sometimes works and sometimes the calculations are incorrect. Have I set the function up incorrectly?
if (guessedLetters.length == figureGuessWordsLength) {
LSdataWordle.CurrrentStreak++;
if (LSdataWordle.CurrrentStreak >= LSdataWordle.MaxStreak) {
LSdataWordle.MaxStreak = LSdataWordle.CurrrentStreak;
}
LSdataWordle.WinPercentage = (
(parseFloat(LSdataWordle.MaxStreak) /
parseFloat(LSdataWordle.DaysPlayed)) *
100
).toFixed(2);
window.localStorage.setItem("dataWordle", JSON.stringify(LSdataWordle));
} else {
LSdataWordle.CurrrentStreak = 0;
window.localStorage.setItem("dataWordle", JSON.stringify(LSdataWordle));
}
}
One thing I am seeing is that you aren't recalculating the win percentage on a loss. That would be why sometimes it probably doesn't make sense for you. So take that completely out of the if statements like below, as well as the window.localstorage call seems to be independent of any conditions.
As to figure out why it won't increment on a win after losing streak, that will come down to what guessedLetters.length is and what figureGuessWordsLength is. put a breakpoint and see if they even match on a win, because the way you posted your code is as if that is a for-sure thing, but it may not be.
if (guessedLetters.length == figureGuessWordsLength) {
LSdataWordle.CurrrentStreak++;
if (LSdataWordle.CurrrentStreak >= LSdataWordle.MaxStreak) {
LSdataWordle.MaxStreak = LSdataWordle.CurrrentStreak;
}
}
else {
LSdataWordle.CurrrentStreak = 0;
}
LSdataWordle.WinPercentage = (
(parseFloat(LSdataWordle.MaxStreak) /
parseFloat(LSdataWordle.DaysPlayed)) *
100
).toFixed(2);
window.localStorage.setItem("dataWordle", JSON.stringify(LSdataWordle));
I am using ag-grid, I want the columns width to be dynamic according to the grid content,
I do it by using:
const onGridReady = params => {
params.columnApi.autoSizeAllColumns();
};
but the width of the grid is alays fixed, I have a space in the side of the grid.
(I can't also send width to the grid, because I can't know what will be the size of the content)
what I need is something how to combine autoSizeAllColumns and sizeColumnsToFit functions.
ag-grid has a property called defaultColDef that can be used for grid settings. If you pass flex: 1 as one of the parameters then all columns will size to fit so that you won't have that empty space on the side (expands to fill). Check out the ag-grid documentation on this page and search for the work "flex" for more details on auto/flex sizing.
If I understand you correctly you want to have the following behaviour for resizing - make sure each column has it's content visible first, but then also make sure that the whole grid width is filled at the least.
Unfortunately there doesn't seem to be a really straightforward way to achieve that. What I did is onGridReady, I would use the autoSizeColumns function to make sure that each column's content is fully visible, and then if there is an additional space left to fill the grid's width I distribute it evenly to each column. Then apply the new column state through gridApi.applyColumnState. Here is an example in vue
that should be fairly easy to transfer to other frameworks (or vanilla js).
interface UseGridColumnResizeOptions {
// we need access to the grid container so we can calculate
// the space that is left unfilled.
gridContainerRef: Ref<null | HTMLElement>;
// for any columns that you don't want to resize for whatever reason
skipResizeColumnIds: string[];
}
export const useGridColumnResize = ({ gridContainerRef, skipResizeColumnIds }: UseGridColumnResizeOptions) => {
const handleResize = ({ columnApi }: AgGridEvent) => {
columnApi.autoSizeAllColumns();
if (!gridContainerRef.value) {
console.warn('Unable to resize columns, gridContainer ref is not provided');
return;
}
const isColumnResizable = (colDef: ColDef) => colDef.resizable && !skipResizeColumnIds.includes(colDef.colId!);
const columns = columnApi.getAllGridColumns().filter((column) => isColumnResizable(column.getColDef()));
if (columns.length === 0) {
return;
}
const lastColumn = columns[columns.length - 1];
const lastColumnLeft = lastColumn.getLeft();
const lastColumnWidth = lastColumn.getActualWidth();
const { width: gridWidth } = gridContainerRef.value.getBoundingClientRect();
const gridSpaceLeftToFill = Math.max(0, gridWidth - (lastColumnLeft! + lastColumnWidth));
if (gridSpaceLeftToFill === 0) {
return;
}
const additionalSpaceForEachColumn = gridSpaceLeftToFill / columns.length;
const columnState = columnApi.getColumnState();
columnState.forEach((column) => {
const skipResizeForColumn = !columns.some((col) => col.getColId() === column.colId);
if (skipResizeForColumn) {
return;
}
column.width = column.width! + additionalSpaceForEachColumn;
});
columnApi.applyColumnState({ state: columnState });
};
return { handleResize };
};
You can plug the handleResize function on row-data-updated event to resize columns whenever new data arrives in the grid or only once in grid-ready or first-data-rendered.
Keep in mind that this implementation plays out well in my case as columns are not movable. I am expecting the last column inside the columns array to be the last visible one in the UI, but that might not always be the case and you might end up with wrong calculation of the space that is left to fill. So you might need to change the way the last visible column in the UI is retrieved to make it work for your case.
Here is an example of what it does:
https://ostralyan.github.io/flood-fill/
This app is for educational purposes, and here is the source code.
Right now, anything bigger than a 100x100 grid gives me performance issues. When I say performance issues I mean that when I click a cell there's a few seconds delay before it renders the next state.
My goal is to optimize this to support 1m squares (1000x1000).
Here is the algorithm I use for this approach
floodFillIterative(i, j) {
const oldColor = this.props.squares[i][j].color;
const newColor = this.getUniqueRandomColor(oldColor);
const squares = this.props.squares.slice();
const stack = [
[i, j]
];
while (stack.length) {
const squareCoordinates = stack.pop();
let newI = squareCoordinates[0];
let newJ = squareCoordinates[1];
if (newI < 0 || newI >= this.props.squaresPerRow) continue;
if (newJ < 0 || newJ >= this.props.squaresPerRow) continue;
let nextSquare = squares[newI][newJ];
if (nextSquare.color !== oldColor) continue;
if (nextSquare.visited) continue;
Array.prototype.push.apply(stack, [
[newI - 1, newJ],
[newI + 1, newJ],
[newI, newJ - 1],
[newI, newJ + 1],
]);
nextSquare.visited = true;
nextSquare.color = newColor;
}
this.setState({ squares });
this.clearVisisted(squares);
}
This algorithm runs in linear time so I'm not sure if optimizing the algorithm will really improve performance that much more. Although I'm open to any suggestions of optimization.
I also have a line of code here
shouldComponentUpdate(nextProps) {
if (nextProps.color !== this.props.color) {
return true;
}
return false;
}
That prevents the squares to rerender if nothing about them have changed. I'm looking for any other way to optimize this app.
Thanks!
Great optimization challenge! The main problem is that each Square is a react component so you are creating a huge amount of elements to render in the DOM.
React is gonna naturally slow down in that case, even when using something like Redux or shouldComponentUpdate like you're using.
I would highly recommend creating a single component using an HTML Canvas for your board rendering instead of Square Components.
Here is a great codepen that is rendering a ton of pixels in realtime: function drawAll() https://codepen.io/gunderson/full/EyXBrr
And here's a good tutorial on building a board on a canvas:
https://codereview.stackexchange.com/questions/164650/drawing-a-board-for-the-board-game-go-html-canvas
Is this horribly inefficient or does it look ok??? How do I test resources used by it?
$.easing.def = "easeOutBack";
$(document).ready(function() {
var numResults = $("#scroll > div").size();
var scrollSize = numResults * 264;
var stopSize = ((numResults - 6) * 264) * -1;
$("#scroll").width(scrollSize);
$("#page-left").hide();
$("#page-right").click(function() {
var marginleft = parseInt(jQuery("#scroll").css("margin-left"));
if(marginleft > stopSize) {
$("#page-left").show();
$(this).hide();
$("#scroll").animate({"margin-left": "-=783px"}, 800, function() {
var marginleft = parseInt(jQuery("#scroll").css("margin-left"));
if(marginleft > stopSize) {
$("#page-right").show();
}
});
}
});
$("#page-left").click(function() {
var marginright = parseInt(jQuery("#scroll").css("margin-left"));
if(marginright < -10) {
$("#page-right").show();
$(this).hide();
$("#scroll").animate({"margin-left": "+=783px"}, 800, function() {
var marginright = parseInt(jQuery("#scroll").css("margin-left"));
if(marginright < -10) {
$("#page-left").show();
}
});
}
});
});
Chrome gives you the ability to take heap snapshots. DeveloperTools->Profiles->HeapSnapshots
You can take snapshot at various time intervals to compare memory usage.
Another option is paid one http://www.softwareverify.com/javascript/memory/feature.html
I don't see any reason why that would consume much in terms of resources. You're just animating things left and right, right? I guess some better coding practices that I'd point out would be to store things you use repeatedly like $("#scroll") in a variable so you don't search the DOM every time for the same thing, and also choosing one of jQuery or $ unless you need to do otherwise.
The real question I'd have is what exactly 783 represents. If it's because your screen is 800 pixels wide, then keep in mind that not everyone will see you page that way.
As for the profiling part, Rizwan's answer gets +1.