ReactJS state value not update correctly on input onChange - javascript

In my app, i have Groups, and i have a select input, to change between group lists. My issue is when i change the select input, the state changes the name of the list, but is not the one im selecting. For example, at initial i have:
All Groups
Group #1
Group #2
When i choose Group #1, the state in the console says "All Groups". If i choose Group #2, the state in the console says "Group #1"
Select Input
<select id="selectedPg" name="selectedPg" onChange={event=> {
this.valueToState(event.target)
this.viewProfileGroup();
}}>
viewProfileGroup()
// View Selected Profile Group
viewProfileGroup = ()=> {
const { selectedPg, allProfilesLists } = this.state
this.setState({
profilesLists: allProfilesLists.filter(pg => pg.profileGroup === selectedPg)
})
console.log(this.state.selectedPg)
}

The issue is the setState is not synchronous, so when you call this.viewProfileGroup() and in the method, you are not operating on the latest state.
The solution is simple.
Just pass event.target to the viewProfileGroup function
this.valueToState(event.target)
this.viewProfileGroup(event.target);
PS, maybe in this way you will do not need this.valueToState at all.

Related

React-table: State doesn't change as expected, and table doesn't update

Codesandbox example: https://codesandbox.io/s/react-table-state-not-updating-hmquq?file=/src/App.js
I am using the react-table package (version 7.1.0).
I have a table which displays some invoices like so:
The user should be able to select some or all of these items using the selected checkbox.
The selected field is not part of the data. However, when the user hits a selected checkbox, the field should toggle and an array storing document numbers should be populated.
To store the document numbers, I have a stateful getter and setter:
const [selectedInvoiceIds, setSelectedInvoiceIds] = useState([]);
To populate the field, I am attempting to simply add the document number to the array immutably, from the onChange of the checkbox:
{
Header: "Selected",
accessor: "selected",
Cell: item => {
const { documentNumber } = item.row.values;
return (
<input
type="checkbox"
checked={selectedInvoiceIds.includes(documentNumber)}
onChange={() => {
setSelectedInvoiceIds([...selectedInvoiceIds, documentNumber]);
}}
/>
);
}
}
When a checkbox is clicked for the first time, the selectedInvoiceIds becomes populated:
selectedInvoiceIds: ["706942"]
The problems are:
The table does not update to reflect the state change, despite this prop on the checkbox:
checked={selectedInvoiceIds.includes(documentNumber)}
The selectedInvoiceIds value gets overwritten when another document number is added, instead of being added to, as if the state is re-initialising to [] somewhere in between.
Can it be explained why these state issues are occurring and how to get around it?
I am aware of the useTableState value exposed by react-table, but I don't know how I can apply it to this use case.
Codesandbox example: https://codesandbox.io/s/react-table-state-not-updating-hmquq?file=/src/App.js
There are multiple issues in this code:
// 1) updates to state should should use callback function if it uses prv state
return (
<input
type="checkbox"
checked={selectedInvoiceIds.includes(documentNumber)}
onChange={() => {
setSelectedInvoiceIds(prvSelectedInovicesId => [...prvSelectedInovicesId, documentNumber]);
}}
/>
);
// 2) also columns is using useMemo, however not providing dependencies, so columns are not being updated when the selectedInvociesIds state gets updated
// 3) you are not yet handling the toggle, this mean you are just always just adding to the array and not removing from it
here is a working version code sandbox
https://codesandbox.io/s/react-table-state-not-updating-wkri3?file=/src/App.js
Because of useMemo, add your array to last parameter
const columns = React.useMemo(
// ....
,
[selectedInvoiceIds]
);
And as we need toggle checkboxed it is more reasonable to keep selected ids in object instead of array and update it as this
setSelectedInvoices({
...selectedInovicesIds,
documentNumber: !selectedInovicesIds[documentNumber]}
)
so it will toggle mark

Filter state doesn't work correctly in React

