React select not showing options after first selection - reactjs

I have a dropdown made with react-select, with multiple options that i get from an api. The code is working, when clicked the dropdown show the options, but when I select one option it stop showing the other ones. I don't what could it be since the isMulti prop is on.
Here is the code:
export const DropDown = ({ itemsOption, placeholder }) => {
const [options, setOptions] = useState([]);
const loadOptions = (op) => {
api.get(`${op}`).then(({ data }) => {
setOptions(
data.map((item) => {
return {
key: item.code,
label: item.name_ptbr,
};
})
);
});
};
useEffect(() => {
loadOptions(itemsOption);
}, []);
return (
<>
<DropStyled>
<Select
isMulti
options={options}
name={placeholder}
placeholder={placeholder}
closeMenuOnSelect={false}
/>
</DropStyled>
</>
);
};

An option needs a value property for react-select to map the data to its options.
So when mapping over the fetched data, add value along with label and key.
return {
key: item.code,
label: item.name_ptbr,
value: item.name_ptbr,
};

Related

Hot to combine two custom components values into one with react-hook-forms

I have a form with a two separate custom components - 1 for datepick and 1 for timepick. Each of these fields returns Date type value. I'm using react-hook-form for the form management. My question is how to combine the values from these two components and send one combined value to the server using the aforementioned library, using 1 single controller? Is it even possible? If not what is the best tactic to do this? Here's a simplification of what it looks like:
function DateTime() {
const {
field: { onChange },
} = useController({
name: 'dateTimeFrom',
})
return (
<>
<DatePicker
onChange={onChange}
/>
<TimePicker
onChange={onChange}
/>
</>
)
}
I made it work using custom useStateCallback hook, so I would be able to setDateTime first, then to run the onChange with the transform getDateTime() function. I'd just want to know if this is a good practise in any way?
function useStateCallback(initialState) {
const [state, setState] = useState(initialState);
const cbRef = useRef(null);
const setStateCallback = useCallback((state, cb) => {
cbRef.current = cb;
setState(state);
}, []);
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
cbRef.current = null; }
}, [state]);
return [state, setStateCallback];
}
import useStateCallback from './useStateCallback'
function DateTime() {
const {
field: { onChange },
} = useController({
name: 'dateTimeFrom',
})
const getDateTime = ({ date, time }: any) =>
`${format(new Date(date), 'MM/dd/yyyy')} ${format(new Date(time), 'HH:mm')}`
const [dateTime, setDateTime] = useStateCallback({ date: null, time: null })
return (
<>
<DatePicker
onChange={(value) => {
setDateTime({ ...dateTime, date: value }, newDateTime =>
onChange(getDateTime(newDateTime))
)
}}
/>
<TimePicker
onChange={(time) => {
setDateTime({ ...dateTime, time: time }, newDateTime =>
onChange(getDateTime(newDateTime))
)
}}
/>
</>
)
}

REACT-Send requests the selected dish options ids to my backend not working

