How to combine useState hook with switch cases in react - reactjs

Here is component with the array of items, counters val and spiceValue and rendering block with adding some quantity of spices for example. Every spice has own price, adding or removing by click on plus or minus. How to implement this logic according to best practices of react?
export const SpiceBlock = ({ isCalculated }) => {
const [spiceQuantity, setSpiceQuantity] = useState(0);
var val = 0;
var spiceValue = 0;
Calling useEffect with passed val as argument in any part of this code could not to read val
useEffect((val) => {
setSpiceQuantity();
}, []);
const spicesCount = (event) => {
const direction = event.target.dataset.direction;
const dataSpice = event.target.dataset.spice;
switch (dataSpice) {
case "guarana":spiceValue = 21;break;
case "zhen":spiceValue = 11;break;
case "cinnamon":spiceValue = 33;break;
case "tapioka":spiceValue = 41;break;
default:return false;
}
if (direction === "plus") {
val += spiceValue;
} else if (val - spiceValue > 0) {
val -= spiceValue;
}
};
const spicesArr = [
{ name: "Guarana", data: "guarana" },{ name: "Zhenshen", data: "zhen" },
{ name: "Cinnamon", data: "cinnamon" },{ name: "Tapioka", data: "tapioka" },
];
return (
<div className={`spiceItems ${isCalculated ? "calculated" : ""}`}>
{isCalculated && spicesArr
? spicesArr.map((spice, index) => (
<div className="counter" key={`spice${index}`}>
<button
className="counter__btn"
data-direction="minus"
data-spice={spice.data}
onClick={spicesCount}
>
-
</button>
<label htmlFor="spice" type="text">
{spice.name}
<input
type="text"
name="spice"
value={val}
disabled
className="counter__value"
/>
{spiceQuantity}
</label>
<button
className="counter__btn"
data-direction="plus"
data-spice={spice.data}
onClick={spicesCount}
>
+
</button>
</div>
))
: null}
</div>
);
};

Set val as a state and add it as a dependency to your useEffect. So that every time you set the value of val the setSpiceQuantity function will be triggered
const [val, setVal] = useState(0);
useEffect(() => {
setSpiceQuantity();
}, [val]);
if (direction === "plus") {
setVal(val += spiceValue)
} else if (val - spiceValue > 0) {
setVal(val -= spiceValue)
}

Any variable that is not a state will disappear after rerender, it's better to store variables in the state
const [value, setValue] = useState()
const getValue = type => {
switch (dataSpice) {
case "guarana": return 21;
case "zhen": return 11;
case "cinnamon": return 33;
case "tapioka": return 41;
}
}
const spicesCount = (event) => {
const direction = event.target.dataset.direction;
const dataSpice = event.target.dataset.spice;
const val = getValue(dataSpice);
if (direction === "plus") {
setValue(value => value + spiceValue)
} else if (val - spiceValue > 0) {
setValue(value => value - spiceValue)
}
};
<input
type="text"
name="spice"
value={value}
disabled
className="counter__value"
/>

Related

I have a question about react array clearing

