I have a config file with some fields for generating input elements inside a component.
I'm trying to generate an AsyncSelect input field and assigning it loadOptions prop through the config. Problem is that the function never gets called.
Here's the object in the configuration for generating the AsyncSelect input:
{
key: 'cityCode',
value: (customer, borrower) => ({
label: borrower.cityName,
value: borrower.cityCode
}),
text: 'city',
disabled: falseFn,
input: REACT_ASYNC_SELECT_INPUT,
props: (borrower, component) => ({
inputValue: component.state.cityName,
loadOptions: () => {
return CitiesService.find(component.state.cityName || '')
.then(records => records.map(record => ({
label: record.name,
value: record.code
})));
},
onInputChange: cityName => {
component.setState({
cityName
});
},
onChange: ({label, value}, {action}) => {
if (action === 'clear') {
component.updateCustomer(component.props.fieldKey + 'cityName', '');
component.updateCustomer(component.props.fieldKey + 'cityCode', -1);
} else {
component.updateCustomer(component.props.fieldKey + 'cityName', label);
component.updateCustomer(component.props.fieldKey + 'cityCode', value);
}
}
}),
render: trueFn
},
Here's the part of the component render utilizing the config file to render different inputs:
<div className="values">
{
BorrowerInfoConfig().map(field => {
if (field.render(borrower)) {
const kebabCaseKey = _.kebabCase(field.key);
const fieldElement = React.cloneElement(field.input, {
className: `${kebabCaseKey}-input`,
value: field.value ? field.value(customer, borrower) : _.get(borrower, field.key),
onChange: e => {
let value = e.target.value;
if (field.options) {
value = Number(value);
}
this.updateCustomer(fieldKey + field.key, value);
},
disabled: field.disabled(borrower),
...field.props(borrower, this)
}, field.options ? Object.keys(field.options).map(option => <option
key={option}
className={`${kebabCaseKey}-option`}
value={option}>
{field.options[option]}
</option>) : null);
return <div key={field.key} className={`value ${kebabCaseKey}`}>
<span>{field.text}</span>
{fieldElement}
</div>;
}
return null;
})
}
</div>
As you can see I use React.cloneElement to clone the input from the config file and assign new properties to it depends on what I get from the config file, in this case 4 custom props:
inputValue
loadOptions
onInputChange
onChange
My problem is that loadOptions is never called, any ideas why? On the other hand inputValue is assign correctly to the cityName state of the component.
My Problem was that REACT_ASYNC_SELECT_INPUT references normal Select and not Select.Async.
Related
I have a list of objects. I want to make an api call once the location field of the object is changed. So for that, I have a useEffect that has id, index and location as its dependencies. I have set a null check for the location, if the location isn't empty, I want to make the api call. But the thing is, the api is being called even when the location is empty, and I end up getting a 400. How can I fix this and make the call once location isn't empty?
const [plants, setPlants] = useState([
{
name: 'Plant 1',
id: uuidv4(),
location: '',
coords: {},
country: '',
isOffshore: false,
}
]);
const [locationIDObject, setlocationIDObject] = useState({
id: plants[0].id,
index: 0
});
const handlePlantLocChange = (id, index, value) => {
setPlants(
plants.map(plant =>
plant.id === id
? {...plant, location: value}
: plant
)
)
setlocationIDObject({
id: id,
index: index
})
}
const getCoords = (id, index) => {
axios.get('http://localhost:3002/api/coords', {
params: {
locAddress: plants[index].location
}
}).then((response) => {
if(response.status === 200) {
handlePlantInfoChange(id, PlantInfo.COORD, response.data)
}
})
}
const handler = useCallback(debounce(getCoords, 5000), []);
useDeepCompareEffect(() => {
if(plants[locationIDObject.index].location !== '')
handler(locationIDObject.id, locationIDObject.index);
}, [ locationIDObject, plants[locationIDObject.index].location])
return (
<div className='plant-inps-wrapper'>
{
plants.map((item, idx) => {
return (
<div key={item.id} className="org-input-wrapper">
<input placeholder={`${item.isOffshore ? 'Offshore' : 'Plant ' + (idx+1) + ' location'}`} onChange={(e) => handlePlantLocChange(item.id, idx, e.target.value)} value={item.isOffshore ? 'Offshore' : item.location} className="org-input smaller-input"/>
</div>
)
})
}
</div>
)
I think your useCallback is not updating when value of your variables is changing and that is the issue:
Although the check is correct, but the call is made for older values of the variable. You should update the dependencies of your useCallback:
console.log(plants) inside getCoords might help you investigate.
Try this:
const handler = useCallback(debounce(getCoords, 5000), [plants]);
So it turns out the issue was with my debouncing function. I don't know what exactly the issue was, but everything worked as expected when I replaced the debouncing function with this:
useEffect(() => {
console.log("it changing")
const delayDebounceFn = setTimeout(() => {
getCoords(locationIDObject.id, locationIDObject.index)
}, 4000)
return () => clearTimeout(delayDebounceFn)
},[...plants.map(item => item.location), locationIDObject.id, locationIDObject.index])
I am wanting to use the "Cascader" component of "Ant Design" but I am having trouble filling it with data. This is my code, which I am doing wrong, sorry I am still a newbie and I need your support please.
function CascaderEmpCliUn(props) {
const optionLists = { a: []}
const [state, setState] = useState(optionLists);
useEffect(() => {
async function asyncFunction(){
const empresas = await props.loginReducer.data.empresas;
const options = [
empresas.map(empresa => ({
value: empresa.id,
label: empresa.siglas,
children: [
empresa.cli_perm.map(cliente => ({
value: cliente.id,
label: cliente.siglas,
children: [
cliente.uunn_perm.map(un => ({
value: un.id,
label: un.nombre,
}))
]
}))
]})
)
];
setState({a : options})
}
asyncFunction();
}, [])
return (
<Cascader options={state.a} placeholder="Please select" />
)
}
ERROR
Not found value in options
I was able to reproduce your error with dummy data whenever I had an empty array of children at any level. I'm not sure why this should be a problem, but it is. So you need to modify your mapping function to check the length of the child arrays. It seems to be fine if passing undefined instead of an empty array if there are no children.
General Suggestions
You don't need to store the options in component state when you are getting them from redux. It can just be a derived variable. You can use useMemo to prevent unnecessary recalculation.
You are passing the entire loginReducer state in your props which is not ideal because it could cause useless re-renders if values change that you aren't actually using. So you want to minimize the amount of data that you select from redux. Just select the empresas.
Revised Code
function CascaderEmpCliUn() {
// you could do this with connect instead
const empresas = useSelector(
(state) => state.loginReducer.data?.empresas || []
);
// mapping the data to options
const options = React.useMemo(() => {
return empresas.map((empresa) => ({
value: empresa.id,
label: empresa.siglas,
children:
empresa.cli_perm.length === 0
? undefined
: empresa.cli_perm.map((cliente) => ({
value: cliente.id,
label: cliente.siglas,
children:
cliente.uunn_perm.length === 0
? undefined
: cliente.uunn_perm.map((un) => ({
value: un.id,
label: un.nombre
}))
}))
}));
}, [empresas]);
return <Cascader options={options} placeholder="Please select" />;
}
The final code of "options" object:
const options = useMemo(() => {
return empresas.map((empresa) => ({
value: empresa.id,
label: empresa.siglas,
children:
empresa.cli_perm.length === 0
? console.log("undefined")
:
empresa.cli_perm.map((cliente) => ({
value: cliente.id,
label: cliente.siglas,
children:
cliente.uunn_perm.length === 0
? console.log("undefined")
:
cliente.uunn_perm.map((un) => ({
value: un.id,
label: un.nombre
}))
}))
}));
}, [empresas]);
I have state in React functional component. It is and array of objects. Every object in that collection has property "selected", which is a boolean. That array looks like this:
const [filterOptions, setFilterOptions] = useState([
{
title: 'highest',
selected: true,
},
{
title: 'lowest',
selected: false,
},
]);
After handleFilter func is executed I need to set state so this array has same title properties but reverse (toggle) selected properties.
This is handleFilter func in which I need to toggle every selected property of array objects:
const handleFilter = () => {
setFilterOptions();
};
function App() {
const [filterOptions, setFilterOptions] = useState([
{
title: 'highest',
selected: true,
},
{
title: 'lowest',
selected: false,
},
]);
const handleFilter = (e) => {
let newArr = [...filterOptions];
let value = e.target.value;
if (value === "lowest") {
newArr[0].selected = true;
newArr[1].selected = false;
} else if (value === "highest") {
newArr[0].selected = false;
newArr[1].selected = true;
}
setFilterOptions(newArr)
};
return (
<div>
<select onChange={handleFilter}>
<option value="lowest">a</option>
<option value="highest">b</option>
</select>
{console.log((filterOptions))}
</div>
);
}
please check hope it will work
var arryObj =[
{
title: 'highest',
selected: true,
},
{
title: 'lowest',
selected: false,
},
]
const handleFilter = (index,value) => {
arryObj[index].selected = value
};
handleFilter(0,false)
console.log(arryObj)
handleFilter(1,true)
console.log(arryObj)
You can pass a function into setFilterOptions to change the state based on the previous state.
const handleFilter = () => {
setFilterOptions(prevState =>
prevState.map(obj => ({...obj, selected: !obj.selected}))
);
};
I have a group of 3 checkboxes and the main checkbox for checking those 3 checkboxes.
When I select all 3 checkboxes I want for main checkbox to become checked.
When I check those 3 checkboxes nothing happens but when I then uncheck one of those trees the main checkbox becomes checked.
Can someone explain to me what actually is happening behind the scenes and help me somehow to solve this mystery of React state? Thanks!
Here is a code snnipet:
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange = id => {
const data = [...this.state.data];
data.forEach(item => {
if (item.id === id) {
item.checked = !item.checked;
}
})
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
this.setState({ data: data, main: everyCheckBoxIsTrue });
}
onMainCheckBoxChange = () => {
let data = [...this.state.data];
data.forEach(item => {
!this.state.main ? item.checked = true : item.checked = false
})
this.setState({
this.state.main: !this.state.main,
this.state.data: data,
});
}
render () {
const checkbox = this.state.data.map(item => (
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
))
}
return (
<input type="checkbox" name="main" checked={this.state.main} onChange={this.onMainCheckBoxChange} />
{checkbox}
)
I can't make a working code snippet based on the code you provided, one of the issues was:
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
where checkbox is not defined.
However, I think you confused about using the old state vs the new state, it'd be simpler to differentiate if you name it clearly, e.g.:
eventHandler() {
const { data } = this.state; // old state
const newData = data.map(each => ...); // new object, soon-to-be new state
this.setState({ data }); // update state
}
Here's a working example for your reference:
class App extends React.Component {
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange(id) {
const { data } = this.state;
const newData = data.map(each => {
if (each.id === id) {
// Toggle the previous checked value
return Object.assign({}, each, { checked: !each.checked });
}
return each;
});
this.setState({
data: newData,
// Check if every checked box is checked
main: newData.every(item => item.checked === true),
});
}
onMainCheckBoxChange() {
const { main, data } = this.state;
// Toggle the previous main value
const newValue = !main;
this.setState({
data: data.map(each => Object.assign({}, each, { checked: newValue })),
main: newValue,
});
}
render () {
const { data, main } = this.state;
return (
<div>
<label>Main</label>
<input
type="checkbox"
name="main"
// TODO this should be automatically checked instead of assigning to the state
checked={main}
onChange={() => this.onMainCheckBoxChange()}
/>
{
data.map(item => (
<div>
<label>{item.id}</label>
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
</div>
))
}
</div>
);
}
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Side note: You might want to consider not to use the main state
You shouldn't be storing state.main to determine whether every checkbox is checked.
You are already storing state that determines if all checkboxes are checked, because all checkboxes must be checked if every object in state.data has checked: true.
You can simply render the main checkbox like this:
<input
type="checkbox"
name="main"
checked={this.state.data.every(v => v.checked)}
onChange={this.onMainCheckBoxChange}
/>;
The line this.state.data.every(v => v.checked) will return true if all of the checkboxes are checked.
And when the main checkbox is toggled, the function can look like this:
onMainCheckBoxChange = () => {
this.setState(prev => {
// If all are checked, then we want to uncheck all checkboxes
if (this.state.data.every(v => v.checked)) {
return {
data: prev.data.map(v => ({ ...v, checked: false })),
};
}
// Else some checkboxes must be unchecked, so we check them all
return {
data: prev.data.map(v => ({ ...v, checked: true })),
};
});
};
It is good practice to only store state that you NEED to store. Any state that can be calculated from other state (for example, "are all checkboxes checked?") should be calculated inside the render function. See here where it says:
What Shouldn’t Go in State? ... Computed data: Don't worry about precomputing values based on state — it's easier to ensure that your UI is consistent if you do all computation within render(). For example, if you have an array of list items in state and you want to render the count as a string, simply render this.state.listItems.length + ' list items' in your render() method rather than storing it on state.
In the following code everything works except for the update of the value in the onChange method.
The expected way it should work is:
initial value is an empty string (✓ works)
when a change is made the value should be the value of the change (✗ does not work)
const mockSetFieldValue = jest.fn(() => '');
beforeAll(async () => {
field = {
name: 'password',
value: mockSetFieldValue(),
// ^^^ initial value is picked up, but not the change in onChange
onChange: (e) => {
console.log(e.target.value) // returns: foo123
mockSetFieldValue.mockReturnValue(() => e.target.value);
// ^^^ this does not update the value
},
};
tree = (
<>
<label htmlFor="password">Password</label>
<MyField field={field} />
</>
);
});
it('input accepts a value', () => {
const { getByLabelText } = render(tree);
const input = getByLabelText(/Password/i);
fireEvent.change(input, { target: { value: 'foo123' } });
expect(input.value).toBe('foo123');
});
How would it be possible to update the onChange method to change the value that is set in my component?
I've tried mockImplementationOnce and mockReturnValue. But they don't seem to work that way.
field.value is set once before all your tests using the current value of the mockSetFieldValue function, which returns the empty string. Changing the mockSetFieldValue function therefore has no effect.
You need to be able to set the field.value in onChange, which you could do like this:
onChange: (e) => {
field.value = e.target.value;
},
It is probably better to mock out the whole onChange function:
const mockOnChange= jest.fn();
beforeAll(async () => {
field = {
name: 'password',
value: '',
onChange: mockOnChange,
};
//...
});
Then in the test:
mockOnChange.mockImplementationOnce((e) => { field.value = e.target.value });
fireEvent.change(input, { target: { value: 'foo123' } });