I am new to react and learning about hooks but I cannot escape from useState at second execution.
My caller function is calling another component called QueryPanel with parameter:
const [availables, setAvailable] = useState<string[]>([]);
useEffect(() => {
context.services.records.getAvailablesList()
.then((resp) => {
if(resp != undefined) {
setAvailables(resp);
}
});
}, []);
return <Panel1 text={availables}></Panel1>;
So Panel1 is opening with a string array or an empty array. What I want to do is that show dropdown and if the parameter is not empty then show first item as default item or if the parameter list is empty then show a string as a default value "No option available"
export const QueryPanel = (props: any) => {
const t = useTrans();
const context = useContext(AppContext);
const [form] = Form.useForm<Store>();
const [defaultValue, setDefaultValue] = useState("asd");
const { Option } = Select;
const onFinish = (values: Store) => {
const queryParams: Query = {
system: values.systemType,
startDate: values.startDate,
endDate: values.endDate,
startFrequency: values.startFrequency,
endFrequency: values.endFrequency
};
context.services.records.query(queryParams);
};
const validateOnFormChange = () => {
form.validateFields();
};
const onFinishFailed = (errorInfo: ValidateErrorEntity) => {
console.log('Failed:', errorInfo);
};
// useEffect(() => {
//
// if (props.text.length !== 0) {
// setDefaultValue(props.text[0]);
// }
// else {
// setDefaultValue("No option available");
// }
// }, []);
// if (props.text.length > 0) {
// setDefaultDbValue(props.text[0]);
// }
// else {
// let defaultDropDownOption:string = "No Db Available";
// setDefaultDbValue(defaultDropDownOption);
// }
My if-else is stuck in infinite loop because whenever I set default value to a value then it goes to same if statement again. How can I escape from it without using extra field ? useEffect is commented out here and the weird thing is when i check the parameter prop.text it is undefined at beginning and thats why the code is not going inside if-else in useEffect and then at second iteration the prop.text is coming as true.
I have tried something with useEffect hook but did not work:
useEffect(() => {
if (props.text.length > 0) {
setDefaultValue(props.text[0]);
}
}, [defaultValue]);
But this time default db is coming empty
Here is the complete code of Panel1 (QueryPanel) but the some names are different
import { Button, Col, DatePicker, Form, InputNumber, Row, Select } from 'antd';
import { Store } from 'antd/lib/form/interface';
import { ValidateErrorEntity } from 'rc-field-form/lib/interface';
import React, {useContext, useEffect, useState} from 'react';
import { AppContext } from '../../../../shared/Context';
import './QueryPanel.less';
import { Query } from '../../../../rest/BackendApi';
import { SystemType } from '../../../../shared/models/SystemType';
import { useTrans } from "../../../../shared/i18n/i18n";
export const QueryPanel = (props: any) => {
const t = useTrans();
const context = useContext(AppContext);
const [form] = Form.useForm<Store>();
const [defaultDbValue, setDefaultDbValue] = useState("Select Db");
const { Option } = Select;
const onFinish = (values: Store) => {
const queryParams: Query = {
system: values.systemType,
startDate: values.startDate,
endDate: values.endDate,
startFrequency: values.startFrequency,
endFrequency: values.endFrequency
};
context.services.records.query(queryParams);
};
const validateOnFormChange = () => {
form.validateFields();
};
const onFinishFailed = (errorInfo: ValidateErrorEntity) => {
console.log('Failed:', errorInfo);
};
// useEffect(() => {
// if (props.text.length !== 0) {
// setDefaultDbValue(props.text[0]);
// }
// else {
// setDefaultDbValue("No db available");
// }
// }, []);
useEffect(() => {
if (!props.text) return;
if (props.text.length > 0) {
setDefaultDbValue(props.text[0]);
}
else setDefaultDbValue("No Db Available");
}, [props.text?.length]);
return (
<div>
<Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed} onValuesChange={validateOnFormChange}>
<Row justify={"space-between"} >
<Col span={4}>
<Form.Item label={t.trans.queryPanel.systemType} name='systemType' className={"inline-form-item"}>
<Select defaultValue={defaultDbValue}>
{props.text.map((element: any) => (
<Option key={element} value={element}>
{element}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item label={t.trans.queryPanel.startDate} name='startDate' className={"inline-form-item"}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (!value || !getFieldValue('endDate') || getFieldValue('endDate') >= value) {
return Promise.resolve();
}
return Promise.reject(t.trans.queryPanel.errors.startDate);
},
}),
]}>
<DatePicker format="DD-MM-YYYY" />
</Form.Item>
</Col>
<Col span={4}>
<Form.Item label={t.trans.queryPanel.endDate} name='endDate' className={"inline-form-item"}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (!value || !getFieldValue('startDate') || getFieldValue('startDate') <= value) {
return Promise.resolve();
}
return Promise.reject(t.trans.queryPanel.errors.endDate);
},
}),
]}>
<DatePicker format="DD-MM-YYYY" />
</Form.Item>
</Col>
<Col span={4}>
<Form.Item label={t.trans.queryPanel.startFrequency} name='startFrequency' className={"inline-form-item"}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (!value) return Promise.resolve();
if (value < 0 || value > 200) {
return Promise.reject(t.trans.queryPanel.errors.startFrequency);
} else if (getFieldValue('endFrequency') && getFieldValue('endFrequency') < value) {
return Promise.reject(t.trans.queryPanel.errors.startFrequencyValues);
}
return Promise.resolve();
},
}),
]}>
<InputNumber />
</Form.Item>
</Col>
<Col span={4}>
<Form.Item label={t.trans.queryPanel.endFrequency} name='endFrequency' className={"inline-form-item"}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (!value) return Promise.resolve();
if (value < 0 || value > 400) {
return Promise.reject(t.trans.queryPanel.errors.endFrequency);
} else if (getFieldValue('startFrequency') && getFieldValue('startFrequency') > value) {
return Promise.reject(t.trans.queryPanel.errors.endFrequencyValues);
}
return Promise.resolve();
},
}),
]}>
<InputNumber />
</Form.Item>
</Col>
<Col span={2}>
<Form.Item className={"inline-form-item"}>
<Button type="primary" htmlType="submit">
{t.trans.queryPanel.search}
</Button>
</Form.Item>
</Col>
</Row>
</Form>
</div>
);
}
The infinite loop is created because React will rerender your <Panel1 /> component whenever the state of the component changes.
In the body of your component you have this if ... else statement sitting without wrapping it in a hook:
if (props.text.length > 0) {
setDefaultDbValue(props.text[0]);
} else {
let defaultDropDownOption:string = "No Db Available";
setDefaultDbValue(defaultDropDownOption);
}
So, either way you will execute setDefaultDbValue() which will update the state and trigger a rerender.
To prevent this loop you can wrap this snipped with useEffect() and use your text.length as a dependency:
useEffect(() => {
if (!props.text) return; // wait until it is defined.
if (props.text.length > 0) setDefaultDbValue(props.text[0]);
else setDefaultDbValue("No Db Available");
}, [props.text?.length]);
There is no need to pass defaultValue as dependency to the useEffect hook. Or do I missunderstand your intention with this?
This aside: the better way to set default values is to do it in the useState(<DEFAULT_VALUE>) hook directly. Just the way you did it. Why is this not an option for you?
Edit
After your edit it is clear that the issue lies in how Ant uses the prop defaultValue. This prop is not reactive - so updating it after the Select has been rendered has no effect.
So you can either wait for the request to resolve before rendering your Panel, or:
If you want to select the first element of your text prop, once it is loaded, you have to set the value of the Select accordingly. Note that you have to update the value via onChange handler, if you bind the value to a state variable.
Here an example:
const Select = antd.Select;
const values = [
'one',
'two',
'three',
];
// This is just a dummy representing your `text` prop
const lazyValues = new Promise((res) => {
setTimeout(() => {
res(values);
}, 2000);
});
const App = () => {
const [text, setText] = React.useState([]);
const [value, setValue] = React.useState();
React.useEffect(() => {
// fake the request
lazyValues.then((values) => setText(values));
}, []);
// if `text` changes its value, set the Select to its first element
React.useEffect(() => {
if (text.length > 0 && !value) setValue(text[0]);
}, [text]);
return <div>
<Select onChange={setValue /* Need to update our value manually */} defaultValue={"Default value visible if there is no value"} value={value}>
{text.map((text, i) => <Select.Option key={i} value={text}>{text}</Select.Option>)}
</Select>
</div>
}
ReactDOM.render(<App />, document.getElementById('app'));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/antd#4.17.4/dist/antd.min.js"></script>
<link rel='stylesheet' type="text/css" href="https://unpkg.com/antd#4.17.4/dist/antd.min.css"></link>
<div id="app"/>
I believe that one way would be to do :
useEffect(() => {
if (props.text.length > 0 && props.text[0] !== defaultValue) {
setDefaultValue(props.text[0]);
}
}, [defaultValue]);
By doing so, you will update the defaultValue only when it differs from props.text[0]
Related
I created a form component using react hook forms. The component is composed from a group of checkboxes and a text input. The text input appears when user click on the last checkbox custom. The idea of this one is: when the user will click on it appears a text input and the user can add a custom answer/option. Ex: if user type test within the input then when the user will save the form, there should appear in an array test value, but custom text should't be in the array. In my application i don't have access to const onSubmit = (data) => console.log(data, "submit");, so i need to change the values within Component component. Now when i click on submit i get in the final array the custom value. Question: how to fix the issue described above?
const ITEMS = [
{ id: "one", value: 1 },
{ id: "two", value: 2 },
{ id: "Custom Value", value: "custom" }
];
export default function App() {
const name = "group";
const methods = useForm();
const onSubmit = (data) => console.log(data, "submit");
return (
<div className="App">
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Component ITEMS={ITEMS} name={name} />
<input type="submit" />
</form>
</FormProvider>
</div>
);
}
export const Component = ({ name, ITEMS }) => {
const { control, getValues } = useFormContext();
const [state, setState] = useState(false);
const handleCheck = (val) => {
const { [name]: ids } = getValues();
const response = ids?.includes(val)
? ids?.filter((id) => id !== val)
: [...(ids ?? []), val];
return response;
};
return (
<Controller
name={name}
control={control}
render={({ field, formState }) => {
return (
<>
{ITEMS.map((item, index) => {
return (
<>
<label>
{item.id}
<input
type="checkbox"
name={`${name}[${index}]`}
onChange={(e) => {
field.onChange(handleCheck(e.target.value));
if (index === ITEMS.length - 1) {
setState(e.target.checked);
}
}}
value={item.value}
/>
</label>
{state && index === ITEMS.length - 1 && (
<input
{...control.register(`${name}[${index}]`)}
type="text"
/>
)}
</>
);
})}
</>
);
}}
/>
);
};
demo: https://codesandbox.io/s/winter-brook-sml0ww?file=/src/Component.js:151-1600
Assuming that the goal is to keep all the selections in the same group field, which must be an array that logs the selected values in provided order, with the custom input value as the last item if specified, perhaps ideally it would be easier to calculate the values in onSubmit before submitting.
But since the preference is not to add logic in onSubmit, maybe an alternative option could be hosting a local state, run the needed calculations when it changes, and call setValue manually to sync the calculated value to the group field.
Forked demo with modification: codesandbox
import "./styles.css";
import { Controller, useFormContext } from "react-hook-form";
import React, { useState, useEffect } from "react";
export const Component = ({ name, ITEMS }) => {
const { control, setValue } = useFormContext();
const [state, setState] = useState({});
useEffect(() => {
const { custom, ...items } = state;
const newItems = Object.entries(items).filter((item) => !!item[1]);
newItems.sort((a, b) => a[0] - b[0]);
const newValues = newItems.map((item) => item[1]);
if (custom) {
setValue(name, [...newValues, custom]);
return;
}
setValue(name, [...newValues]);
}, [name, state, setValue]);
const handleCheck = (val, idx) => {
setState((prev) =>
prev[idx] ? { ...prev, [idx]: null } : { ...prev, [idx]: val }
);
};
const handleCheckCustom = (checked) =>
setState((prev) =>
checked ? { ...prev, custom: "" } : { ...prev, custom: null }
);
const handleInputChange = (e) => {
setState((prev) => ({ ...prev, custom: e.target.value }));
};
return (
<Controller
name={name}
control={control}
render={({ field, formState }) => {
return (
<>
{ITEMS.map((item, index) => {
const isCustomField = index === ITEMS.length - 1;
return (
<React.Fragment key={index}>
<label>
{item.id}
<input
type="checkbox"
name={name}
onChange={(e) =>
isCustomField
? handleCheckCustom(e.target.checked)
: handleCheck(e.target.value, index)
}
value={item.value}
/>
</label>
{typeof state["custom"] === "string" && isCustomField && (
<input onChange={handleInputChange} type="text" />
)}
</React.Fragment>
);
})}
</>
);
}}
/>
);
};
Ok, so after a while I got the solution. I forked your sandbox and did little changes, check it out here: Save Form values in ReactJS using checkboxes
Basically, you should have an internal checkbox state and also don't register the input in the form, because this would add the input value to the end of the array no matter if that value is "".
Here is the code:
import "./styles.css";
import { Controller, useFormContext } from "react-hook-form";
import { useEffect, useState } from "react";
export const Component = ({ name, ITEMS }) => {
const { control, setValue } = useFormContext();
const [state, setState] = useState(false);
const [checkboxes, setCheckboxes] = useState(
ITEMS.filter(
(item, index) => index !== ITEMS.length - 1
).map(({ value }, index) => ({ value, checked: false }))
);
useEffect(() => {
setValue(name, []); //To initialize the array as empty
}, []);
const [inputValue, setInputValue] = useState("");
const handleChangeField = (val) => {
const newCheckboxes = checkboxes.map(({ value, checked }) =>
value == val ? { value, checked: !checked } : { value, checked }
);
setCheckboxes(newCheckboxes);
const response = newCheckboxes
.filter(({ checked }) => checked)
.map(({ value }) => value);
return state && !!inputValue ? [...response, inputValue] : response;
};
const handleChangeInput = (newInputValue) => {
const response = checkboxes
.filter(({ checked }) => checked)
.map(({ value }) => value);
if (state) if (!!newInputValue) return [...response, newInputValue];
return response;
};
return (
<Controller
name={name}
control={control}
render={({ field, formState }) => {
return (
<>
{ITEMS.map((item, index) => {
return (
<>
<label>
{item.id}
<input
type="checkbox"
name={`${name}[${index}]`}
onChange={(e) => {
if (index === ITEMS.length - 1) {
setState(e.target.checked);
return;
}
field.onChange(handleChangeField(e.target.value));
}}
value={item.value}
/>
</label>
{state && index === ITEMS.length - 1 && (
<input
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
field.onChange(handleChangeInput(e.target.value));
}}
type="text"
/>
)}
</>
);
})}
</>
);
}}
/>
);
};
I want to use an MUI stepper to replace a Select component. The select component is used to indicate the status of the document the user is working in (New, In Progress, Complete, etc.). I have managed to display the correct status in the stepper, but I cannot interact with it to move the status forward or back.
This is my stepper file. I am passing the status value through props:
export default function IntakeStatusBar(props) {
const { status } = props;
const classes = useStyles();
const [activeStep, setActiveStep] = useState(0);
const steps = ["New", "In Progress", "Completed"];
useEffect(() => {
if (status === "In Progress") {
setActiveStep(1);
} else if (status === "Completed") {
setActiveStep(2);
} else setActiveStep(0);
}, [status, activeStep]);
const handleStep = (step) => () => {
setActiveStep(step);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label, index) => (
<Step key={label}>
<StepButton onClick={handleStep(index)}>{label}</StepButton>
</Step>
))}
</Stepper>
</div>
);
}
This is where I call and display the stepper:
export default function IntakeDetails() {
const [details, setDetails] = useState("");
const onTextChange = (e) => {
var id = e.target.id ? e?.target.id : e?.target.name;
var value = e.target.value;
setDetails({ ...details, [id]: value });
}
....
return (
<IntakeStatusBar status={details?.Status} onChange={onTextChange} />
// This is the Select drop down menu I have been using
<TextField
label="Status"
error={requiredField && details?.Status?.length <= 0}
value={details?.Status}
disabled={!(adminRole && isSolutionsTab && details?.Status !== "In Plan")}
select
onChange={onTextChange}
>
{details.StatusList?.map((choice) => {
return (
<MenuItem key={choice} value={choice}>
{choice}
</MenuItem>
);
})}
</TextField>
)
}
This is what the status field looks like in JSON:
{
Status: "New"
}
besides changing this:
<StepButton onClick={() => handleStep(index)}>{label}</StepButton>
you have to change this:
const handleStep = (step) => {
setActiveStep(step);
};
and set Stepper to nonLinear if you want user to click on steps:
<Stepper nonLinear activeStep={activeStep} alternativeLabel>
I also commented out useEffect since I had no idea what its purpose is and it's messing with activeStep state.
I have an outsideAlerter component that functions elsewhere on my site. I am now using it on a repeatable component and for some reason it is clearing my state effectively breaking my desired outcome.
below is my wrapper component that detects if you click outside of its children
import React, { useRef, useEffect } from "react";
/**
* Hook that alerts clicks outside of the passed ref
*/
function useOutsideAlerter(ref, onClickOutside) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
//console.log(onClickOutside);
onClickOutside();
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
}
/**
* Component that alerts if you click outside of it
*/
export default function OutsideAlerter(props) {
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef, props.onClickOutside);
return <div ref={wrapperRef}>{props.children}</div>;
}
Below is my controller component, it handles state
const TableFilterDropdownController = ({style, rows, colKey, activeFilters, addActiveFilter}) => {
const [tableFilterState, setTableFilterState] = useState(
{
state: INACTIVE,
iconColor: "black",
filter: "",
filteredRows: [...rows],
localActiveFilters: []
}
);
useEffect(() => {
let state = tableFilterState.state;
let localActiveFilters = tableFilterState.localActiveFilters;
if (state === INACTIVE && localActiveFilters.length > 0) {
setTableFilterState({...tableFilterState, state: ACTIVE})
}
}, [tableFilterState.state])
//filter out repeats and rows that don't match input
useEffect(() => {
let filter = tableFilterState.filter
if (filter !== "") {
let tempFilteredRows = [];
rows.map(row => {
if (row[colKey].toLowerCase().includes(filter.toLowerCase()) &&
!tempFilteredRows.includes(row[colKey])) {
tempFilteredRows.push(row[colKey]);
}
})
setTableFilterState({...tableFilterState, filteredRows: tempFilteredRows})
}
else {
let tempFilteredRows = [];
rows.map(row => {
if (!tempFilteredRows.includes(row[colKey])) {
tempFilteredRows.push(row[colKey]);
}
})
setTableFilterState({...tableFilterState, filteredRows: tempFilteredRows});
}
}, [tableFilterState.filter, rows])
const onClick = () => {
if (tableFilterState.state === DROP_DOWN) {
console.log(tableFilterState)
if (tableFilterState.localActiveFilters.length > 0) {
//setState(ACTIVE)
setTableFilterState({...tableFilterState, state: ACTIVE});
}
else {
//setState(INACTIVE)
setTableFilterState({...tableFilterState, state: INACTIVE});
}
}
else {
//setState(DROP_DOWN)
setTableFilterState({...tableFilterState, state: DROP_DOWN});
}
}
//something here is breaking it and resetting on click outside
const onClickOutside = () => {
setTableFilterState({...tableFilterState, state: INACTIVE});
}
let addLocalActiveFilter = (filter) => {
let newActiveFilters = [...tableFilterState.localActiveFilters];
const index = newActiveFilters.indexOf(filter);
if (index > -1) {
newActiveFilters.splice(index, 1);
} else {
newActiveFilters.push(filter);
}
setTableFilterState({...tableFilterState, localActiveFilters: newActiveFilters});
}
return (
<TableFilterDropdown
style={style}
color={tableFilterState.iconColor}
state={tableFilterState.state}
onClick={onClick}
onClickOutside={onClickOutside}
dropLeft={true}
filter={tableFilterState.filter}
setFilter={e => setTableFilterState({...tableFilterState, filter: e.target.value})}
>
{tableFilterState.filteredRows.map((item, index) => {
return (
<CheckboxInput
value={item}
label={item}
key={index}
onChange={e => {
addActiveFilter(e.target.value);
addLocalActiveFilter(e.target.value)
}}
isChecked={tableFilterState.localActiveFilters.includes(item)}
/>
);
})}
</TableFilterDropdown>
);
}
export default TableFilterDropdownController;
And lastly below is the UI component
const TableFilterDropdown = ({style, state, color, children, onClick, onClickOutside, dropLeft, filter, setFilter}) => {
useEffect(() => {
console.log("state change")
console.log(state);
}, [state])
return (
<div
className={`sm:relative inline-block ${style}`}
>
<OutsideAlerter onClickOutside={onClickOutside}>
<IconButton
type="button"
style={`relative text-2xl`}
onClick={onClick}
>
<IconContext.Provider value={{color: color}}>
<div>
{state === DROP_DOWN ?
<AiFillCloseCircle /> :
state === ACTIVE ?
<AiFillFilter /> :
<AiOutlineFilter />
}
</div>
</IconContext.Provider>
</IconButton>
{state === DROP_DOWN ?
<div className={`flex flex-col left-0 w-screen sm:w-32 max-h-40 overflow-auto ${dropLeft ? "sm:origin-top-left sm:left-[-2.5rem]" : "sm:origin-top-right sm:right-0"} absolute mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10`} role="menu" aria-orientation="vertical" aria-labelledby="menu-button">
<SearchBar label={"Search"} placeholder={"Search"} value={filter} onChange={setFilter} />
{children}
</div>
: null}
</OutsideAlerter>
</div>
);
For some reason whenever you click outside the component the tableFilterState gets set to
{
state: INACTIVE,
iconColor: "black",
filter: "",
filteredRows: [],
localActiveFilters: []
}
Which is not intentional, the tableFilterState should stay the same, only state should change. I can't figure this out so please help!!!
When you call useOutsideAlerter and pass onClickOutside handler it captures tableFilterState value and use it in a subsequent calls. This is a stale state. You could try this approach or use refs as described in docs:
const onClickOutside = () => {
setTableFilterState(tableFilterState => ({
...tableFilterState,
state: INACTIVE,
}));
}
I'm building a dynamic table using React-Table and i want to add a new row of editable cells.
At the moment i can add new row but only when i press the global edit button i can edit it, instead i want to add a row which would be editable at first.
This is my code -
Main component
function StyledTable() {
useEffect(() => {
dispatch(getData(mokeJsonData));
}, []);
const [datatoColumns] = useState(columnDataaa.slice(1));
const [skipPageReset, setSkipPageReset] = useState(false);
const data = useSelector((state) => state.dataReducer.data);
const dispatch = useDispatch();
const columns = useMemo(
() => [
{
Header: "",
id: "expander",
Cell2: ({ row }) => {
return (
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded ? "-" : "+"}
</span>
);
},
Cell: () => {
return <div></div>;
},
},
{
Header: columnDataaa[0].Header,
accessor: columnDataaa[0].accessor,
Cell: ({ value, row }) => {
return (
<FlexDiv>
<HighlightOffIcon
style={{ marginRight: "5px", color: "grey", width: "20px" }}
onClick={() => dispatch(deleteRow(row.index))}
/>
{value}
</FlexDiv>
);
},
},
...datatoColumns,
],
[]
);
useEffect(() => {
setSkipPageReset(false);
}, [data]);
const renderRowSubComponent = useCallback(
({ row }) => ({
values: row.original.addInfo && row.original.addInfo,
}),
[]
);
return (
<Styles>
<h1>הגדרת מנהל</h1>
<Table
columns={columns}
skipPageReset={skipPageReset}
renderRowSubComponent={renderRowSubComponent}
/>
</Styles>
);
}
export default StyledTable;
Editable Cell
const EditableCell = ({
value: initialValue,
row: { index },
column: { id, editable, type, width, valueOptions },
}) => {
const [value, setValue] = useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const dispatch = useDispatch();
const onBlur = () => {
if (value === "") return alert("requiredddd");
return dispatch(updateMyData({ index, id, value }));
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
if (type === "singleSelect")
return (
<InputSelect
value={value}
onChange={onChange}
onBlur={onBlur}
style={{ width: width }}
>
{valueOptions.map((item, i) => {
return (
<option value={item.label} key={i}>
{item.label}
</option>
);
})}
</InputSelect>
);
if (type === "date")
return (
<DatePicker
style={{ width: width }}
type="date"
disabled={editable === false}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
return (
<input
style={{ width: width }}
disabled={editable === false}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
};
export default EditableCell;
Add Row function
addRow: (state, action) => {
const obj = {};
action.payload.slice(1).forEach((item) => {
obj[item.accessor] = '';
});
if (
obj &&
Object.keys(obj).length === 0 &&
Object.getPrototypeOf(obj) === Object.prototype
)
return;
else {
state.data.splice(0, 0, obj);
state.originalData = state.data;
}
},
Thanks
Pass the state variable and method to the useTable() root hook. custom plugin hooks and other variables/methods maintaining the component state are returned from the table instance. These you can later retrieve from anywhere you want.
const {
// all your hooks...
} = useTable(
{
columns,
data,
// all your other hooks...
updateMyData,
// pass state variables so that we can access them in edit hook later
editableRowIndex, // index of the single row we want to edit
setEditableRowIndex // setState hook for toggling edit on/off switch
},
// other hooks...
(hooks) => {
hooks.allColumns.push((columns) => [
// other hooks such as selection hook
...columns,
// edit hook
{
accessor: "edit",
id: "edit",
Header: "edit",
Cell: ({ row, setEditableRowIndex, editableRowIndex }) => (
<button
className="action-button"
onClick={() => {
const currentIndex = row.index;
if (editableRowIndex !== currentIndex) {
// row requested for edit access
setEditableRowIndex(currentIndex);
} else {
// request for saving the updated row
setEditableRowIndex(null); // keep the row closed for edit after we finish updating it
const updatedRow = row.values;
console.log("updated row values:");
console.log(updatedRow);
// call your updateRow API
}
}}
>
{/* single action button supporting 2 modes */}
{editableRowIndex !== row.index ? "Edit" : "Save"}
</button>
)
}
]);
}
);
you can found example from bellow link
github repo link: https://github.com/smmziaul/only-one-row-editable
code sandbox link: https://codesandbox.io/s/github/smmziaul/only-one-row-editable
I have made a search field with debouncing. Everything works fine but when I try to empty the search field with backspace it continuously re-show all characters and does not remove them(the first character is always there).
you can see it in the attached gif
my parent component
class ParentComponent extends React.Component {
this.queryParam = {
keyword: ''
}
keywordSearch = value => {
const {
history: { push },
match: { url },
location: { search },
} = this.props;
queryParams = {...queryParams, keyword: value, };
push(`${url}?${queryString})
};
render() {
<SearchComponent
value={this.queryParams.keyword}
onUpdate={this.keywordSearch}
/>
}
}
my search field component
const SearchComponent = ({ value, onUpdate }) => {
const [fieldValue, setFieldValue] = useState(value);
const handleChange = ({ target: { value } }) => {
debounceFunc(() => {
onUpdate(value);
}, 300);
setFieldValue(value);
};
return (
<Input
value={fieldValue || value}
disableUnderline
onChange={handleChange}
className={classes.root}
placeholder='Search'
startAdornment={
<InputAdornment position="start">
<Search className={classes.icon} fontSize="small" />
</InputAdornment>
}
/>
}
here is my custom debounce component
export const debounceFunction = () => {
let timeOut = null;
return (callBack, wait) => {
if (timeOut) clearTimeout(timeOut);
timeOut = setTimeout(() => {
callBack();
}, wait);
};
};
export const debounceFunc = debounceFunction();
the problem is in this debounce function. can anyone help me in this regard? why it isn't removing the first character?
Thanks
First problem is <Input value={fieldValue || value}
=> use just the local state:
<Input value={fieldValue} to change the visible value immediatelly.
Second problem is this.queryParams.keyword being an instance property, not a React State
=> use this.state.... and this.setState(...) (or Hooks) to update debounced state in the parent
I have changed debounceFunction parameter and the way to use it. Can you give it a try
const SearchComponent = ({ value, onUpdate }) => {
const [fieldValue, setFieldValue] = useState(value);
const debouncedUpdate = debounceFunction(onUpdate, 300);
const handleChange = ({ target: { value } }) => {
debouncedUpdate(value);
setFieldValue(value);
};
return (
<Input
value={fieldValue || value}
disableUnderline
onChange={handleChange}
className={classes.root}
placeholder='Search'
startAdornment={
<InputAdornment position="start">
<Search className={classes.icon} fontSize="small" />
</InputAdornment>
}
/>
}
export const debounceFunction = (callBack, wait) => {
let timeOut = null;
return () => {
if (timeOut) clearTimeout(timeOut);
timeOut = setTimeout(() => {
callBack();
}, wait);
};
};
export const debounceFunc = debounceFunction();