Updating material ui select options from input - javascript

I'm new to Reactjs, here i have material ui select element, as you can see i have default values for select element, and also by clicking 'ADD USER' button and submitting, i can add new values to select element, and from select element i can also delete options, my question here is how can i edit specific option from select element, i have added EditUser component for that when option is clicked, but dont know how to update it, any advice ?
my code:
https://codesandbox.io/s/material-ui-multiple-select-with-select-all-option-forked-ysglz8?file=/src/AddUser.js
App.js:
import React, { useState } from "react";
import Checkbox from "#material-ui/core/Checkbox";
import InputLabel from "#material-ui/core/InputLabel";
import ListItemIcon from "#material-ui/core/ListItemIcon";
import ListItemText from "#material-ui/core/ListItemText";
import MenuItem from "#material-ui/core/MenuItem";
import FormControl from "#material-ui/core/FormControl";
import Select from "#material-ui/core/Select";
import DeleteIcon from "#material-ui/icons/Delete";
import CreateIcon from "#material-ui/icons/Create";
import { MenuProps, useStyles } from "./utils";
import AddUser from "./AddUser";
import {
Button,
List,
ListItem,
Dialog,
DialogTitle,
DialogContent
} from "#material-ui/core";
import EditUser from "./EditUser";
function App() {
const rawOptions = [
"Oliver Hansen",
"Van Henry",
"April Tucker",
"Ralph Hubbard",
"Omar Alexander",
"Carlos Abbott",
"Miriam Wagner",
"Bradley Wilkerson",
"Virginia Andrews",
"Kelly Snyder"
];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [options, setOptions] = useState(rawOptions);
const [openAddModal, setOpenAddModal] = useState(false);
const [openUpdateModal, setOpenUpdateModal] = useState(false);
const handleChange = (event) => {
console.log("vals", event.target);
const value = event.target.value;
setSelected(value);
console.log("values", selected);
};
function addUser(newArray) {
setOptions(newArray);
}
const openAddUser = () => {
setOpenAddModal(true);
};
const openUpdateUser = (e) => {
e.stopPropagation();
setOpenUpdateModal(true);
};
const closeAddModal = () => {
setOpenAddModal(false);
};
const closeUpdateModal = () => {
setOpenUpdateModal(false);
};
const updateUser = (updateUser) => {
setOptions(updateUser);
};
return (
<FormControl className={classes.formControl}>
<div>
<InputLabel id="mutiple-select-label">Multiple Select</InputLabel>
<Select
labelId="mutiple-select-label"
multiple
variant="outlined"
value={selected || []}
onChange={handleChange}
renderValue={(selected) => selected}
MenuProps={MenuProps}
>
{options.map((option, index) => (
<MenuItem key={option.id} value={option}>
<ListItemIcon>
<Checkbox checked={selected?.includes(option)} />
</ListItemIcon>
<ListItemText primary={option.title}>{option}</ListItemText>
<DeleteIcon
onClick={(e) => {
e.stopPropagation();
setOptions(options.filter((o) => o !== option));
console.log("run");
}}
/>
<ListItemIcon>
<CreateIcon onClick={openUpdateUser} />
</ListItemIcon>
</MenuItem>
))}
</Select>
<Button onClick={openAddUser} style={{ backgroundColor: "#287B7A" }}>
Add User
</Button>
</div>
<p>{selected}</p>
<AddUser
openAddModal={openAddModal}
handleClose={closeAddModal}
array={options}
addUser={addUser}
/>
<EditUser
openUpdateModal={openUpdateModal}
handleClose={closeUpdateModal}
array={options}
updateUser={updateUser}
/>
</FormControl>
);
}
export default App;

