Im new to hook and so is react,Ive been watching some tutorials lately,I saw Ben awad's video for dynamic forms and I tried replicating it.There he used a callback inside the useState updater function which seems new to me.He used link setPeople(currentPeople => {}) What is the argument currentPeople come from and why its used,Can you someone please explain,Thanks in advance!
import { useState } from "react";
import "./App.css";
import { generate } from "shortid";
interface Person {
id: string;
firstName: string;
lastName: string;
}
function App() {
const [people, setPeople] = useState<Person[]>([
{
id: "5",
firstName: "Aashiq",
lastName: "Ahmed",
},
]);
return (
<>
<h2 style={{ textAlign: "center" }}>Dynamic Form </h2>
<div style={{ textAlign: "center" }}>
<button
onClick={() => {
setPeople((currentPeople) => [
...currentPeople,
{
id: generate(),
firstName: "",
lastName: "",
},
]);
}}
>
add new person
</button>
{people.map((p, index) => {
return (
<div key={p.id}>
<input
placeholder="first name"
value={p.firstName}
onChange={(e) => {
const firstName = e.target.value;
setPeople((
currentPeople
) =>
currentPeople.map((x) =>
x.id === p.id ? { ...x, firstName } : x
)
);
}}
/>
<input
placeholder="last name"
value={p.lastName}
onChange={(e) => {
const lastName = e.target.value;
setPeople((currentPeople) =>
currentPeople.map((x) =>
x.id === p.id ? { ...x,lastName } : x
)
);
}}
/>
<button onClick={()=> setPeople( currentPeople =>currentPeople.filter(x=> x.id !== p.id) )}>x</button>
</div>
);
})}
<div>
<pre>{JSON.stringify(people, null, 3)}</pre>
</div>
</div>
</>
);
}
export default App;
No better explanation than the official one.
Here is the link: https://reactjs.org/docs/hooks-reference.html#usestate
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
Your currentPeople is what the variable name suggests, the current value of the
const [people, setPeople] = useState<Person[]
For example:
You might send only one person that you want to add to your people's array, so you end up just attaching that Person to an already existing array of Persons.
Sure you could use setPeople([...people, newPerson]) but this wouldn't be correct in 100% of places because people might not have the latest value so the callback function comes to the rescue.
It is the current value of that state, this might come in handy, when you want to use the previous state to calculate the next state. An example would be a toggle function, that would toggle a modal or sth.
const [isOpen, setIsOpen] = useState(false);
const toggleIsOpen = () => setIsOpen(open=>!open);
Related
what i am creating here is a sorter with 3 inputs like shown here . This sorter will get some data from a table. Now i'm setting state with initSelect and i'm passing it the fields array but when i console.log(select) it gives me the object shown in the image which is incorrect from the behaviours i want {sorterParam1: 'Date', sorterParam2: '', sorterParam3: ''}
The first input has to have a default value of 'Date' always , but it can change to other values in the dropdown list like name , username ect . When i console log the select state it is messed up as it's always selecting the last one on the fields array , how can i change the initSelects function to correctly build the object i want.
Also the tricky thing which i can't seem to do is , if this Date value is selcted , in the second input, the date value should not be there. And if in the second input we select another value like Earth , Earth and Date should not be in the 3rd input and so on. So basically it means filtering out values . I need serious help as this is for the company i work on
Excepted Behaviour: Dynamically update value every time i select one input element like
{sorterParam1: 'Date', sorterParam2: '', sorterParam3: ''}
When selectin 'Date' for example , it shouldn't not be included in the dropdownlist on sorterParam2, sorterParam3.
/*eslint-disable*/
import React, { useState, useMemo } from 'react';
import TextField from '#mui/material/TextField';
import Button from '#mui/material/Button';
import { GridSortModel } from '#mui/x-data-grid';
import SorterField from './SorterField';
const initSelects = (fields) => {
let object = {};
fields.map((item) => {
console.log(item, 'item');
object = {
...item,
[item.name]: item.defaultValue ? item.defaultValue : '',
};
});
return object;
};
const Sorter = ({ menuItemsValue, setSortData }: SortProps) => {
const fields: SorterProps[] = [
{
name: 'sorterParam1',
title: 'Sort by',
optional: false,
defaultValue: 'Date',
},
{
name: 'sorterParam2',
title: 'Then by',
optional: true,
},
{
name: 'sorterParam3',
title: 'Then by',
optional: true,
},
];
const [select, setSelect] = useState<any>(() => initSelects(fields));
const getMenuItems = useMemo(() => {
return menuItemsValue.filter((item) => select.sorterParam1 !== item);
}, [menuItemsValue, select]);
const handleSelectChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
console.log(111, name, value);
setSelect({ ...select, [name]: value });
setSortData(sortOptions);
};
const handleClearAllInputs = () => {
setSelect({
sorterParam1: '',
sorterParam2: '',
sorterParam3: '',
});
};
const handleConfirm = () => {};
return (
<TextField
label="Advanced Sorting"
className={styles.sorter__inputs}
id="sorter-parameter-1"
variant="standard"
InputProps={{
disableUnderline: true,
}}
select
SelectProps={{
IconComponent: (props) => <NewIcon {...props} />,
}}
sx={{
fontSize: '12px',
width: '100%',
'& .MuiInputBase-input:focus': {
backgroundColor: 'transparent !important',
},
'& .MuiInputLabel-root': {
color: '#9E9E9E',
},
'& .MuiTextField-root': {
fontSize: '13px',
},
'& .MuiOutlinedInput-root': {
backgroundColor: '#fff',
},
}}
>
{fields.map((option, index) => (
<SorterField
key={option.name}
menuItemsValue={getMenuItems}
name={option.name}
option={option}
count={fields.length}
handleChange={handleSelectChange}
index={index + 1} // setData={setData}
/>
))}
<div className={styles.sorter__inputControllers}>
<Button
className={styles.sorter__clearAllInput}
onClick={() => handleClearAllInputs()}
>
Clear All
</Button>
<Button
onClick={() => handleConfirm()}
className={styles.sorter__confirmInput}
>
Confirm
</Button>
</div>
</TextField>
);
};
export default Sorter;
<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>
This is the SorterField Component code if that might be helpful
/*eslint-disable*/
import React, { useState } from 'react';
import TextField from '#mui/material/TextField';
import { MenuItem } from '#mui/material';
import { SorterProps } from '../../types/Sorter';
import { ReactComponent as SorterLine } from '../../assets/img/sortLine.svg';
import styles from '../../assets/components/Sorter/sorter.module.scss';
type SorterFieldProps = {
menuItemsValue: string[];
option: SorterProps;
count: number;
name: string;
handleChange: (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;
index: number;
};
function SorterField({
option,
count,
menuItemsValue,
handleChange,
index,
}: SorterFieldProps) {
const handleSorting = () => {};
return (
<div className={styles.sorter__container}>
<div className={styles.sorter__header}>
<p className={styles.sorter__label}>
{option.title}{' '}
{option.optional && (
<sup className={styles.sorter__optional}>*Optional</sup>
)}
</p>
<div className={styles.sorter__numbers__container}>
{Array.from({ length: count }, (_, i) => i + 1).map((number) => (
<>
{number === index ? (
<>
<span className={styles.sorter__number}>{index}</span>
</>
) : (
<>
<span className={styles.sorter__numbers}>{number}</span>
</>
)}
</>
))}
</div>
</div>
<div className={styles.sorter__inputs}>
<TextField
className={[styles.sorter__input, styles.sorter__field__input].join(
' '
)}
variant="outlined"
label="Select"
select
SelectProps={{
IconComponent: () => <NewIcon />,
}}
value={option.defaultValue}
onChange={handleChange}
name={option.title}
size="small"
>
{menuItemsValue.map((title, idx) => (
<MenuItem key={idx} value={title}>
{title}
</MenuItem>
))}
</TextField>
<div onClick={handleSorting} className={styles.sorter__sortOrder}>
<div className={styles.sorter__sortOrder__alphabetical}>
<span>A</span>
<span>Z</span>
</div>
<div className={styles.sorter__sortOrder__sortLine}>
<SorterLine />
</div>
</div>
</div>
</div>
);
}
export default SorterField;
<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>
You're re-assigning a value to object in each iteration of fields.map().
If you want to build object as a map of field item name to defaultValue (or blank string), use this instead...
return Object.fromEntries(
fields.map(({ name, defaultValue }) => [name, defaultValue ?? ""])
);
See Object.fromEntries()
Also, move the fields declaration outside your component. It's static content so can be omitted from being declared every render and from any hook dependencies.
You could also use fields.reduce() which is basically the same thing
return fields.reduce(
(obj, { name, defaultValue }) => ({
...obj,
[name]: defaultValue ?? "",
}),
{}
);
As for removing selected options as you iterate through the fields, that's a little trickier.
You could use a memo hook to create an iterable data structure that includes the available options for that particular iteration.
For example
const fieldsWithOptions = useMemo(() => {
const taken = new Set(Object.values(select));
const available = menuItemsValue.filter((item) => !taken.has(item));
return fields.map((field) => ({
...field,
options: select[field.name]
? [select[field.name], ...available] // include the current selection
: available,
}));
}, [menuItemsValue, select]);
Then map over fieldsWithOptions instead of fields and use option.options instead of getMenuItems.
I need to update the state on main page, the problem is that I update values 3 levels down...
This component is where I get all the cities, putting the data on the State and map through cities.
CitiesPage.tsx
export const CitiesPage = () => {
const [cities, setCities] = useState<City[]>([]);
useEffect(() => {
getCities().then(setCities);
}, []);
return (
<>
<PageTitle title="Cities" />
<StyledCitySection>
<div className="headings">
<p>Name</p>
<p>Iso Code</p>
<p>Country</p>
<span style={{ width: "50px" }}></span>
</div>
<div className="cities">
{cities.map((city) => {
return <CityCard key={city.id} city={city} />;
})}
</div>
</StyledCitySection>
</>
);
};
On the next component, I show cities and options to show modals for delete and update.
CityCard.tsx.
export const CityCard = ({ city }: CityProps) => {
const [showModal, setShowModal] = useState(false);
const handleModal = () => {
setShowModal(true);
};
const handleClose = () => {
setShowModal(false);
};
return (
<>
{showModal && (
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} />
</ModalPortal>
)}
<StyledCityCard>
<p className="name">{city.name}</p>
<p className="isoCode">{city.isoCode}</p>
<p className="country">{city.country?.name}</p>
<div className="options">
<span className="edit">
<FiEdit size={18} onClick={handleModal} />
</span>
<span className="delete">
<AiOutlineDelete size={20} />
</span>
</div>
</StyledCityCard>
</>
);
};
and finally, third levels down, I have this component.
EditCityForm.tsx.
export const EditCityForm = ({ city, closeModal }: Props) => {
const [updateCity, setUpdateCity] = useState<UpdateCity>({
countryId: "",
isoCode: "",
name: "",
});
const handleChange = (
evt: ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { target } = evt;
setUpdateCity({ ...updateCity, [target.name]: target.value });
};
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
updateCityHelper(cityId, updateCity);
closeModal(false);
};
useEffect(() => {
setUpdateCity({
isoCode: city.isoCode,
name: city.name,
countryId: city.country?.id,
});
}, [city]);
return (
<form onSubmit={handleSubmit}>
<Input
label="Iso Code"
type="text"
placeholder="Type a IsoCode..."
onChange={handleChange}
name="isoCode"
value={updateCity.isoCode}
/>
<Input
label="Name"
type="text"
placeholder="Paste a Name.."
onChange={handleChange}
name="name"
value={updateCity.name}
/>
<CountrySelect
label="Country"
onChange={handleChange}
value={city.country?.name || ""}
name="countryId"
/>
<Button type="submit" color="green" text="Update" />
</form>
);
};
Edit form where retrieve data passed from CityCard.tsx and update State, passing data to a function that update Info, closed modal and... this is where I don't know what to do.
How can I show the info updated on CitiesPage.tsx when I submitted on EditCityForm.tsx
Any help will be appreciated.
Thanks!
You are storing the updated value in a different state, namely the updateCity state, but what you should be doing is update the origional cities state. While these two states are not related, and at the same time your UI is depend on cities state's data, so if you wish to update UI, what you need to do is update cities' state by using it's setter function setCities.
Just like passing down state, you pass it's setters as well, and use the setter function to update state's value:
// CitiesPage
{cities.map((city) => {
return <CityCard key={city.id} city={city} setCities={setCities} />;
})}
// CityCard
export const CityCard = ({ city, setCities }: CityProps) => {
// ...
return (
// ...
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} setCities={setCities} />
</ModalPortal>
// ...
)
}
// EditCityForm
export const EditCityForm = ({ city, closeModal, setCities }: Props) => {
// const [updateCity, setUpdateCity] = useState<UpdateCity>({ // no need for this
// countryId: "",
// isoCode: "",
// name: "",
// });
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
setCities(); // your update logic
closeModal(false);
};
}
I'd advise you to use React-Redux or Context API whenever you have nested structures and want to access data throughout your app.
However in this case you can pass setCities and cities as a prop to CityCard and then pass this same prop in the EditCityForm component and you can do something like this in your handleSubmit.
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
let updatedCities = [...cities];
updatedCities.forEach(el => {
if(el.id == updateCity.id) {
el.countryCode = updateCity.countryCode;
el.name = updateCity.name;
el.isoCode = updateCity.isoCode;
}
})
setCities(updatedCities);
closeModal(false);
};
I want to when the user clicks submit button it should add new object. with the given value which is stored in list varaible.It should not delete the old objects,only name Property should be added.
import { useState } from "react";
export default function Add() {
const [todo, addTodo] = useState([{ name: "cat", time: "2 minutes ago" }]);
const [list, setList] = useState();
const add = () => {
addTodo({...todo, todo: {
name: list
}})
};
return (
<div>
<input
type="text"
value={list}
onChange={(e) => setList(e.target.value)}
/>
<button onClick={add}>Add new</button>
{todo.map((item, i) => (
<li key={i}>
{item.name}
<span style={{ marginLeft: "15px" }}>{item.time}</span>
</li>
))}
</div>
);
}
When your state update is dependent on the previous state value ,its a good practice to make the state updater to have an inline function .
addTodo(existingTodos => ([...existingTodos, { name: list}] ))
Here we are saying , get the exisitingTodos list and append the new todo along with the exisitingTodos .
const add = () => {
addTodo(existingTodos => ([...existingTodos, { name: list}] ))
};
Because todo is an array so you should pass an array when call addTodo
addTodo([
...todo,
{
name: list,
},
]);
Hey I'm new to React and I'm having a problem with my prop which I'm passing to my ChildComponent. I am using "react-select" and have two multiselects. Originally I wanted to show in each select the value for the corresponding select I get from the state.
value={optionsColor.filter(item => ( myTest.color.includes(item.value)))}
But this is not possible because one of my calls is always "undefined". For example "myTest.Color" and "myTest.Car" one of them is "undefined" but I don't know why?
In my code (ChildComponent) I have two console.logs which illustrate this.
For example, if I select Color and have previously selected a car in Car, the console.log output looks like this.
undefined
blue
But I want it to output both.
import {useState} from "react";
import ChildComponent from "./ChildComponent";
const ParentComponent = () => {
const [step, setStep] = useState(0)
const [myTest, setMyTest] = useState(
{
color: ['defaultColor'],
car: ['defaultCar'],
}
)
const handleChange = (e, action) => {
setMyTest({ [action.name]: e ? e.map(x => x.value) : [] })
}
return (
<div>
<div className="card-body container mt-3">
<h2>Product data input Intended Use</h2>
<div className="card p-2 mt-5">
<form className="mb-4">
<div className="form"></div>
<ChildComponent myTest={myTest} handleChange={handleChange}/>
</form>
</div>
</div>
</div>
)
}
export default ParentComponent;
import React, { useState } from 'react';
import Select from 'react-select';
const optionsColor = [
{ value: 'blue', label: 'Blue' },
{ value: 'red', label: 'Red' },
{ value: 'yellow', label: 'Yellow' }
]
const optionsCar = [
{ value: 'bmw', label: 'BMW' },
{ value: 'ford', label: 'Ford' },
{ value: 'vw', label: 'VW' },
]
const ChildComponent = ({ handleChange, myTest}) => {
return (
<div>
<h4>Car {console.log(myTest.car)}</h4>
<Select
name="car"
options={optionsCar}
className="mb-3"
onChange={handleChange}
//value={intendedUse.sex === undefined ? '' : optionsSex.filter(item => (intendedUse.sex.includes(item.value)))}
isMulti
autoFocus
isSearchable
/>
<h4>Color {console.log(myTest.color)}</h4>
<Select
name="color"
options={optionsColor}
className="mb-3"
onChange={handleChange}
//value={intendedUse.age === undefined ? '': optionsAge.filter(item => ( intendedUse.age.includes(item.value)))}
isMulti
autoFocus
isSearchable
/>
</div>
)
}
export default ChildComponent;
the problem lies here.
setMyTest({ [action.name]: e ? e.map(x => x.value) : [] })
When you're updating your myTest state you're actually replacing both of the fields with the field you're setting.
Try something like this:
setMyTest(myTest => ({...myTest, { [action.name]: e ? e.map(x => x.value) : [] }}));
In that way, you have a new object with both the field that changed and the one that didn't.
I am not able to get an updated state from the custom hook.
the example is simple. this is a custom hook allowing to switch from dark to light theme
inside the custom hook, I use useMemo to return a memoized value, but this does not seem to update each time I update the state with switchOn.
I do not wish to use a context in this example
import { useState, useMemo, useCallback } from "react";
const useTheme = (initial) => {
const [isOn, turnOn] = useState(false);
const switchOn = useCallback((e) => turnOn(e.target.checked), [turnOn]);
return useMemo(() => {
return {
theme: isOn ? "light" : "dark",
buttonTheme: isOn ? "dark" : "light",
lightsIsOn: isOn ? "On" : "Off",
isChecked: isOn,
switchOn,
};
}, [switchOn, isOn]);
};
export default useTheme;
import { useState, useRef, useReducer } from "react";
import useTheme from "./useTheme";
import todos from "./reducer";
import "./App.css";
const Item = ({ id, text, done, edit, remove }) => {
const { buttonTheme } = useTheme();
const isDone = done ? "done" : "";
return (
<div style={{ display: "flex" }}>
<li className={isDone} key={id} onClick={() => edit(id)}>
{text}
</li>
<button type="button" onClick={() => remove(id)} className={buttonTheme}>
<small>x</small>
</button>
</div>
);
};
const SwitchInput = () => {
const { switchOn, isChecked, lightsIsOn } = useTheme();
return (
<>
<label class="switch">
<input type="checkbox" onChange={switchOn} checked={isChecked} />
<span class="slider round"></span>
</label>
<span>
{" "}
Lights <b>{lightsIsOn}</b>
</span>
<br /> <br />
</>
);
};
const initialState = {
items: [],
};
const init = (state) => {
return {
...state,
items: [
{ id: 1, text: "Learning the hooks", done: false },
{ id: 2, text: "Study JS", done: false },
{ id: 3, text: "Buy conference ticket", done: false },
],
};
};
function App() {
const inputRef = useRef();
const [state, dispatch] = useReducer(todos, initialState, init);
const [inputValue, setInputValue] = useState(null);
const { theme } = useTheme();
const handleOnChange = (e) => setInputValue(e.target.value);
const handleOnSubmit = (e) => {
e.preventDefault();
dispatch({ type: "add", payload: inputValue });
inputRef.current.value = null;
};
return (
<div className={`App ${theme}`}>
<SwitchInput />
<form onSubmit={handleOnSubmit}>
<input ref={inputRef} type="text" onChange={handleOnChange} />
<ul>
{state.items.map((item) => (
<Item
{...item}
key={item.id}
remove={(id) => dispatch({ type: "remove", payload: { id } })}
edit={(id) => dispatch({ type: "edit", payload: { id } })}
/>
))}
</ul>
</form>
</div>
);
}
export default App;
my custom hook: useTheme.js
below, the component where I want to access logic from custom hook: App.js
I use an App.css to apply theme style : dark and light
below a demo. We can see that the values do not change
How is an effective way to subscribe to state changes share global states between components?
You need to pass the values of useTheme() from <App> to <SwitchInput> to share it as follows.
function App() {
const { theme, switchOn, isChecked, lightsIsOn } = useTheme();
return (
<div className={`App ${theme}`}>
<SwitchInput
switchOn={switchOn}
isChecked={isChecked}
lightsIsOn={lightsIsOn}
/>
</div>
);
}
Then, <SwitchInput> will receive them in props.
const SwitchInput = ({ switchOn, isChecked, lightsIsOn }) => {
return (
<>
<label class="switch">
<input type="checkbox" onChange={switchOn} checked={isChecked} />
<span class="slider round"></span>
</label>
<span>
{" "}
Lights <b>{lightsIsOn}</b>
</span>
<br /> <br />
</>
);
};
In your example, useTheme() is called in both <App> and <SwitchInput>, but theme and other values are initialized separately and are not shared between each component.