What I'm trying to do here is to build a dynamic component, that will be responsible to take an array of objects and the form will be built based on my formState. So I made my initial State and used .map to make the loop over the state and mapped the keys to making the label, value, and inputs appear based on the state. but my problem is at onChange. How to update the value key in every object and set the new state for it. any advice, please.
import { useState } from "react";
import InputText from "./components";
import useForm from "./hooks/useForm";
function App() {
interface formStateT {
id: number;
label: string;
value: any;
error: string;
}
const formState = [
{
id: 0,
label: "firstName",
value: "",
error: "",
},
{
id: 1,
label: "lastName",
value: "",
error: "",
},
];
const { form, validate, setForm, checkValidHandler } = useForm(formState);
const [error, setError] = useState("");
const submitFormHandler = (e: { preventDefault: () => void }) => {
e.preventDefault();
checkValidHandler();
// write form logic
// setError() will be used to take the error message
console.log(form);
};
return (
<form onSubmit={(e) => submitFormHandler(e)}>
{form.map((f: formStateT) => (
<InputText
key={f.id}
label={f.label}
value={f.value}
onChange={(e) => {
// Im trying here to update the value key of every label key.
// setForm({ ...form, [f.label.valueOf()]: f.value })
}}
valid={f.value === "" ? validate.notValid : validate.valid}
errorMsg={error === "" ? f.error : error}
classes={"class"}
/>
))}
<button>Submit</button>
</form>
);
}
export default App;
From your comment, f.value = e.target.value; is a state mutation and should be avoided, the setForm([...form]); is masking the mutation.
In App create an onChangeHandler function that takes the onChange event object and the index you want to update. Unpack the value from the onChange event and update the state. The handler should use a functional state update to update from the previous state, and create a shallow copy of the form array, using the index to shallow copy and update the correct array element.
Example:
// curried function to close over index in scope, and
// return handler function to consume event object
const onChangeHandler = index => e => {
const { value } = e.target;
setForm(form => form.map((el, i) =>
i === index
? { ...el, value }
: el
));
};
...
<form onSubmit={submitFormHandler}>
{form.map((f: formStateT, index: number) => (
<InputText
key={f.id}
label={f.label}
value={f.value}
onChange={onChangeHandler(index)} // <-- invoke and pass mapped index
valid={f.value === "" ? validate.notValid : validate.valid}
errorMsg={error === "" ? f.error : error}
classes={"class"}
/>
))}
<button>Submit</button>
</form>
Related
Here I'm trying to reset selected radio buttons on this list,
however it doesn't work because
I previously change input check from {checked} to {user.checked}. Refer from UserListElement.tsx below
Therefore, I tried the following two methods.
in useEffect(), set user.userId = false
useEffect(() => {
user.checked = false;
}, [isReset, user]);
→ no change.
setChecked to true when addedUserIds includes user.userId
if (addedUserIds.includes(`${user.userId}`)) {
setChecked(true);
}
→ Unhandled Runtime Error
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Any suggestion on how to make this this work?
UserListElement.tsx
export const UserListElement = ({
user,
handleOnMemberClicked,
isReset,
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isReset: boolean;
}) => {
const [checked, setChecked] = useState(user.checked);
const addedUserIds = addedUserList.map((item) => item.userId) || [];
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
const checkedState = e.target.checked;
setChecked(checkedState); //not called
user.checked = checkedState;
handleOnMemberClicked(checkedState, user.userId);
};
useEffect(() => {
setChecked(false);
}, [isReset, user]);
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
return (
<li>
<label className={style.checkboxLabel}>
<input
type="checkbox"
className={style.checkboxCircle}
checked={user.checked}
// checked={checked}
onChange={(e) => handleOnChange(e)}
/>
<span>{user.name}</span>
</label>
</li>
);
};
UserList.tsx
export const UserList = (props: {
showsUserList: boolean;handleClose: () => void;corporationId: string;currentUserId: string;samePerson: boolean;twj: string;
}) => {
const [isReset, setReset] = useState(false);
.......
const resetAll = () => {
setReset(!isReset);
setCount((addedUserList.length = 0));
setAddedUserList([]);
setUserName('');
};
......
return ( <
> < div > xxxxx <
ul className = {
`option-module-list no-list option-module-list-member ${style.personListMember}`
} > {searchedUserList.map((user, i) => (
<UserListElement user = { user }
handleOnMemberClicked = { handleOnMemberClicked }
isReset = { isReset }
key = {i} />
)) }
</ul>
/div>
<a className="is-secondary reservation-popup-filter-reset" onClick={resetAll}>
.....
}
UseAddUserList.tsx
export class UserDetail {
constructor(public userId: string | null, public name: string | null) {}
}
export let addedUserList: UserDetail[] = [];
export let setAddedUserList: Dispatch<SetStateAction<UserDetail[]>>;
export const useAddUserList = (idList: UserDetail[]) => {
[addedUserList, setAddedUserList] = useState(idList);
};
Further Clarification:
Default view
Searched option (showed filtered list)
I use user.checked because when using only checked, the checked state does not carry on from filtered list view to the full view (ex. when I erase searched word or close the popup).
The real answer to this question is that the state should NOT be held within your component. The state of checkboxes should be held in UsersList and be passed in as a prop.
export const UserListElement = ({
user,
handleOnMemberClicked,
isChecked
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isChecked: boolean;
}) => {
// no complicated logic in here, just render the checkbox according to the `isChecked` prop, and call the handler when clicked
}
in users list
return searchedUserList.map(user => (
<UserListElement
user={user}
key={user.id}
isChecked={addedUserIds.includes(user.id)} <-- THIS LINE
handleOnMemberClicked={handleOnMemberClicked}
/>
)
You can see that you almost had this figured out because you were doing this in the child:
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
Which indicates to you that the checkdd value is entirely dependent on the state held in the parent, which means there is actually no state to be had in the child.
Also, in React, NEVER mutate things (props or state) like - user.checked = true - that's a surefire way to leave you with a bug that will cost you a lot of time.
Hopefully this sheds some light
In your UserListElement.tsx you are setting state in render, which triggers renders the component again, and again set the state which again triggers re-render and the loop continues. Try to put your condition in the useEffect call, also you mutate props, so don't set user.checked = true. Instead call setter from the parent component, where it is defined.
useEffect(() => {
setChecked(false);
if (addedUserIds.includes(user.userId)) {
setChecked(true);
}
}, [user]);
I'm building a chat app, I have 3 main components from parent to child in this hierarchical order: Chat.js, ChatLine.js, EditMessage.js.
I have a function updateMessage in Chat component that I need to pass to the second child EditMessage, but it causes a rerender of every ChatLine when I click on Edit button and begin typing.
I can't figure out how to memoize it so it only causes a rerender on the ChatLine I'm editing.
It only works if I pass it to ChatLine as :
updateMessage={line.id === editingId ? updateMessage : null}
instead of :
updateMessage={updateMessage}
But I want to avoid that and memoize it instead so it doesn't cause a rerender after each letter I type while editing a message.
This is the whole code: (also available in CodeSandbox & Netlify)
(Chat.js)
import { useEffect, useState } from "react";
import ChatLine from "./ChatLine";
const Chat = () => {
const [messages, setMessages] = useState([]);
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
useEffect(() => {
setMessages([
{ id: 1, message: "Hello" },
{ id: 2, message: "Hi" },
{ id: 3, message: "Bye" },
{ id: 4, message: "Wait" },
{ id: 5, message: "No" },
{ id: 6, message: "Ok" },
]);
}, []);
const updateMessage = (editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
};
return (
<div>
<p>MESSAGES :</p>
{messages.map((line) => (
<ChatLine
key={line.id}
line={line}
editValue={line.id === editingId ? editValue : ""}
setEditValue={setEditValue}
editingId={editingId}
setEditingId={setEditingId}
updateMessage={updateMessage}
/>
))}
</div>
);
};
export default Chat;
(ChatLine.js)
import EditMessage from "./EditMessage";
import { memo } from "react";
const ChatLine = ({
line,
editValue,
setEditValue,
editingId,
setEditingId,
updateMessage,
}) => {
return (
<div>
{editingId !== line.id ? (
<>
<span>{line.id}: </span>
<span>{line.message}</span>
<button
onClick={() => {
setEditingId(line.id);
setEditValue(line.message);
}}
>
EDIT
</button>
</>
) : (
<EditMessage
editValue={editValue}
setEditValue={setEditValue}
setEditingId={setEditingId}
editingId={editingId}
updateMessage={updateMessage}
/>
)}
</div>
);
};
export default memo(ChatLine);
(EditMessage.js)
import { memo } from "react";
const EditMessage = ({
editValue,
setEditValue,
editingId,
setEditingId,
updateMessage,
}) => {
return (
<div>
<textarea
onKeyPress={(e) => {
if (e.key === "Enter") {
// prevent textarea default behaviour (line break on Enter)
e.preventDefault();
// updating message in DB
updateMessage(editValue, setEditValue, editingId, setEditingId);
}
}}
onChange={(e) => setEditValue(e.target.value)}
value={editValue}
autoFocus
/>
<button
onClick={() => {
setEditingId(null);
setEditValue("");
}}
>
CANCEL
</button>
</div>
);
};
export default memo(EditMessage);
Use the useCallback React hook to memoize the updateMessage callback so it can be passed as a stable reference. The issue is that each time Chat is rendered when editValue state updates it is redeclaring the updateMessage function so it's a new reference and triggers each child component it's passed to to rerender.
import { useCallback } from 'react';
...
const updateMessage = useCallback(
(editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
// If updating messages state use functional state update to
// avoid external dependencies.
},
[]
);
I am making a form that uses many fields to post the data into a database.
I have over 80 fields like "title, body HTML, price, compare price, vendor, weights", etc.
and my code is very repetitive, is there a way to make my code shorter? I shaved a lot of my code because it's over 600 lines of code and would be too confusing to post the whole thing
I made 2 separate functions handleChange and selectHandler as little helpers to get the value of the dropdowns datalist inputs to be stored into state... the values have to be stored in separate states as I need each one to do an axios call to store its specific fields into the right data field.
import React, { useState } from "react";
function handleChange(e, setter) {
return setter({ value: e.target.value });
}
function selectHandler(setter) {
return (
<>
<input
list="headers"
placeholder="Select one"
onChange={(e) => handleChange(e, setter)}
/>
{/* headers comes from mapped api in another file */}
<datalist id="headers">{headers}</datalist>
</>
);
}
function PushToDB() {
const [showExtraImageInputs, setShowExtraImageInputs] = useState(false);
const [titleHeader, setTitleHeader] = useState();
const [handleHeader, setHandleHeader] = useState();
const [descriptionHtmlHeader, setDescriptionHtmlHeader] = useState();
const [image1Header, setImage1Header] = useState();
const [image2Header, setImage2Header] = useState();
const [altText1, setAltText1] = useState();
const [altText2, setAltText2] = useState();
return (
<>
<form onSubmit={(e) => e.preventDefault()}>
// each label uses the helpers to get the dropdown values and store it in state
<label>Title: {selectHandler(setTitleHeader)}</label>
<label>Body html: {selectHandler(setDescriptionHtmlHeader)}</label>
<label>Handle: {selectHandler(setHandleHeader)}</label>
<label>Image: {selectHandler(setImage1Header)}</label>
<label>Img alt text: {selectHandler(setAltText1)}</label>
{/* ADD MORE IMAGES */}
{showExtraImageInputs && (
<>
<div>Image 2: {selectHandler(setImage2Header)}</div>
<div>Img alt text 2: {selectHandler(setAltText2)}</div>
</>
)}
</form>
</>
);
}
export default PushToDB;
this is how the axios data looks like. as you can see I need each value from state. and again, its over 80 fields.
useEffect(() => {
if (pushState && apiData) {
let productValues = apiData.data.data;
productValues.map((e) => {
let url = `url`;
return axios
.get(url)
.then((res) => {
if (res) {
// if the data is already in db, do not push
if (res.data.products.length === 0)
// if there is no data then push data
return setProductData({
variables: {
// values from state
title: e[titleHeader?.value],
descriptionHtml: e[descriptionHtmlHeader?.value],
handle: e[handleHeader?.value],
img1: e[image1Header?.value] ?? "",
alt1: e[altText1?.value],
img2 : e[image2Header?.value] ?? '',
alt2: e[altText2?.value],
img3: e[image3Header?.value] ?? '',
// and so on
},
});
}
// this is the logger of whats being pushed into the database
})
.then((res) => {
if (res)
return axios.post("http://localhost:4000/api/v1/Extradb", {
data: {
title: res?.data?.productCreate?.product?.title,
handle: res?.data?.productCreate?.product?.handle,
item_id: res?.data?.productCreate?.product?.id,
},
});
});
});
}
}, []);
came out with a solution... I just needed to make an object
function App() {
const [userInputs, setUserInputs] = useState({})
function handleChange(e) {
const { value, name } = e.target
setUserInputs(prevState => ({
...prevState,
[name]: value
}))
}
function handleInputNaming(name) {
let capitilizedWord = name.charAt(0).toUpperCase() + name.slice(1);
return (<input placeholder={capitilizedWord} name={name} value={userInputs[name]} onChange={handleChange} />)
}
return (
<div className="App">
{handleInputNaming('title')}
{handleInputNaming('handle')}
{handleInputNaming('image')}
</div>
);
}
export default App;
I have a component within a component. The parent component (Form) is expensive to render because of complex caluculations. Because of this if I hold down a key in the username input in the child component (UserForm) the page starts dropping frames (new characters don't show for a bit) because it renders the parent (which is expensive) as quickly as possible. To solve this I started storing a local instance of the username prop into state in the child component and then debouncing that state before passing it up to the parent.
The issue is when the parent component modifies the username (such as by reverting the changes) while the debounce in the child component is still in progress, the parent component's modification is overwritten by the child component's debounced output.
const UserForm = ({ value, onChange }) => {
const debouncedOnChange = useCallback(
lodash.debounce(updatedValue => {
onChange(updatedValue);
}, 500),
[onChange]
);
const [username, setUsername] = useState(value.username);
useEffect(() => {
setUsername(value.username);
}, [value.username]);
const handleUsernameOnChange = useCallback(
event => {
setUsername(event.target.value);
debouncedOnChange({
...value,
username: event.target.value,
});
},
[debouncedOnChange]
);
return (
<div>
<input value={username} onChange={handleUsernameOnChange} />
</div>
);
};
const INITIAL_STATE = {
user: {
username: 'John',
},
dog: {
name: 'Cliffard'
}
};
// <Form/> Is expensive to render because of complex calculations
const Form = () => {
const [value, setValue] = useState(INITIAL_STATE);
const [backup, setBackup] = useState(INITIAL_STATE);
return (
<form>
<UserForm
value={value.user}
onChange={updatedUserFormValue => {
setValue({
...value,
updatedUserFormValue,
});
}}
/>
<button
onClick={() => {
setValue({
...value,
user: { ...backup.user },
});
}}
>
Reset User Form
</button>
</form>
);
};
I've been Working on redux-less CRUD project. Creating a categories list using contextAPI/hooks in combination. I have succeed, the "CR and D" (Create, Read and Delete) parts, but struggling with "U" (update) part, I couldn't get this to work after submit for update change.
Starting with how I did with the reducer components...
reducer.js
import uniqid from 'uniqid';
export const categoryReducer = (state, action) => {
switch (action.type) {
case 'ADD_CATEGORY':
return [
...state,
{
id: uniqid(),
name: action.name,
suffix: action.suffix
}
];
//**MY ISSUES**
case 'UPDATE_CATEGORY':
return (
state.findIndex(item => item.id === action.id),
state.slice(0, action.id, action.name, action.suffix)
);
case 'REMOVE_CATEGORY':
return state.filter(item => item.id !== action.id);
default:
return state;
}
};
the 'ADD_CATEGORY' and 'DELETE_CATEGORY' can be ignored since I don't have issues. The 'UPDATE_CATEGORY' feel a little sketchy to me, I have feeling the syntax is incorrect. From my understanding, in order to make update changes. An selected item need to be scanned from array into matched id. Once the id is matched, The changes can be updated after submit. And I am not sure I could figure out how to add that syntax.
Now the The Edit Form component with dispatch inside handleSubmit function...
EditCategoryForm.js
import React, { useState, useContext, useEffect } from 'react';
//context
import { CategoryContext } from '../contexts/categoryContext';
function EditCategoryForm() {
const { dispatch } = useContext( CategoryContext);
/*Not not get this to work */
const handleSubmit = (e, name, suffix) => {
e.preventDefault();
dispatch({
type: 'EDIT_CATEGORY',
name,
suffix
});
};
const handleChange = e => {
const { name, value } = e.target;
setInputValue(prevState => ({ ...prevState, [name]: value }));
};
return (
<form onSubmit={handleSubmit}>
<h1>Edit</h1>
<input
type="text"
name="name"
value={inputValue.name}
onChange={handleChange}
/>
<input
type="text"
name="suffix"
value={inputValue.suffix}
onChange={handleChange}
/>
<button>Submit Change</button>
<button onClick={() => setEditing(false)}>Cancel</button>
</form>
);
}
export default EditCategoryForm;
Again, I believe, the syntax have been implemented wrong. The goal is to dispatch into separated values of name and suffix based on input value. I didn't not get anything succeed and Its been struggling for me lately. How do I fix this problem? What would be the best practice or a proper way to achieve "U" (update) part inside reducer and dispatcher? Your help is appreciated and thanks in advance.
case 'UPDATE_CATEGORY':
let update_obj = state.find( obj => obj.id === action.id);
update_obj['name'] = action.name;
update_obj['suffix'] = action.suffix;
return [...state, update_obj]
If I am not mistaken, this should return the original state, along with the specific element updated
Or you can do:
const newArr = state.map( obj => {
if obj.id === action.id{
obj['name'] = action.name;
obj['suffix'] = action.suffix;
}
return obj
});
return newArr;
In the update case, I'd recommend mapping through your existing items:
If the mapped item has the same id as the updated one, then replace it
Keep the existing item otherwise
Here's the code:
case 'UPDATE_CATEGORY':
return state.map(item => {
if (item.id === action.id) {
return {...item, name: action.name, suffix: action.suffix };
}
return item;
});
The part where you initialise your state is missing. I'm assuming it's something like this:
const [inputValue, setInputValue] = useState({name: '', suffix: ''})
The handleSubmit function receives a single parameter (event), you need to use the values from your state:
// remove the extra parameters
const handleSubmit = (e /*, name, suffix */) => {
e.preventDefault();
dispatch({
type: 'UPDATE_CATEGORY',
id: inputValue.id,
name: inputValue.name,
suffix: inputValue.suffix,
});
};
Codesandbox here