I have an array set in state like:
const Theme = {
name: "theme",
roots: {
theme: Theme,
},
state: {
theme: {
quiz: {
quizGender: null,
quizSleepComfort: {
justMe: {
soft: null,
medium: null,
firm: null,
},
partner: {
soft: null,
medium: null,
firm: null,
}
},
},
},
},
actions: {
// ...
},
};
I then have a component that has checkboxes, one for soft, medium, and firm. The code for the component is:
const Question = ({ state }) => {
const [checkedItems, setCheckedItems] = useState([]);
const checkboxes = [
{
label: "Firm",
value: "firm",
},
{
label: "Medium",
value: "medium",
},
{
label: "Soft",
value: "soft",
},
];
state.theme.quiz.quizSleepComfort.justMe = checkedItems;
return (
<QuestionCommonContainer>
{checkboxes.map((item, id) => (
<QuizCheckbox
label={item.label}
name={item.label}
value={item.value}
selected={checkedItems[item.value] === true}
onChange={(e) => {
setCheckedItems({
...checkedItems,
[e.target.value]: e.target.checked,
});
}}
/>
))}
</QuestionCommonContainer>
);
};
export default connect(Question);
This specific component is just interacting with state.theme.quiz.quizSleepComfort.justMe object, not the partner object.
As of right now when a checkbox is selected, let's say the checkbox for "firm" is checked, the state gets updated to what looks like this:
...
quizSleepComfort: {
justMe: {
firm: true,
},
partner: {
soft: null,
medium: null,
firm: null,
}
},
...
I am trying to figure out how I would be able to alter this components code so that instead of setting the justMe object to include only the items that are checked (in this case "firm"), it should keep the other items as well ("soft", "medium") as null.
Please let me know if there is more info i should provide.
Okay. So the following is bad practice
state.theme.quiz.quizSleepComfort.justMe = checkedItems;
You should pass a function to the Question component, something like onChange.
The onChange function should update the state in your parent component. Use the spread operator ... to get a copy of the old object. for example
const onChange = (newState) =>
setState((oldState) => ({
...oldState,
justMe: { ...oldState.justMe, ...newState },
}));
the resulting object will contain all the properties of the original state but will overwrite any property set on newState in justMe. If the property that you want to update is more nested, just repeat the steps of spreading.
--- UPDATE ---
I have added an example that I think is close to what you are trying to achieve.
const Parent = () => {
const [state, setState] = useState(initialState);
const onChange = useCallback(
(newState) =>
setState((oldState) => ({
...oldState,
theme: {
...oldState.theme,
quiz: {
...oldState.theme.quiz,
quizSleepComfort: {
...oldState.theme.quizSleepComfort,
justMe: {
...oldState.theme.quizSleepComfort.justMe,
...newState,
},,
},
},
},
})),
[],
);
return <Question onChange={onChange} />;
};
const checkboxes = [
{
label: 'Firm',
value: 'firm',
},
{
label: 'Medium',
value: 'medium',
},
{
label: 'Soft',
value: 'soft',
},
];
const Question = ({ onChange }) => {
const [checkedItems, setCheckedItems] = useState([]);
useEffect(() => {
onChange(checkedItems);
}, [checkedItems, onChange]);
return (
<QuestionCommonContainer>
{checkboxes.map((item, id) => (
<QuizCheckbox
label={item.label}
name={item.label}
value={item.value}
selected={checkedItems[item.value] === true}
onChange={(e) => {
setCheckedItems((oldCheckedItems) => ({
...oldCheckedItems,
[e.target.value]: e.target.checked,
}));
}}
/>
))}
</QuestionCommonContainer>
);
};
export default connect(Question);
As you are having a really nested object to update, it might be worth taking a look at Object.assign
Related
I need to set the active classname to multiple onclick items inside a .map
I need the list of active items that were clicked
The items that were clicked will be highlighted in yellow, and when i click the same item again it should be removed from active list items.
const [data, setData] = useState([]);
const [activeIndicies, setActiveIndicies] = useState(() =>
data?.map(() => false)
);
useEffect(() => {
// This data is coming from the API response
const data = [
{ id: 1, name: "one" },
{ id: 2, name: "two" },
{ id: 3, name: "three" }
];
setData(data);
}, []);
return statement
onClick={() => {
setActiveIndicies(
activeIndicies.map((bool, j) => (j === index ? true : bool))
);
}}
Code Sandbox
Thank you.
try this one:
import "./styles.css";
import React, { useState, useEffect } from "react";
export default function App() {
const [data, setData] = useState([
{ id: 1, name: "one", active: false },
{ id: 2, name: "two", active: false },
{ id: 3, name: "three", active: false }
]);
return (
<div className="App">
<h2>Set active className to multiple items on .map</h2>
{data?.map((item, index) => {
return (
<p className={data[index].active ? "selected" : "notselected"}
onClick={() => {
setData((prevState) =>
_.orderBy(
[
...prevState.filter((row) => row.id !== item.id),
{ ...item, active: !item.active }
],
["name"],
["asc"]
)
);
}}
>
{item.name}
</p>
);
})}
</div>
);
}
You can acheive this by simply making some minor changes to your code:
// Changing the state value to an object so that it can
// store the active value for exact item ids
const [activeIndicies, setActiveIndicies] = useState({});
Then inside of .map()
....
// Checking if there is any value for the item id which is being mapped right now.
const selected = activeIndicies[item.id];
return (
<p
className={selected ? "selected" : "notselected"}
onClick={() => {
/* Then setting the state like below where it toggles
the value for particular item id. This way if item is
selected it will be deselected and vice-versa.
*/
setActiveIndicies((prevState) => {
const newStateValue = !prevState[item.id];
return { ...prevState, [item.id]: newStateValue };
});
}}
// Key is important :)
key={item.id}
>
{item.name}
</p>
);
Hello, friends!
I solved this problem in a more convenient way for me )
const data = [
{ id: 1, name: "Ann", selected: true },
{ id: 2, name: "Serg", selected: false },
{ id: 3, name: "Boris", selected: true },
];
//you don't even need to have a boolean field in the object -
//it will be added by itself on the first click on the element
// const data = [{ name:"Ann", id:1}, { name:"Serg", id:2 },{ name:"Boris", id:3 },]
const [users, setUsers] = useState(data); // no square brackets-[data]
function handleActive(item) {
setUsers((prev) => {
return prev.map((itemName) => {
if (itemName.name === item.name) {
return { ...itemName, selected: !itemName.selected };
// itemName.selected = !itemName.selected // or so
}
return itemName;
});
});
}
return (
{
users.map((item, i) => {
// let current = item.selected === true
// or so -> className={current === true ? 'users': ''}
return (
<div
onClick={() => handleActive(item)}
className={item.selected === true ? "active" : ""}
key={i}
>
{item.name}
</div>
);
})
}
);
I have a new app I'm working on that has a lot of CRUD functionality. A list/grid and then a modal with multiple tabs of content and a save button in the modal header. I'm wondering about patterns to separate state logic from the JSX component so it's easier to read/maintain.
I read about "lifting state", but that makes this item component unruly with the callbacks (and it'll get worse for sure as more detail is added). Is it possible to put the handlers and the state itself in a custom hook and then pull them in where needed, instead of having it at "the closest common ancestor"? For example,
const {company, setCompany, updateCompany, getCompanyByCode, createCompany, onNameChangeHandler, onTitleChangeHandler, etc.} from "./useCompany"
Would all users of this hook have the same view of the data and be able to see and effect updates? I've read about putting data fetching logic in hooks, but what about change handlers for what could be 20 or more fields? I think it's the clutter of the callbacks that bothers me the most.
Here is a shortened version of a component that renders the tabs and data for a specific domain item. Is this a use case for context and/or custom hooks?
import {
getCompanyByCode,
updateCompany,
createCompany,
companyImageUpload,
} from "../../api/companyApi";
const initialState: CompanyItemDetailModel = {
code: "",
name: { en: "", es: "" },
title: { en: "", es: "" },
description: { en: "", es: "" },
displayOrder: 0,
enabled: false,
};
export interface CompanyItemProps {
onDetailsDialogCloseHandler: DetailsCancelHandler;
companyCode: string;
onSaveHandler: () => void;
}
const CompanyItem = (props: CompanyItemProps) => {
const { CompanyCode, onDetailsDialogCloseHandler, onSaveHandler } = props;
const classes = useStyles();
const [tabValue, setTabValue] = useState<Number>(0);
const [isLoading, setIsLoading] = useState(true);
const nameRef = useRef({ en: "", es: "" });
const [Company, setCompany] = useState<CompanyItemDetailModel>(initialState);
const [saveGraphics, setSaveGraphics] = useState(false);
useEffect(() => {
async function getCompany(code: string) {
try {
const payload = await getCompanyByCode(code);
nameRef.current.en = payload.name.en;
setCompany(payload);
} catch (e) {
console.log(e);
} finally {
setIsLoading(false);
}
}
if (CompanyCode.length > 0) {
getCompany(CompanyCode);
}
}, [CompanyCode]);
function handleTabChange(e: React.ChangeEvent<{}>, newValue: number) {
setTabValue(newValue);
}
function detailsDialogSaveHandler() {
const CompanyToSave = {
...Company,
name: JSON.stringify({ ...Company.name }),
description: JSON.stringify({ ...Company.description }),
title: JSON.stringify({ ...Company.title }),
};
if (CompanyCode.length > 0) {
updateCompany(CompanyToSave as any).then((e) => handleSaveComplete());
} else {
createCompany(CompanyToSave as any).then((e) => handleSaveComplete());
}
setSaveGraphics(true);
}
function handleSaveComplete() {
onSaveHandler();
}
function detailsDialogCloseHandler(e: React.MouseEvent) {
onDetailsDialogCloseHandler(e);
}
function handleNameTextChange(e: React.ChangeEvent<HTMLInputElement>) {
setCompany((prevState) => ({
...prevState,
name: {
...prevState.name,
en: e.target.value,
},
}));
}
function handleCompanyCodeTextChange(e: React.ChangeEvent<HTMLInputElement>) {
setCompany((prevState) => ({
...prevState,
code: e.target.value,
}));
}
function handleEnabledCheckboxChange(e: React.ChangeEvent<HTMLInputElement>) {
setCompany((prevState) => ({
...prevState,
enabled: e.target.checked,
}));
}
function handleTitleTextChange(e: React.ChangeEvent<HTMLInputElement>) {
setCompany((prevState) => ({
...prevState,
title: {
...prevState.title,
en: e.target.value,
},
}));
}
return (
<DetailsDialog>
<div className={classes.root}>
<Form>
<ModalHeader
name={nameRef.current}
languageCode={"en"}
onTabChange={handleTabChange}
onCancelHandler={detailsDialogCloseHandler}
onSaveHandler={detailsDialogSaveHandler}
tabTypes={[
DetailsTabTypes.Details,
DetailsTabTypes.Images,
]}
/>
<TabPanel value={tabValue} index={0} dialog={true}>
<CompanyDetailsTab
details={Company}
isEditMode={CompanyCode.length > 1}
languageCode={"en"}
handleNameTextChange={handleNameTextChange}
handleCompanyCodeTextChange={handleCompanyCodeTextChange}
handleEnabledCheckboxChange={handleEnabledCheckboxChange}
handleTitleTextChange={handleTitleTextChange}
handleDescriptionTextChange={handleDescriptionTextChange}
/>
</TabPanel>
<TabPanel value={tabValue} index={1} dialog={true}>
<ImageList />
</TabPanel>
</Form>
</div>
</DetailsDialog>
);
};
export default CompanyItem;
I have a parent componenet called FormLeadBuilderEdit, and it is use useState hook I pass the setState(setCards in my case) function down to to my child componenet called Card. For some reason in the child componenet when I call setCard("vale") the state doesnt update. So I am not sure what I am doing wrong.
Any help would be great
Thanks
FormLeadBuilderEdit Component
import React, { useState, useEffect } from "react";
import { Card } from "./Card";
const FormLeadBuilderEdit = ({ params }) => {
const inputs = [
{
inputType: "shortText",
uniId: Random.id(),
label: "First Name:",
value: "Kanye",
},
{
inputType: "phoneNumber",
uniId: Random.id(),
label: "Cell Phone Number",
value: "2813348004",
},
{
inputType: "email",
uniId: Random.id(),
label: "Work Email",
value: "kanye#usa.gov",
},
{
inputType: "address",
uniId: Random.id(),
label: "Home Address",
value: "123 White House Avenue",
},
{
inputType: "multipleChoice",
uniId: Random.id(),
label: "Preferred Method of Contact",
value: "2813348004",
multipleChoice: {
uniId: Random.id(),
options: [
{
uniId: Random.id(),
label: "Email",
checked: false,
},
{
uniId: Random.id(),
label: "Cell Phone",
checked: false,
},
],
},
},
{
inputType: "dropDown",
uniId: Random.id(),
label: "How did you find us?",
value: "2813348004",
dropDown: {
uniId: Random.id(),
options: [
{
uniId: Random.id(),
label: "Google",
},
{
uniId: Random.id(),
label: "Referral",
},
],
},
},
];
const [cards, setCards] = useState([]);
setCards(inputs)
return (
<>
<Card
key={card.uniId + index}
index={index}
id={card.uniId}
input={card}
setCards={setCards}
params={params}
cards={cards}
/>
</>
);
};
export default FormLeadBuilderEdit;
Cart Component
import React, { useRef } from "react";
import { Random } from "meteor/random";
export const Card = ({ setCards, cards }) => {
const addOption = () => {
const newCards = cards;
newCards.map((card) => {
if (card.inputType === "multipleChoice") {
card.multipleChoice.options.push({
uniId: Random.id(),
label: "test",
checked: false,
});
}
});
console.log(newCards);
setCards(newCards);
return (
<>
<button onClick={addOption} type="button">
Add Option
</button>
</>
);
};
React uses variable reference as a way to know which state has been changed and then triggers re-renders.
So one of the first thing you would like to know about state is that "Do not mutate state directly".
Reference: https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly
Instead, produce a new state that contains changes (which has different variable reference) :
const addOption = () => {
const newCards = cards.map((card) => {
if (card.inputType === "multipleChoice") {
const newOption = {
uniId: Random.id(),
label: "test",
checked: false,
};
card.multipleChoice.options = [...card.multipleChoice.options, newOption];
}
return card;
});
setCards(newCards);
// setCards(cards); <- this refer to the current `cards` which will not trigger re-render
};
As pointed out by one of the users, you are passing an empty cards array on which you are performing map operation which not surprisingly returns an empty array itself, hence you are not getting any state changes.
The logic of passing the setCards is correct.
Here is a small example where state changes are taking place and also showing.
import React, { useState } from "react";
const App = () => {
const [cards, setCards] = useState([]);
return (
<>
<Card
setCards={setCards}
cards={cards}
/>
<p>{cards.toString()}</p>
</>
);
};
const Card = ({ setCards, cards }) => {
const addOption = () => {
setCards(["1","2"]);
};
return (
<>
<button onClick={addOption} type="button">
Add Option
</button>
</>
);
};
export default App;
Screenshot:
Codesandbox Link
When a user adds additional information, a mutation is made to the database adding the new info, then the local state is updated, adding the new information to the lead.
My mutation and state seem to get updated fine, the issue seems to be that the state of the Material Table component does not match its 'data' prop. I can see in the React Dev tools that the state was updated in the parent component and is being passes down, the table just seems to be using stale data until I manually refresh the page.
I will attach images of the React Devtools as well as some code snippets. Any help would be much appreciated.
Devtools Material Table data prop:
Devtools Material Table State
Material Table Parent Component:
const Leads = () => {
const [leadState, setLeadState] = useState({});
const [userLeadsLoaded, setUserLeadsLoaded] = React.useState(false);
const [userLeads, setUserLeads] = React.useState([]);
const { isAuthenticated, user, loading } = useAuth()
const [
createLead,
{ data,
// loading: mutationLoading,
error: mutationError },
] = useMutation(GQL_MUTATION_CREATE_LEAD);
const params = { id: isAuthenticated ? user.id : null };
const {
loading: apolloLoading,
error: apolloError,
data: apolloData,
} = useQuery(GQL_QUERY_ALL_LEADS, {
variables: params,
});
useEffect(() => {
if (apolloData) {
if (!userLeadsLoaded) {
const { leads } = apolloData;
const editable = leads.map(o => ({ ...o }));
setUserLeads(editable);
setUserLeadsLoaded(true);
};
}
}, [apolloData])
if (apolloLoading) {
return (
<>
<CircularProgress variant="indeterminate" />
</>
);
};
if (apolloError) {
console.log(apolloError)
//TODO: Do something with the error, ie default user?
return (
<div>
<div>Oh no, there was a problem. Try refreshing the app.</div>
<pre>{apolloError.message}</pre>
</div>
);
};
return (
<>
<Layout leadState={leadState} setLeads={setUserLeads} leads={userLeads} setLeadState={setLeadState} createLead={createLead}>
{apolloLoading ? (<CircularProgress variant="indeterminate" />) : (<LeadsTable leads={userLeads} setLeads={setUserLeads} />)}
</Layout>
</>
)
}
export default Leads
Handle Submit function for adding additional information:
const handleSubmit = async (event) => {
event.preventDefault();
const updatedLead = {
id: leadState.id,
first_name: leadState.firstName,
last_name: leadState.lastName,
email_one: leadState.email,
address_one: leadState.addressOne,
address_two: leadState.addressTwo,
city: leadState.city,
state_abbr: leadState.state,
zip: leadState.zipCode,
phone_cell: leadState.phone,
suffix: suffix,
address_verified: true
}
const { data } = await updateLead({
variables: updatedLead,
refetchQueries: [{ query: GQL_QUERY_GET_USERS_LEADS, variables: { id: user.id } }]
})
const newLeads = updateIndexById(leads, data.updateLead)
console.log('New leads before setLeads: ', newLeads)
setLeads(newLeads)
// setSelectedRow(data.updateLead)
handleClose()
};
Material Table Component:
const columnDetails = [
{ title: 'First Name', field: 'first_name' },
{ title: 'Last Name', field: 'last_name' },
{ title: 'Phone Cell', field: 'phone_cell' },
{ title: 'Email', field: 'email_one' },
{ title: 'Stage', field: 'stage', lookup: { New: 'New', Working: 'Working', Converted: 'Converted' } },
{ title: 'Active', field: 'active', lookup: { Active: 'Active' } },
];
const LeadsTable = ({ leads, setLeads }) => {
const classes = useStyles();
const { user } = useAuth();
const [isLeadDrawerOpen, setIsLeadDrawerOpen] = React.useState(false);
const [selectedRow, setSelectedRow] = React.useState({});
const columns = React.useMemo(() => columnDetails);
const handleClose = () => {
setIsLeadDrawerOpen(!isLeadDrawerOpen);
}
console.log('All leads from leads table render: ', leads)
return (
<>
<MaterialTable
title='Leads'
columns={columns}
data={leads}
icons={tableIcons}
options={{
exportButton: false,
hover: true,
pageSize: 10,
pageSizeOptions: [10, 20, 30, 50, 100],
}}
onRowClick={(event, row) => {
console.log('Selected Row:', row)
setSelectedRow(row);
setIsLeadDrawerOpen(true);
}}
style={{
padding: 20,
}}
/>
<Drawer
variant="temporary"
open={isLeadDrawerOpen}
anchor="right"
onClose={handleClose}
className={classes.drawer}
>
<LeadDrawer onCancel={handleClose} lead={selectedRow} setLeads={setLeads} setSelectedRow={setSelectedRow} leads={leads} />
</Drawer>
</>
);
};
export default LeadsTable;
Try creating an object that contains refetchQueries and awaitRefetchQueries: true. Pass that object to useMutation hook as a 2nd parameter. See example below:
const [
createLead,
{ data,
loading: mutationLoading,
error: mutationError },
] = useMutation(GQL_MUTATION_CREATE_LEAD, {
refetchQueries: [{ query: GQL_QUERY_GET_USERS_LEADS, variables: { id: user.id } }],
awaitRefetchQueries: true,
});
Manually updating cache. Example blow is adding a new todo. In your case you can find and update the record before writing the query.
const updateCache = (cache, {data}) => {
// Fetch the todos from the cache
const existingTodos = cache.readQuery({
query: GET_MY_TODOS
});
// Add the new todo to the cache (or find and update an existing record here)
const newTodo = data.insert_todos.returning[0];
cache.writeQuery({
query: GET_MY_TODOS,
data: {todos: [newTodo, ...existingTodos.todos]}
});
};
const [addTodo] = useMutation(ADD_TODO, {update: updateCache});
I have a state object that contains an array of objects:
this.state = {
feeling: [
{ name: 'alert', status: false },
{ name: 'calm', status: false },
{ name: 'creative', status: false },
{ name: 'productive', status: false },
{ name: 'relaxed', status: false },
{ name: 'sleepy', status: false },
{ name: 'uplifted', status: false }
]
}
I want to toggle the boolean status from true to false on click event. I built this function as a click handler but it doesn't connect the event into the state change:
buttonToggle = (event) => {
event.persist();
const value = !event.target.value
this.setState( prevState => ({
status: !prevState.status
}))
}
I'm having a hard time following the control flow of the nested React state change, and how the active event makes the jump from the handler to the state object and vice versa.
The whole component:
export default class StatePractice extends React.Component {
constructor() {
super();
this.state = {
feeling: [
{ name: 'alert', status: false },
{ name: 'calm', status: false },
{ name: 'creative', status: false },
{ name: 'productive', status: false },
{ name: 'relaxed', status: false },
{ name: 'sleepy', status: false },
{ name: 'uplifted', status: false }
]
}
}
buttonToggle = (event) => {
event.persist();
const value = !event.target.value
this.setState( prevState => ({
status: !prevState.status
}))
}
render() {
return (
<div>
{ this.state.feeling.map(
(stateObj, index) => {
return <button
key={ index }
onClick={ this.buttonToggle }
value={ stateObj.status } >
{ stateObj.status.toString() }
</button>
}
)
}
</div>
)
}
}
In order to solve your problem, you should first send the index of the element that is going to be modified to your toggle function :
onClick = {this.buttonToggle(index)}
Then tweak the function to receive both the index and the event.
Now, to modify your state array, copy it, change the value you are looking for, and put it back in your state :
buttonToggle = index => event => {
event.persist();
const feeling = [...this.state.feeling]; //Copy your array
feeling[index] = !feeling[index];
this.setState({ feeling });
}
You can also use slice to copy your array, or even directly send a mapped array where only one value is changed.
Updating a nested object in a react state object is tricky. You have to get the entire object from the state in a temporary variable, update the value within that variable and then replace the state with the updated variable.
To do that, your buttonToggle function needs to know which button was pressed.
return <button
key={ index }
onClick={ (event) => this.buttonToggle(event, stateObj.name) }
value={ stateObj.status } >
{ stateObj.status.toString() }
</button>
And your buttonToggle function could look like this
buttonToggle = (event, name) => {
event.persist();
let { feeling } = this.state;
let newFeeling = [];
for (let index in feeling) {
let feel = feeling[index];
if (feel.name == name) {
feel = {name: feel.name, status: !feel.status};
}
newFeeling.push(feel);
}
this.setState({
feeling: newFeeling,
});
}
Here's a working JSFiddle.
Alternatively, if you don't need to store any more data per feeling than "name" and "status", you could rewrite your component state like this:
feeling: {
alert: false,
calm: false,
creative: false,
etc...
}
And buttonToggle:
buttonToggle = (event, name) => {
event.persist();
let { feeling } = this.state;
feeling[name] = !feeling[name];
this.setState({
feeling
});
}
I think you need to update the whole array when get the event. And it is better to not mutate the existing state. I would recommend the following code
export default class StatePractice extends React.Component {
constructor() {
super();
this.state = {
feeling: [
{ name: "alert", status: false },
{ name: "calm", status: false },
{ name: "creative", status: false },
{ name: "productive", status: false },
{ name: "relaxed", status: false },
{ name: "sleepy", status: false },
{ name: "uplifted", status: false },
],
};
}
buttonToggle = (index, value) => (event) => {
event.persist();
const toUpdate = { ...this.state.feeling[index], status: !value };
const feeling = [...this.state.feeling];
feeling.splice(index, 1, toUpdate);
this.setState({
feeling,
});
};
render() {
return (
<div>
{this.state.feeling.map((stateObj, index) => {
return (
<button
key={index}
onClick={this.buttonToggle(index, stateObj.status)}
value={stateObj.status}
>
{stateObj.status.toString()}
</button>
);
})}
</div>
);
}
}