How can I retrieve the dishId selected from my options react - select that shows me thedishType in order to send them my parent component FormRender and to the backend.
My first dropdown shows me: Menu1, Menu2...
My second one: type2...
So if I click ontype4, how can I store the related dishId(here = 4). I can click on several values i.e: type2 andtype3.
How do I keep the dish ids i.e : 2 and 3 and send them to my FormRender parent
Menus(first page of my multi - step form):
export default function Menus() {
const [selectionMenus, setSelectionMenus] = useState({});
const [selectionDishes, setSelectionDishes] = useState({});
const [menus, setMenus] = useState([])
const [date, setDate] = useState('')
useEffect(() => {
axios
.post(url)
.then((res) => {
console.log(res);
setMenus(res.data.menus);
})
.catch((err) => {
console.log(err);
});
}, []);
const names = menus?.map(item => {
return {
label: item.menuId,
value: item.name
}
})
const types = menus?.flatMap(item => {
return item.dishes.map(d => ({
label: d.dishId,
value: d.dishType
}))
})
const handle = (e) => {
if (e?.target?.id === undefined) return setInfo(e);
if (e?.target?.id === undefined) return setSelectionMenus(e);
if (e?.target?.id === undefined) return setSelectionDishes(e);
switch (e.target.id) {
case "date":
setDate(e.target.value);
break;
...
default:
}
}
};
return (
<>
<form>
<div>My menus</div>
<label>
Dishes :
<Dropdown
options={names}
value={selectionMenus}
setValue={setSelectionMenus}
isMulti={true}
/>
</label>
<label>
<Dropdown
options={types}
value={selectionDishes}
setValue={setSelectionDishes}
isMulti={true}
/>
</label>
<label>
Date:
<div>
<input
type="date"
name='date'
value={date}
onChange={handle}
id="date"
/>
</div>
</label>
...
</form>
<div>
<button onClick={() => nextPage({ selectionDishes, selectionMenus, date })}>Next</button>
</div>
</>
);
}
Here the parent Component FormRender that is supposed to retrieve the values of all dishId selected and send them to the backend:
export default function FormRender() {
const [currentStep, setCurrentStep] = useState(0);
const [info, setInfo] = useState();
const [user, setUser] = useState();
const headers = ["Menus", "Details", "Final"];
const steps = [
<Menus
nextPage={(menu) => {
setInfo(menu);
setCurrentStep((s) => s + 1);
}}
/>,
<Details
backPage={() => setCurrentStep((s) => s - 1)}
nextPage={setUser}
/>,
<Final />
];
useEffect(() => {
if (info === undefined || user === undefined) return;
const data = {
date: info.date,
id: //list of dishId selected but don't know how to do that??
};
}, [info, user]);
return (
<div>
<div>
<Stepper steps={headers} currentStep={currentStep} />
<div >{steps[currentStep]}</div>
</div>
</div>
);
}
Dropdown:
export default function Dropdown({ value, setValue, style, options, styleSelect, isMulti = false }) {
function change(option) {
setValue(option.value);
}
return (
<div onClick={(e) => e.preventDefault()}>
{value && isMulti === false ? (
<Tag
selected={value}
setSelected={setValue}
styleSelect={styleSelect}
/>
) : (
<Select
value={value}
onChange={change}
options={options}
isMulti={isMulti}
/>
)}
</div>
);
}
Here my json from my api:
{
"menus": [
{
"menuId": 1,
"name": "Menu1",
"Description": "Descritption1",
"dishes": [
{
"dishId": 2,
"dishType": "type2"
},
{
"dishId": 3,
"dishType": "type3"
},
{
"dishId": 4,
"dishType": "type4"
}
]
},
...
]
}
You already store the selected values inside the selectionMenus and selectionDishes states. So, if you want to send them to the parent FormRender component you can instead create those two states inside that component like this:
export default function FormRender() {
const [selectionMenus, setSelectionMenus] = useState();
const [selectionDishes, setSelectionDishes] = useState();
....
}
Then pass those values to the Menus component:
<Menus
selectionMenus={selectionMenus}
setSelectionMenus={setSelectionMenus}
selectionDishes={selectionDishes}
setSelectionDishes={setSelectionDishes}
nextPage={(menu) => {
setInfo(menu);
setCurrentStep((s) => s + 1);
}}
/>
Subsequently, you will have to remove the state from the Menus component and use the one you receive from props:
export default function Menus({ selectionMenus, setSelectionMenus, selectionDishes, setSelectionDishes }) {
/*const [selectionMenus, setSelectionMenus] = useState({}); //remove this line
const [selectionDishes, setSelectionDishes] = useState({});*/ //remove this line
...
}
Finally, you can use inside your useEffect hook the two states and map them to only get the selected ids:
useEffect(() => {
// ... other logic you had
if(selectionDishes?.length && selectionMenus?.length){
const data = {
date: info.date,
id: selectionDishes.map(d => d.dishId),
idMenus: selectionMenus.map(m => m.menuId)
};
}
}, [info, user, selectionMenus, selectionDishes]);
react-select has options to format the component:
getOptionLabel: option => string => used to format the label or how to present the options in the UI,
getOptionValue: option => any => used to tell the component what's the actual value of each option, here you can return just the id
isOptionSelected: option => boolean => used to know what option is currently selected
onChange: option => void => do whatever you want after the input state has changed
value => any => if you customize the above functions you may want to handle manually the value
Hope it helps you

Unable to overwrite local variable in Typescript

