How to properly use state hook with useSelector? - reactjs

I have two react-select elements in my app and the 2nd one should be populated with options based on the selection of the first. It works but when the Select #1 is cleared, #2 is not being reset.
So I decided to implement useState to help me update/reset the options for Select #2 but it's acting wonky. Am I missing something or is there a better way to accomplish this?
export const dataViewPage = ({
getData,
loadLocationsByType,
classes,
}) => {
const [stateValue, setStateValue] = useState('');
const [store, setStore] = useState('');
const [storeOptions, setStoreOptions] = useState([]); // Constant to populate options for Select #2
const locationType = 'store';
const Input = styled('input')({ display: 'none' });
/* Returns locations in selected state in array of objects with location_id and name */
const locations = useSelector((state) =>
getFilteredLocations(state, stateValue, locationType)
);
useEffect(() => { // Fetches locations data on state change
stateValue &&
loadLocationsByType({ locationType: locationType, state: stateValue });
}, [stateValue, loadLocationsByType]);
useEffect(() => { // Sets store options in expected format
if (locations) {
setStoreOptions(
convertObjArrayToSelectOptions(locations, 'location_id', 'name')
);
} else { // Reset if no state is selected?
setStoreOptions([]);
}
}, [locations, stateValue]);
}
/* Main input functions */
const handleStateChange = (e) => {
let stateEntered = e ? e.value : '';
setStateValue(stateEntered);
};
const handleStoreChange = (e) => {
let selectedStore = e ? e.value : '';
setStore(`T${selectedStore}`);
console.log(selectedStore);
};
return (
<div>
<div>
<p>Enter or select state</p>
<Select
classes={classes}
placeholder="Select state"
options={states}
onChange={handleStateChange}
isClearable
/>
</div>
<div>
<p>Select store:</p>
<Select
options={storeOptions}
isClearable
onChange={handleStoreChange}
/>
</div>
<label>
<Input type="submit" />
<Button
onClick={handleSubmit}
>
View data
</Button>
</label>
</div>
);
};

Related

how to update input defaultvalue using useRef (after the initial render?)

