Which SectionHeader is Sticky in react-native? - reactjs

I'm using React-Native's SectionList component to implement a list with sticky section headers. I'd like to apply special styling to whichever section header is currently sticky.
I've tried 2 methods to determine which sticky header is currently "sticky," and neither have worked:
I tried to use each section header's onLayout prop to determine each of their y offsets, and use that in combination with the SectionList onScroll event to calculate which section header is currently "sticky".
I tried to use SectionList's onViewableItemsChanged prop to figure out which header is currently sticky.
Approach #1 doesn't work because the event object passed to the onLayout callback only contains the nativeEvent height and width properties.
Approach #2 doesn't work because the onViewableItemsChanged callback appears to be invoked with inconsistent data (initially, the first section header is marked as viewable (which it is), then, once it becomes "sticky," it is marked as not viewable, and then with further scrolling it is inexplicable marked as viewable again while it is still "sticky" [this final update seems completely arbitrary]).
Anyone know a solution that works?

While you creating stickyHeaderIndices array in render() method you should create an object first. Where key is index and value is offset of that particular row, eg. { 0: 0, 5: 100, 10: 200 }
constructor(){
super(props);
this.headersRef = {};
this.stickyHeadersObject = {};
this.stickyHeaderVisible = {};
}
createHeadersObject = (data) => {
const obj = {};
for (let i = 0, l = data.length; i < l; i++) {
const row = data[i];
if (row.isHeaderRow) {
obj[i] = row.offset;
}
}
return obj;
};
createHeadersArray = data => Object.keys(data).map(str => parseInt(str, 10))
render() {
const { data } = this.props;
// Expected that data array already has offset:number and isHeaderRow:boolean values
this.stickyHeadersObject = this.createHeadersObject(data);
const stickyIndicesArray = this.createHeadersArray(this.stickyIndicesObject);
const stickyHeaderIndices = { stickyHeaderIndices: stickyIndicesArray };
return (<FlatList
...
onScroll={event => this.onScroll(event.nativeEvent.contentOffset.y)}
{...stickyHeaderIndices}
...
renderItem={({ item, index }) => {
return (
{ // render header row
item.isHeaderRow &&
<HeaderRow
ref={ref => this.headersRef[index] = ref}
/>
}
{ // render regular row
item.isHeaderRow &&
<RegularRow />
}
);
}}
/>)
Then you have to monitor is current offset bigger than your "titleRow"
onScroll = (offset) => {
Object.keys(this.stickyHeadersObject).map((key) => {
this.stickyHeaderVisible[key] = this.stickyHeadersObject[key] <= offset;
return this.headersRef[key] && this.headersRef[key].methodToUpdateStateInParticularHeaderRow(this.stickyHeaderVisible[key]);
});
};

Related

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

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?

React.js - Using one component multiple times with different settings

I'm trying to create a re-usable component that wraps similar functionality where the only things that change are a Title string and the data that is used to populate a Kendo ComboBox.
So, I have a navigation menu that loads (at the moment) six different filters:
{
context && context.Filters && context.Filters.map((item) => getComponent(item))
}
GetComponent gets the ID of the filter, gets the definition of the filter from the context, and creates a drop down component passing in properties:
function getComponent(item) {
var filterDefinition = context.Filters.find(filter => filter.Id === item.Id);
switch (item.DisplayType) {
case 'ComboBox':
return <DropDownFilterable key={item.Id} Id={item.Id} Definition={filterDefinition} />
default:
return null;
}
}
The DropDownFilterable component calls a service to get the data for the combo box and then loads everything up:
const DropDownFilterable = (props) => {
const appService = Service();
filterDefinition = props.Definition;
console.log(filterDefinition.Name + " - " + filterDefinition.Id);
React.useEffect(() => {
console.log("useEffect: " + filterDefinition.Name + " - " + filterDefinition.Id);
appService.getFilterValues(filterDefinition.Id).then(response => {
filterData = response;
})
}, []);
return (
<div>
<div className="row" title={filterDefinition.DisplayName}>{filterDefinition.DisplayName}</div>
<ComboBox
id={"filterComboBox_" + filterDefinition.Id}
data={filterData}
//onOpen={console.log("test")}
style={{zIndex: 999999}}
dataItemKey={filterDefinition && filterDefinition.Definition && filterDefinition.Definition.DataHeaders[0]}
textField={filterDefinition && filterDefinition.Definition && filterDefinition.Definition.DataHeaders[1]}
/>
</div>
)
}
Service call:
function getFilterValues(id) {
switch(id) {
case "E903B2D2-55DE-4FA3-986A-8A038751C5CD":
return fetch(Settings.url_getCurrencies).then(toJson);
default:
return fetch(Settings.url_getRevenueScenarios).then(toJson);
}
};
What's happening is, the title (DisplayName) for each filter is correctly rendered onto the navigation menu, but the data for all six filters is the data for whichever filter is passed in last. I'm new to React and I'm not 100% comfortable with the hooks yet, so I'm probably doing something in the wrong order or not doing something in the right hook. I've created a slimmed-down version of the app:
https://codesandbox.io/s/spotlight-react-full-forked-r25ns
Any help would be appreciated. Thanks!
It's because you are using filterData incorrectly - you defined it outside of the DropDownFilterable component which means it will be shared. Instead, set the value in component state (I've shortened the code to include just my changes):
const DropDownFilterable = (props) => {
// ...
// store filterData in component state
const [filterData, setFilterData] = React.useState(null);
React.useEffect(() => {
// ...
appService.getFilterValues(filterDefinition.Id).then(response => {
// update filterData with response from appService
setFilterData(response);
})
}, []);
// only show a ComboBox if filterData is defined
return filterData && (
// ...
)
}
Alternatively you can use an empty array as the default state...
const [filterData, setFilterData] = React.useState([]);
...since the ComboBox component accepts an array for the data prop. That way you won't have to conditionally render.
Update
For filterDefinition you also need to make sure it is set properly:
const [filterDefinition, setFilterDefinition] = React.useState(props.Definition);
// ...
React.useEffect(() => {
setFilterDefinition(props.Definition);
}, [props.Definition]);
It may also be easier to keep filterDefinition out of state if you don't expect it to change:
const filterDefinition = props.Definition || {};

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.

