React Updating Components after Updating 2D Grid in Redux Store? - reactjs

I'm having an architectural dilemma. I am making a website to display pathfinding algorithms. I am using Redux to store a 2D array which represents the state of each cell in the grid.
The way my application works is that I have a classes that inherit from a base class called PathfindingAlgorithm which has a function called pass() that does one pass of the algorithm and updates the grid in the store.
Below is an example of Depth First Search being implemented. My 'SET_CELL_STATE' redux action updates the 2D grid in my redux store with the newState. Coordinate just holds {x, y} values, and coordinateMap is there for backtracking on the path.
export class DepthFirstSearch extends PathfindingAlgorithm {
private stack: coordinate[] = [this.startIndex];
private coordinateMap: Map<coordinate, coordinate> = new Map();
private current: any;
public pass(): boolean {
const {grid} = store.getState();
if (this.stack.length !== 0) {
this.currentIndex = this.stack[this.stack.length - 1];
this.stack.pop();
if (grid[this.currentIndex.y][this.currentIndex.x] === CellState.Exit) {
return true;
} else if (grid[this.currentIndex.y][this.currentIndex.x] === CellState.Empty) {
store.dispatch({
type: 'SET_CELL_STATE',
x: this.currentIndex.x,
y: this.currentIndex.y,
newState: CellState.Visited});
}
const neighbors = super.getUnvisitedNeighbors(this.currentIndex.x, this.currentIndex.y);
neighbors.forEach(val => {
this.coordinateMap.set(val, this.currentIndex);
this.stack.push(val);
});
}
return false;
}
}
I want to call pass until I find an exit and have my view components update in response to the 2D grid. For the view, I dynamically generate cells based on the 2D grid.
function Board(props: any) {
const grid = props.grid;
const renderBoard = () => {
return grid.map((gridRow: Array<CellState>, i: number) => {
return gridRow.map((gridItem: CellState, j: number) => {
return (
<div key={uuidv4()}>
<Cell x={j} y={i}/>
</div>);
})
});
}
return(
<BoardView
renderBoard = { renderBoard }
/>
);
}
function BoardView(props: any) {
const { renderBoard } = props;
return(
<div className="board">
{ renderBoard() }
</div>
);
}
How can I efficiently achieve this looping effect? Making a while loop inside the button that starts it does not update the components until the loop is finished which is bad since I want to animate this in the future. Should I just directly change the Cells straight from the DOM?

Related

React component not re-rendering on component state change

I am working on a sidebar using a recursive function to populate a nested list of navigation items.
Functionally, everything works except for the re-render when I click on one of the list items to toggle the visibility of the child list.
Now, when I expand or collapse the sidebar (the parent component with its visibility managed in its own state), the list items then re-render as they should. This shows me the state is being updated.
I have a feeling this possibly has something to do with the recursive function?
import React, { useState } from "react";
import styles from "./SidebarList.module.css";
function SidebarList(props) {
const { data } = props;
const [visible, setVisible] = useState([]);
const toggleVisibility = (e) => {
let value = e.target.innerHTML;
if (visible.includes(value)) {
setVisible((prev) => {
let index = prev.indexOf(value);
let newArray = prev;
newArray.splice(index, 1);
return newArray;
});
} else {
setVisible((prev) => {
let newArray = prev;
newArray.push(value);
return newArray;
});
}
};
const hasChildren = (item) => {
return Array.isArray(item.techniques) && item.techniques.length > 0;
};
const populateList = (data) => {
return data.map((object) => {
return (
<>
<li
key={object.name}
onClick={(e) => toggleVisibility(e)}
>
{object.name}
</li>
{visible.includes(object.name) ? (
<ul id={object.name}>
{hasChildren(object) && populateList(object.techniques)}
</ul>
) : null}
</>
);
});
};
let list = populateList(data);
return <ul>{list}</ul>;
}
export default SidebarList;
There are many anti patterns with this code but I will just focus on rendering issue. Arrays hold order. Your state does not need to be ordered so it's easier to modify it, for the case of demo I will use object. Your toggle method gets event, but you want to get DOM value. That's not necessary, you could just sent your's data unique key.
See this demo as it fixes the issues I mentioned above.

best way to update state array with delay in a loop

