React-select is going blank when the options array changes (codesandbox included) - javascript

I am trying to use react-select in combination with match-sorter as described in this stackoverflow answer (their working version). I have an initial array of objects that get mapped to an array of objects with the value and label properties required by react-select, which is stored in state. That array is passed directly to react-select, and when you first click the search box everything looks good, all the options are there. The onInputChange prop is given a call to matchSorter, which in turn is given the array, the new input value, and the key the objects should be sorted on. In my project, and reproduced in the sandbox, as soon as you type anything into the input field, all the options disappear and are replaced by the no options message. If you click out of the box and back into it, the sorted options show up the way they should. See my sandbox for the issue, and here's the sandbox code:
import "./styles.css";
import { matchSorter } from "match-sorter";
import { useState } from "react";
import Select from "react-select";
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: <div>{obj.name}</div>,
name: obj.name
};
};
export default function App() {
const [options, setOptions] = useState(objs.map((obj) => myMapper(obj)));
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(matchSorter(options, val, { keys: ["name", "value"] }));
}}
/>
);
}
I am sure that the array in state is not getting removed or anything, I've console logged each step of the way and the array is definitely getting properly sorted by match-sorter. It's just that as soon as you type anything, react-select stops rendering any options until you click out and back in again. Does it have something to do with using JSX as the label value? I'm doing that in my project in order to display an image along with the options.

I had to do two things to make your code work:
Replaced label: <div>{obj.name}</div> with label: obj.name in your mapper function.
I am not sure if react-select allows html nodes as labels. Their documentation just defines it as type OptionType = { [string]: any } which is way too generic for anything.
The list supplied to matchSorter for matching must be the full list (with all options). You were supplying the filtered list of previous match (from component's state).
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: obj.name, // -------------------- (1)
name: obj.name
};
};
const allOptions = objs.map((obj) => myMapper(obj));
export default function App() {
const [options, setOptions] = useState(allOptions);
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(
matchSorter(
allOptions, // ----------------> (2)
val,
{ keys: ["name", "value"]
}
));
}}
/>
);
}

Related

React Native and Typescript - Need an approach to create a flexible table component

