Setting the values of an object in an array in React? - javascript

I am building a swipeable card in React. The card contains 4 slides. The values displayed in the card rely on the user input.
First I am defining a sample object like this:
const initialState = { id: '', title: '', name: '', image: ''};
Inside my component, I am defining the array state like:
const [card, setCard] = useState([initialState]);
I am displaying the card side by side along with the user input fields for users to view the cards as they compose. So whenever the user adds/edits a specific value of the card he can view it live on the card.
We can set the state of an object for each field like this:
<Input id='title' name='title' placeholder="Enter Title" type='text' value={card.title} onChange={handleChange}/>
Handle Change function:
const handleChange = (e) => {
setCard({ ...card, [e.target.name]: e.target.value });
}
But this is not possible for the above-mentioned array of objects. So how to handle this situation?
Whenever a user swipes the previous/next card the fields must be populated with the appropriate values so that he can edit them. Simply, a user must be able to edit any field at any time. Whenever a user adds a new card a new object must be pushed to the array state.
Full code:
const initialState = { id: '', title: '', name: '', image: ''};
const Home = () => {
const [card, setCard] = useState([initialState]);
const isdisabled = true;
const handleChange = (e) => {
setCard({ ...card, [e.target.name]: e.target.value });
}
const handleAdd = () => {
//TODO
}
return (
<Flex>
<Center>
<Flex bg="white" w="lg" h="420" borderRadius="lg" m="7" p="2" alignItems="center">
<Box w="48" align="center">
<IconButton aria-label='Go to previous' disabled borderRadius="full" bg='gray.200' color='black' icon={<ChevronLeftIcon w={6} h={6}/>} />
</Box>
<Box>
<Image src={card[0].image} w="full" h="44" objectFit="cover" objectPosition="0 0" borderRadius="lg" />
<Heading color="black" size='lg'>{card[0].title}</Heading>
<Text color="black" size='40'>{card[0].namee}</Text>
</Box>
<Box w="48" align="center">
<IconButton aria-label='Go to previous' disabled borderRadius="full" bg='gray.200' color='black' icon={<ChevronRightIcon w={6} h={6}/>} />
</Box>
</Flex>
</Center>
<Flex direction="column" w="lg" gap="4" m="7">
<Input placeholder="Enter Title" value={card[0].title} onChange={handleChange}/>
<Input placeholder="Enter Name" value={card[0].name} onChange={handleChange}/>
<Button onClick={handleClick}>Upload Image</Button>
<Button onClick={handleAdd}>Add another slide</Button>
<Button colorScheme="blue">Done</Button>
</Flex>
</Flex>
)
}
export default Home
How to seamlessly do this? Any help would be appreciated. Thank you.

your card state is array of objects need to update array first object
const handleChange = (e) => {
const arr = [...card]
arr[0] = {...arr[0], [e.target.name]: e.target.value }
setCard(arr);
}

#Gabriele Petrioli's answer is the perfect solution to my problem except it needs a little tweaking:
Add activeCardIndex to both navigation handlers' dependency list:
const handleGotoNext = useCallback(() => {
// you need to also handle not allowing to go beyond the max
if(activeCardIndex < cards.length-1){
setActiveCardIndex(prevActive => prevActive + 1);
}
}, [activeCardIndex]);
const handleGotoPrevious = useCallback(() => {
// you need to also handle not allowing to go below 0
if(activeCardIndex > 0){
setActiveCardIndex(prevActive => prevActive - 1);
}
}, [activeCardIndex]);
And the handleChange function:
const handleChange = useCallback((e) => {
setCards(prevCards => prevCards.map((card, index) => {
if (index === activeCardIndex) {
return { ...card,
[e.target.name]: e.target.value
}
}else {
return card;
}
}));
}, [activeCardIndex]);

You would likely need an additional state variable, specifying the active card
something like
const [cards, setCards] = useState([initialState]);
const [activeCardIndex, setActiveCardIndex] = useState(0);
handleGotoNext = useCallback(() => {
// you need to also handle not allowing to go beyond the max
setActiveCardIndex(prevActive => prevActive + 1);
}, []);
const handleGotoPrevious = useCallback(() => {
// you need to also handle not allowing to go below 0
setActiveCardIndex(prevActive => prevActive - 1);
}, []);
const handleChange = useCallback((e) => {
setCards(prevCards => prevCards.map((card, index) => {
if (index === activeCardIndex) {
return { ...card,
[e.target.name]: e.target.value
}
}
return card;
}));
}, [activeCardIndex]);
const handleAdd = useCallback(() => {
const newCards = [...cards, { ...initialState
}];
setCards(newCards);
setActiveCardIndex(newCards.length - 1);
}, [cards]);
const activeCard = cards[activeCardIndex];
// for the rendering you should use the activeCard constant, instead of cards[n]
return (
<Flex>
...
<Image src={activeCard.image} w="full" h="44" objectFit="cover" objectPosition="0 0" borderRadius="lg" />
...
</Flex>
)