My Parent component retrieves the checked inputs value from its child component.
So I have a function (handleChecked) that takes two parameters : one 'id' and one checked status 'isChecked' :
const handleChecked = useCallback(({ id, isChecked }) => {
const newCheckedValues = checkedValues.filter(current => current !== id);
setCheckedValues(newCheckedValues);
if (isChecked) {
newCheckedValues.push(id);
setCheckedValues(newCheckedValues);
}
}, [checkedValues]);
What my function is supposed to do :
1 - Get id and checked status from the clicked input,
2 - Check the state for duplicate id's,
3 - If present, remove it,
4 - Save the state,
5 - If checked, store the id in a temporary array,
6 - Save the new state.
What it does :
Well, all the task above except adding the value to the new state.
When I click a new input, the parent "checkedValues" state is empty and starts over from nothing.
Meaning that the temporary array created as the result of the filtered state, is also empty.
Right now, this function just adds an id in the state, then replace it by the new clicked input.
And I need to gather all the checked values and store them in the state before sending that to an api later. I manage to present my "expectations" in this sandbox :
https://codesandbox.io/s/react-hooks-filter-state-array-toggle-input-checked-gz504
It works, the only difference is it's in the same component and uses html form event and not props legacy.
Finally, I deconstructed my component in 3 :
A Parent component with the checkedValues state which calls a "ListItem" child, passing datas and states as props ;
A ListItem component which contains the handleCheck function and map the datas to display, passing the infos and the function to each mapped child.
An Item component which displays an input with its datas and the handleCheck function + deals with its own isChecked state to modify CSS dynamically according to its status.
This answer is of course customized to my own purposes and needs. It's not the most general but it answers my question and can maybe help someone. Thanks again #ChrisG for your help

With react-select, select multiple items matching search simultaneously