const Component = ()=>{
const [list, setList] = useState(getLocalStorage());
const [isEditing, setIsEditing] = useState(false);
const [itemToEdit, setItemToEdit] = useState();
const refContainer = useRef(null);
const putLocalStorage = () => {
localStorage.setItem("list", JSON.stringify(list));
};
const editItem = (id) => {
refContainer.current.focus();
setItemToEdit(() => {
return list.find((item) => item.id === id);
});
setIsEditing(true);
};
const handleSubmit = (e)=>{
e.preventDefault();
let nameValue = refContainer.current.value;
if (isEditing){
setList(list.map((item)=>{
if (item.id === itemToEdit.id){
return {...item, name: nameValue};
}
else {
return item;
}
);
}
else {
let newItem = {
id: new Date().getItem().toString(),
name: nameValue,
}
setList([...list, newItem])
}
nameValue="";
setIsEditing(false);
}
useEffect(() => {
putLocalStorage();
}, [list]);
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" ref={refContainer} defaultValue={isEditing ? itemToEdit.name : ""}/>
<button type="submit">submit</button>
</form>
<div>
{list.map((item) => {
const { id, name } = item;
return (
<div>
<h2>{name}</h2>
<button onClick={() => editItem(id)}>edit</button>
<button onClick={() => deleteItem(id)}>
delete
</button>
</div>
);
})}
</div>
</div>
)
}
So this part:
<input type="text" ref={refContainer} defaultValue={isEditing ? itemToEdit.name : ""} />
I want to show to users what they are editing by displaying the itemToEdit on the input.
It works on the first time when the user clicks edit button
But after that, the defaultValue does not change to itemToEdit
Do you guys have any idea for the solution?
(i could use controlled input instead, but i want to try it with useRef only)
Otherwise, placeholder will be the only solution...
The defaultValue property only works for inicial rendering, that is the reason that your desired behavior works one time and then stops. See a similar question here: React input defaultValue doesn't update with state
One possible solution still using refs is to set the itemToEdit name directly into the input value using ref.current.value.
const editItem = (id) => {
refContainer.current.focus();
setItemToEdit(() => {
const item = list.find((item) => item.id === id);
refContainer.current.value = item.name;
return item;
});
setIsEditing(true);
};

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

React - I have to click a button twice to display api data

I would like to display API data on a button click. My state is set to an empty array initially and I guess that's why it shows an empty array at first click, but what could I write differently to show data on first click ? After the initial click it works.
My code:
const Search = () => {
const [textInput, setTextInput] = useState('');
const [tickers, setTickers] = useState([]);
const [prices, setPrices] = useState([]);
const [isClicked, setIsClicked] = useState(false);
const inputHandler = (e) => {
setTextInput(e.target.value);
}
const showData = async (e) => {
e.preventDefault();
const url = `https://www.alphavantage.co/query?function=TIME_SERIES_MONTHLY&symbol=${textInput}&apikey=${process.env.REACT_APP_ALPHA_VANTAGE_API_KEY}`
try {
const data = await axios.get(url);
if(data) {
setPrices([data.data['Monthly Time Series']['2021-11-30']]);
}
} catch(err) {
console.log(err)
}
console.log(prices);
setIsClicked(true);
setTextInput('');
}
return (
<StyledSearch>
<h1>Security Price Monitor App </h1>
<form onSubmit={submitSearch}>
<input type="text" value={textInput} onChange={inputHandler} placeholder='Enter Ticker'/>
<button type="submit" onClick={showData}>Search</button>
</form>
{isClicked &&
<Chart tickers = {tickers} setTickers={setTickers} prices={prices} setPrices={setPrices}/>
}
</StyledSearch>
)
}
Try to change your conditional rendering condition to:
{prices.length > 0 && (
<Chart
tickers={tickers}
setTickers={setTickers}
prices={prices}
setPrices={setPrices}
/>
)}
I think that you can remove the isClicked state. It is redundant.

Cannot remove inputs array with filter

I am trying to remove an input field with filter function but it's not working.
In the following code add operation works fine but remove operation is not working properly ,it is not removing the corresponding element.Another problem the values on the inputs fields not present when the component re-renders.so experts guide me how i can achieve removing the corresponding row when the remove button is clicked and the input values should not be reset when the component re-renders
So when I refresh the page and click to remove an input it will clear all other input data. How can I fix this problem ?
Update adding full component in question:
const Agreement = (props) => {
const { agreement, editable, teamData, teamId, fetchTeamData } = props;
const [editing, setEditing] = useState(false);
const [title, setTitle] = useState("");
const [showErrors, setShowErrors] = useState(false);
const [errorsArr, setErrorsArr] = useState();
const initialFormState = {
rule_0: teamData.rules.rule_0,
rule_1: teamData.rules.rule_1,
rule_2: teamData.rules.rule_2,
rule_3: teamData.rules.rule_3,
creator: teamData.User.public_user_id,
};
const [updateTeamData, setUpdateTeamData] = useState(initialFormState);
const [inputs, setInputs] = useState(
teamData.rules.map((el) => ({
...el,
guid: uuidV4(),
}))
);
const handleChange = (event) => {
const { name, value } = event.target;
// Update state
setUpdateTeamData((prevState) => ({
...prevState,
[name]: value,
}));
};
// Add more input
const addInputs = () => {
setInputs([...inputs, { name: `rule_${inputs.length + 1}` }]);
};
// handle click event of the Remove button
const removeInputs = (index) => {
const newList = inputs.filter((item, i) => index !== i); // <-- compare for matching index
setInputs(newList);
};
const clearInput = (dataName) => {
setUpdateTeamData((prevState) => {
delete prevState[dataName];
return {
...prevState,
};
});
};
const handleSubmit = async (event) => {
event.preventDefault();
setEditing(false);
// Send update request
const res = await axios.put(`/api/v1/teams/team/${teamId}`, updateTeamData);
// If no validation errors were found
// Validation errors don't throw errors, it returns an array to display.
if (res.data.validationErrors === undefined) {
// Clear any errors
setErrorsArr([]);
// Hide the errors component
setShowErrors(false);
// Call update profiles on parent
fetchTeamData();
} else {
// Set errors
setErrorsArr(res.data.validationErrors.errors);
// Show the errors component
setShowErrors(true);
}
};
const handleCancel = () => {
setEditing(false);
};
useEffect(() => {
if (agreement === "default") {
setTitle(defaultTitle);
// setInputs(teamData.rules);
} else {
setTitle(agreement.title ?? "");
}
}, [agreement, teamData]);
// console.log("teamData.rules", teamData);
console.log("inputs", inputs);
return (
<div className="team-agreement-container">
{!editing && (
<>
<h4 className="team-agreement-rules-title">{title}</h4>
{editable && (
<div className="team-agreement-rules">
<EditOutlined
className="team-agreement-rules-edit-icon"
onClick={() => setEditing(true)}
/>
</div>
)}
{teamData.rules.map((rule, index) => (
<div className="team-agreement-rule-item" key={`rule-${index}`}>
{rule ? (
<div>
<h4 className="team-agreement-rule-item-title">
{`Rule #${index + 1}`}
</h4>
<p className="team-agreement-rule-item-description">
- {rule}
</p>
</div>
) : (
""
)}
</div>
))}
</>
)}
{/* Edit rules form */}
{editing && (
<div className="team-agreement-form">
{showErrors && <ModalErrorHandler errorsArr={errorsArr} />}
<h1>Rules</h1>
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.guid}>
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
{inputs.length < 4 && (
<div className="team-agreement-add-rule">
<button type="submit" onClick={addInputs}>
<Add />
</button>
</div>
)}
<div className="div-button">
<button className="save-button" onClick={handleSubmit}>
Save
</button>
<button className="cancel-button" onClick={handleCancel}>
Cancel
</button>
</div>
</div>
)}
</div>
);
};
export default Agreement;
When i do console.log(inputs) this is the data that I got:
0: 0: "t" 1: "e" 2: "s" guid: "e18595a5-e30b-4b71-8fc2-0ad9c0e140b2"
proto: Object 1: 0: "d" 1: "a" 2: "s" 3: "d" 4: "a" 5: "s" guid: "537ca359-511b-4bc6-9583-553ea6ebf544" ...
Issue
The issue here is that you are using the array index as the React key. When you mutate the underlying data and reorder or add/remove elements in the middle of the array then the elements shift around but the React key previously used doesn't move with the elements.
When you remove an element then all posterior elements shift forward and the index, as key, remains the same so React bails on rerendering the elements. The array will be one element shorter in length and so you'll see the last item removed instead of the one you actually removed.
Solution
Use a React key that is intrinsic to the elements being mapped, unique properties like guids, ids, name, etc... any property of the element that guarantees sufficient uniqueness among the dataset (i.e. the siblings).
const [inputs, setInputs] = useState(teamData.rules);
const removeInputs = (index) => {
// compare for matching index
setInputs(inputs => inputs.filter((item, i) => index !== i));
};
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.id}> // <-- use a unique property
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
If your teamData.rules initial state value doesn't have any unique properties to use then you can map this to a new array and add a sufficient id property.
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: generateId()***,
})));
*** this is a function you need to define yourself, or import from a module like uuid***
import { v4 as uuidV4 } from 'uuid';
...
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: uuidV4(),
})));
// Add more input
const addInputs = () => {
setInputs(inputs => [
...inputs,
{
name: `rule_${inputs.length + 1}`,
guid: uuidV4();
},
]);
};
Then when mapping use the guid property.
<div className="agreement-form-grid" key={data.guid}>
The issue is because you are trying to compare index with array item in filter method. You should use the second argument in filter which denotes the array index of the current iterating item
const removeInputs = (index) => {
const newList = inputs.filter((item,i) => index !== i);
setInputs(newList);
};
That's your solution, you are trying with item but you are comparing it with index that's wrong. You should do it like this,
const newList = inputs.filter((item, key) => index !== key);