Related

TypeError: employee.push is not a function

I have a form where the user can add input fields with a click event. I built a handler where the user can add multiple text fields and insert multiple names that will be sent to a database.
I was successful in adding input fields, following this link
I ran into a challenge where I could not submit the string data to the database. I solved this issue by pushing the value of the input field to the employee array.
My problem: when I add a second text field a get the error employee.push is not a function.
My question how can I solve this? For example, if the user wants to add two names by adding two text fields to input the names, how can I fix the code to send the names to the DB?
I know I have to map the result of the original map to do this, but I am not sure how to insert the target value from the handler event in the employee array (where the name strings are stored in the DB).
It got the code to only work once, when you add one name and submit, the name is sent to the database, however when more than one name is added (even before the submit) the code breaks.
I am using context and useState hooks:
AddAccountsContxt.js
import React, { useState, createContext } from "react";
import axios from "axios";
export const AddAccountsContxt = createContext();
export const AddAccountsContxtProvider = (props) => {
const [company, setCompany] = useState("");
const [address, setAddress] = useState("");
const [phone, setPhone] = useState("");
const [details, setDetails] = useState([]);
const [employee, setEmployee] = useState([]);
const addAccountHandler = (e) => {
e.preventDefault();
setDetails([...details, employee]);
console.log('employee', details)
details.push({employee:employee});
axios({
method: "POST",
url: "http://localhost:5000/insert-form",
data: {
company: company,
address: address,
phone: phone,
// employee: employee,
details: details
},
});
};
return (
<>
<AddAccountsContxt.Provider
value={[
company,
setCompany,
address,
setAddress,
phone,
setPhone,
employee,
setEmployee,
details,
addAccountHandler,
]}
>
{props.children}
</AddAccountsContxt.Provider>
</>
);
};
EmployeeName.js
import { React, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { Col } from "react-bootstrap";
import { Row } from "react-bootstrap";
const EmployeeName = ({ employee, details, setEmployee}) => {
const [inputList, setInputList] = useState([{ employee: "" }]);
const handleInputChange = (e, index) => {
const { name, value } = e.target;
const list = [...inputList];
list[index][name] = value;
setInputList(list);
console.log("value", value);
//below code implamented to push name string to employee array
setEmployee(...employee, value);
if (value) {
employee.push({value:value});
}
};
const handleRemoveClick = (index) => {
const list = [...inputList];
list.splice(index, 1);
setInputList(list);
};
const AddInputField = () => {
setInputList([...inputList, { employee: "" }]);
};
return (
<>
<Button onClick={AddInputField}>
click here to add company employees
</Button>
{inputList.length > 0 &&
inputList.map((x, i) => {
return (
<Row key={i}>
<Col sm={11}>
<Form.Group>
<Form.Control
placeholder="Add Employee Name"
onChange={(e) => handleInputChange(e, i)}
value={x.employee}
name="employee"
type="text"
/>
</Form.Group>
</Col>
<Col>
{inputList.length - 1 === i ? (
<Button
onClick={AddInputField}
> Add
</Button>
) : (
<Button onClick={() => handleRemoveClick(i)}>Remove</Button>
)}
</Col>
</Row>
);
})}
</>
);
};
export default EmployeeName;
In React, state should be treated as immutable, so you shouldn't even be pushing to elements to employee, but rather pass in an entirely new array to setEmployee.
When you call setEmployee(...employee, value) in handleInputChange of EmployeeName, you set employee as the first element of its previous value, which makes it no longer an array on its next render, causing the TypeError when attempting to push. Instead put a useEffect with a dependency on the inputList so that when the inputList is set, map the inputs to an employee.
const EmployeeName = ({ setEmployee }) => {
const [inputList, setInputList] = useState([{ employee: '' }]);
useEffect(() => setEmployee(inputList.map((input) => ({ value: input.employee }))), [inputList]);
const handleInputChange = (event, index) => {
const { name, value } = event.target;
const list = [...inputList];
list[index][name] = value;
setInputList(list);
};
const handleRemoveClick = (index) => setInputList(inputList.filter((_, i) => i !== index));
const AddInputField = () => setInputList([...inputList, { employee: '' }]);
return (
<>
<Button onClick={AddInputField}>click here to add company employees</Button>
{inputList.map((input, index) => (
<Row key={index}>
<Col sm={11}>
<Form.Group>
<Form.Control
placeholder='Add Employee Name'
onChange={(event) => handleInputChange(event, index)}
value={input.employee}
name='employee'
type='text'
/>
</Form.Group>
</Col>
<Col>
{inputList.length - 1 === index ? (
<Button onClick={AddInputField}> Add</Button>
) : (
<Button onClick={() => handleRemoveClick(index)}>Remove</Button>
)}
</Col>
</Row>
))}
</>
);
};
This also applies to all other states like detail which should be rewritten to just setDetail([...details, {employee}] instead of attempting to push to state.

How to get values of dynamic/multiple fields on formsubmit with React?

I have a form where the user can add Textfields in order to add more items, and I'm struggling to get the items out of the form when they submit it.
The Textfields are created by a component function (as below) and the whole form is not contained in one function, as I've seen in many examples, which means that I can't simply store the values as a state and read them later (I think..?).
The fields are generated with the following code:
const itemNameTextFields = () => {
const [itemNames, setItemNames] = React.useState(['']);
const addItemNameField = () => setItemNames([...itemNames, '']);
const handleInputChange = (i, e) => {
const values = [...itemNames];
values[i] = e.target.value;
setItemNames(values);
}
return (
itemNames.map((itemName, i) => (
<Box component='span' key={`item_${i}`} sx={{display: 'flex', flexWrap: 'wrap'}}>
<TextField
value={itemName}
onChange={e => handleInputChange(i, e)}
label={`Item ${i+1}`}
name={`item_${i}`}
className={classes.itemName}
/>
{itemNames.length-1 === i &&
<Button
endIcon={<Icon icon={plusCircleOutline} />}
onClick={addItemNameField}
>
<small>Add another</small>
</Button>
}
</Box>
));
);
}
And my code for handling the form being submitted is something like this:
const handleSubmit = e => {
e.preventDefault();
let obj = {
/* ... */
name : e.target['name'].value || '',
items : [] //this is what I am missing
};
console.log(obj);
}
This may not be a good way of doing this, however.
What I'd like to achieve is either:
for items within obj to be an array containing the values of each Textfield from the first code block. E.g obj = {items: ['item1', 'item2',/ .../]}
or
for obj to contain each item individually. E.g obj = {item_1: 'item1', item_2: 'item'2, /.../}
Ideally without any third-party libraries where a solution can be found without them.
One possible 'solution' I've thought about would be having a hidden input element with a state that'd contain the array and then getting the value of that on formsubmit, rather than each individual field, but this results in the array being 'sent' as a comma-separated list, which could cause issues if any of the values have commas in.
Alternatively, finding a way to pass the value of each input field to handleSubmit (perhaps through props?) could work as well, but I'm not quite sure how to achieve this.
Thanks :)
You can add a redux store to pass the parameters as follows;
const React = window.React;
const ReactDOM = window.ReactDOM;
const {Box, Button, TextField} = window.MaterialUI;
const Redux = window.Redux;
function itemReducer(state = [], action) {
switch (action.type) {
case 'SET':
let tmp = [...state];
tmp[action.index] = action.item;
return state = tmp;
default:
return state;
}
}
const store = Redux.createStore(itemReducer, [])
const handleSubmit = e => {
e.preventDefault()
let obj = {
name : e.target.innerHTML || '',
items : store.getState()
}
console.log(obj)
}
const App = () => {
const itemNameTextFields = () => {
const [itemNames, setItemNames] = React.useState([''])
const addItemNameField = () => {
setItemNames([...itemNames, '']);
}
const handleInputChange = (i, e) => {
store.dispatch({
type:"SET",
index:i,
item:e.target.value
});
const values = [...itemNames]
values[i] = e.target.value
setItemNames(values)
}
return (
itemNames.map((itemName, i) => (
<Box component='span' key={"item_" + i} sx={{display: 'flex', flexWrap: 'wrap', marginBottom:1}}>
<TextField
value={itemName}
onChange={e => handleInputChange(i, e)}
label={"Item " + i}
name={"item" + i}
variant="outlined"
fullWidth
/>
{itemNames.length-1 === i &&
<Button
size='small'
onClick={addItemNameField}
disabled={!itemNames[itemNames.length-1].length > 0}
style={{marginLeft:4}}
>
<small>Add another</small>
</Button>
}
</Box>
))
)
}
return itemNameTextFields();
}
ReactDOM.render(
<div><App/><Button onClick={handleSubmit}>Submit</Button></div>,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/redux#4.1.1/dist/redux.js"></script>
<script src="https://unpkg.com/#material-ui/core#latest/umd/material-ui.development.js"></script>
<div id="root"></div>
You could keep track of the items in the element that renders the form, in other words move the usestate to the form element. The pass the state value and setter to the child texfield elements and call the statesetter instead of letting those elements have their own state.
You should be able to access that state within your onsubmit
Sometimes react behaves in a mysterious way. You can try directly adding the mapping value and name from itemNames array.
const handleInputChange = (i, e) => {
//const values = [...itemNames];
//values[i] = e.target.value;
itemNames[i] = e.target.value
setItemNames([...itemNames]);
}
In this way you can update the array of itemNames.
I ended up solving this problem by managing state inside the main form component rather than in each individual form element component, and then passing the useState functions to each component, as such:
const FormElement = ({value, onChange)} => {
const handleChange = e => {
onChange(e.target.value)
}
return (
<TextField
{...props}
onChange={handleChange}
value={value}
/>
)
}
const AddNewEventForm = () => {
const classes = useStyles()
const [formElementValue, setFormElementValue] = React.useState('')
const handleSubmit = e => {
e.preventDefault()
/* ... */
console.log(formElementValue)
}
return (
<form onSubmit={handleSubmit}>
<Box>
<FormElement value={formElementValue} onChange={setFormElementValue} />
</Box>
<Box>
<Button type="submit">
Submit
</Button>
</Box>
</form>
)
}
By handling the state in this way, the data from the child components can be accessed by handleSubmit inside the parent function.

React Props wont update in Child Components

Following Situation.
I have a functional Parent Component like this:
function TestAutomationTab() {
const theme = createMuiTheme({
typography: {
htmlFontSize: 10,
useNextVariants: true,
},
});
const [szenarios, setSzenarios] = useState([]);
const [filterSzenario, setFilterSzenario] = useState('ALL');
const [data, setData] = useState([{}]);
const [runAll, setRunAll] = useState(false);
const [runAllButton, setRunAllButton] = useState('RUN ALL');
useEffect(() => {
fetchDistinctSzenarios();
fetchTestfaelle();
}, []);
async function fetchDistinctSzenarios() {
const response = await Api.getDistinctTestautoSzenarios();
setSzenarios(response.data);
setSzenarios(oldState => [...oldState, 'ALLE']);
}
function handleFilterChange(event) {
setFilterSzenario(event.target.value);
fetchTestfaelle();
}
async function fetchTestfaelle() {
const response = await Api.getAllOeTestfaelle();
response.data.forEach((e) => {
e.status = 'wait';
e.errorStatus = '';
e.statusText = '-';
});
setData(response.data);
}
function sendSingleCase(id) {
data.forEach((e) => {
if(e.id === id){
e.status = 'sending';
}
})
}
return (
<React.Fragment>
<MuiThemeProvider theme={theme}>
<div style={styles.gridContainer}>
<Upload />
<TestautomationSzenarioFilter
/>
<DocBridgePieChart />
<div style={styles.uebersicht}>
{filterSzenario.length ? <OeTestfallAccordion
choosenFilter={filterSzenario}
testData={data}
runAll={runAll}
sendSingleCase={sendSingleCase}
/> : <div>Wähle Szenario</div>}
</div>
</div>
</MuiThemeProvider>
</React.Fragment>
);
}
OeTestfallAccordion
function OeTestfallAccordion(props) {
const data = props.testData;
return (
<React.Fragment>
{data.map(e => (<OeTestfall
key={e.id}
szenario={e.szenario}
testid={e.testfallid}
json={e.json}
status={e.status}
runAll={props.runAll}
errorStatus={e.errorStatus}
statusText={e.statusText}
sendSingleCase={props.sendSingleCase}
/>))}
</React.Fragment>
);
}
OeTestfall
function OeTestfall(props) {
const { szenario, testid, json } = props;
const [open, setOpen] = useState(false);
function handleOpen(event) {
event.stopPropagation();
setOpen(true);
}
function handleClose() {
setOpen(false);
}
return (
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreOutlined />}>
<OeTestfallSummary
szenario={szenario}
testid={testid}
json={json}
status={props.status}
handleClose={handleClose}
handleOpen={handleOpen}
open={open}
statusText={props.statusText}
errorStatus={props.errorStatus}
sendSingleCase={props.sendSingleCase}
/>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<div>ForNoError</div>
</ExpansionPanelDetails>
<ExpansionPanelActions>
<Button
variant="outlined"
color="primary"
>
Bearbeiten
</Button>
<Button
variant="outlined"
color="secondary"
>
Löschen
</Button>
</ExpansionPanelActions>
</ExpansionPanel>
);
}
OeTestfallSummery
function OeTestfallSummary(props) {
const { handleOpen } = props;
const [status, setStatus] = useState('');
const [statusText, setStatusText] = useState('');
const [errorStatus, setErrorStatus] = useState('');
useEffect(() => {
setErrorStatus(props.errorStatus);
setStatusText(props.statusText);
setStatus(props.status);
}, []);
return (
<div style={styles.summaryWrapper}>
<Typography align="center" variant="subtitle1">
TestID: {props.testid}
</Typography>
<Typography align="center" variant="subtitle1" style={{ fontWeight: 'bold' }}>
{props.szenario}
</Typography>
<Button
size="small"
variant="outlined"
color="primary"
onClick={handleOpen}
>
JSON
</Button>
<Tooltip title="VorneTooltip" style={styles.lightTooltip} placement="left">
<Chip
color="secondary"
variant="outlined"
label={status}
/>
</Tooltip>
<StatusChip
status={errorStatus}
/>
<OeJsonViewer json={JSON.parse(props.json)} open={props.open} handleClose={props.handleClose} stopEventPropagation />
<Tooltip
title="ToolTipTitel"
style={styles.lightTooltip}
placement="top"
>
<Chip
color="primary"
variant="outlined"
label={statusText}
/>
</Tooltip>
<Button variant="contained" color="primary" onClick={() => props.sendSingleCase(props.testid)} >
Run
</Button>
<Button variant="contained" color="primary" onClick={() => console.log(status)} >
test
</Button>
</div>
);
}
In my OeTestfallAccordion the prop testData does not update. If i try to console.log it inside my childComponent it has the old Value like before i execute the sendSinglecase function. What do i need to do, that i update the Data correctly that my child component gets notified that the props had changed and it has to rerender.
EDIT:
I tried some new things and can narrow down the problem. In my TestAutomationTab Component i send the whole data State to the OeTestfallAccordion Child Component. In this OeTestfallAccordion Component i split up the Array of Data which consists of multiple Objects like:
0: {id: 41, testfallid: 1, json: "{\"testCaseData\":{\"baseData\":{\"Check\":\"Thing…e\":\"alle\",\"tuwid\":\"2909\"}},\"testType\":\"Test\"}}", ID: null, businessId: null, …}
1: {id: 42, testfallid: 2, json: "{\"testCaseData\":{\"baseData\":{\"testfallid\":\"1…e\":\"alle\",\"tuwid\":\"2909\"}},\"testType\":\"Test\"}}", edcomAuftragsId: null, businessId: null, …}
When i hit the function sendSingleCase in my Parent Component TestAutomationTab i just change one single Parameter of the Object. The whole construct of Data keeps the same. The Child Component doesnt recognize that i changed something in the Object of Data.
But i dont know why? I also tried to useEffect on Props change in my Child COmponent when the props are changed. But it never gets executed even tho some attributes got updated inside the props.data.
function OeTestfallAccordion(props) {
const testData = props.testData;
const [data, setData] = useState(testData);
useEffect(() => {
setData(testData);
console.log("triggered");
}, [props]);
...
}
Okay things worked out a bit.
I changed the sendSingleCase function to first Copy the whole state in a Temp variable. Change one Attribute inside an Object and then setData (inside useState) with the tempData Variable. So the whole State gets renewed and the child components recognize the change and rerender.
But it seems not to be very fast. Always to copy the whole Data in a new Variable and then reassign it is very Ressource heavy. Is there a better solution?
function sendSingleCase(id) {
const tempState = [...data];
tempState.forEach((e) => {
if (e.testfallid === id) {
e.status = "pressed";
console.log(e.status);
}
});
setData(tempState);
}

How to get the number of checked checkboxes in React.js?

I started learning React not so long ago. Decided to make some kind of "life checklist" as one of my beginner projects. I have been using Functional Components in the core.
FYI:
I have data.js as an array of objects where "action", "emoji" and unique ID are stored.
I import it into my App.js.
const App = () => {
//Looping over data
const items = data.map((item) => {
return (
<ChecklistItem action={item.action} emoji={item.emoji} key={item.id} />
);
});
return (
<>
<GlobalStyle />
<StyledHeading>Life Checklist</StyledHeading>
<StyledApp>{items}</StyledApp>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
Here is my <ChecklistItem/> component:
const ChecklistItem = ({ action, emoji }) => {
//State
const [isActive, setIsActive] = useState(false);
//Event Handlers
const changeHandler = () => {
setIsActive(!isActive);
};
return (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
);
};
export default ChecklistItem;
I would be satisfied with the functionality so far, but I need to show how many "active" checklist items were chosen in the parent <App/> component like "You have chosen X items out of {data.length}. How can I achieve this?
I assume that I need to lift the state up, but cannot understand how to implement this properly yet.
You can do that by simply creating a state for storing this particular count of active items.
To do that, you would need to update your <App/> component to something like this
const App = () => {
const [activeItemsCount, setActiveItemsCount] = useState(0);
//Looping over data
const items = data.map((item, index) => {
return (
<ChecklistItem
key={index}
action={item.action}
emoji={item.emoji}
setActiveItemsCount={setActiveItemsCount}
/>
);
});
return (
<>
<h1>Life Checklist</h1>
<div>{items}</div>
<div>Active {activeItemsCount} </div>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
And then in your <ChecklistItem /> component, you would need to accept that setActiveItemsCount function so that you can change the state of the activeItemsCount.
import React, { useState, useEffect } from "react";
const ChecklistItem = ({ action, emoji, setActiveItemsCount }) => {
const [isActive, setIsActive] = useState(false);
const changeHandler = () => {
setIsActive(!isActive);
};
useEffect(() => {
if (!isActive) {
setActiveItemsCount((prevCount) => {
if (prevCount !== 0) {
return prevCount - 1;
}
return prevCount;
});
}
if (isActive) {
setActiveItemsCount((prevCount) => prevCount + 1);
}
}, [isActive, setActiveItemsCount]);
return <input type="checkbox" checked={isActive} onChange={changeHandler} />;
};
export default ChecklistItem;
By using the useEffect and the checks for isActive and 0 value, you can nicely increment or decrement the active count number by pressing the checkboxes.
How about this?
const data = [
{ action: '1', emoji: '1', id: 1 },
{ action: '2', emoji: '2', id: 2 },
{ action: '3', emoji: '3', id: 3 },
];
const ChecklistItem = ({ action, emoji, isActive, changeHandler }) => {
return (
<div isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<div>{emoji}</div>
<div>{action}</div>
</div>
);
};
const PageContainer = () => {
const [checkedItemIds, setCheckedItemIds] = useState([]);
function changeHandler(itemId) {
if (checkedItemIds.indexOf(itemId) > -1) {
setCheckedItemIds((prev) => prev.filter((i) => i !== itemId));
} else {
setCheckedItemIds((prev) => [...prev, itemId]);
}
}
const items = data.map((item) => {
const isActive = checkedItemIds.indexOf(item.id) > -1;
return (
<ChecklistItem
isActive={isActive}
changeHandler={() => changeHandler(item.id)}
action={item.action}
emoji={item.emoji}
key={item.id}
/>
);
});
return (
<div className="bg-gray-100">
<div>{items}</div>
<h2>
You have chosen {checkedItemIds.length} items out of {data.length}
</h2>
</div>
);
};
When data is used by a child component, but the parent needs to be aware of it for various reasons, that should be state in the parent component. That state is then handed to the child as props.
One way to do this would be to initialize your parent component with a piece of state that was an array of boolean values all initialized to false. Map that state into the checkbox components themselves and hand isActive as a prop based on that boolean value. You should then also hand the children a function of the parent that will change the state of the boolean value at a certain index of that array.
Here's a bit of a contrived example:
// Parent.tsx
const [checkBoxes, setCheckboxes] = useState(data.map(data => ({
id: data.id,
action: data.action,
emoji: data.emoji
isActive: false,
})));
const handleCheckedChange = (i) => {
setCheckboxes(checkBoxes => {
checkBoxes[i].isActive = !checkBoxes[i].isActive;
return checkBoxes;
})
}
return(
checkBoxes.map((item, i) =>
<ChecklistItem
action={item.action}
emoji={item.emoji}
key={item.id}
index={i}
isActive={item.isActive}
handleChange={handleCheckedChange}
/>
)
);
// CheckListItem.tsx
const CheckListItem = ({ action, emoji, index, isActive, handleChange }) => (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={() => handleChange(index)} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
)

How to update state value of variable that uses custom Hook

My Component has form input fields. These made use of a useState hook with their value and setValue for each input field. I want to optimize my component so the input fields made use of the same custom Hook which I called useFormInput
Inspired by Dan Abramov https://youtu.be/dpw9EHDh2bM see at 49:42
This works perfectly. However now I want to update the username after a new exercise is created. This is in the onSubmit method. But I'm not sure how to do this. Before I refactored I could use setUserName(), but now username is set by the generic custom hook function useFormInput
the username has an onChange method, so I thought I can maybe use this. However this uses the e.target.value because it is used for an input field.
Component:
I commented out the setUserName(''), here I want to update the username
const CreateExercise = () => {
const inputEl = useRef(null)
const username = useFormInput('')
const description = useFormInput('')
const duration = useFormInput(0)
const date = useFormInput(new Date())
const [users, setUsers] = useState([])
useEffect(() => {
axios
.get('http://localhost:5000/users/')
.then(res => {
if (res.data.length > 0) {
setUsers(res.data.map(user => user.username))
}
})
.catch(err => console.log(err))
}, [])
const onSubmit = e => {
e.preventDefault()
const exercise = {
username: username.value,
description: description.value,
duration: duration.value,
date: date.value
}
axios
.post('http://localhost:5000/exercises/add', exercise)
.then(res => console.log(res.data))
debugger
// setUsername('')
window.location = '/'
}
custom Hook useFormInput:
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue)
const handleChange = e => {
const newValue = e.target ? e.target.value : e
setValue(newValue)
}
return {
value,
onChange: handleChange
}
}
I expect the value in the state of username is updated to an empty string ' '
Complete code is on my repo on https://github.com/jeltehomminga/mern-tracker
Instead of trying to maintain more than 1 state, I'd recommend combining all state into one object. Then you can move everything into your custom hook. In addition, always make sure you handle and communicate any errors to the user.
Working example:
State as an object
hooks/useFormHandler (the API defined below is an object with functions to mimic API calls -- you'll replace this with real API calls. Also, if you wanted to make this hook reusable for other form components, then you'll need to remove the useEffect and handleSubmit functions from the custom hook and place them inside the specified functional component instead)
import { useCallback, useEffect, useState } from "react";
import API from "../../API";
// create a custom useFormHandler hook that returns initial values,
// a handleChange function to update the field values and a handleSubmit
// function to handle form submissions.
const useFormHandler = initialState => {
const [values, setValues] = useState(initialState);
// on initial load this will attempt to fetch users and set them to state
// otherwise, if it fails, it'll set an error to state.
useEffect(() => {
API.get("http://localhost:5000/users/")
.then(res => {
if (res.data.length > 0) {
setValues(prevState => ({
...prevState,
users: res.data.map(({ username }) => username)
}));
} else {
setValues(prevState => ({
...prevState,
error: "Unable to locate users."
}));
}
})
.catch(err =>
setValues(prevState => ({ ...prevState, error: err.toString() }))
);
}, []);
// the handleChange function will first deconstruct e.target.name and
// e.target.value, then in the setValues callback function, it'll
// spread out any previous state before updating the changed field via
// [name] (e.target.name) and updating it with "value" (e.target.value)
const handleChange = useCallback(
({ target: { name, value } }) =>
setValues(prevState => ({ ...prevState, error: "", [name]: value })),
[]
);
// the handleSubmit function will send a request to the API, if it
// succeeds, it'll print a message and reset the form values, otherwise,
// if it fails, it'll set an error to state.
const handleSubmit = useCallback(
e => {
e.preventDefault();
const exercise = {
username: values.username,
description: values.description,
duration: values.duration,
date: values.date
};
// if any fields are empty, display an error
const emptyFields = Object.keys(exercise).some(field => !values[field]);
if (emptyFields) {
setValues(prevState => ({
...prevState,
error: "Please fill out all fields!"
}));
return;
}
API.post("http://localhost:5000/exercises/add", exercise)
.then(res => {
alert(JSON.stringify(res.message, null, 4));
setValues(prevState => ({ ...prevState, ...initialState }));
})
.catch(err =>
setValues(prevState => ({ ...prevState, error: err.toString() }))
);
},
[initialState, setValues, values]
);
return {
handleChange,
handleSubmit,
values
};
};
export default useFormHandler;
components/CreateExerciseForm
import isEmpty from "lodash/isEmpty";
import React, { Fragment } from "react";
import { FaCalendarPlus } from "react-icons/fa";
import Spinner from "react-spinkit";
import Button from "../Button";
import Input from "../Input";
import Select from "../Select";
import useFormHandler from "../../hooks/useFormHandler";
const fields = [
{ type: "text", name: "description", placeholder: "Exercise Description" },
{ type: "number", name: "duration", placeholder: "Duration (in minutes)" },
{
type: "date",
name: "date",
placeholder: "Date"
}
];
// utilize the custom useFormHandler hook within a functional component and
// pass it an object with some initial state.
const CreateExerciseForm = () => {
const { values, handleChange, handleSubmit } = useFormHandler({
username: "",
description: "",
duration: "",
date: "",
error: ""
});
// the below will show a spinner if "values.users" hasn't been fulfilled yet
// else, it'll show the form fields. in addition, if there's ever a
// "values.error", it'll be displayed to the user.
return (
<form
style={{ width: 500, margin: "0 auto", textAlign: "center" }}
onSubmit={handleSubmit}
>
{isEmpty(values.users) ? (
<Spinner name="line-scale" />
) : (
<Fragment>
<Select
name="username"
placeholder="Select a user..."
handleChange={handleChange}
value={values.username}
selectOptions={values.users}
style={{ width: "100%" }}
/>
{fields.map(({ name, type, placeholder }) => (
<Input
key={name}
type={type}
name={name}
placeholder={placeholder}
onChange={handleChange}
value={values[name]}
/>
))}
<Button type="submit">
<FaCalendarPlus style={{ position: "relative", top: 2 }} />
Create Exercise
</Button>
</Fragment>
)}
{values.error && <p>{values.error}</p>}
</form>
);
};
export default CreateExerciseForm;
State as independent data types
Or, if you insist on using separated states, then create a resetValue function in the useFormInput hook:
const useFormInput = initialValue => {
// initialize state from "initialValue"
const [value, setValue] = useState(initialValue)
// handle changes to the "value" state via updating it
// with e.target.value
const handleChange = useCallback(({ target: { value } => {
setValue(value)
}, []);
// reset the value back to initialValue
const resetValue = useCallback(() => {
setValue(initialValue);
}, []);
return {
value,
handleChange,
resetValue
}
}
Then, destructure properties for the username (and other states, if needed):
const CreateExercise = () => {
// use ES6 destructure and aliasing to extract and rename the
// "value" (as username), "handleChange" function (as
// handleUsernameChange) and "resetValue" function (as resetUsername)
const {
value: username,
handleChange: handleUsernameChange,
resetValue: resetUsername
} = useFormInput('')
...other form state
...useEffect(() => {}, [])
const handleSubmit = useCallback(e => {
e.preventDefault();
const exercise = {
username: username,
description: description,
duration: duration,
date: date
};
axios
.post('http://localhost:5000/exercises/add', exercise)
.then(res => {
console.log(res.data)
// only reset the username if the exercise was successfully
// created
resetUsername();
})
.catch(err => console.log(err.toString());
}, [date, description, duration, resetUsername, username]);
return ( ...form )
}
I took a look and did a PR - Formik implementation w/validation.
Here is the PR - https://github.com/jeltehomminga/mern-tracker/pull/1
UI View
<>
<h3>Create New Exercise Log</h3>
<pre>{JSON.stringify({ formData }, null, 2)}</pre>
<ExerciseForm {...{ users }} onChange={data => setFormData(data)} />
</>
CreateExercise Form
import React from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import DatePicker from "react-datepicker";
import cx from "classnames";
const requiredMessage = "Required";
const exerciseFormSchema = Yup.object().shape({
username: Yup.string().required(requiredMessage),
description: Yup.string()
.min(2, "Too Short!")
.required(requiredMessage),
duration: Yup.number()
.integer()
.min(1, "Min minutes!")
.max(60, "Max minutes!")
.required(requiredMessage),
date: Yup.string().required(requiredMessage)
});
const ExerciseForm = ({ users = [], onChange }) => {
return (
<Formik
initialValues={{
username: "",
description: "",
duration: "",
date: ""
}}
validationSchema={exerciseFormSchema}
onSubmit={values => onChange(values)}
>
{({
values,
touched,
errors,
handleChange,
handleBlur,
isSubmitting,
setFieldValue
}) => {
const getProps = name => ({
name,
value: values[name],
onChange: handleChange,
onBlur: handleBlur,
className: cx("form-control", {
"is-invalid": errors[name]
})
});
return isSubmitting ? (
// Replace this with whatever you want...
<p>Thanks for the Exercise!</p>
) : (
<Form>
<FormControl label="Username">
<>
<select {...getProps("username")}>
<>
<option value="default">Select user...</option>
{users.map(person => (
<option key={person} value={person.toLowerCase()}>
{person}
</option>
))}
</>
</select>
<FormErrorMessage {...{ errors }} name="username" />
</>
</FormControl>
<FormControl label="Description">
<>
<Field {...getProps("description")} />
<FormErrorMessage {...{ errors }} name="description" />
</>
</FormControl>
<FormControl label="Duration in minutes">
<>
<Field {...getProps("duration")} type="number" />
<FormErrorMessage {...{ errors }} name="duration" />
</>
</FormControl>
<FormControl label="Date">
<>
{/* Was present before refactor */}
<div>
<DatePicker
{...getProps("date")}
selected={values.date}
minDate={new Date()}
onChange={date => setFieldValue("date", date)}
/>
<FormErrorMessage {...{ errors }} name="date" />
</div>
</>
</FormControl>
<button type="submit" className="btn btn-primary">
Create Exercise log
</button>
</Form>
);
}}
</Formik>
);
};
export default ExerciseForm;
// Created to manage label and parent className
const FormControl = ({ label, children }) => (
<div className="form-group">
<label>{label}:</label>
{children}
</div>
);
const FormErrorMessage = ({ name, errors }) => {
const error = errors && errors[name];
return error ? (
<div
class="invalid-feedback"
// Add inline style override as error message cannot sit as sibling to datePicker (bootstrap css)
style={{ display: "block" }}
>
{error}
</div>
) : null;
};

Categories