I am using react-select to display a searchable drop-down list of items, of which the user can select multiple items. My list is quite long and the user will often wish to multi-select many items which match the same filter string, which is a bit of a tedious procedure because each time you select an item the dropdown disappears and you need to re-type the search.
For example, the following sandbox has a react-select which lists lots of apples and cheeses. In order to select all the Apples, one would have to keep typing "Apple" and choosing one apple at a time.
https://codesandbox.io/s/2l99lry5p
Coming from desktop UI background, I naturally want to be able to type a search query and press Ctrl-A to select all of the matching search results and add them to my list, or Ctrl-Click to cherry pick multiple items from the matching set. But as far as I can tell there's no support for any hotkey like this in react-select.
Does the react-select API have any way that I can implement a "select all" hotkey which would select everything that matches the current search filter (or even an explicit "select all matches" button on the page would be fine)? I cannot see any programmatic way to get access to the set of objects which match the filter. Is this something that I would need to fork react-select to implement or is it possible to do this via the existing API somehow?
React Select has built-in props that can be used to prevent the menu from closing on select and persist the search string.
First prevent the menu from closing on select by setting closeMenuOnSelect to false.
Then onInputChange store the search string in state if the action equals 'input-change'.
Setting inputValue to this.state.value will persist the search string in the input field.
class Foo extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
}
handleInputChange = (value, e) => {
if (e.action === 'input-change') {
this.setState({value});
}
}
render() {
return (
<Select
isMulti
name="colors"
options={options.map(x => MakeOption(x))}
className="basic-multi-select"
classNamePrefix="select"
/* added these props */
closeMenuOnSelect={false}
onInputChange={this.handleInputChange}
inputValue={this.state.value}
/>
)
}
}
Updated sandbox: https://codesandbox.io/s/yvmzx6pn6z
I hacked up something that kind of does what I want but is quite ugly:
https://codesandbox.io/s/j7453qrmv
To use:
Try searching "apple", then press "Add all matching items to selection"
The approach:
As #wdm mentioned, there's a onInputChanged you can hook in to.
In onInputChanged, get the matching items store them in the state of the component
I add a button near the Select which allows the user to choose to copy the matching set of items into another state variable chosenItems
The react-select Select component has a value property that you can provide to programmatically choose the items. I pass state.chosenItems in to this.
This works but there were many things that make this a pain:
The onInputChanged handler gets called before the items matching the filter appear. I attempted to grab the matching items by DOM queries but it did not work because onInputChanged is too early. So rather than relying on react-select's filtering logic, I'm replicating the filtering logic in the onInputChanged handler. This is not great as there could be a discrepancy between my filtering logic and the displayed list of matching items.
Whenever you click after typing a search, the react-select clears the search, which invokes the onInputChanged event again. So by clicking on the custom "Add All Matching Items" button, it removes the filter, clearing the list, invoking onInputChanged and setting the state with a new list of matching items. To deal with this I needed to have a previousMatchingOptions state variable which keeps track of the matching items from the previous call to onInputChanged. This seems like a terrible hack.
Likewise, I attempted to hide/show my "Select All Matching Items" button based on whether there were currently more than one item that matches the search, but I was similarly thwarted by timing issues. I attempted to hack around this but kept getting caught up in corner cases.
The UI I came up with for "Select All Matching Items" doesn't feel integrated with the react-select very well. It would be nicer if it was part of their component rather than beside it.
By using the values property of the Select component, you are bypassing the component's internal management of its state, so the normal way of adding, removing, and clearing items does not work without reimplementing all that in a custom onChange handler which modifies the state.chosen which is passed to values. Managing this myself seems also less than desirable.
So, I have a solution, but if someone has something has a suggestion that is much cleaner and/or simpler I would be happy to hear it!
It seems like forking the control and doing these changes internal to the component might be the better way to go.
In my onInputChanged I attempted to get the matching search results directly from the DOM using some getElementsByClassName hackery, though this approach did not work because the onInputChanged
A very simple way of implementing a "Select All" option is overriding React-Select component with a custom component. For that you first need to import it as
import { default as ReactSelect } from 'react-select';
then create a custom component which defines a new Property named "allowSelectAll", and selects all the options when this property is set to 'true'.
const Select = props => {
if (props.allowSelectAll) {
if (props.value && (props.value.length === props.options.length)) {
return (
<ReactSelect
{...props}
value={[props.allOption]}
onChange={selected => props.onChange(selected.slice(1))}
/>
);
}
return (
<ReactSelect
{...props}
options={[props.allOption, ...props.options]}
onChange={selected => {
if (
selected.length > 0 &&
selected[selected.length - 1].value === props.allOption.value
) {
return props.onChange(props.options);
}
return props.onChange(selected);
}}
/>
);
}
return <ReactSelect {...props} />;
};
Select.propTypes = {
options: PropTypes.array,
value: PropTypes.any,
onChange: PropTypes.func,
allowSelectAll: PropTypes.bool,
allOption: PropTypes.shape({
label: PropTypes.string,
value: PropTypes.string
})
};
Select.defaultProps = {
allOption: {
label: "Select all",
value: "*"
}
};
Note: You can simply copy and paste the above given code and it will work absolutely fine.
And once that is done you can simply use the new 'Select' component with 'allowSelectAll' property set to true.
<Select allowSelectAll={true} isMulti={true} isSearchable={true} options={options} />
You can use the filterOption function like this:
<select
options={[{label: 'test', value: 1, customData: 'bla blub test'}]}
filterOption={(option, filter) => {
const { customData } = option.customData;
if(customData.toLowerCase().indexOf(filter.toLowerCase()) >= 0) {
return true;
}
}} />
Hope this will help you :)

ReactJS - ReactiveSearch - Custom event on suggestion selection