There is an array in the parent class(TodolistActivity), and the child class(TaskCallFunc) displays the elements in the array. When I use a.list= []; to clear the array, there is no clearing on the page
but a.list.length = 0 is ok. why?
Here is my code:
interface IListData {
list: IActivityData[]
}
interface IActivityData {
id: number,
content: string,
finish: boolean
}
export function TodolistActivity(activty: IListData) {
const [acty, setActivity] = useState(activty);
const [content, setContent] = useState('');
const input_ref = React.createRef<HTMLInputElement>();
const [selectCount, setSelect] = useState(0);
const handleAdd = () => {
if (input_ref.current) {
if (input_ref.current.value === '') {
alert("输入内容 不能为空!");
return;
}
let id = acty.list.length;
acty.list.unshift({ id: id, content: content, finish: false })
let a = { ...acty }
setActivity(a);
input_ref.current.value = "";
}
}
const calcuateSelect = () => {
let a = acty.list.filter((v, i) => { return v.finish === true })
setSelect(a.length);
}
const handleChange = (input: React.ChangeEvent<HTMLInputElement>) => {
setContent(input.target.value);
}
const clearTask = () => {
let a = { ...acty};
a.list= [];
//a.list.length = 0;
setActivity(a);
}
return (
<div>
<input type='text' onChange={handleChange} ref={input_ref} />
<button className="add task" onClick={handleAdd}>add task</button>
<button className="clear task" onClick={clearTask}>clear task</button>
{console.log(acty)}
<TaskCallFunc data={acty} action={() => { calcuateSelect() }} />
<br />
<label htmlFor="">select{selectCount}/{acty.list.length}</label>
</div>
);
}
interface ItaskCell {
data: IListData,
action: () => void
}
function TaskCallFunc(taskData: ItaskCell) {
const [data, setData] = useState(taskData);
const HandleSlecet = (x: number) => {
for (let index = 0; index < data.data.list.length; index++) {
if (data.data.list[index].id === x) {
let newState = { ...data };
newState.data.list[index].finish = !data.data.list[index].finish;
setData(newState);
data.action();
}
}
}
const handleMap = () => {
return data.data.list.map((v, i) => { return <li key={v.id}>{v.id}: {v.content} <input type="checkbox" checked={v.finish} onChange={() => { HandleSlecet(v.id) }} /> </li> });
}
return (
<ul>{handleMap()}</ul>
);
}
If you know the answer, please let me know thank you
TaskCallFunc component doesn't "listen" for changes on the taskData prop to update the local copy stored in state. Use an useEffect hook with a dependency on taskData prop to update the state when it changes.
function TaskCallFunc(taskData: ItaskCell) {
const [data, setData] = useState(taskData);
useEffect(() => {
setData(taskData);
}, [taskData]);
...
You can clear the array easely by doing setActivity({ list: [] }) consider also to add useEffect as Drew says to listen for changes

React update state of a button based on another button

For this project I am currently working on, I need to highlight the button that was clicked on each layer/row. However, the way I have right now it highlights every button that was clicked.
I need something like this:
correct highlighted path
But then when I click on the same row, it does not remove the highlight from the button that I pressed before. How can I update and reset the state of the previous button that was clicked? I tried to use the useRef hook for this but I haven't been successful so far.
wrong highlighted path
EDIT: Added code
This is the code that I have for the component of each row in the website:
function StackRow({ data, partition, level, index, onClick, getInfo, isApp }) {
const classes = useStyles({ level: level });
const rowRef = useRef();
const handleSelectedButtons = (flag, setFlag, btnRef) => {
console.log(rowRef);
};
return (
<Card
key={partition + '_' + index + '_' + level}
className={classes.card}
id={level}
ref={rowRef}
>
{data.map((field) => {
return (
<StackItem
key={partition + '_' + field[0] + '_' + level}
data={field[0]}
info={field[1]}
level={level}
onClick={onClick}
getInfo={getInfo}
isApp={isApp}
handleSelectedButtons={handleSelectedButtons}
rowRef={rowRef}
/>
);
})}
</Card>
);
}
And this is the code I have for each button of the row:
function StackItem({
data,
info,
level,
onClick,
getInfo,
isApp,
handleSelectedButtons,
}) {
const [flag, setFlag] = useState(false);
const btnRef = useRef();
const styleProps = {
backgroundColor: flag ? '#06d6a0' : level % 2 === 0 ? '#22223b' : '#335c67',
};
const classes = useStyles(styleProps);
return (
<Button
ref={btnRef}
isselected={flag.toString()}
key={data}
className={classes.button}
variant="outlined"
onClick={(event) => {
onClick(event, setFlag, btnRef);
handleSelectedButtons(flag, setFlag, btnRef);
getInfo(info, level, isApp);
}}
disableElevation={true}
>
{data}
</Button>
);
}
There are some useless variables and states there because I have been trying all sort of stuff to do this.
EDIT: Added data sample & project structure
Data looks like:
{
application: {
cmake: {
info: str,
versions: {
version_no: {
application: {...}
}
}
},
gcc: {...},
git: {...},
intel: {...},
.
.
.
}
}
The structure of the project is like:
App
L Stack
L StackRow
L StackItem
Where App is the entire application, Stack is the container for everything in the images apart from the search box, StackRow matches one row of the Stack, and StackItem is one item/button from the StackRow.
EDIT: Added Stack component
function Stack({ data, partition, getInfo }) {
const [level, setLevel] = useState(0);
const [cards, setCards] = useState([]);
const [isApp, setIsApp] = useState(true);
const [selected, setSelected] = useState([]);
const [prevLevel, setPrevLevel] = useState(-1);
const cardsRef = useRef();
const handleClick = (event, setFlag, btnRef) => {
let rows = cardsRef.current.childNodes;
let currBtn = event.target.innerText;
let curr;
for (let i = 0; i < rows.length; i++) {
let rowItems = rows[i].childNodes;
for (let j = 0; j < rowItems.length; j++) {
if (currBtn === rowItems[j].textContent) {
curr = rowItems[j].parentElement;
}
}
}
let id;
for (let i = 0; i < rows.length; i++) {
if (curr.textContent === rows[i].textContent) {
id = i;
}
}
if (level === id) {
if (id % 2 === 0) {
setIsApp(true);
if (selected.length === 0) {
setSelected([...selected, data[currBtn].versions]);
} else {
let lastSelected = selected[selected.length - 1];
setSelected([...selected, lastSelected[currBtn].versions]);
}
} else {
let lastSelected = selected[selected.length - 1];
setSelected([...selected, lastSelected[currBtn].child]);
setIsApp(false);
}
setPrevLevel(level);
setLevel(level + 1);
} else {
let newSelected = selected.slice(0, id);
if (id % 2 === 0) {
setIsApp(true);
if (newSelected.length === 0) {
setSelected([...newSelected, data[currBtn].versions]);
} else {
let lastSelected = newSelected[newSelected.length - 1];
setSelected([...newSelected, lastSelected[currBtn].versions]);
}
} else {
let lastSelected = newSelected[newSelected.length - 1];
setSelected([...newSelected, lastSelected[currBtn].child]);
setIsApp(false);
}
setPrevLevel(level);
setLevel(id + 1);
}
setFlag(true);
};
useEffect(() => {
let fields = [];
let lastSelected = selected[selected.length - 1];
if (level % 2 !== 0) {
fields = Object.keys(lastSelected).map((key) => {
let path = lastSelected[key].path;
let module = lastSelected[key].module_name;
let info = 'module: ' + module + ' path: ' + path;
return [key, info];
});
} else {
if (selected.length !== 0)
fields = Object.keys(lastSelected).map((key) => {
let info = lastSelected[key].info;
return [key, info];
});
}
if (fields.length > 0) {
if (level > prevLevel) {
setCards((prevState) => [...prevState, fields]);
} else {
setCards((prevState) => [
...prevState.slice(0, selected.length),
fields,
]);
}
}
}, [selected, level, prevLevel]);
useEffect(() => {
let fields = Object.keys(data).map((key) => {
let info = data[key].info;
return [key, info];
});
setCards([fields]);
setLevel(0);
}, [data]);
useEffect(() => {
setLevel(0);
setPrevLevel(-1);
setSelected([]);
}, [partition]);
if (cards) {
return (
<div ref={cardsRef}>
{cards.map((card, index) => (
<StackRow
data={card}
partition={partition}
level={index}
index={cards.indexOf(card)}
onClick={handleClick}
getInfo={getInfo}
isApp={isApp}
/>
))}
</div>
);
} else return null;
}
EDIT: Added data sample
{
cmake: {
info: "A cross-platform, open-source build system. CMake is a family of tools designed to build, test and package software.",
versions: {
"3.17.3": {
child: {},
module_name: "cmake/3.17.3",
path: "/opt/apps/nfs/spack/var/spack/environments/matador/modules/linux-centos8-x86_64/Core/cmake/3.17.3.lua",
version_no: "3.17.3"
}
}
},
gcc: {
info: "...",
versions: {
"8.4.0": {
child: {
cmake: {...},
cuda: {...},
cudnn: {...},
openmpi: {...},
.
.
.
},
module_name: "...",
path: "...",
version_no: "..."
}
"9.3.0": {...},
"10.1.0": {...}
}
}
}

Multiple Filtering in React table without libraries (React-table) with react-hooks

I have a filters by id and by firstName - but how can I use them together?
How can I use 3-4-5 filters together that are related to each other? When I clear the filter - them doesn't work correctly
How can I get the result of first filter when I clear the second and vice versa.
What is the best practice?
table view:
import React, { useState, useEffect, useCallback, useMemo } from "react";
import BootstrapTable from "./BootstrapTable";
import useFetchHook from "../useFetchHook";
export default function BootstrapTableContainer() {
const [filterValue, setFilterValue] = useState("");
const [idFilter, setIdFilter] = useState("all");
const [firstNameFilter, setFirstNameFilter] = useState("");
const [data, loading, error, setError] = useFetchHook(
`http://www.filltext.com/?rows=${20}&id={number|1000}&firstName={firstName}&lastName={lastName}&email={email}&category=[1,2]&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}`,
[]
);
const [dataTable, setDataTable] = useState(data);
const setFilterId = useCallback((value) => {
setIdFilter(value);
}, [setIdFilter]);
const setFilterFirstName = useCallback((value) => {
setFirstNameFilter(value);
}, [setFirstNameFilter]);
useEffect(() => {
let newData;
switch (idFilter) {
case "all":
newData = [...dataTable];
break;
case "0":
newData = dataTable.filter((obj) => {
return obj.id >= 0 && obj.id < 300;
});
break;
case "300":
newData = dataTable.filter((obj) => {
return obj.id >= 300 && obj.id < 600;
});
break;
case "600":
newData = dataTable.filter((obj) => {
return obj.id >= 600 && obj.id <= 1000;
});
break;
default:
break;
}
if (!firstNameFilter) {
newData = [...newData]
} else {
newData = newData.filter((obj) => {
return obj.firstName
.toLowerCase()
.includes(firstNameFilter.toLowerCase());
});
}
setDataTable(newData);
}, [idFilter, setDataTable, firstNameFilter]);
const bootstrapTable = useMemo(() => {
return (
<BootstrapTable
idFilter={idFilter}
setFilterId={setFilterId}
firstNameFilter={firstNameFilter}
setFilterFirstName={setFilterFirstName}
data={dataTable}
/>
);
}, [dataTable, idFilter, setFilterId, firstNameFilter, setFilterFirstName]);
return (
<div>
{bootstrapTable}
</div>
);
}
You have so simple state properties to solve your problem. I'll write some ideas to youo
import React, { useState, useEffect, useCallback, useMemo } from "react";
import BootstrapTable from "./BootstrapTable";
import useFetchHook from "../useFetchHook";
const filterById = (dataTable, value) => {
switch (value) {
case "0":
return dataTable.filter((obj) => {
return obj.id >= 0 && obj.id < 300;
});
case "300":
return dataTable.filter((obj) => {
return obj.id >= 300 && obj.id < 600;
});
case "600":
return dataTable.filter((obj) => {
return obj.id >= 600 && obj.id <= 1000;
});
case "all":
return [...dataTable];
}
};
export default function BootstrapTableContainer() {
const [filters, setFilters] = useState([]);
const [data, loading, error, setError] = useFetchHook(
`http://www.filltext.com/?rows=${20}&id={number|1000}&firstName={firstName}&lastName={lastName}&email={email}&category=[1,2]&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}`,
[]
);
const [dataTable, setDataTable] = useState(data);
useEffect(() => {
let newData = dataTable;
filters.forEach(({ name, value }) => {
switch (name) {
// here we filter by id
case "id":
newData = filterById(newData, value);
break;
// here we filter by firstname
case "firstname":
// newData = filterByFirstName(newData, value);
break;
// ...
default:
break;
}
});
setDataTable(newData);
}, [filters]);
const bootstrapTable = useMemo(() => {
return (
<BootstrapTable
filters={filters}
handleFilterChandge={(e) => {
// e - input event onChange:
// <input name="firstname"
// onChange={props.handleFilterChandge}
// value={props.filters.firstname}
// />
//
setFilters([
...filters,
{ name: e.target.name, value: e.target.value },
]);
}}
data={dataTable}
/>
);
}, [dataTable, filters]);
return <div>{bootstrapTable}</div>;
}

How to implement input auto-tab in React (focus on next input element on keyup event)?

Using React as library, I have several blocks of input text, each one with maxlength=1, and I would like to implement a function that everytime a character is entered in an input box the focus goes to the next one.
This is the list of input elements I'm talking about:
And this is a minimal representation on CodesSandbox: https://codesandbox.io/s/react-autotab-6kewb.
How can I get the desired behaviour in React?
This is the relevant snippet:
const autoTab = e => {
const BACKSPACE_KEY = 8;
const DELETE_KEY = 46;
if (e.keyCode === BACKSPACE_KEY) {
// TODO: implement backwards autoTab
} else if (e.keyCode !== DELETE_KEY) {
// TODO: implement forwards autoTab
}
};
const blocks = Array.from({ length: 10 }, (element, index) => (
<input className="block" key={index} maxLength={1} onKeyUp={autoTab} />
));
I have added tab indexes to your input elements and used them to navigate between items.
const autoTab = e => {
const BACKSPACE_KEY = 8;
const DELETE_KEY = 46;
let tabindex = $(e.target).attr("tabindex") || 0;
tabindex = Number(tabindex);
if (e.keyCode === BACKSPACE_KEY) {
tabindex -= 1;
} else if (e.keyCode !== DELETE_KEY) {
tabindex += 1;
}
const elem = $("[tabindex=" + tabindex + "]");
if (elem[0]) {
elem.focus();
}
};
const blocks = Array.from({ length: 10 }, (element, index) => (
<input
className="block"
tabIndex={index}
key={index}
maxLength={1}
onKeyUp={autoTab}
/>
));
EDIT: Here is a new way using refs and the Demo based on your code sandbox.
const autoTab = e => {
const BACKSPACE_KEY = 8;
const DELETE_KEY = 46;
let tabindex = $(e.target).attr("data-index") || 0;
tabindex = Number(tabindex);
let elem = null;
if (e.keyCode === BACKSPACE_KEY) {
elem = tabindex > 0 && elemRefs[tabindex - 1];
} else if (e.keyCode !== DELETE_KEY) {
elem = tabindex < elemRefs.length - 1 && elemRefs[tabindex + 1];
}
if (elem) {
elem.current.focus();
}
};
const Input = props => {
const ref = React.createRef();
elemRefs.push(ref);
return (
<input
className="block"
data-index={props.index}
ref={ref}
maxLength={1}
onKeyUp={props.autoTab}
/>
);
};
const blocks = Array.from({ length: 10 }, (element, index) => (
<Input key={index} index={index} autoTab={autoTab} />
));
const App = () => <div className="App">{blocks}</div>;
You have to use ref to focus on next element, you can use index along with your ref name to distinguish element
like this:
this.refs[yourrefname].nextSibling.focus();
and your input element should have ref with unique name
<input ref={yourrefname + index} className="block" key={index} maxLength={1} onKeyUp={autoTab} />
I solved it by creating a simple CSS selector
const autoTab = (e: ChangeEvent<HTMLInputElement>) => {
if (`${e.target.value.length}` === e.target.getAttribute("maxlength")) {
var inputs = document.getElementsByClassName("autotab");
for (let i = 0; i < inputs.length; i++) {
if (e.target == inputs[i]) {
i++;
if (i < inputs.length) {
let next: any = inputs[i];
next.focus();
}
}
}
}
};
return (
<>
<div>
<input
className="autotab"
type="text"
maxLength={1}
onChange={(e) => {
autoTab(e);
}}
/>
</>
);

React update state object using hooks (useState) works, but no rerender

I have a weird problem with React hooks (useState).
I have a page which checks my bills and my bank account and checks wether salary is in, and then moves my money to different jars. The page has 2 buttons: check preconditions (salary enough, etc), and run script.
The first one (preconditions) works as expected, also the output (in below code the var currentstate), when I update the state (with setPreconditions) nothing happens.
So, my thought was that state isnt updated, until I found out that when I changed some other field with state (for example salary) the page rerenders and the correct data for currentstate (state preconditions) is displayed.
Why is this happening?
const Bunq = ({auth}) => {
const [accounts, setAccounts] = useState([]);
const [preconditions, setPreconditions] = useState({run: false, succeeded: false, accountsExist: [], balanceSufficient: true, incomeSufficient: true, sparen: null, maandtotaal: 0, balance: null});
const [rekeningen, setRekeningen] = useState([]);
const [salaris, setSalaris] = useState(getLocalStorage('bunq_salaris') || '');
const [eigen_geld, setEigenGeld] = useState(getLocalStorage('bunq_eigen_geld') || '');
const [sparen, setSparen] = useState(0);
const [page_loaded, setPageLoaded] = useState(false);
const [script_running, setScriptRunning] = useState(false);
useEffect(() => {
setLocalStorage('bunq_salaris', salaris);
}, [salaris]);
useEffect(() => {
setLocalStorage('bunq_eigen_geld', eigen_geld);
}, [eigen_geld]);
.......................
const checkPreconditions = () => {
//check
//setScriptRunning(true);
const algemeen_account = getAccountByName("Algemeen");
let maandnummer = (new Date()).getMonth()+1;
let currentstate = preconditions;
currentstate.succeeded = true;
currentstate.maandtotaal = 0;
currentstate.incomeSufficient = true;
currentstate.balanceSufficient = true;
currentstate.balance = algemeen_account.balance.value;
rekeningen.map(rekening => {
currentstate.maandtotaal += rekening["totaal_" + maandnummer];
let foundaccount = getAccountByName(rekening.rekening);
if(foundaccount == null && rekening["totaal_" + maandnummer] > 0){
currentstate.succeeded = false;
currentstate.accountsExist.push(rekening.rekening)
console.log("Rekening bestaat niet: " + rekening.rekening);
}
});
if((parseFloat(algemeen_account.balance.value)) < salaris){
currentstate.balanceSufficient = false;
currentstate.succeeded = false;
}
if((currentstate.maandtotaal + eigen_geld) > salaris){
currentstate.incomeSufficient = false;
currentstate.sparen = 0;
currentstate.succeeded = false;
}else{
currentstate.sparen = (salaris - currentstate.maandtotaal - eigen_geld);
if(currentstate.balanceSufficient){
currentstate.sparen = (currentstate.sparen + (Math.round(algemeen_account.balance.value) - salaris));
}
//console.log(currentstate);
if(currentstate.sparen < 0){
currentstate.sparen = 0;
currentstate.incomeSufficient = false;
currentstate.succeeded = false;
}else{
currentstate.incomeSufficient = true;
}
}
setPreconditions(currentstate);
//setPreconditions('test');
console.log(currentstate, preconditions);
//setScriptRunning(false);
//this.setState({preconditions: currentstate});
}
.........................
return (<div><h1>Bunq</h1>
<DefaultTable data={rekeningen} columns={rekeningColumns} loading={rekeningen.length === 0} pageSize={15}/>
<Form>
<Row>
......................
<Button variant="primary" onClick={() => {checkPreconditions();console.log(preconditions);}} disabled={!page_loaded || script_running}>Controleer</Button>
</Row>
</Form>
<ListGroup>
{JSON.stringify(preconditions)}
{preconditions.balance !== null ?<ListGroup.Item variant="success">Huidig saldo Algemene rekening: {preconditions.balance}</ListGroup.Item> : ""}
{preconditions.accountsExist.map((rek, i) => {return <ListGroup.Item key={i} variant="danger">Rekening {rek} bestaat niet</ListGroup.Item>})}
{preconditions.balanceSufficient === false ? <ListGroup.Item variant="danger">Niet voldoende saldo. Salaris nog niet binnen?</ListGroup.Item> : ""}
{preconditions.incomeSufficient === false ? <ListGroup.Item variant="danger">Niet voldoende inkomen om alle rekeningen te betalen</ListGroup.Item> : ""}
{preconditions.sparen !== null ? <ListGroup.Item variant="success">Er wordt {preconditions.sparen} gespaard</ListGroup.Item> : ""}
</ListGroup>
</div>
);
}
You are mutating your state, so when you call setPreconditions(currentstate) you are updating the state with the exact same object reference, which React will treat as no state being updated.
You can create a copy of the preconditions object instead.
const checkPreconditions = () => {
const algemeen_account = getAccountByName("Algemeen");
let maandnummer = new Date().getMonth() + 1;
let currentstate = { ...preconditions };
// ...
}

Resources