Rendering every iteration of an array sort in React

I'm trying to build a simple app in react that takes an unsorted array (this.state.dataset) then insertion sorts it and renders each iteration of the sorting process.
Because React is asynchronous placing a setstate after each array change does not work. Any ideas how to get around this?
I was able to do this with bubble sort by simply exiting the function after each array change then rendering it, then restarting the bubble sort with the updated array. However I can't get that approach to work here. Very inefficient I know, sorry I am new to this..
render() {
return (
<div className="buttons">
<button onClick={this.sort}>Insertion Sort</button>
</div>
);
}
sort(e) {
this.myInterval = setInterval(() => {
let arr = this.insertionSort();
this.setState({
dataset: arr,
});
}
}, 1000);
insertionSort(e) {
let inputArr = this.state.dataset
let length = inputArr.length;
for (let i = 1; i < length; i++) {
let key = inputArr[i];
let j = i - 1;
while (j >= 0 && inputArr[j] > key) {
inputArr[j + 1] = inputArr[j];
j = j - 1;
return inputArr //Added by me to stop loop and render (probably wrong solution)
}
inputArr[j + 1] = key;
return inputArr //Added by me to stop loop and render (probably wrong solution)
}
return inputArr;
};
(I have the this.state.dataset rendering in my render() method but excluded for brevity's sake
One possible solution may be to store the values required for iterating the array in the state. On each modification, you can update the state and return from the sorting function.
Because React is asynchronous placing a setState after each array change does not work. Any ideas how to get around this?
The setState function takes a second callback parameter that is called when the state updates. This means you can effectively treat state updates as synchronous.
Here is working implementation using this method:
(This is written in React Native but the logic is the same for regular React)
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
i: 1,
j: 0,
data: [...initialData],
};
}
render() {
return (
<View style={styles.container}>
<FlatList
data={this.state.data}
renderItem={({ item }) => <Text>{item}</Text>}
/>
<Button title="Sort" onPress={this.sort} />
<Button title="Reset" onPress={this.reset} />
</View>
);
}
sort = () => {
const { i, j, key, data: oldData } = this.state;
const nextSort = () => setTimeout(this.sort, 500);
if (i == oldData.length) {
return;
}
const data = [...oldData];
const value = key ? key : data[i];
if (j >= 0 && data[j] > value) {
data[j + 1] = data[j];
this.setState({ j: j - 1, data, key: value }, nextSort);
return;
}
data[j + 1] = value;
this.setState({ i: i + 1, j: i, data, key: undefined }, nextSort);
};
reset = () => this.setState({ data: [...initialData], i: 1, j: 0, key: undefined });
}
One final thing worth mentioning in your existing implementation is the mutation of state. When you have an array in your state, you cannot modify the array as this will cause issues when you call setState.
For example, your variable dataSet is stored in the state. You then set inputArr to a reference of the state value dataSet with:
let inputArr = this.state.dataset
When you now call inputArr[j + 1] = inputArr[j]; the original array (in your state) is updated. If you call setState with inputArr React will compare it against dataSet. As you have modified dataSet the values will match inputArr which means the state won't update.
You can see a workaround for this issue in my solution where I copy the dataSet into a new array with:
const data = [...oldData]
This will prevent the array in your state from updating.