I am learning react by working on a sorting algorithm visualizer and I want to update the state array that is rendered, regularly in a loop.
Currently I am passed an array with pairs of values, first indicating the current index and value, and second with its sorted index and value.
[(firstIdx, value), (sortedIdx, value), (secondIdx, value), (sortedIdx, value) ... etc]
some actual values:
`[[1, 133], [0, 133], [2, 441], [2, 441], [3, 13], [0, 13] ... ]`
What I want to do is cut the value out of the array, splice it into the correct position, while updating the state array rendered in each step. effectively creating an animation with the state array.
Right now when I run below function, my state array instantly becomes the sorted array because of the batching. I would like there to be a delay between each state update in the loop.
code snippet that I've tried.
insertionSort(changeArray) {
const arrayBars = document.getElementsByClassName('array-bar')
// I want to keep track which index to move from/to so I instantiate it outside the loop.
let [barOneIdx, barOneValue] = [0, 0];
let [barTwoIdx, barTwoValue] = [0, 0];
// Copy of the state array that I will modify before setting the state array to this.
let auxArray = this.state.array.slice();
for (let i = 0; i < changeArray.length; i++) {
// This tells me whether it is the first or second pair of values.
let isFirstPair = 1 % 2 !== 1;
if (isFirstPair) {
// first set of values is the current index + height
[barOneIdx, barOneValue] = changeArray[i];
// Changes the current bar to green.
setTimeout(() => {
arrayBars[barOneIdx].style.backgroundColor = 'green';
}, i * 300);
} else {
// second set of values is the sorted index + height.
[barTwoIdx, barTowValue] = changeArray[i];
// Cut the current bar out of the array.
let cutIdx = auxArray[barOneIdx];
auxArray.splice(barOneIdx, 1);
// Splice it into the sorted index
auxArray.splice(barTwoIdx, 0, cutIdx);
// Changes the color of the bar at the correct sorted
// index once, and then again to revert the color.
setTimeout(() => {
// Set the state array with the new array. NOT WORKING
// Instantly sets state array to final sorted array.
// I want this to run here with a delay between each loop iteration.
this.setState({ array: auxArray });
arrayBars[barTwoIdx].style.backgroundColor = SECONDARY_COLOR;
}, i * 300);
setTimeout(() => {
arrayBars[barTwoIdx].style.backgroundColor = PRIMARY_COLOR;
}, i * 300);
}
}
}
https://codesandbox.io/s/eager-yonath-xpgjl?file=/src/SortingVisualizer/SortingVisualizer.jsx
link to my project so far with all the relevant functions and files.
On other threads say not to use setState in a loop as they will be batched and run at the end of the block code. Their solutions won't work for my project though as I want to create an animation with the state array.
What would be the best way to implement this?
If you are familiar with ES6 async/await you can use this function
async function sleep(millis) {
return new Promise((resolve) => setTimeout(resolve, millis));
}
async function insertionSort() {
// you code logic
await sleep(5000) //delay for 5s
this.setState({array : auxArray});
}
I have put together a basic version of what you're trying to achieve. I depart from your approach in a few important ways, one of which is the use of an async function to delay state updates. I also change the way the original array is generated. I now create an array that includes the height of the bars as well as its color. This is necessary to accomplish the color changes while moving the bars to their right spots.
There is also no need for your getInsertionSortAnimations function any more as the sorting is done inside the class using the reduce function. I will just paste the entire code here for future reference, but here is the Sandbox link: https://codesandbox.io/s/damp-bush-gkq2q?file=/src/SortingVisualizer/SortingVisualizer.jsx
import React from "react";
import "./SortingVisualizer.css";
//import { getMergeSortAnimations } from "../SortingAlgorithms/MergeSort";
import { setTimeout } from "timers";
// Original color of the array bars.
const PRIMARY_COLOR = "aqua";
// Color we change to when we are comparing array bars.
const SECONDARY_COLOR = "green";
// Speed of the animation in ms.
const ANIMATION_SPEED_MS = 400;
// Number of array bars.
const NUMBER_OF_BARS = 10;
const sleep = (millis) => {
return new Promise((resolve) => setTimeout(resolve, millis));
};
function arraymove(arr, fromIndex, toIndex) {
var element = arr[fromIndex];
arr.splice(fromIndex, 1);
arr.splice(toIndex, 0, element);
}
export default class SortingVisualizer extends React.Component {
constructor(props) {
super(props);
this.state = {
array: []
};
}
// React function runs first time component is rendered, client side only.
componentDidMount() {
this.resetArray();
}
resetArray() {
const array = [];
for (let i = 0; i < NUMBER_OF_BARS; i++) {
array.push({
height: randomIntfromInterval(10, 200),
color: PRIMARY_COLOR
});
}
this.setState({ array });
}
animateSorting = async (sorted_array) => {
const { array } = this.state;
for (let i = 0; i < sorted_array.length; i++) {
const orig_index = array.findIndex(
(item) => item.height === sorted_array[i]
);
array[orig_index].color = SECONDARY_COLOR;
this.setState(array);
await sleep(ANIMATION_SPEED_MS);
arraymove(array, orig_index, i);
this.setState(array);
if (orig_index !== i) await sleep(ANIMATION_SPEED_MS);
}
};
insertionSort() {
const { array } = this.state;
const sorted = array.reduce((sorted, el) => {
let index = 0;
while (index < sorted.length && el.height < sorted[index]) index++;
sorted.splice(index, 0, el.height);
return sorted;
}, []);
this.animateSorting(sorted);
}
render() {
const { array } = this.state;
return (
// Arrow function to use "this" context in the resetArray callback function: this.setState({array}).
// React.Fragment allows us to return multiple elements under the same DOM.
<React.Fragment>
<div className="button-bar">
<button onClick={() => this.resetArray()}>Generate Array</button>
<button onClick={() => this.insertionSort()}>Insertion Sort</button>
<button onClick={() => this.mergeSort()}>Merge Sort</button>
<button onClick={() => this.quickSort()}>Quick Sort</button>
<button onClick={() => this.heapSort()}>Heap Sort</button>
<button onClick={() => this.bubbleSort()}>Bubble Sort</button>
</div>
<div className="array-container">
{array.map((item, idx) => (
<div
className="array-bar"
key={idx}
// $ dollarsign makes a css variable???
style={{
backgroundColor: `${item.color}`,
height: `${item.height}px`
}}
></div>
))}
</div>
</React.Fragment>
);
}
}
// Generates random Integer in given interval.
// From https://stackoverflow.com/questions/4959975/generate-random-number-between-two-numbers-in-javascript
function randomIntfromInterval(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
Gotcha: There is a bug in the code however, which should be a piece of cake for you to fix. Notice how it behaves when two or more bars happen to have the same exact height.

Central State Management without Redux or Mobx?

Recently I contemplated the idea of having central state management in my React apps without using Redux or Mobx, instead opting to create something similar to the application class in Android. In any event, I implemented something similar to this:
Create a store folder and a file called store.js in it whose contents are:
// State
let state = {
users: {},
value: 0
};
// Stores references to component functions
let triggers = [];
// Subscription Methods
export const subscribe = trigger => {
triggers.push(trigger);
trigger();
}
export const unsubscribe = trigger => {
let pos = -1;
for (let i in triggers) {
if (triggers[i]===trigger) {
pos = i;
break;
}
}
if (pos!==-1) {
triggers.splice(pos, 1);
}
}
// Trigger Methods
let triggerAll = () => {
for (let trigger of triggers) {
trigger();
}
}
// State Interaction Methods
export const setUser = (name, description) => {
state.users[name] = description;
triggerAll();
}
export const removeUser = name => {
if (name in state.users) {
delete state.users[name];
}
triggerAll();
}
export const getAllUsers = () => {
return state.users;
}
export const getUser = name => {
if (!(name in state.users)) {
return null;
}
return state.users[name];
}
export const getValue = () => {
return state.value;
}
export const setValue = value => {
state.value = value;
triggerAll();
}
And connecting to this store in the following manner:
// External Modules
import React, { Component } from 'react';
import {Box, Text, Heading} from 'grommet';
// Store
import {subscribe, unsubscribe, getAllUsers} from '../../store/store';
class Users extends Component {
state = {
users: []
}
componentDidMount() {
subscribe(this.trigger); // push the trigger when the component mounts
}
componentWillUnmount() {
unsubscribe(this.trigger); // remove the trigger when the component is about to unmount
}
// function that gets triggered whenever state in store.js changes
trigger = () => {
let Users = getAllUsers();
let users = [];
for (let user in Users) {
users.push({
name: user,
description: Users[user]
});
}
this.setState({users});
}
render() {
return <Box align="center">
{this.state.users.map(user => {
return <Box
style={{cursor: "pointer"}}
width="500px"
background={{color: "#EBE7F3"}}
key={user.name}
round
pad="medium"
margin="medium"
onClick={() => this.props.history.push("/users/" + user.name)}>
<Heading margin={{top: "xsmall", left: "xsmall", right: "xsmall", bottom: "xsmall"}}>{user.name}</Heading>
<Text>{user.description}</Text>
</Box>
})}
</Box>;
}
}
export default Users;
Note. I've tested this pattern on a website and it works. Check it out here. And I apologize I am trying to keep the question concise for stackoverflow, I've provided a more detailed explanation of the pattern's implementation here
But anyway, my main question, what could be the possible reasons not to use this, since I assume if it was this simple, people wouldn't be using Redux or Mobx. Thanks in advance.
That's what Redux and MobX basically do, you are wrong in thinking that at their core concept they are much different. Their size and complexity came as a result of their effort to neutralize bugs and adapt to a vast variety of application cases. That's it. Although they might be approaching the task from different angles, but the central concept is just that. Maybe you should familiarize your self with what they actually do underneath.
Btw, you do not need to store redundant state in your component, if all you need is to trigger the update. You can just call forceUpdate() directly:
// function that gets triggered whenever state in store.js changes
trigger = () => {
this.forceUpdate();
}
That's similar to what Redux and MobX bindings for react do under the hood.

React + d3v4: Trouble moving node positions in Beeswarm Chart using the correct motion

I have managed to create a Beeswarm plot which updates the node positions based on new data selected through a dropdown. However, i am unable to program the nodes to shift positions using the correct motion. Currently, on initial render, i see a radial clustering/collision motion that positions all nodes. Each time i change the dropdown selection, this motion reoccurs to render the new nodes. However, i would like to see a nice 'pull' motion moving the nodes from where they currently are to their new positions.
I'm using React to draw all the nodes, d3 calculates the x and y.
Beeswarm Component
class BeeswarmPlot extends Component {
createNodes = (data) => {
...
}
render() {
// Receives data from another component and creates nodes for each record
var nodes = this.createNodes(this.props.data)
return (
<svg>
<ForceGraph
nodes={nodes}
/>
</svg>
)
}
}
export default BeeswarmPlot;
Force layout component
class ForceGraph extends Component {
constructor() {
super();
this.state = {nodes: []};
}
componentDidMount() {
this.updateNodePositions(this.props.nodes)
}
componentDidUpdate(prevProps, prevState) {
if (this.props.nodes != prevProps.nodes) {
this.updateNodePositions(this.props.nodes)
}
}
componentWillUnmount() {
this.force.stop()
}
updateNodePositions = (nodes) => {
this.force = d3.forceSimulation(nodes)
.force("x", d3.forceX(d => d.cx))
.force("y", d3.forceY(d => d.cy))
.force("collide", d3.forceCollide(3))
this.force.on('tick', () => this.setState({nodes}))
}
render() {
const {nodes} = this.state
return (
<Dots
data={nodes}
/>
)
}
}
export default ForceGraph;
EDIT: Using state to update node positions received from parent as props is a bad idea, as the page re-renders with each state change. I will never be able to achieve the transition motion of node positions!
Hence, i have decided to change method to using props to update node positions without state change. However, now, I am unable to shift the node positions, which remain where they are no matter what i select in dropdown.
export default class BubbleChart extends Component {
constructor(props) {
super(props)
this.bubbles = null;
this.nodes = [];
this.forceStrength = 0.03;
// Initialize a template of force layout.
this.simulation = d3.forceSimulation()
.velocityDecay(0.2)
.force('x', d3.forceX().strength(this.forceStrength).x(d => d.x))
.force('y', d3.forceY().strength(this.forceStrength).y(d => d.y))
.force('charge', d3.forceManyBody().strength(this.charge))
.force("collide", d3.forceCollide(3))
}
componentDidMount() {
this.container = select(this.refs.container)
// Calculate node positions from raw data received. The output of this function will create positions that overlap which each other. Separation of the nodes will be dealt with in force layout
this.createNodes()
// Create circles for the nodes
this.renderNodes()
// ticked function helps to adjust node positions based on what i specifies in force layout template above
this.simulation.nodes(this.nodes)
.on('tick', this.ticked)
}
// When new props (raw data) are received, this fires off
componentDidUpdate() {
// Recalculates node positions based on new props
this.createNodes()
// Adjust node positions
this.simulation.nodes(this.nodes)
.on('tick', this.ticked)
// I think this 're-energises' force layout to enable nodes to move to their new positions
this.simulation.alpha(1).restart()
}
charge = (d) => {
return -Math.pow(d.radius, 2.0) * this.forceStrength;
}
createNodes = () => {
var data = this.props.lapsData.slice()
....MORE CODE.....
this.nodes = nodes
}
renderNodes = () => {
this.bubbles = this.container.selectAll('.bubble')
.data(this.nodes, d => d.id)
.enter().append('circle')
.attr('r', d => d.radius)
.attr('fill', d => d.color)
}
ticked = () => {
this.bubbles
.attr('cx', d => d.x)
.attr('cy', d => d.y)
}
render() {
return (
<svg width={this.wrapper.width} height={this.wrapper.height}>
<g transform={"translate(" + (this.axisSpace.width + this.margins.left) + "," + (this.margins.top) + ")"} ref='container' />
</svg>
)
}
}

Filtering an icon from an array of icon strings for re-render

I'm trying to take an e.target.value which is an icon and filter it out from an array in state, and re-render the new state minus the matching icons. I can't seem to stringify it to make a match. I tried pushing to an array and toString(). CodeSandbox
✈ ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
Here is the code snippet (Parent)
removeMatches(icon) {
const item = icon;
const iconsArray = this.props.cardTypes;
const newIconsArray =iconsArray.filter(function(item) {
item !== icon
})
this.setState({ cardTypes: newIconsArray });
}
This is a function in the parent component Cards, when the child component is clicked I pass a value into an onClick. Below is a click handler in the Child component
handleVis(e) {
const item = e.target.value
this.props.removeMatches(item)
}
First of all, there's nothing really different about filtering an "icon" string array from any other strings. Your example works like this:
const icons = ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
const icon = "✈";
const filteredIcons = icons.filter(i => i !== icon);
filteredIcons // ["♘", "♫", "♫", "☆", "♘", "☆"]
Your CodeSandbox example has some other issues, though:
Your Card.js component invokes this.props.removeMatches([item]) but the removeMatches function treats the argument like a single item, not an array.
Your Cards.js removeMatches() function filters this.props.cardTypes (with the previously mentioned error about treating the argument as a single item not an array) but does not assign the result to anything. Array.filter() returns a new array, it does not modify the original array.
Your Cards.js is rendering <Card> components from props.cardTypes, this means that Cards.js is only rendering the cards from the props it is given, so it cannot filter that prop from inside the component. You have a few options:
Pass the removeMatches higher up to where the cards are stored in state, in Game.js as this.state.currentCards, and filter it in Game.js which will pass the filtered currentCards back down to Cards.js.
// Game.js
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
// ...
<Cards cardTypes={this.state.currentCards} removeMatches={this.removeMatches} />
// Cards.js
<Card removeMatches={this.props.removeMatches}/>
// Card.js -- same as it is now
Move Cards.js props.cardTypes into state (ex state.currentCards) within Cards.js, then you can filter it out in Cards.js and render from state.currentCards instead of props.cardTypes. To do this you would also need to hook into componentWillReceiveProps() to make sure that when the currentCards are passed in as prop.cardTypes from Game.js that you update state.currentCards in Cards.js. That kind of keeping state in sync with props can get messy and hard to follow, so option 1 is probably better.
// Cards.js
state = { currentCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ currentCards: nextProps.cardTypes });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
render() {
return (
<div>
{ this.state.currentCards.map(card => {
// return rendered card
}) }
</div>
);
}
Store all the removed cards in state in Cards.js and filter cardTypes against removedCards before you render them (you will also need to reset removedCards from componentWillReceiveProps whenever the current cards are changed):
// Cards.js
state = { removedCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ removedCards: [] });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
removedCards: [...prevState.removedCards, ...items]
}));
}
render() {
const remainingCards = this.props.cardTypes.filter(card => {
return this.state.removedCards.indexOf(card) < 0;
});
return (
<div>
{ remainingCards.map(card => {
// return rendered card
})}
</div>
);
}
As you can see, keeping state in one place in Game.js is probably your cleanest solution.
You can see all 3 examples in this forked CodeSandbox (the second 2 solutions are commented out): https://codesandbox.io/s/6yo42623p3

Resources