I am building an autocomplete component to search current users and add them to a team. The search works fine except I need to customize a couple things that I can't figure out how to do.
First, this component is modeled after GitHub's invite user modal. You can start typing to search for a current user and it populates any results it finds. However, if it doesn't find any results it only shows one item to invite that user via email. How can I edit the full state of the result list for a DataSearch component? All I can see is to edit the content of each suggestion via the onSuggestion prop. I need to be able to say, "If there are zero results, display this."
Second, when a suggestion from the autocomplete result list is selected, I need to be able to reset the search box because I am populating a list that the user can see. Right now, the search box is populated with the value of the suggestion. So, I am populating the list below of selected results just fine; but, I still need to be able to reset the search box when that result is selected.
Help????
CodeSandbox link
For the first part of the problem, you can use the prop onNoResults on any of the results components to render custom JSX when no results are found. From the docs:
onNoResults String or JSX [optional]
show custom message or component when no results founds.
<ResultList
...
// custom JSX when no results are found
onNoResults={
<section>
<input />
<button>Add</button>
</section>
}
/>
For the second part of the problem, there are two ways IMO you may approach this.
Using a ReactiveComponent which lets you create a custom ReactiveSearch component.
Using customQuery
I'll explain how to approach this using customQuery but it might be better to create a custom component depending on which approach suits your needs best.
In the example I've shared my DataSearch looks like this:
<DataSearch
title="DataSearch"
dataField={["original_title", "original_title.search"]}
categoryField="authors.raw"
componentId="BookSensor"
customQuery={this.customQuery}
defaultSelected={this.state.value}
onValueSelected={this.updateState}
/>
The reason for using a customQuery is to get full control of which query gets applied to retrieve the results. ReactiveSearch is designed to work reactively. When a value is set into the DataSearch, the ResultList would react to this change. Having a customQuery lets us control which query is fired for this change. Also I'm keeping the value of DataSearch in the state so I can clear it up when the query gets fired. Here's what I'm using in the example:
customQuery = (value, props) => {
// find the current query using defaultQuery from DataSearch
const currentQuery = DataSearch.defaultQuery(value, props);
// clear the value in component's state
this.setState({
value: ""
});
if (value.length) {
// store this query
this.prevQuery = currentQuery;
return currentQuery;
}
// if the value is empty, that is it was cleared, return the previous query instead
return this.prevQuery;
};
So whenever the value is cleared, I just return the previous query so the result list shows the correct results for the previous query (before the value was cleared).
Also in order to control the value of DataSearch from my component I'm using the defaultSelected and onValueSelected props. They work quite similar to how you would create a controlled component in react. Docs
Again, it might be better to create a custom component using ReactiveComponent if this approach sounds complicated to customize this flow.
Demo for this answer using customQuery

How does react component pass value to the previous page component

I have two component, FormComponent and OrderListComponent. There is one field on the FormComponent to ask users to select a value from a list which is from OrderListComponent. So I put a button on the field in FormComponent, when users click on that button, I use router.push('XXXX') to navigate the page to OrderListComponent to show all the options for user. When users click one of the options on OrderListComponent, I use router.goBack() to go back to FormComponent. Here is the problem, how does FormComponent get the selected value from OrderListComponent? I know that I can save the selection as a state in redux store, but I wander whether there is a more direct way to solve this issue.
The best way is to have state manager. Redux state will solve it elegantly. Though as Random User mentioned in his answer; localStorage/sessionStorage can be used to do this.
But passing the values by URL will be tricky when there are too many items selected for Order.
The state can be changed so that having anything in state will give consistent output at OrderListCompoennt.
You can have order state and push the items to that state. And then redirect the user to OrderListComponent rather than doing router.push('XXXX').
OrderListComponent will watch the state and show the items accordingly.
The FormComponent will also watch the order state for marking the currently added items. With this way, there will not be any router.goBack(); just the redirects by router.push
And when your app will be at FormComponent, order state will automatically be responsible to mark the items which are currently in order.
Storing the selected value in redux store or component state itself would be better choice for this, but you can also
store it in localStorage
store it in sessionStorage
when redirecting the user back, pass it as a url parameter
If you're using any one of these methods, In FormComponent you can check if the value exists and perform operations accordingly.
Example code
OrderListComponent
let handleChange = (value) => {
localStorage.orderListValue = value
}
This will replace any existing value as well.., Use your brain here.
FormComponent
if ( localStorage.orderListValue ) {
console.log('Yay.. we have data');
} else {
// ask the user to select the List first.
}

Categories

Resources