Currently your users haven't any ids or something that they can be identified with.
Try to make users array of objects like this
const rawOptions = [
{
id: 0,
name: "Oliver Hansen"
},
];
Make your inputs and etc. accept array of objects.
After this in your Edit component, you should pass there selected user object and set default state for your input value (so your can really edit it and not input a new value)
const [value, setValue] = useState(props.user.name);
And in your someFunction, that acts like handleSubmit function, pass your user object, or user id and new value. It will look like
const someFunction = (event) => {
event.preventDefault();
if (value) {
props.hanldeSubmit(props.user.id, value)
props.handleClose() // Might be better to put it into your handleSubmit in parent component
}
};
And finally in your App.js, create handleSubmit function that accepts user id and value, and modify your state in it. Find user by ID and put a new value.
Do not forget to pass this function into your EditUser component.
Hope that helped you!
UPDATE
Ooookay, so, you might also want to start with less hard examples, but lets stick to what we have. I'll note here some problems that I found, and explain how to make it work.
First of all, always name functions and variables correctly, you should understand what function or variable do only by its name (ideally), I understand that this is just an example code and etc, but this makes this point only more important, because when you learn something new, its good not to make it harder for yourself.
Second thing, just for some case, I don't know if this mistake or not, so I mention this:
<CreateIcon
onClick={() =>
openUpdateUser({ id: option.id, name: option.name })
}
/>
Here you pass object, and in openUpdateUser you accept e (event) as a first parameter. Just for you to know, you will get event in your anonymous function and it wouldn't be passed further in openUpdateUser, to pass it, you should write it like this:
<CreateIcon
onClick={(e) =>
openUpdateUser(e, { id: option.id, name: option.name })
}
/>
Okay, let's get back to business.
The first real problem here: You have your options in one state, and selected options in other, so when you add some user, you will see users from selected. What problem does it cause? When you will try to update user in your options, it might be updated, but you wouldn't see any changes in selected, because it two different states.
We will solve it by making one source of information. Now we will store in selected not users, but users ids and in render we will get users from our options by ids.
// before
<p>{selected}</p>
// after
{selected.map(selectedUserId => (
<div>{options.filter(option => option.id === selectedUserId)[0].name}</div>
))}
Now, any changes to options will affect your selected users. Also, update your code to add\remove ids and not user objects.
Let's go further, now you have your selected user and method for updating in edit component, let's go edit:
// EditUser.js
const [value, setValue] = useState(props.edit.name); // set user name as default value to edit it
function changeValue(e) {
setValue(e.target.value);
}
const someFunction = (event) => {
event.preventDefault();
if (value) {
props.updateUser({id: props.edit.id, name: value}) // Pass user id and new value to our update function
props.handleClose();
}
};
So, now we have our new value in update function, the only one thing left is to save those updates. We'll do it in easy way:
// Normally here would be some api call for user update
const updateUser = (updateUser) => {
const temp = [...options] // Not deep copy of our options
temp[temp.findIndex(user => user.id === updateUser.id)].name = updateUser.name;
setOptions(temp)
};
And thats all, now it should work as expected.
Also as improve, you can restructure your options array of objects to make it easier to modify data.
(yep, I know that it was my suggestion, but anyway :) )
Currently it looks like this:
const rawOptions = [
{
id: 0,
name: "Oliver Hansen"
},
];
We can make it object of objects, where key will be id of user:
const rawOptions = {
0: {
id: 0,
name: "Oliver Hansen"
},
};
//Now to get user you can just do
options[userId]
// To get users array
Object.values(options)
// To modify user
const updateUser = (updateUser) => {
setOptions({...options}, [updateUser.id]: updateUser)
};
Just like previously, I wouldn't make those changes into codesandbox, the best way to learn programming is to write some code by yourself :)
If you will find any other issues or questions, feel free to ask, hope it helps :)

Related

Stop checkbox values being reset after page load

