Facing a strange issue with the latest stable releases of Nextjs and React, where state and view updates are out-of-sync.
Example (v1): using useMemo
import { useState, useMemo } from "react";
const items = [
{ id: 1, name: "apple" },
{ id: 2, name: "orange" },
{ id: 3, name: "mango" }
];
export default function IndexPage() {
const [desc, setDesc] = useState(true);
const sortedItems = useMemo(() => [...(desc ? items : items.reverse())], [
desc
]);
return (
<div>
<button onClick={() => setDesc((s) => !s)}>change order</button>
<br />
<span>desc: {desc.toString()}</span>
<pre>{JSON.stringify(sortedItems, null, 2)}</pre>
</div>
);
}
Example (v2): using useState
import { useState, useMemo } from "react";
const items = [
{ id: 1, name: "apple" },
{ id: 2, name: "orange" },
{ id: 3, name: "mango" }
];
export default function IndexPage() {
const [state, setState] = useState({ desc: true, items });
function handleSort() {
setState((s) =>
s.desc
? { desc: false, items: [...items.reverse()] }
: { desc: true, items: [...items] }
);
}
return (
<div>
<button onClick={handleSort}>change order</button>
<br />
<span>desc: {state.desc.toString()}</span>
<pre>{JSON.stringify(state.items, null, 2)}</pre>
</div>
);
}
Package versions:
"next": "12.1.6",
"react": "18.2.0",
"react-dom": "18.2.0",
As you can see from the output below, array elements are not updated on the view front based on the current sort state (in both the examples). Multiple clicks are required to do so.
Is this a random bug or am I fooling myself!?
CodeSandbox - Contains both code examples
Calling .reverse on a list will mutate it. Then wherever you reference that list again, it will be reversed.
You don't need to copy the items on to state either, you only need the isReversed state.
const Comp = () => {
const [isReversed, setIsReversed] = useState(false);
const toggle = () => setIsReversed(r => !r);
const list = useMemo(() => isReversed ? [...items].reverse() : items, [isReversed]);
// use list
}
From the useState example, separating the states for checking if it has been sorted and state for holding the items would be a great start.
import { useState } from "react";
const items = [
{ id: 1, name: "apple" },
{ id: 2, name: "orange" },
{ id: 3, name: "mango" },
];
export default function IndexPage() {
const [areSorted, setAreSorted] = useState(false);
const [itemsArr, setItemsArr] = useState(items);
function handleSort() {
setAreSorted(!areSorted);
setItemsArr(itemsArr.reverse());
}
return (
<div>
<button onClick={handleSort}>change order</button>
<br />
<span>{"Desc : " + areSorted}</span>
<pre>{JSON.stringify(itemsArr, null, 2)}</pre>
</div>
);
}
Related
So I have this component called counters where the state looks something like this
state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 3 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
],
};
And I'm trying to increment the value every time a corresponding button is clicked. This is the function I wrote for it.
handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
It doesn't work. And here are some additional observations.
When I console.log the local counters object (copied from state.counters) it returns an additional row at the end with id: -1 and value: NaN
The variable counter (thats being passed as a parameter) is from a child component. It's supposed to return 0 if the first button is clicked, 1 if the second button is clicked and so on. When I console.log it it seems to be returning the correct values.
by the looks of it, the problem seems to lie in the line
const index = counters.indexOf(counter);
As the value of index is always returned as -1.
You can use the index for updating the corresponding record in the array.
const idx = this.state.counters.findIndex((counter) => counter.id === id);
Complete implementation:-
import React from "react";
import "./styles.css";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 3 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
}
handleClick = (id) => {
const idx = this.state.counters.findIndex((counter) => counter.id === id);
const counters = [...this.state.counters];
counters[idx] = { ...counters[idx], value: counters[idx].value++ };
this.setState(counters);
};
render() {
return (
<div>
{this.state.counters.map((counter) => (
<div>
<button onClick={() => this.handleClick(counter.id)}>
Button {counter.id}
</button>
<span>Value {counter.value}</span>
<hr />
</div>
))}
</div>
);
}
}
Codesandbox - https://codesandbox.io/s/musing-black-cippbs?file=/src/App.js
try this for get the index:
const index = counters.findIndex(x => x.id === counter.id);
Adding my answer here just in case another confused soul stumbles upon it.
Although it seemed that the problem lied within line
const index = counters.indexOf(counter);
It actually was in the child component where the function was being invoked. Within the parameter I was passing counters.value whereas the handleincrement function within the parent component was expecting not the value, rather the complete object.
The code in its working condition is as below
Parent Component:
import React, { Component } from "react";
import Counter from "./counter";
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 3 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
],
};
handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
console.log(index);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
Child Component:
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import React, { Component } from "react";
class Counter extends Component {
render() {
return (
<button
onClick={() => this.props.onIncrement(this.props.counter)}
className="btn btn-secondary btn-sm"
>
Increment
</button>
)
}
]
The Problem
I have a list of detail tags and I would like to have an opened details tag close when another one opens.
I am dynamically rendering a list of details tags
Stack
I am using React and hooks
My attempts
I set the open attribute is set with useState and updated when a details tag is clicked, but this does not seem to work.
Here is a link to a code sandbox
import { useState } from "react";
const arr = [
{ name: "Jim", age: 22 },
{ name: "Sarah", age: 42 },
{ name: "Don", age: 7 }
];
export default function App() {
const [open, setOpen] = useState(false);
const toggleDetails = (index) => {
setOpen(!open);
};
return (
<ul>
{arr?.map((thing, index) => (
<details key={index} open={open} onClick={() => toggleDetails(index)}>
<summary>{thing.name}</summary>
{thing.age}
</details>
))}
</ul>
);
}
I added an "id" key as presented in your codesandbox to do the following, use toggleDetails to set the id of the current opened detail and then in the open prop check if the current object id in the array matches this of the state.
If it does, open is true, else it is false.
import { useEffect, useState } from "react";
const arr = [
{ id: "03F03BBE", name: "Jim", age: 22 },
{ id: "D37DEF7F1E7E", name: "Julie", age: 42 },
{ id: "8D61", name: "Don", age: 7 }
];
export default function App() {
const [openId, setOpenId] = useState('');
const toggleDetails = (thingId) => {
setOpenId(thingId);
};
return (
<ul>
{arr?.map((thing, index) => (
<details key={thing.id} open={openId === thing.id} onClick={() => toggleDetails(thing.id)}>
<summary>{thing.name}</summary>
{thing.age}
</details>
))}
</ul>
);
}
This works for me:
import { useState } from "react";
const faqs = [
{ id: "03F03BBE", name: "Jim", age: 22 },
{ id: "D37DEF7F1E7E", name: "Julie", age: 42 },
{ id: "8D61", name: "Don", age: 7 },
];
export default function App() {
const [openFaqId, setOpenFaqId] = useState("");
const clickActiveFaq = (id) => (e) => {
e.preventDefault();
setOpenFaqId(id !== openFaqId ? id : "");
};
return (
<div>
{faqs?.map((faq) => (
<details
key={faq.id}
open={openFaqId === faq.id}
onClick={clickActiveFaq(faq.id)}
>
<summary>{faq.name}</summary>
{faq.age}
</details>
))}
</div>
);
}
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
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
I'm trying to build a treeview component in react where data for the tree is fetched based on the nodes expanded by the user.
Problem
I want to replace the code inside handleChange with data from my server, so that I append the data i fetch to the tree state. How can I achieve this with react?
The data i get can look like this:
{
"children": [
{
"id": "2212",
"parentId": "3321",
"name": "R&D",
"address": "homestreet"
},
{
"id": "4212",
"parentId": "3321",
"name": "Testing",
"address": "homestreet"
}
]
}
My Code
import React, { useState } from "react";
import { makeStyles } from "#material-ui/core/styles";
import TreeView from "#material-ui/lab/TreeView";
import ExpandMoreIcon from "#material-ui/icons/ExpandMore";
import ChevronRightIcon from "#material-ui/icons/ChevronRight";
import TreeItem from "#material-ui/lab/TreeItem";
const useStyles = makeStyles({
root: {
height: 216,
flexGrow: 1,
maxWidth: 400
}
});
export default function FileSystemNavigator() {
const classes = useStyles();
const initialData = {
root: [
{
id: "1",
label: "Applications"
}
],
};
const [tree, setTree] = useState(initialData);
const handleChange = (event, nodeId) => {
setTimeout(() => {
const newTree = {
...tree,
[nodeId]: [
{
id: "2",
label: "Calendar"
},
{
id: "3",
label: "Settings"
},
{
id: "4",
label: "Music"
}
]
};
setTree(newTree);
}, 1000); // simulate xhr
};
const renderTree = children => {
return children.map(child => {
const childrenNodes =
tree[child.id] && tree[child.id].length > 0
? renderTree(tree[child.id])
: [<div />];
return (
<TreeItem key={child.id} nodeId={child.id} label={child.label}>
{childrenNodes}
</TreeItem>
);
});
};
return (
<TreeView
className={classes.root}
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
onNodeToggle={handleChange}
>
{renderTree(tree.root)}
</TreeView>
);
}
If I am understanding correctly, you want to replace your "fake" setTimeout implementation of an API call with a real call using fetch.
In this case, it's as simple as calling fetch inside of the handleChange handler and updating your state with new items that you get back as a result.
function FileSystemNavigator() {
const initialData = {...}
const [tree, setTree] = React.useState(initialData)
const handleChange = (event, nodeId) => {
const handleResult = (data) => {
const items = data.children.map(item => {
return { id: item.id, label: item.name }
})
setTree({
root: [...tree.root, ...items]
})
}
const handleError = (error) => {
// handle errors appropriately
console.error(error.message)
}
fetch("https://api.myjson.com/bins/1aqhsc")
.then(res => res.json())
.then(handleResult)
.catch(handleError)
}
// ...
return (...)
}
This should do the trick.
Note that I've used your sample API endpoint that you've provided in the comments, so you will have to change the handleResult callback inside of the handleChange handler to make sure you're parsing out your new data appropriately.
If you'd like to see a quick example, I created a CodeSandbox with a button that can be clicked to fetch more data and display it in a list:
Demo
Let me know if you have any questions.