There's an array looking as follows:
[[3,0], [6,0], [2,0], [9,0]....]
I'm trying to create a React/Redux reducer that changes the value of one of the 0s to 1. I click on an element and an action is dispatched. Idx is the index of an element in an array (eg. 0, 1, 2, 3)
export const toggleTile = (idx) => {
return {
type: TOGGLE_TILE,
idx
};
};
The reducer below does not work as I'd like it to be. I just created the skeleton of the conditional statements. If I click on a tile with index 3 (so the fourth tile), it changes the [n,0] to [n,1] for all elements. First of all it should only do if I click any of the tiles, and it should change [n,0] to [n,1] only for the clicked tile so I'm trying to change the 3 in the code below to the index of an 'i' element being mapped.
export default (state = [], action = {}) => {
switch (action.type) {
case LOAD_GRID:
return action.payload || [];
case TOGGLE_TILE:
return state.map((i) => {
if (action.idx === 3) {
return (i[1] === 0
? [i[0], parseInt(i[1], 10) + 1]
: [i[0], parseInt(i[1], 10) - 1]
);
}
return [i[0], i[1]];
});
default:
return state;
}
};
A grid component:
export default class Grid extends Component {
render() {
const mygrid = [];
this.props.inGrid.forEach((r, i) => {
mygrid.push(
<Square
key={i}
idx={i}
sqValue={r}
toggleTile={this.props.toggleTile}
/>
);
});
const { grid } = styles;
return (
<View style={grid}>
{mygrid}
</View>
);
}
}
export default class Square extends Component {
myaction() {
this.props.toggleTile(this.props.idx);
console.log(this.props.idx);
}
render() {
const { square, textStyle, squareActive } = styles;
const { sqValue } = this.props;
return (
<TouchableHighlight
style={[square, sqValue[1] && squareActive]}
onPress={this.myaction.bind(this)}
>
<View>
<Text style={textStyle}>{sqValue[0]},{sqValue[1]}</Text>
</View>
</TouchableHighlight>
);
}
}
Please advise.
There are a number of ways you can do this, with varying degrees of verbosity (due to Redux's insistence on immutability), but here's a pretty straightforward one:
case TOGGLE_TILE:
const nextValue = state[action.idx].slice(); // Make a copy of the tuple to be toggled
nextValue[1] = nextValue[1] === 0 ? 1 : 0; // Toggle it
const nextState = state.slice(); // Make a copy of the state
nextState[action.idx] = nextValue; // Replace the old tuple with the toggled copy
return nextState;
Or:
case TOGGLE_TILE:
const prevValue = state[action.idx];
const nextState = state.slice();
nextState[action.idx] = [ prevValue[0], prevValue[1] === 0 ? 1 : 0 ];
return nextState;
Ok, I'm gonna try and see what we can do with just the following portion of code you shared.
I would like to note that the code presented is not succinct. It would be a great benefit to yourself, your team, as well as anyone here on this site if your code was refactored the more you understand what you need to build.
// So state is just an array of arrays...
var state = [3,0], [6,0], [2,0], [9,0]];
return state.map((i) => { // i => [3,0] or [9,0] !! i is not index !!
// Map is going to iterate over the entire array of arrays.
if (action.idx === 3) {
// action.idx is what comes in from the click.
// Here is where your doing your work.
// If the first element of "i" is zero, then
// return the same array but add 1 to the second element of array.
// so [3,0] or [4,0] should become [3,1] or [4,1] but only for #3 as
// action.idx === 3 says to only change when... Nope, this is not how it will work. You need your exception in the MAP.
return (i[1] === 0 ? [i[0], parseInt(i[1], 10) + 1] : [i[0], parseInt(i[1], 10) - 1]);
}
// ?? Why don't you just return i, as i is each array of numbers.
return [i[0], i[1]];
});
// It seams to me that something like this should work, just plug and play.
// I am assuming a few things here that I will spell out. If they are incorrect, let me know and I'll change it.
// state will be an array of arrays that only contain two numbers each.
// They may or may not be in original order.
// The second element of each array will be either 0 or 1.
var state = [3,0], [6,0], [2,0], [9,0]];
state.map(function(cell){ // call it what you want, you called it "i".
if(cell[0] === action.idx){ // If clicked action index is === to cell[0]
// You could just hard code 3 as you did above, but this makes it dynamic.
// and only changes the cell that was clicked.
cell[1] = cell[1] ? 1 : 0; // if cell[1] is 0, then it is falsey, no need for complex logic. No need to parseInt if they are numbers to begin with. But if you do, then use "+" to change a string to number.
}
return cell;
});
Without notes
var state = [3,0], [6,0], [2,0], [9,0]];
state.map(function(cell){
if(cell[0] === action.idx){
cell[1] = cell[1] ? 1 : 0;
}
return cell;
});
Related
I have an issue where my state is only set on the second click. Ive been reading about lifting up the state which Ive tried to implement but I am still running into the same issue with this button. There are multiple components that the state is passed through and maybe thats the issue I could use some help understanding how to resolve this issue Ill try and outline in the simplest way:
I have my parent table component:
const TableView = ({...all the props}) => {
const [sort, setSortBy] = useState({
sortBy: "",
order: "",
});
const handleOnsortChange = (values) => {
setSortBy(values);
trimData(items.sort(sorter), pageNumber, 50);
};
// sort function
const sorter = (a, b) => { ...}
return (
<Table>
<TableHeader handleOnsortChange={handleOnsortChange} sort={sort} />
...
)
Table Header Component:
const TableHeader = ({ handleOnsortChange, sort }) => {
const onSortChange = (value) => {
handleOnsortChange(value);
};
return (
<thead className="payroll-list-header">
<tr>
<th>
<span>Name</span> <SortView onChange={onSortChange} type={"fullName"} sort={sort} />
<div className="arrow"></div>
</th> ...
)
Sorter Component:
const SortView = ({ onChange, type, sort: { sortBy, order } }) => {
const onClick = (order) => {
onChange({ sortBy: type, order });
};
return (
<div className="sort">
<div onClick={() => onClick("asc")} className="icon-up">
<Octicon
icon={TriangleUp}
className={`${order === "asc" && sortBy == type ? "active" : ""}`}
></Octicon>
</div>
<div onClick={() => onClick("desc")} className="icon-down">
<Octicon
icon={TriangleDown}
className={`${order === "desc" && sortBy == type ? "active" : ""}`}
></Octicon>
</div>
</div>
);
};
UPDATE:
TrimData & sorter function: outside of these functions the state for sort is updated but inside when i console sort its empty.
//sort providers
const sorter = (a, b) => {
const nameA = a.FullName.toUpperCase();
const nameB = b.FullName.toUpperCase();
let compare = 0;
if (sort.sortBy === "fullName") {
if (nameA > nameB) {
compare = 1;
} else if (nameA < nameB) {
compare = -1;
}
if (sort.order === "asc") {
return compare;
} else if (sort.order === "desc") {
return compare * -1;
}
};
const trimData = (items, pageNumber, rows) => {
const pages = items.length > 0 ? Math.ceil(items.length / rows) : 1;
const trimStart = (pageNumber - 1) * rows;
const trimEnd = trimStart + rows;
const trimedData = items.slice(trimStart, trimEnd);
setEndRange(trimEnd);
setTrimedData(trimedData);
setNumberOfPages(pages);
};
You can see sort is passed through to the sort component and set in the parent. Ive tried setting it in the descendant components but that didn't help. Also passing setSortBy offered the same result. I hope this isnt to big of a code chunk and someone can offer advice to a junior dev :)
The reason sorting is only working on the second click is because on the first click, while the sort value updates, the sort used by trimData function is the old sort object before the click was made.
So let's think about it, sorter is a function with a sort object that is set to {order: "", sortBy: ""} on its first render. When you click on a button, this is the sort object that is being used by your sorter function on your items.sort. Updating sort in setSortBy will then update the components with the new value, and the components will rerender. When the component has rerendered, the sorter function is using the sort that was just set, lets say {order: "asc", sortBy: "fullName"} (but that function hasn't yet been called). When you click a second time, let's say on the desc now, then the function is called with the sort object that was used to render the component i.e. the {order: "asc", sortBy: "fullName"} one, not the sort object that you passed to update the state {order: "desc", sortBy: "fullName"}.
Trying not to be too confusing as there are many approaches you can take, the most direct option you have is to make the "sorter" function using sort values you want to update your state with before you pass it into your items.sort(...):
const handleOnsortChange = (values) => {
// `values` is what you want the next `sort` value to be.
setSortBy(values);
// but `sort` wont be updated yet, so just use `values` instead.
const sorterWithCorrectSort = (a, b) => {
const nameA = a.FullName.toUpperCase();
const nameB = b.FullName.toUpperCase();
let compare = 0;
if (values.sortBy === "fullName") { // "sort" renamed to "values"
if (nameA > nameB) {
compare = 1;
} else if (nameA < nameB) {
compare = -1;
}
if (values.order === "asc") { // "sort" renamed to "values"
return compare;
} else if (values.order === "desc") { // "sort" renamed to "values"
return compare * -1;
}
};
trimData(items.sort(sorterWithCorrectSortValues), pageNumber, 50);
}
There are better ways to format the code, but I believe this is at the heart of the issue. Your sort object in your sorter function is the one that was used to render the component, not the one you used to update the state with.
I'm building a board game in react.js and now I'm trying to add a feature which allows the user to flick through the game. I thought I nailed it but it doesn't work. I checked whether my objects are immutable(they should be), I also checked if the function that is to return the game step is called, and there's no problem at all neither. I don't know why it does not work. How can I solve it?
Here's the constructor function with the game data:
constructor(props) {
super(props);
this.state = {
// The Game Data Comes Here
history: [{
squares: [
Array(8).fill(null),
Array(8).fill(null),
Array(8).fill(null),
[null,null,null,'black','white',null,null,null],
[null,null,null,'white','black',null,null,null],
Array(8).fill(null),
Array(8).fill(null),
Array(8).fill(null)
]
}],
stepNumber: 0,
blackIsNext: true,
winner: null
}
}
Here's where I render:
render() {
// datas that will be rendered regularly come here
const history = this.state.history
const current = history[this.state.stepNumber]
const moves = history.map((_, move) => {
const desc = move ? 'Go to move #' + move : 'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
// Return the game scene here
return (
<div className="master-container">
<GameBoard squares={current.squares} onClick={(row,col) => {
if (!this.state.winner) {
const elems = this.checkElementsAround(this.checkEmptySpaces())
for (let el=0;el<elems.length;el++) {
const turning = this.checkTurningStones(elems[el].directions, this.state.blackIsNext)
if (turning.length !== 0) {
turning.unshift([row,col])
if (row === elems[el].coordinates[0] && col === elems[el].coordinates[1]) {
this.handleMove(turning)
this.setWinnerAndTurn()
// Debug
//console.log(history.length)
console.log(moves)
break
}
}
}
}
}}/>
<div>{(!this.state.winner) ? "Player Turn: " + `${(this.state.blackIsNext) ? 'Black' : 'White'}` : 'WINNER: ' + this.state.winner}</div>
<div>{(this.state.winner) ? moves : null}</div>
</div>
)
}
There are some functions that I won't put, because what they're doing is almost irrelevant, they don't change the data. And also I won't put the setWinnerAndTurn function neither, since it only defines the game winner if the game is over or it switches player turns, but the issue here must be with the way I handle the history data.
Function that handles moves and the one that does jump to another step of the game
handleMove(cords) {
// You'll return if the game is already over or the value of the square is NOT null
if (this.state.winner) {
return
}
// Handle the recently made move here
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[this.state.stepNumber];
const squares = current.squares.slice()
// You'll handle the click here
for (var i=0;i<cords.length;i++) {
squares[cords[i][0]][cords[i][1]] = (this.state.blackIsNext) ? "black" : "white"
}
this.setState({
history: history.concat([{squares: squares}]),
stepNumber: history.length,
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
blackIsNext: (step % 2) === 0,
});
}
If you think there's something missing to solve the problem, please do let me know.
The assignment operation in this line is a mutation of your component's state.
squares[cords[i][0]][cords[i][1]] = (this.state.blackIsNext) ? "black" : "white"
It's a mutation because squares is a shallow copy of an array from this.state. The squares array is new but the arrays inside of squares are the same.
You are dealing with deeply nested data so it's really hard to update without mutation. I would honestly recommend a helper like immer which allows you to do assignment operations on a draft state.
Without a helper you have to map through squares and modify the inner arrays (at least the ones with changed elements). I think this is correct, but double check that I don't have rows and columns mixed up.
// Handle the recently made move here
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[this.state.stepNumber];
// Apply changes to every square
const color = this.state.blackIsNext ? "black" : "white";
const nextSquares = current.squares.map((row, y) =>
row.map((square, x) =>
// find if this [x,y] is in the cords array and replace it if it is
cords.some((cord) => cord[0] === x && cord[1] === y) ? color : square
)
);
// Update the state
this.setState({
history: history.concat({ squares: nextSquares }),
stepNumber: history.length
});
useEffect(() => {
setShowProducts(true);
if (_cloneArray(currentProducts) > 0) {
sortByPrice();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const defaultProducts = () => {
let sortedProducts = _cloneArray(currentProducts);
return sortedProducts;
}
const sortByPrice = (e) => {
let sortValue = e.value;
let sortedProducts = _cloneArray(currentProducts);
if (sortValue === "lowest") {
sortedProducts = _sortArray(sortedProducts, "locationPrice");
} else if (sortValue === "highest") {
sortedProducts = _sortArray(sortedProducts, "locationPrice", "desc");
} else if (sortValue === "default") {
sortedProducts = defaultProducts();
}
setCurrentProducts(sortedProducts);
}
return (
<Menu menuButton={<MenuButton><CgSortAz size={20}/></MenuButton>} onClick={sortByPrice}>
<MenuItem value="default">Default</MenuItem>
<MenuItem value="lowest">Price: Low to High</MenuItem>
<MenuItem value="highest">Price: High to Low</MenuItem>
</Menu>
)
So here I've created the sort items feature in ascending and descending order and I want to return to default state, which is not working after so many trials. Please I need some help here
The issue is that once you sort an array and you override your variable there is no way to tell what the original order was.
const numbers = [9,1,8,2,7,3,6,4,5];
numbers.sort((a, b) => a - b);
console.log(numbers); // [1,2,3,4,5,6,7,8,9]
There is no way to turn back [1,2,3,4,5,6,7,8,9] into the initial value, because there is no info stored about the initial value.
To solve this we should keep the original array around.
const numbers = [9,1,8,2,7,3,6,4,5];
// `sort()` mutates the array, so we have to make a copy first
// to prevent `numbers` from changing
let sorted = Array.from(numbers).sort((a, b) => a - b);
console.log(sorted); // [1,2,3,4,5,6,7,8,9]
Now if we want to restore the original order we can simply do:
sorted = numbers;
// or create a copy `Array.from(numbers)` if you intent to mutate `sorted`
The same applies for React. A common way to solve this would be to have 2 states. One containing the initial/default array, the second containing the sorted variant.
const [currentProducts, setCurrentProducts] = useState(...);
const [sortedProducts, setSortedProducts] = useState(currentProducts);
When sorting, store the result as sortedProducts. If you want to reset sortedProducts simply assign it to currentProducts.
const sortByPrice = (e) => {
let sortValue = e.value;
let products = _cloneArray(sortedProducts);
if (sortValue === "lowest") {
products = _sortArray(products, "locationPrice");
} else if (sortValue === "highest") {
products = _sortArray(products, "locationPrice", "desc");
} else if (sortValue === "default") {
products = currentProducts;
}
setSortedProducts(products);
}
Note that you should use sortedProducts in your view instead of currentProducts.
Since we never update currentProducts there is no real reason for it to be a state. This could just be a constant or a property (wherever the values comes from). If the values comes from an external API (or something async) it makes sense to keep currentProducts as an state, because it has to be set once fetched.
Here is an example that keeps the original order NUMBERS around and stores the sorted variant in a separate state sorted:
const NUMBERS = [9,1,8,2,7,3,6,4,5];
function Numbers() {
const [sorted, setSorted] = React.useState(NUMBERS);
const sort = (e) => {
switch (e.target.value) {
case "asc":
setSorted(Array.from(NUMBERS).sort((a, b) => a - b));
break;
case "desc":
setSorted(Array.from(NUMBERS).sort((a, b) => b - a));
break;
case "default":
setSorted(NUMBERS);
break;
}
};
return <React.Fragment>
<div className="sort-actions">
<button onClick={sort} value="asc">asc</button>
<button onClick={sort} value="desc">desc</button>
<button onClick={sort} value="default">default</button>
</div>
<p>{JSON.stringify(sorted)}</p>
</React.Fragment>;
}
ReactDOM.render(<Numbers />, document.querySelector("#numbers"));
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="numbers"></div>
Instead of showing every single item inside 'portfolioComponents', I want to show a specific amount of items, from a specific number ('startArrayHere') in the array, and raise and lower from which number ('startArrayHere') I start showing items from the Array. I know the For-Loop is wrong, but I just can't figure out how to do it - can anybody help me out here?
class Portfolio extends Component {
constructor(){
super ()
this.state = {
portitems: portfolioItems,
startArrayHere: 0,
amountOfItemsToShow: 6
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
if(this.state.startArrayHere < (portfolioItems.length -
this.state.amountOfItemsToShow))
this.setState(prevState => {
return {
startArrayHere: startArrayHere + 1
}
})
}
render(){
const portfolioComponents = this.state.portitems.map(item =>
<PortfolioTemp key={item.id} portfolioitem={item} />)
return (
<div>
<button onClick={() => this.handleClick()}>STATE CHANGE</button>
<div className="portfolio-container">
{
for (let i = 0; i < this.state.amountOfItemsToShow; i++){
portfolioComponents[i + this.state.startArrayHere]
}
}
</div>
</div>
)
}
}
export default Portfolio;
To get a subset of an Array in JS, use the .slice method
Array.slice(startIndexInclusive, endIndexExclusive)
Reference:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
Example:
const a = ['Hello', 'World', 'Foo', 'Bar']
console.log(a.slice(0, 1)) // prints: ['Hello']
console.log(a.slice(0, 2)) // prints: ['Hello', 'World']
console.log(a.slice(2, a.length) // prints: ['Foo', 'Bar']
console.log(a.slice(0, a.length) // prints the entire array ... note: this would be pointless, as you could just print 'a' itself
So, to incorporate your amountOfItems you'd have to just do
a.slice(startIndex, startIndex + amountOfItems)
I hope this helps somewhat.
You can create a shallow copy of an array between any two indicies with Array.prototype.slice(). In practice, you would use this to make a truncated copy of portfolioItems from startArrayHere to startArrayHere + amountOfItemsToShow + 1, and render those items only.
What is the best way to implement a Set of coordinates in JavaScript? I would like to be able to do things like:
let s=new Set();
s.add([1,1]);
if (s.has([1,1])) // false, since these are different arrays
The above doesn't work, since the Set is storing a reference to the array instead of the contents.
You can subclass Set for more flexibility.
class ObjectSet extends Set{
add(elem){
return super.add(typeof elem === 'object' ? JSON.stringify(elem) : elem);
}
has(elem){
return super.has(typeof elem === 'object' ? JSON.stringify(elem) : elem);
}
}
let s=new ObjectSet();
s.add([1,1]);
console.log(s.has([1,1]))
console.log(s.has([1,2,3]));
console.log([...s]);
console.log([...s].map(JSON.parse));//get objects back
This can be done with strings:
let s=new Set();
s.add("1,1");
s.add("2,2");
console.log(s.has("1,1"), s.has("1,2")); // true false
However, I would prefer to do this with some type of numeric tuple to avoid repeated string conversion logic.
If you only plan to store pairs of coords, another possibility is to use a combination of a Map (for the first coord) and a Set (for the second coord).
function TupleSet() {
this.data = new Map();
this.add = function([first, second]) {
if (!this.data.has(first)) {
this.data.set(first, new Set());
}
this.data.get(first).add(second);
return this;
};
this.has = function([first, second]) {
return (
this.data.has(first) &&
this.data.get(first).has(second)
);
};
this.delete = function([first, second]) {
if (!this.data.has(first) ||
!this.data.get(first).has(second)
) return false;
this.data.get(first).delete(second);
if (this.data.get(first).size === 0) {
this.data.delete(first);
}
return true;
};
}
let mySet = new TupleSet();
mySet.add([0,2]);
mySet.add([1,2]);
mySet.add([0,3]);
console.log(mySet.has([1,3]));
console.log(mySet.has([0,2]));
mySet.delete([0,2]);
console.log(mySet.has([0,2]));
Unfortunately, unlike a normal Set for which:
You can iterate through the elements of a set in insertion order.
— MDN Set
This approach will, for the example above, iterate in the order:
[0,2]
[0,3]
[1,2]
I was building a game when I came across this problem.
Here's a typescript class that might be able to help you. It uses a tree to do its magic.
You should be able to easily modify this to use arrays instead of the x and y parameters
// uses an internal tree structure to simulate set-like behaviour
export default class CoordinateSet {
tree: Record<number, Record<number, boolean>> = {}
add(x: number, y: number) {
this.tree[x] ||= {}
this.tree[x][y] = true;
}
remove(x: number, y: number) {
// if the coordinate doesn't exist, don't do anything
if (!this.tree[x] || !this.tree[y]) {
return;
}
// otherwise, delete it
delete this.tree[x][y];
// if the branch has no leaves, delete the branch, too
if (!Object.keys(this.tree[x]).length) {
delete this.tree[x]
}
}
has(x: number, y: number) {
return !!this.tree[x]?.[y];
}
}
And tests, which will also show you how it works:
import CoordinateSet from "./CoordinateSet";
describe("CoordinateSet", () => {
it("Can add a coordinate", () => {
const cs = new CoordinateSet();
expect(cs.has(1,1)).toBeFalsy();
cs.add(1, 1);
expect(cs.has(1,1)).toBeTruthy();
});
it("Can remove a coordinate", () => {
const cs = new CoordinateSet();
cs.add(1, 1);
expect(cs.has(1,1)).toBeTruthy();
cs.remove(1,1);
expect(cs.has(1,1)).toBeFalsy();
})
})
If we can assume that our tuples are finite integers, they could be encoded as a float.
class TupleSet extends Set{
add(elem){
return super.add((typeof elem === 'object'
&& Number.isSafeInteger(elem[0])
&& Number.isSafeInteger(elem[1]))
? elem[0]+elem[1]/10000000 : elem);
}
has(elem){
return super.has((typeof elem === 'object'
&& Number.isSafeInteger(elem[0])
&& Number.isSafeInteger(elem[1]))
? elem[0]+elem[1]/10000000 : elem);
}
}
function TupleSetParse(elem){
return (Number.isFinite(elem)?
[Math.round(elem),Math.round((elem-Math.round(elem))*10000000)]:elem);
}
let s=new TupleSet();
s.add([1,5]);
s.add([1000000,1000000]);
s.add([-1000000,-1000000]);
console.log(s.has([1,5])); // true
console.log(s.has([1,2])); // false
console.log([...s].map(TupleSetParse));
// [ [ 1, 5 ], [ 1000000, 1000000 ], [ -1000000, -1000000 ] ]
Of course, this is limited in range. And it is fragile to some malformed input, so additional error checking should be added. However, after some testing, this method is only 25% better in speed and memory usage than the JSON.stringify approach. So, JSON is the preferred approach.