Hello I am working on a pop up window where the user can filter a table of data.
The filter is selected using checkboxes.
My issue:
On page load there is a useEffect that changes every checkbox to false. This is based on the props coming in from the API.
I'd like on page load (and when the filter opens) that the checkbox state is stored based on what the user has selected previously in their session
code:
Filter component*
[...]
import FilterSection from "../FilterSection";
const Filter = ({
open,
handleClose,
setFilterOptions,
[..]
roomNumbers,
}) => {
const [roomValue, setRoomValue] = React.useState();
const [roomListProp, setRoomListProp] = React.useState(); // e.g. [["roomone", false], ["roomtwo", true]];
const sendRoomFilterData = (checkedRoomsFilterData) => {
setRoomValue(checkedRoomsFilterData);
};
const setCheckboxListPropRoom = (data) => {
setRoomListProp(data);
};
// extract, convert to an object and pass back down? or set local storage and get
// local storage and pass back down so that we can get it later?
const convertToLocalStorageFilterObject = (roomData) => { // []
if (roomData !== undefined) {
const checkedRooms = roomData.reduce((a, curval) => ({ ...a, [curval[0]]: curval[1] }), {});
localStorage.setItem("preserved", JSON.stringify(checkedRooms)); // sets in local storage but values get wiped on page load.
}
};
React.useEffect(() => {
const preservedFilterState = convertToLocalStorageFilterObject(roomListProp);
}, [roomListProp]);
const applyFilters = () => {
setFilterOptions([roomValue]);
handleClose();
};
const classes = CurrentBookingStyle();
return (
<Dialog
fullWidth
maxWidth="sm"
open={open}
onClose={() => handleClose(false)}
>
<DialogTitle>Filter By:</DialogTitle>
<DialogContent className={classes.margin}>
<FilterSection
filterName="Room number:"
filterData={roomNumbers}
setFilterOptions={sendRoomFilterData}
setCheckboxListProp={setCheckboxListPropRoom}
/>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={applyFilters}>
Apply Filters
</Button>
</DialogActions>
</Dialog>
);
};
Filter Section used in Filter
import {
TableCell,
Typography,
FormControlLabel,
Checkbox,
FormGroup,
} from "#material-ui/core";
const FilterSection = ({
filterData, filterName, setFilterOptions, setCheckboxListProp
}) => {
const [checkboxValue, setCheckboxValue] = React.useState({});
const [checkboxFilterList, setCheckboxFilterList] = React.useState([]);
const handleCheckboxChange = (event) => {
setCheckboxValue({
...checkboxValue,
[event.target.name]: event.target.checked, // room1: true
});
};
const = () => filterData // ["room1" "room2"]; comes from API
.filter((val) => !Object.keys(checkboxValue).includes(val))
.reduce((acc, currval) => ({
...acc, [currval]: false, // converts array to object and sets values to false
}), checkboxValue);
React.useEffect(() => {
const transformedCheckboxListItems = Object.entries(convertToObject());
setCheckboxFilterList(transformedCheckboxListItems);
setFilterOptions(transformedCheckboxListItems.filter(([, val]) => val).map(([key]) => key));
setCheckboxListProp(transformedCheckboxListItems);
}, [checkboxValue]);
return (
<>
<Typography style={{ fontWeight: "bold" }}>{filterName}</Typography>
<FormGroup row>
{checkboxFilterList.map(([key, val]) => (
<TableCell style={{ border: 0 }}>
<FormControlLabel
control={(
<Checkbox
checked={val}
onChange={handleCheckboxChange}
name={key}
color="primary"
/>
)}
label={key}
/>
</TableCell>
))}
</FormGroup>
</>
);
};
What i have tried:
I have created a reusable component called "FilterSection" which takes takes data from the API "filterData" and transforms it from an array to an object to set the initial state for the filter checkboxes.
On page load of the filter I would like the checkboxes to be true or false depending on what the user has selected, however this does not work as the convertToObject function in my FilterSection component converts everything to false again on page load. I want to be able to change this but not sure how? - with a conditional?
I have tried to do this by sending up the state for the selected checkboxes to the Filter component then setting the local storage, then the next step would be to get the local storage data and somehow use this to set the state before / after page load. Unsure how to go about this.
Thanks in advance
I am not sure if I understand it correctly, but let me have a go:
I have no idea what convertToObject does, but I assume it extracts the saved filters from localStorage and ... updates the filter value that has just been changed?
Each time the FilterSection renders for the first time, checkboxValue state is being initialised and an useEffect runs setCheckboxListProp, which clears the options, right?
If this is your problem, try running setCheckboxListProp directly in the handleCheckboxChange callback rather than in an useEffect. This will ensure it runs ONLY after the value is changed by manual action and not when the checkboxValue state is initialised.
I solved my problem by moving this line:
const [checkboxValue, setCheckboxValue] = React.useState({});
outside of the component it was in because every time the component re-rendered it ran the function (convertToObject() which reset each checkbox to false
by moving the state for the checkboxes up three layers to the parent component, the state never got refreshed when the or component pop up closed. Now the checkbox data persists which is the result I wanted.
:D

Set state for dynamically generated component in React

I'm reusing a couple of external components to create my custom Combobox in strapi app.
Values are received from server so I need to add options dynamically.
Currently there is the following code:
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected
}) {
const [value, setValue] = useState('');
const combo = (<Combobox label="Country" value={value} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value="{entry.id}">{entry.name}</ComboboxOption>
);
})}
</Combobox>);
// setValue(valueSelected)
return combo;
}
And everything goes good until I try so set 'selected' option basing on another set of data. In static world I could just say useState(valueSelected) and it will work. But as code generated dynamically, there is no related option yet, so I get failure like "Failed to get 'props' property of undefined".
I tried to put this combobox into a variable and set state between creation and returning it (commented setValue line before the return statement) but then app gets in a loop and returns "Too many re-renders".
Does anyone has an idea of how to change/rewrite this to be able to set selected value for dynamically created combobox?
So I assume that the values are dynamically fetched and passed to the ComboboxCustom.
I think you can add setValue(valueSelected) inside an useEffect.
onChange of the prop valueSelected.something like,
useEffect(() => {
setValue(valueSelected)
}, [valueSelected])
Also handle the return when the value is not yet loaded. like before doing valuesList.map, first check if valueList ? (render actual) : (render empty)
Hope this helps!!
Thanks,
Anu
Finally I got working solution based on answer from #Anu.
Cause valuesList is got as GET-request from another hook, I have to check values are already present (first hook hit gives [] yet) and bind Combobox state updating to change of valuesList also. Though I don't fell like this solution is perfect.
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected,
}) {
const [value, setValue] = useState('');
let combo = null;
useEffect(() => {
if(combo && combo?.props?.children?.length > 0 && valuesList.length > 0) {
setValue(valueSelected)
}
}, [valueSelected, valuesList])
combo = (<Combobox label="Country" value={value?.toString()} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>);
return combo;
}
After that I decided avoid creating custom component based on already published as I'll need to add and process event listeners that are added for us in the existing components. So I placed this code directly into my modal and it also works:
const [countries, setCountries] = useState([]);
const [deliveryCountryValue, setDeliveryCountryValue] = useState('');
useEffect(async () => {
const countriesReceived = await countryRequests.getAllCountries();
setCountries(countriesReceived);
}, []);
useEffect(() => {
// If there is no selected value yet, set the one we get from order from server
const valueDelivery = deliveryCountryValue != '' ? deliveryCountryValue : order.country?.id;
if(countries.length > 0) {
setDeliveryCountryValue(valueDelivery);
order.country = countries.find(x => x.id == valueDelivery);
}
}, [deliveryCountryValue, countries])
<Combobox key='delivery-combo' label="Country" value={deliveryCountryValue?.toString()} onChange={setDeliveryCountryValue}>
{countries.map((entry) => {
return(
<ComboboxOption key={'delivery'+entry.id} value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>

Re-render List after deleting item in child component

I want to build a dashboard for a blog. I have a page, listing all blog posts using a component for each list item. Now, inside each list item, I have a button to delete the post.
So far, everything is working. The post gets deleted, and if I reload the page, it is gone from the list. But I can't get it to re-render the page automatically, after deleting a post. I kind of cheated here using window.location.reload() but there has to be a better way?
This is my Page to build the list of all Posts
import {
CCol,
CContainer,
CRow,
CTable,
CTableHead,
CTableRow,
CTableHeaderCell,
CTableBody,
} from "#coreui/react";
import { useState, useEffect } from "react";
import DashboardSidebar from "../../components/dashboard/Sidebar";
import { getAllBlogPosts } from "../../services/blogService";
import BlogListItem from "../../components/dashboard/blog/BlogListItem";
import "./Dashboard.scss";
const AdminBlogListView = () => {
const [blogposts, setBlogposts] = useState([]);
useEffect(() => {
getBlogPosts();
}, []);
async function getBlogPosts() {
const response = await getAllBlogPosts();
setBlogposts(response.data);
}
// console.log(blogposts);
return (
<div className="adminContainer">
<div className="adminSidebar">
<DashboardSidebar />
</div>
<div className="adminContent">
<CContainer fluid>
<CRow className="mb-3">
<CCol>
<CTable>
<CTableHead>
<CTableRow>
<CTableHeaderCell scope="col">#</CTableHeaderCell>
<CTableHeaderCell scope="col">Titel</CTableHeaderCell>
<CTableHeaderCell scope="col">Content</CTableHeaderCell>
<CTableHeaderCell scope="col"></CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{blogposts.map((post) => {
return <BlogListItem key={post._id} post={post} />;
})}
</CTableBody>
</CTable>
</CCol>
</CRow>
</CContainer>
</div>
</div>
);
};
export default AdminBlogListView;
And this is the BlogListItem Component
import { useState, useEffect } from "react";
import {
CTableRow,
CTableHeaderCell,
CTableDataCell,
} from "#coreui/react";
import CIcon from "#coreui/icons-react";
import * as icon from "#coreui/icons";
import {
deleteBlogPost,
getBlogPostById,
// updateBlogPost,
} from "../../../services/blogService";
import { useNavigate } from "react-router-dom";
const BlogListItem = (props) => {
const id = props.post._id;
const [visible, setVisible] = useState(false);
const [post, setPost] = useState({
title: "",
content: "",
});
useEffect(() => {
getBlogPostById(id)
.then((response) => setPost(response.data))
.catch((error) => console.log(error));
}, []);
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
window.location.reload();
};
return (
<>
<CTableRow>
<CTableHeaderCell scope="row">1</CTableHeaderCell>
<CTableDataCell>{post.title}</CTableDataCell>
<CTableDataCell>{post.content}</CTableDataCell>
<CTableDataCell>
<CIcon
icon={icon.cilPencil}
size="lg"
onClick={() => setVisible(!visible)}
/>
<CIcon
icon={icon.cilTrash}
className="deleteButton"
size="lg"
color=""
onClick={handleDelete}
/>
</CTableDataCell>
</CTableRow>
</>
);
};
export default BlogListItem;
What can I do instead of window.location.reload() to render the AdminBlogListView after deleting an item? I tried using useNavigate() but that doesn't do anything
Thanks in advance :)
You can pass a reference to a function from the parent component AdminBlogListView into the child component BlogListItem, such that it is invoked when a blog post is deleted. That function will have the effect of either repopulating the blog posts or manually removing it from the data (that implementation bit is up to you).
Solution 1: Repopulate all blog posts on deletion
This is a quick fix with a bit of code smell (because you're essentially querying the server twice: once to delete the post and another to fetch posts again). However it is an escape-hatch type of situation and is simple to implement.
When you are rendering BlogListItem, we can pass a function, say onDelete, which will invoke getBlogPosts() to manually repopulate the blog posts from your server:
<BlogListItem key={post._id} post={post} onDelete={getBlogPosts} />
Then it is a matter of ensuring BlogListItem invokes onDelete() when deleting a blog post:
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
// Invoke the passed in `onDelete` function in component props
props.onDelete();
};
Solution 2: Delete a specific blog post by ID in the parent
Similar to the solution above, but ensure that you are passing a function from the parent that can delete a post by a specific ID (from the argument). This saves you an additional trip to the server.
In your component AdminBlogListView, define a function that can mutate the blogposts state by removing a blog post by ID. This can be done by leveraging functional updates:
const onDelete = (id) => {
setBlogposts((currentBlogPosts) => {
const foundBlogPostIndex = currentBlogPosts.findIndex(entry => entry._id === id);
// If we find the blog post with matching ID, remove it
if (foundBlogPostIndex !== -1) currentBlogPosts.splice(foundBlogPostIndex, 1);
return currentBlogPosts;
})
}
NOTE: The code above assumes that the blog post ID is stored in the _id key. I have simply inferred that from your code, since you have not shared the shape of the data.
Then in your BlogListItem component, it's the same logic as solution #1, but you need to pass the ID into it when invoking it:
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
// Invoke the passed in `onDelete` function in component props with post ID as an argument
props.onDelete(post._id);
};

Only rerender the affected child in list of components while state resides in parent React

I'm building a chat app, I have 3 components from parent to child in this hierarchical order: Chat, ChatLine, EditMessage.
I'm looping through messages state in Chat to display multiple ChatLine components as a list, and I pass some state to ChatLine and then to EditMessage.
I need the state :
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
to remain in the parent component Chat so I can have access to it later there.
Anyway, now when I click on the Edit button, the EditMessage component shows a textarea, and I'm setting state onChange in it, but everytime I click the Edit button or type a letter in the textarea all the components rerender as I see in React DevTool Profiler, even the children that didn't get affected, I only need the Chat and affected ChatLine to rerender at most.
The whole code is available in CodeSandbox, and deployed in Netlify.
And here it is here also :
(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" },
]);
}, []);
return (
<div>
<p>MESSAGES :</p>
{messages.map((line) => (
<ChatLine
key={line.id}
line={line}
editValue={editValue}
setEditValue={setEditValue}
editingId={editingId}
setEditingId={setEditingId}
/>
))}
</div>
);
};
export default Chat;
(ChatLine.js)
import EditMessage from "./EditMessage";
import { memo } from "react";
const ChatLine = ({
line,
editValue,
setEditValue,
editingId,
setEditingId,
}) => {
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}
/>
)}
</div>
);
};
export default memo(ChatLine);
(EditMessage.js)
import { memo } from "react";
const EditMessage = ({ editValue, setEditValue, editingId, setEditingId }) => {
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);
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..");
};
The problem is that all of the child components see their props change any time any of them is in the process of being edited, because you're passing the current editing information to all of the children. Instead, only pass the current editing text (editValue) to the component being edited, not to all the others.
ChatLine doesn't use editValue when it's not the instance being edited. So I'd do one of two things:
Use a different component for display (ChatLine) vs. edit (ChatLineEdit). Almost the entire body of ChatLine is different depending on whether that line is being edited or not anyway. Then only pass editValue to ChatLineEdit.
Pass "" (or similar) as editValue to the one not being edited. In the map in Chat: editValue={line.id === editingId ? editValue : ""}.
Pass an "are equal" function into memo for ChatLine that doesn't care what the value of editValue is if line.id !== editingId. By default, memo does a shallow check of all props, but you can take control of that process by providing a function as the second argument. For instance:
export default memo(ChatLine, (prevProps, nextProps) => {
// "Equal" for rendering purposes?
return (
// Same chat line
prevProps.line === nextProps.line &&
// Same edit value setter (you can leave this out, setters from `useState` never change)
prevProps.setEditValue === prevProps.setEditValue && // ***
// Same editingId
prevProps.editingId === prevProps.editingId &&
// Same editingId setter (you can leave this out too)
prevProps.setEditingId === prevProps.setEditingId && // ***
(
// Same edit value...
prevProps.editValue === prevProps.editValue ||
// OR, we don't care because we're not being edited
nextProps.line.id !== nextProps.editingId
)
);
});
This is fragile, because it's easy to get the check wrong, but it's another option.
I would go for #1. Not even passing props to components that they don't need is (IMHO) the cleanest approach.

