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.
Related
i'm sorry for the disturbance,
i'm a trying React Native for the first time ( I'm a Full Stack Engineer React NodeJS ),
i tried by differents tips to put AsyncStorage.getItem inside my state, then display in the map,
but everytime, "Error map undefined", but if i put the value inside my State Array, it's working,
i tried with JSON Stringify, JSON Parse... Like in WEB,
but not working...
Here is my code :
import { useEffect, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import RadioForm from 'react-native-simple-radio-button';
import AsyncStorage from '#react-native-async-storage/async-storage';
const SelectOption = () => {
const [value, setValue] = useState([]);
const saveOption = (item) => {
try {
setValue([...value, {name: item, id: Math.random()}]);
} catch (e) {
console.log(e);
}
};
useEffect(() => {
AsyncStorage.setItem('option', JSON.stringify(value));
}, [value]);
// Put GetItem in the state
useEffect(() => {
const getOption = async () => {
try {
const jsonValue = await AsyncStorage.getItem('option');
if (jsonValue !== null) {
setValue(JSON.parse(jsonValue));
}
} catch (e) {
console.log(e);
}
};
getOption();
}, []);
AsyncStorage.getItem('option').then((value) => {
console.log(value);
});
const radioProps = [
{label: 'Option 1', value: 'option1'},
{label: 'Option 2', value: 'option2'},
{label: 'Option 3', value: 'option3'}
];
return (
<View style={styles.sectionContainer}>
<RadioForm
radio_props={radioProps}
initial={0}
onPress={(value) => {
saveOption(value);
}}
/>
{value.map((item) => {
return <Text key={item.id}>{item.name}</Text>;
})
}
</View>
);
};
const styles = StyleSheet.create({
sectionContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
export default SelectOption;
Thanks :pray:
i tried with JSON Stringify, JSON Parse... Like in WEB,
You wants to set value in local storage so, don't need to set the value in useEffect hook. Just add in your saveOption function like
const saveOption = (item) => {
try {
let newValue = [...value, {name: item, id: Math.random()}]
AsyncStorage.setItem('option', newValue);
setValue(newValue);
} catch (e) {
console.log(e);
}
};
And make a getOption async function like
const getOption = async () => {
try {
const jsonValue = await AsyncStorage.getItem('option');
if (jsonValue) {
await setValue(jsonValue);
}
} catch (e) {
console.log(e);
}
};
Then render in JSX like
{value.map((item) => {
return <Text key={item.id}>{item.name}</Text>;
}
This is my output that I get from this GET url: https://localhost/get-all
but I can't save this value in the useState: const [dataCat, setDataCat] = useState([])
When I display it in the console, it is displayed correctly, but it returns an empty array in the state
{
"categories": [
{
"id": 1,
"name": "test1",
"slug": "intelligence-and-memory-tests",
"description": null,
},
{
"id": 2,
"name": "test2",
"slug": "occupational-and-organizational-tests",
"description": null,
},
{
"id": 3,
"name": "test3",
"slug": "love-and-marriage-tests",
},
]
}
this is my useEffect:
useEffect(() => {
const fetchData = async () =>{
try {
const {data} = await axios.get('https://localhost/get-all');
console.log(data);
setDataCat(data)
} catch (error) {
console.error(error.message);
}
}
fetchData();
}, []);
You can display it like this, and it will store the data in your useState(). I've created that formattedData to recreate your object
import { useEffect, useState } from "react";
import axios from "axios";
import "./styles.css";
export default function App() {
const [dataCat, setDataCat] = useState([]);
const [newDataCat, setNewDataCat] = useState([]);
// console.log("dataCat", dataCat);
// console.log("newDataCat", newDataCat);
const formattedData = (infoData) => {
let newDataCat = [];
infoData.forEach((item) =>
newDataCat.push({
id: item.id,
name: item.name,
slug: item.slug,
description: item.description
})
);
return newDataCat;
};
useEffect(() => {
const fetchData = async () => {
try {
const { data } = await axios.get(
"https://localhost/get-all"
);
setDataCat(data.categories);
} catch (error) {
console.error(error.message);
}
};
fetchData();
}, []);
useEffect(() => {
const cat = formattedData(dataCat);
setNewDataCat(cat);
}, [dataCat]);
return (
<>
{newDataCat &&
newDataCat.map((item) => {
return (
<>
<h2>{item.id}</h2>
<h2>{item.name}</h2>
<h2>{item.slug}</h2>
</>
);
})}
</>
);
}
I have a React app. There are components rendered from mapped data like the following:
function App () {
const [price, setPrice] = useState(1);
const cardDetails = [
{
id: 1,
title: 'card 1',
setPrice: setPrice
},
{
id: 2,
title: 'card 2',
setPrice: setPrice
}
]
const renderCards = (cardDetails) => {
return (
cardDetails.map((c) => {
<Card cardData={c} />
})
)
};
return (
<>
{renderCards(cardDetails)}
</>
)
}
It's working well. Now I'd like to move the cardDetails data into a JSON file. I defined the following cardDetails.js:
export const cardDetails = [
{
id: 1,
title: 'card 1',
setPrice: setPrice
},
{
id: 2,
title: 'card 2',
setPrice: setPrice
}
]
However, I can't pass function setPrice in the JSON file, any idea what I could do to use the external JSON file?
Since the setPrice function only exists in App, it can't be in the separate file. You asked "How to store a function name in JSON?" and while you could do that (setPrice: "setPrice", and then when mapping the cards replace it with the setPrice function), it doesn't really buy you anything.
But it's simple to have App add it to the cards as it's passing them to the Card component: <Card cardData={{ ...c, setPrice }} /> That uses spread syntax to spread out the object from c into a new object and adds setPrice to the new object.
To avoid creating new objects on every render (which might force Card to re-render unnecessarily, if Card is memoized), we can use useMemo to memoize the array of extended cards like this:
const fullCards = useMemo(() => (
cardDetails.map((card) => ({...card, setPrice}))
), [cardDetails]);
...and then use fullCards for the map.
Full version:
In cardDetails.js:
export const cardDetails = [
{
id: 1,
title: "card 1",
},
{
id: 2,
title: "card 2",
},
];
Your component:
import { cardDetails } from "./cardDetails.js";
function App() {
const [price, setPrice] = useState(1);
const fullCards = useMemo(() => (
cardDetails.map((card) => ({...card, setPrice}))
), [cardDetails]);
const renderCards = (cardDetails) => cardDetails.map((c) => {
<Card key={c.id} cardData={c} />;
});
return renderCards(fullCards);
}
Or simply:
function App() {
const [price, setPrice] = useState(1);
const fullCards = useMemo(() => (
cardDetails.map((card) => ({...card, setPrice}))
), [cardDetails]);
return fullCards.map((c) => {
<Card key={c.id} cardData={c} />;
});
}
Note that I added the key prop to the Card elements. You need a key on elements in arrays; details in the React documentation here.
That would also work just fine if you wanted to store the data in a JSON file like:
[
{
"id:: 1,
"title": "card 1",
},
{
"id": 2,
"title": "card 2",
},
]
...and then load and parse that JSON for use in App.
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>
);
}
I have a simple problem here which I can't figure out. I wanted to hide menus depending on the condition.
For example if status contains at least one "Unlinked". "All unlinked images" menu should appear. I did used .some and I wonder why it doesn't return a boolean.
Codesandbox is here Click here
const showDeleteAllInvalidButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Invalid");
};
const showDeleteAllUnlinkedButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Unlinked");
};
The methods do return a boolean. But in the menus array you are assigning a function reference not the result -
show: showDeleteAllInvalidButton // function reference
show is now assigned a reference to the function showDeleteAllInvalidButton not the result of productImages?.some. You need to invoke the functions when assigning -
show: showDeleteAllInvalidButton() // result of productImages?.some
In your menus object you have a key that contains a function, so if you want this function to filter out your elements you need to execute the show method in side the filter method.
import React, { useState } from "react";
import Button from "#mui/material/Button";
import MenuItem from "#mui/material/MenuItem";
import KeyboardArrowDownIcon from "#mui/icons-material/KeyboardArrowDown";
import CustomMenu from "../../Menu";
const products = [
{
productName: "Apple",
productImages: [
{
status: "Unlinked"
}
]
},
{
productName: "Banana",
productImages: [
{
status: "Unlinked"
}
]
},
{
productName: "Mango",
productImages: [
{
status: "Unlinked"
},
{
status: "Unlinked"
}
]
}
];
const HeaderButtons = () => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const showDeleteAllInvalidButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Invalid");
};
const showDeleteAllUnlinkedButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Unlinked");
};
const menus = [
{
id: 1,
name: "Invalid images",
action: () => {
handleClose();
},
show: showDeleteAllInvalidButton
},
{
id: 2,
name: "Unlinked images",
action: () => {
handleClose();
},
show: showDeleteAllUnlinkedButton
},
{
id: 3,
name: "All images",
action: () => {
handleClose();
},
show: () => true // not that I changed it to a function for consistency, but you can check for type in the filter method instead of running afunction
}
];
return (
<div>
<Button
color="error"
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
variant="outlined"
onClick={handleClick}
endIcon={<KeyboardArrowDownIcon />}
>
Options
</Button>
<CustomMenu anchorEl={anchorEl} open={open} onClose={handleClose}>
{menus
.filter((e) => e.show()) // here is your mistake
.map(
({
id = "",
action = () => {},
icon = null,
name = "",
divider = null
}) => (
<>
<MenuItem key={id} onClick={action} disableRipple>
{icon}
{name}
</MenuItem>
{divider}
</>
)
)}
</CustomMenu>
</div>
);
};
export default HeaderButtons;
In your code, it will always render because your filter functions are evaluating as truth.