How to set the first value in a select dropdown to as a default react

I have a react program that displays a table based on values of a dropdown. I want the program to display the table by default based on the first value in the dropdown.
The first value in the dropdown is very different from the value made as default, and the dropdown values are always changing. So the data can be misleading when it loads for the first time.
here is a snippet of my code with a little description within. Thanks.
const CompletenessApp = () => {
const [completeness, setcompleteness] = useState([]);
const [loading, setloading] = useState(false);
const [valueSelected, setValueSelected] = useState({value:'all'});
const [periodSelected, setPeriodSelected] = useState({value:'20200702'}); // the default value that is used to filter the data.
const valid = [
{id:"all", variable:"all"},{id:"true", variable:"true"}, {id:"false", variable:"false"}, {id:"null", variable:"null"},
];
useEffect(() => {
const fetchData = async () => {
try{
const res = await axios.get('http://localhost:8000/api/completeness/');
setcompleteness(res.data);
setloading(true);
} catch (e) {
console.log(e)
}
}
fetchData();
}, []);
// when the page loads for the first time it filters the data based on the period value set in the state. I want the data to be filtered based on the first value in the dropdown instead, the first value in the dropdown is different from the default value set.
const handlePeriodChange = e => {
setPeriodSelected({value : e.target.value})
}
const handleValueChange = e => {
let boolvalue = Boolean
e.target.value === 'true'? boolvalue = true:
e.target.value === 'false'? boolvalue = false:
e.target.value === 'all'? boolvalue = 'all':
boolvalue=null
setValueSelected({value : boolvalue})
}
//filtered data to be displayed
const filteredCompleteness = completeness.filter(
completedata=> (completedata.period === periodSelected.value)
&&(valueSelected.value !== 'all'?completedata.complete === valueSelected.value:{}))
return(
<div>
<div className="selection-row">
<div className="stats-columns">
<div className="stats-label">Period</div>
//the relevant dropdown is here
<select onChange={e => handlePeriodChange(e)}>
{Array.from(new Set(completeness.map(obj => obj.period)))
.sort().reverse()
.map(period => {
return <option value={period}>{period}</option>
})}
</select>
</div>
<div className="stats-columns">
<div className="stats-label">Value</div>
<select onChange={e => handleValueChange(e)}>
{valid.map((obj) => {
return <option value={obj.id}>{obj.variable}</option>
})
}
</select>
</div>
</div>
<hr></hr>
<div className="results-table">
//table is displayed here
</div>
</div>
);
}
export default CompletenessApp;
// above return
const options = Array.from(new Set(completeness.map((obj) => obj.period)))
.sort()
.reverse()
.map((period) => {
return {
value: period,
label: period,
};
});
// in jsx
<select defaultValue={options[0].value} onChange={e => handlePeriodChange(e)}>
{
options.map((obj) => {
return <option value={obj.value}>{obj.label}</option>;
})
}
</select>
Try this and let me know.

Resources