how to fix form submission with useEffect hook (as is: need to click submit twice)

App takes user options and creates an array objects randomly, and based on user options. (it's a gamer tag generator, writing to learn react.js). As is, App is a functional component and I use useState to store array of objects (gamertags) and the current selected options.
I use formik for my simple form. It takes two clicks to get a new item with updated options. I know why, options in state of App doesn't not update until it rerenders as the function for form submission is async. Therefore, all of my options are updated, after the first click, and are correct with the second because they were updated with the rerendering and after I needed them.
I know the solution is to use a useEffect hook, but despite reading over other posts and tuts, I don't understand how to apply it. It's my first instance of needing that hook and I'm still learning.
I wrote a simplified App to isolate the problem as much as possible and debug. https://codesandbox.io/s/morning-waterfall-impg3?file=/src/App.js
export default function App() {
const [itemInventory, setItemInventory] = useState([
{ options: "apples", timeStamp: 123412 },
{ options: "oranges", timeStamp: 123413 }
]);
const [options, setOptions] = useState("apples");
const addItem = (item) => {
setItemInventory([item, ...itemInventory]);
};
const createItem = () => {
return { options: options, timeStamp: Date.now() };
};
class DisplayItem extends React.Component {
render() { // redacted for brevity}
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
addItem(createItem());
};
const UserForm = (props) => {
return (
<div>
<Formik
initialValues={{
options: props.options
}}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
console.log(values);
props.onUpdate(values);
}}
>
{({ values }) => (
<Form> //redacted for brevity
</Form>
)}
</Formik>
</div>
);
};
return (
<div className="App">
<div className="App-left">
<UserForm options={options} onUpdate={onFormUpdate} />
</div>
<div className="App-right">
{itemInventory.map((item) => (
<DisplayItem item={item} key={item.timeStamp} />
))}
</div>
</div>
);
}
This is probably a "layup" for you all, can you help me dunk this one? Thx!
Solved problem by implementing the useEffect hook.
Solution: The functions that create and add an item to the list, addItem(createItem()), become the first argument for the useEffect hook. The second argument is the option stored in state, [options]. The callback for the form, onFormUpdate only updates the option in state and no longer tries to alter state, i.e. create and add an item to the list. The useEffect 'triggers' the creation and addition of a new item, this time based on the updated option because the updated option is the second argument of the hook.
Relevant new code:
useEffect( () => {
addItem(createItem());
}, [options]);
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
//addItem used to be here
};

Categories

Resources