I'm trying to create a table component that can display its cells flexibly. Every cells on a column displays the same cell component. The table component take an array of object as initial data and an array of column properties. Table should looks like this:
interface MyTableProps {
columns: ColumnProps[];
initialData: Array<any>; // Should array of object
callbackTableDataChanged: (newData) => void; // being used to send modified table data back to MyTable's father components.
}
interface ColumnProps {
label: string;
objectPropertyName: string; // (*)
renderCell: "cell-type-1" | "cell-type-2" | "cell-type-3" | ... // (**)
}
const MyTable: React.FC<MyTableProps> = tableProps => {
// The table should have its own a copy of initial data, because of many reasons
const [tableData, setTableData] = useState(tableProps.initialData);
const handleDataChange = (thePropertyName: string, rowIndex: number, newValue: any) => {
// This function changes the table data array,
// whenever the cell component (MyCellComp1, MyCellComp2, ...) on a column make a change of value.
// For example: MyCellComp1 is a time picker, whenever a new timestamp selected,
// it will send new time value through a callback, to its father component (aka MyTable).
// Then, this func will do the changes to the corresponding "place" in the data array object of this MyTable.
}
const renderRows = (column: ColumnProps, rowIndex: number) => {
switch (column.renderCell) {
case cell-type-1: return <CellComp1 ... onDataChanged={cellData => handleDataChange(column.objectPropertyName, rowIndex, cellData)}/>
case cell-type-2: return <CellComp2 ... onDataChanged={cellData => handleDataChange(column.objectPropertyName, rowIndex, cellData)}/>
...
}
}
return (
{renderColumnLabels}
<Flatlist data={tableData} renderItem={({col, index}) => renderRows(col, index)}/>
)
}
(*): "objectPropertyName" is a string that has value which is a name of one of the properties of initial object data. Forgive my awkward grammar!
(**): I use "renderCell" to tell the table what it should render at specific column. All cells on a column have the same component type.
The table should be used like this:
const initialData = [
{id: "123", name: "Tom"}, {id: "456", name: "Jerry"}, ...
]
const columns: ColumnProps[] = [
{label: "The ID", objectPropertyName: "id", renderCell: "cell-type-1"},
{label: "The name", objectPropertyName: "name", renderCell: "cell-type-2"},
...
];
<MyTable columns={columns} initialData={initialData} ... />
// This is how table looks like:
// column 1 column 2
// labels row: The ID The name
// row 1 : <CellComp1 /> <CellComp2 />
// row 2 : <CellComp1 /> <CellComp2 />
// ...
The problem is that with above approach, with the way I declared how cells on a column should be rendered, it limits the variety of cell component that MyTable could display. I declare 3 values for ColumnProps.rendercell, columns can display only 3 kinds of cell components.
My solution is that ColumnProps will have a new props which is a function that return a component:
interface ColumnProps {
label: string;
objectPropertyName: string; // (*)
renderCell: "cell-type-1" | "cell-type-2" | "cell-type-3" | ... // (**)
renderCellComponent?: () => JSX.Element
}
The new "renderCellComponent" prop will replace default cell components with whatever it returns.
However, with is method, I cant change the data array of MyTable with its function "handleDataChange".
For example:
const initialData = [
{id: "123", name: "Tom"}, {id: "456", name: "Jerry"}, ...
]
const columns: ColumnProps[] = [
{label: "The ID", objectPropertyName: "id", renderCell: "cell-type-1", renderCellComponent: () => <NewCellComp1 ... />},
{label: "The name", objectPropertyName: "name", renderCell: "cell-type-2", renderCellComponent: () => <NewCellComp2 ... />},
...
];
<MyTable columns={columns} initialData={initialData} ... />
As you can see, NewCellComp1 and NewCellComp2 need to access the function "handleDataChange" of MyTable just like the way CellComp1 and CellComp2 did. In order to do that, I tried to use React.forwardRef and useImperativeHandle to MyTable:
const MyTable: React.FC<MyTableProps> = React.forwardRef((tableProps, ref) => {
...
useImperativeHandle(ref, () => {
refFuncHandleDataChange(objectPropertyName: string, rowIndex, newValue) {
handleDataChange(objectPropertyName, rowIndex, newValue);
}
})
return ...
})
Now, MyTable should be used like this:
const initialData = [
{id: "123", name: "Tom"}, {id: "456", name: "Jerry"}, ...
]
const columns: ColumnProps[] = [
{...other props, objectPropertyName: "id", renderCellComponent: () => <NewCellComp1 onDataChanged={data => refTable.current?.refFuncHandleDataChange("id", ..., data)} />},
{...other props, objectPropertyName: "name", renderCellComponent: () => <NewCellComp2 onDataChanged={data => refTable.current?.refFuncHandleDataChange("name", ..., data)} />},
...
];
const refTable = useRef();
<MyTable ref={refTable} columns={columns} initialData={initialData} ... />
As you can see, with this approach, I still lack the row index of the cell component that make a change of data. When cell rendering is declared inside MyTable, I can access to row index easily thanks to Flatlist, but outside MyTable does not offer that luxury, because "renderCellComponent" function of ColumnProps is a declaration for all cells on a column. This is where I'm stuck.
In conclusion, I want to create a table component that be able to display every kind of components on its cells. Also, keep its properties as "simple" as possible. You can see that I'm currently must declare only data and columns props. But my approach seems to be impossible to do that.
Can you share me an improvement for what I did or an entirely new approach for this problem. Thank you!

Creating a createSelector in Redux that does not rerender unrelated components