Managing React states correctly giving strange error

I'm trying to make a hangman game using React.
Here is the code: https://pastebin.com/U1wUJ28G
When I click on a letter I get error:
Here's AvailableLetter.js:
import React, {useState} from 'react';
import classes from './AvailableLetter.module.css';
const AvailableLetter = (props) => {
const [show,setShow]=useState(true);
// const [clicked, setClicked]=useState(false);
// const [outcome,setOutcome]=useState(false);
// if (show)
// {
// setClicked(true);
// }
const play = (alphabet) => {
const solution = props.solution.split('');
if (solution.indexOf(alphabet)<0)
{
return false;
}
else
{
return true;
}
}
if (!show)
{
if (play(props.alphabet))
{
props.correct();
// alert('correct');
}
else
{
props.incorrect(); // THIS CAUSES ERROR!!!!
// alert('wrong');
}
}
return (
show ? <span show={show} onClick={()=>{setShow(false)}} className={classes.AvailableLetter}>{props.alphabet}</span> : null
);
}
export default AvailableLetter;
I suspect the error is caused by not managing state properly inside AvailableLetter.js. But I don't know why the error is showing pointing to Hangman.js.
Here's what's pointed to by props.incorrect:
Game.js:
guessedIncorrectHandler = (letter) => {
const index = this.state.availableLetters.indexOf(letter);
let newAvailableLetters = [...this.state.availableLetters];
newAvailableLetters.splice(index,1);
let newUsedLetters = [...this.state.usedLetters];
newUsedLetters.push(letter);
const oldValueLives = this.state.lives;
const newValueLives = oldValueLives - 1;
this.setState({
usedLetters: newUsedLetters,
availableLetters: newAvailableLetters,
lives: newValueLives
});
};
Applied fix kind user suggested on
lives: newValueLives < 0 ? 0 : newValueLives
But now when I lick on a letter I get multiple letters get added randomly to correct letters and incorrect letters on a single click.
If I interrupt guessedIncorrectHandler with return true:
guessedIncorrectHandler = (letter) => {
const index = this.state.availableLetters.indexOf(letter);
let newAvailableLetters = [...this.state.availableLetters];
newAvailableLetters.splice(index,1);
let newUsedLetters = [...this.state.usedLetters];
newUsedLetters.push(letter);
const oldValueLives = this.state.lives;
const newValueLives = oldValueLives - 1;
console.log('[newValueLives] ',newValueLives); return true; // HERE INTERRUPTED
this.setState({
usedLetters: newUsedLetters,
availableLetters: newAvailableLetters,
lives: newValueLives < 0 ? 0 : newValueLives
});
};
Now when I click 'a' for example (which is correct since solution is 'apple') it behaves correctly.
When app is running I get warning:
Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
in AvailableLetter (at Letters.js:8)
...
Probably the cause for this warning is inside AvailableLetter.js inside return:
show ? <span show={show} onClick={()=>{setShow(false)}} className={classes.AvailableLetter}>{props.alphabet}</span> : null
Tried to set up codesandbox here: https://codesandbox.io/s/fast-breeze-o633v
The issue can occur if the lives value is a negative number.
Try to secure that possibility:
this.setState({
usedLetters: newUsedLetters,
availableLetters: newAvailableLetters,
lives: newValueLives < 0 ? 0 : nevValueLives, // if lives is negative, assign 0
});

Resources