I am trying to create a dropdown using a dynamic list of items returned from a function call in Typescript but I am having trouble with overwriting the variables defined in the code and constantly getting their initial values as return.
I manage to read the value of dropdown items as returned by the function just fine but am unable to assign those values to the dropdown items parameter. Here´s my code:
let tempList: myObject[] = []; //unable to overwrite
const myComponent = () => {
let dropdownItems=[{label: ""}]; //unable to overwrite
fetchAllDropdownItems().then((x)=> tempList = x).catch((err)=> console.log(err))
//Converting myObject array to a neutral array
myList.forEach((y)=> dropdownItems.push(y))
console.log(tempList) // returns [{label: "A"}, {label: "B"}]
console.log(dropdownItems)// returns [{label: ""}, {label: "A"}, {label: "B"}]
return (
<GridContainer>
<GridItem>
<Dropdown
items={dropdownItems} // results in initial value of dropdownItems i.e {label: ""}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
And here´s how myObject looks like:
export interface myObject {
label: string;
}
Previously I had defined items in my dropdown as items={[{ label: "A" }, {label: "B"}]} which is essentially the same structure as what I am getting from fetchAllDropdownItems-function return but my goal is to not hard code the items.
I need help in figuring out why I am unable to overwrite variables and would appreciate any advice/suggestions. Thanks in advance.
The problem isn't that you aren't updating tempList, it's that your component renders before you do and nothing tells it to re-render because you're not using that list as state.
With the code structure you've shown, you have two options:
Make your entire module wait until you've fetched the list from the server, before even exporting your component, by using top-level await (a stage 3 proposal well on its way to stage 4 ["finished"] with good support in modern bunders)
or
Have your component re-render once the list is received
Here's an example of #1:
const dropdownItems: myObject[] = await fetchAllDropdownItems();
// top-level await −−−−−−−−−−−−−−−^
const myComponent = () => {
return (
<GridContainer>
<GridItem>
<Dropdown
items={dropdownItems}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
Note, again, that it loads the items once then reuses them, loading them when this module is loaded. That means that it may load the items even if your component is never actually used.
There are a bunch of different ways you can spin #2.
Here's one of them:
const dropdownItems: Promise<myObject[]> = fetchAllDropdownItems();
const myComponent = () => {
const [items, setItems] = useState<myObject[] | null>(null);
useEffect(() => {
let unmounted = false;
dropdownItems
.then(items => {
if (!unmounted) {
setItems(items);
}
})
.catch(error => {
// ...handle/display error loading items...
});
return () => {
unmounted = true;
};
}, []);
return (
<GridContainer>
<GridItem>
<Dropdown
items={items || []}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
That loads the items proactively as soon as your module is loaded, and so like #1 it will load them even if your component is never used, but that means the items are (probably) there and ready to be used when your component is first used.
But the "probably" in the last paragraph is just that: The fetch may still be unsettled before your component is used. That's why the component uses the promise. That means that in that example your component will always render twice: Once blank, then again with the list. It'll be fairly quick, but it will be twice. Making it happen just once if the items are already loaded is possible, but markedly complicates the code:
let dropdownItems: myObject[] | null = null;
const dropdownItemsPromise: Promise<myObject[]>
= fetchAllDropdownItems().then(items => {
dropdownItems = items;
return items;
})
.catch(error => {
// ...handle/display error loading items...
});
const myComponent = () => {
const [items, setItems] = useState<myObject[] | null>(dropdownItems);
useEffect(() => {
let unmounted = false;
if (items === null) {
dropdownItemsPromise.then(items => {
if (!unmounted) {
setItems(items);
}
});
}
return () => {
unmounted = true;
};
}, [items]);
return (
<GridContainer>
<GridItem>
<Dropdown
items={items || []}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
Or you might want to wait to load the items until the first time your component is used:
let dropdownItems: myObject[] | null = null;
const myComponent = () => {
const [items, setItems] = useState<myObject[] | null>(dropdownItems);
useEffect(() => {
let unmounted = false;
if (items === null) {
fetchAllDropdownItems().then(items => {
dropdownItems = items;
if (!unmounted) {
setItems(items);
}
})
.catch(error => {
if (!unmounted) {
// ...handle/display error loading items...
}
});
}
return () => {
unmounted = true;
};
}, [items]);
return (
<GridContainer>
<GridItem>
<Dropdown
items={items || []}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
Or you might want to load the items every time the component is used (perhaps they change over time, or the component is rarely used and caching them in the code seems unnecessary):
const myComponent = () => {
const [items, setItems] = useState<myObject[] | null>(dropdownItems);
useEffect(() => {
let unmounted = false;
fetchAllDropdownItems().then(items => {
if (!unmounted) {
setItems(items);
}
})
.catch(error => {
if (!unmounted) {
// ...handle/display error loading items...
}
});
return () => {
unmounted = true;
};
}, [items]);
return (
<GridContainer>
<GridItem>
<Dropdown
items={items || []}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;

Control subarray of state in form React

I have a container with boxes (divs), and inside these Boxes there's N checkboxes. The boxes and the checks are fetched from a server and listed on screen. I need to Control the checkboxes state to set a checked according a other fetch. How can i do that?
Edit:
I did a fetch to get the box (modules). Each module have an array of checkboxes (that i call features):
modules = [{
id,
name,
features: [{
id,
value
}]
}
]
i created a new state 'checkModules' to control the 'checked' propertie from checkbox. When i do the fetchModules, i set the checkModules according to the fetched data.
const PermissionsPage = () => {
const [modules, setModules] = useState([]);
const [checkModules, setCheckModules] = useState([]); //state to control the checkboxes 'checked' propertie
useEffect(() => {
api.get('/modules').then(res => {
setModules(res.data.modules);
setCheckModules(
res.data.modules.map(m => ({
id: m.id,
features: m.features.map(f => ({ id: f.id, checked: false }))
}))
);
}
});
const handleChange = (m,f,e) => {
let temp = checkModules;
//the propertie is changed, but the checkbox don't 'check'.
temp[m].features[f].checked = !temp[m].features[f].checked;
setCheckModules(temp);
}
return (){
<div>
{modules.map((module, m) => (
<div>
<span>{module.name}</span>
{module.features.map((feature, f) => (<checkbox onChange={() => handleChange(m,f)} checked={checkModules[m] !== undefined ? checkModules[m].features[f].checked : false}/>))}
</div>
)}
</div>
}
}
i tried to reproduce the code simplifying the use of components =).
I solve it. I'll post the changes =).
const PermissionsPage = () => {
const [modules, setModules] = useState([]);
const [checkModules, setCheckModules] = useState([]); //state to control the checkboxes 'checked' propertie
useEffect(() => {
api.get('/modules').then(res => {
setModules(res.data.modules);
setCheckModules(
res.data.modules.map(m => ({
id: m.id,
name: m.name,
features: m.features.map(f => ({ id: f.id, name: f.name, checked: false }))
}))
);
}
});
const handleChange = (m,f,e) => {
let temp = [...checkModules]; //that's the point. You need to take the state as a new array;
temp[m].features[f].checked = !temp[m].features[f].checked;
setCheckModules(temp);
}
return (){
<div>
{checkModules.map((module, m) => (
<div>
<span>{module.name}</span>
{checkModules[m].features.map((feature, f) => (<checkbox onChange={() => handleChange(m,f)} checked={feature.checked}/>))}
</div>
)}
</div>
}
}

Adding new columns dynamically with mui-datatable

I want to add a new column in mui-datatable every time a button is pressed. However datatables doesn't seem to be rendering it. However, if I clicked on the add column button, and then select the dropdown, the new columns appear. Furthermore the selected dropdown value does not seem to be reflected as well . I've narrowed the issue down to using const [columns,setColumns] = useState(...) for the column, but without that, I can't add any new columns dynamically at all. Appreciate any help to get me out of this pickle, thank you.
https://codesandbox.io/s/unruffled-sun-g77xc
const App = () => {
function handleChange(event) {
setState({ value: event.target.value });
}
const [state, setState] = useState({ value: "coconut" });
const [columns, setColumns] = useState([
{
name: "Column 1",
options: {}
},
{
name: "Column with Input",
options: {
customBodyRender: (value, tableMeta, updateValue) => {
return (
<div>
<select value={state.value} onChange={handleChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</div>
);
}
}
}
]);
const options = {
responsive: "scroll"
};
const addColumn = () => {
columns.push({
name: "NewColumn",
label: "NewColumn"
});
setColumns(columns);
};
const data = [["value", "value for input"], ["value", "value for input"]];
return (
<React.Fragment>
<MUIDataTable columns={columns} options={options} data={data} />
//add a new column if this button is clicked
<button onClick={addColumn}>Add Column</button>
</React.Fragment>
);
};
Your new column wasn't actually getting pushed to the columns variable. When you're using useState, you can't make changes to the variable unless you use setColumns, otherwise it won't trigger a rerender. Try this:
const addColumn = () => {
setColumns([ ...columns, {
name: "NewColumn"
}]);
};
Or this:
const addColumn = () => {
const editableColumns = [...columns];
editableColumns.push({
name: "NewColumn"
});
setColumns(editableColumns);
};
Both will work, it's just your preference.
You can test if it's editing the columns with this:
useEffect(() => console.log(columns), [columns]);

Resources