I have a simple document maker that allows users the create multiple fields of two types (input field and label field). They can also rearrange the field via drag and drop. My data structure is based on the Redux guide on data normalization.
LIST field is input type (e.g. children id 21 and 22) - onChange will dispatch action and allows the input text to modified without re-rendering other fields.
With LABEL field we allow users to select from a dropdown list of only 3 labels (Danger, Slippery, Flammable). Selecting one with remove it from the dropdown list to prevent duplicate label. For example, if there are "Danger" and "Slippery" in the field, the dropdown field will only show one option "Flammable". To achieve this, I use createSelector to get all children and filter them base on their parent (fieldId). This gives me an array of existing labels ["Danger","Slippery"] in that fieldId. This array will then be used to filter from a fixed dropdown array of three options with useEffect.
Now whenever I update the input text, it will also re-render the LABEL field (based on the Chrome React profiler).
It does not affect performance but I feel like I am missing something with my createSelector.
Example:
export const documentSlice = createSlice({
name: "document",
initialState: {
fields: {
1: { id: 1, children: [11, 12] },
2: { id: 2, children: [21, 22] },
},
children: {
11: { id: 11, type: "LABEL", text: "Danger", fieldId: 1 },
12: { id: 11, type: "LABEL", text: "Slippery", fieldId: 1 },
21: { id: 21, type: "LIST", text: "", fieldId: 2 },
22: { id: 22, type: "LIST", text: "", fieldId: 2 },
},
fieldOrder:[1,2]
},
});
createSelector
export const selectAllChildren = (state) => state.document.children;
export const selectFieldId = (state, fieldId) => fieldId;
export const getChildrenByFieldId = createSelector(
[selectAllChildren, selectFieldId],
(children, fieldId) => {
const filterObject = (obj, filter, filterValue) =>
Object.keys(obj).reduce(
(acc, val) =>
obj[val][filter] !== filterValue ? acc : [...acc, obj[val].text],
[]
);
const existingChildren = filterObject(children, "fieldId", filterId);
return existingChildren;
}
);
After more reading up, this is what finally works for me. Hopefully someone will find it useful.
Given the normalized data, the object reduce function can be simplified.
slice.js
export const getChildrenByFieldId = createSelector(
[selectAllChildren, selectFieldId],
(children, fieldId) => {
// get the children from fields entry => array [11,12]
const childrenIds = state.document.fields[fieldId].children
let existingChildrenText = [];
// loop through the created array [11,12]
childrenIds.forEach((childId) => {
// pull data from the individual children entry
const childText = children[childId].text;
existingChildrenText.push(childText);
});
return existingChildrenText;
}
);
To prevent re-render, we can use shallowEqual comparison on the output array to compare only the values of the array.
//app.js
import { shallowEqual, useSelector } from "react-redux";
const childrenTextData = useSelector((state) => {
return getChildrenByFieldId(state, blockId);
}, shallowEqual);

Using react-select with multiple inputs

I have multiple react-select inputs, each have their own separate options array. The issue i'm having is I'm now sure how to properly store the output of each select input.
I want to use this select output to POST to my backend but i'm unsure how to do it all with a single handler function.
This is what I have so far, i have 2 paragraphs to just output the appropriate result from the select fields but i can't seem to get it working.
This is the codesandbox i have:
https://codesandbox.io/s/busy-yonath-rlj9e?file=/src/App.js
In your code you are using the useState() hook and everytime a user selects an option, you are setting your state variable to the selected value.
The problem is that everytime you run the setSelectOptions({ e }) function, you are overwriting the existing data with the value of 'e'.
What you can do is:
Create a selectHandler function that takes in 2 arguments (the new value and the value it corresponds to in the state variable)
The code will look something like this:
import "./styles.css";
import Select from "react-select";
import { useEffect, useState } from "react";
export default function App() {
const [selectOptions, setSelectOptions] = useState({
brand: "",
model: "",
color: "",
bodyType: "",
fuelType: "",
transmission: "",
location: ""
});
const brandOptions = [
{ value: "bmw", label: "Bmw" },
{ value: "audi", label: "Audi" },
{ value: "toyota", label: "Toyota" },
{ value: "nissan", label: "Nissan" }
];
const transmissionOptions = [
{ value: "automatic", label: "Automatic" },
{ value: "manual", label: "Manual" }
];
useEffect(() => console.log(selectOptions), [selectOptions])
const handleChange = (e, type) => {
setSelectOptions((previousState) => ({
...previousState,
[type]: e.value,
}));
};
return (
<div className="App">
selected brand: {selectOptions.brand}
<br />
selected transmission:{selectOptions.transmission}
<div className="mb-2" style={{ width: "40%", margin: "0 auto" }}>
<Select
value={selectOptions.brand}
onChange={(e) => handleChange(e, "brand")}
placeholder="Select brand"
options={brandOptions}
/>
<Select
value={selectOptions.transmission}
onChange={(e) => handleChange(e, "transmission")}
placeholder="Select transmission"
options={transmissionOptions}
/>
</div>
</div>
);
}
Just as an explanation, all I am doing in the setSelectOptions() function is passing in the previous values of the state variable and updating the value coinciding to the select field.
Note: Insert this code into your project, I ran it and it worked so let me know if it did help!

React Props doesnt show info properly in the confirmation page

I'm designing a form in React that has a main form builder (Create Job.js) and some form pages (AdditionalInfo.js) and (Confirmation.js). this form had a tag input that allows you to choose tags from a drop-down list provided by an API. the selected items need to be shown later in the confirmation page.
This is my main form builder that has props and functions:(CreateJob.js)
state = {
step:1,
Title:'',
requirements:'',
Location:'',
Benefits:'',
Company:'',
InternalCode:'',
Details:'',
Tags:[],
Address:'',
Department:'',
Salary:''
}
handleDropDown = input => value => {
this.setState({ [input]: value });
}
render () {
const { step } = this.state
const {Title,Benefits,Company,InternalCode,Detailss,Tags,Address,Department,Salary,requirements,Location } = this.state;
const values ={Title,Benefits,Company,InternalCode,Detailss,Tags,Address,Department,Salary,requirements,Location}
return (
<div>
....
<AdditionalInfo
nextStep={this.nextStep}
prevStep={this.prevStep}
handleChange={this.handleChange}
handleChangeRaw={this.handleChangeRaw}
handleDropDown={this.handleDropDown}
values={values}
/>
<Confirmation
nextStep={this.nextStep}
prevStep={this.prevStep}
values={values}
/>
....
and this is my form page which includes the list from API and the drop down using react-select(AdditionalInfo.js):
export class AdditionalInfo extends Component {
state = {
locations:[],
departments: [],
tagsList:[],
}
componentDidMount() {
axios.get('/api/jobs/list-tags',{headers:headers}).then(respo =>{
console.log(respo.data)
this.setState({
tagsList:respo.data.map(Tags=>({label: Tags.name, value: Tags.id}))
})
console.log(this.state.tagsList)
})
}
render() {
const {values, handleDropDown} = this.props
<Select placeholder='Select from pre-created Tags 'onChange={handleDropDown('Tags')} defaultValue={values.Tags} required isMulti options={this.state.tagsList}/>
...
this is the list of tags received from the API:
Object { label: "MongoDB", value: 1 }
​
Object { label: "JavaScript", value: 2 }
​
Object { label: "HTML", value: 3 }
​
Object { label: "CSS", value: 4 }
...
And this is my Confirmation page which needs to show the info received from previous pages (Confirmation.js)
.....
render () {
const {
values: {
Title, Benefits,
Company, InternalCode, Detailss, Department,Tags, Salary,requirements,Location
}} = this.props
<Row> Tags: {Tags.join(", ")}</Row>
....
the problem is that, instead of showing tags on the page like putting the labels next to each other
:JavaScript,
MongoDB,
... it shows this
: [object Object], [object Object], [object Object], [object Object]. sorry for the long code but Im a beginner in JavaScript and I dont know how to handle it so it shows the labels. How can I achieve this?
You are doing great, and you have done right, just simple tweak you need.
If React show anything like [Object Object] it means you are trying to render Javascript Object not a single value because you have got Tags from props which is an Array of objects.
Use it like this, it will work like butter -
import React from 'react';
const Confirmation = () => {
const tags = [ // which you got from props
{ label: "MongoDB", value: 1 },
{ label: "JavaScript", value: 2 },
{ label: "HTML", value: 3 },
{ label: "CSS", value: 4 }
];
return (
<div>
{tags.map(tag => tag.label).join(', ')} {/* map over tags to get the array of tag labels */}
</div>
);
}
export default Confirmation;

Dropdown with multiple selection limit

I relatively new to React and Semantic UI as well.
There is a component called Dropdown with a props multiple and selection, which allows to select multiple items.
On the output my state looks like this:
const selectedItems = [
{key: 1, value: 1, text: 'Item 1'},
{key: 2, value: 2, text: 'Item 2'},
{key: 3, value: 3, text: 'Item 3'},
];
How can I do setup limit of N amount of elements?
Many thanks
The Semantic UI React Dropdown option provides a function called as onAddItem. You will have to use the value data key and do something like this:
const onAddItem = (event, data) => {
1.Fetch the state of the selected values, stored in the value key
2. Check if the limit is greater than 2
3. If the condition is met, then add
4. Else, show an error
}
Documentation Link
Well according to https://react.semantic-ui.com/modules/dropdown#dropdown-example-multiple-selection you need to create controlled component, which means you will bind value={this.state.selectedItems} then you will bind onChange={(e,data) => { this.handleChange(e,data )} and in your code
onChange (e, data) {
const currentItems = this.state.selectedItems
if (currentItems.length <= MAX_SELECTION ) {
currentItems.push(data)
this.setState({
selectedItems: currentItems
})
}
}
this will crate controlled component which will allows you to control state by yourself, and you will limit changing state, probably you will need to also handle removing items from state inside this onChange event.
I would like to suggest another approach.
set useState directly to the dropdown value.
import React, { useState } from 'react';
import { Dropdown } from 'semantic-ui-react';
const MAX_FRUITS_SELECTION = 3;
const FruitsSelect = () => {
const [fruitId, setFruitId] = useState([]);
const optionsFRUITSFake = [
{ key: 1, value: 1, text: 'Orange' },
{ key: 2, value: 2, text: 'Lemon' },
{ key: 3, value: 3, text: 'Apple' },
{ key: 4, value: 4, text: 'Banana' },
{ key: 5, value: 5, text: 'Melon' },
{ key: 6, value: 6, text: 'Pineapple' }
];
const handleDropFilterFruit = (e: any, data?: any) => {
if (data.value.length <= MAX_FRUITS_SELECTION) {
setFruitId(data.value);
}
};
return (
<Dropdown
placeholder="Select Fruits"
onChange={handleDropFilterFruit}
value={fruitId}
fluid
multiple
selectOnNavigation={false}
search
selection
options={optionsFRUITSFake}
/>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<FruitsSelect />
</React.StrictMode>,
rootElement
);
<!DOCTYPE html>
<html lang="en">
<body>
<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="root"></div>
</body>
</html>
I'm posting my workaround here. It's probably more short and simple.
At first, save the values in the state (preferably redux state) after every onChange. React state also would do fine. Then, make it disabled when a certain array length of the value is reached.
const districtData = ['Dhaka', 'Bagerhat', 'Bandarban',
'Barguna', 'Barishal', 'Bhola']
const [districtValue, setDistrictValue] = useState();
<Dropdown
onChange={async (e, { name, value }) => {setDistrictValue(value)}}
options={districtData.map((currentValue, index) => {
return {
key: `${currentValue}`,
value: `${currentValue}`,
text: `${currentValue}`,
disabled: districtValue.length > 2 ? true : false
}
// Do other things here
// Max 3 values here, then options will be disabled.
// Will be live again if any selected options are less than 3 here
/>

Categories