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.
Related
Thanks all in advance.
Going by the screenshot:
enter image description here
I wish to use a dynamic variable (filter) in line 33 but it keeps throwing error. If I use the name properties of the object, Filter_M (eg: "ALL", it works fine. If I use the the values too by using the functions (eg:
(order) => !order.completed
) directly, it works fine.
How can I resolve this?
Note: I am following similar procedure as used in MDN article. See:
Back to the Filter Buttons
A portion of my code is shown below while links to react components are shown at the bottom:
import React from "react";
import FilterButton from "./components/filterButton";
import MyOrder from "./components/MyOrder";
// const ORDER = [
// { cname: 'ki', item: 'liveChicken', quantity: 0, id: "order-0", completed: true },
// { cname: 'fu', item: 'egg', quantity: 0, id: "order-1", completed: false },
// { cname: 'nu', item: 'chicken', quantity: 0, id: "order-2", completed: false}
// ]
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
// <React.StrictMode>
// <App2 orders={ORDER} />
// </React.StrictMode>
// );
const FILTER_M = {
ALL: () => true,
Active: (order) => !order.completed,
Completed: (order) => order.completed
};
const FILTER_NAMES = Object.keys(FILTER_M);
function App(props) {
const chickenPrice = 5000;
const [filter, setFilter] = useState('All');
const [orders, setOrders] = useState(props.orders)
const orderList = orders
.filter(FILTER_M[filter])
.map((order) => (
<MyOrder
item={order.item}
quantity={order.quantity}
cname={order.cname}
id={order.id}
completed={order.completed}
key={order.id}
/>))
const filterList = FILTER_NAMES.map((cname) => (
<FilterButton
key={cname}
cname={cname}
isPressed={cname === filter}
setFilter={setFilter}
/>
));
}
Code Sandbox
I am expecting to display dynamically, the tasks/orders either active, completed, with all orders shown by default.
I have tried hard-coding it by using the property name or the values and it all worked out fine. But using the named variable in object (FILTER_MAP) bracket notation which is the only way of accessing named variable as name property of an object.
Based on the MDN article you provided this code should work, I can't see the complete picture of your code but anyway this code below is working :
const DATA = [
{ id: 'todo-0', name: 'Eat', completed: true },
{ id: 'todo-1', name: 'Sleep', completed: false },
{ id: 'todo-2', name: 'Repeat', completed: false },
];
const filterBy= {
All: (order) => true,
Active: (order) => !order.completed,
Completed: (order) => order.completed,
};
const DisplayList = () => {
const [filter, setFilter] = useState('All');
const [orders, setOrders] = useState(DATA);
console.log(filterBy[filter]);
const orderList = orders.filter(filterBy[filter]).map(order => (
<li key={order.id} id={order.id}>
<p>{order.name}</p>
<p>{`${order.completed}`}</p>
</li>
));
return (
<>
<button onClick={() => setFilter('Active')}>
view active orders ⬇
</button>
{orderList}
</>
);
};
export default function App() {
return (
<DisplayList />
);
}
a working sample in code sandbox
The problem is here :
const FILTER_MAP = {
ALL: () => true, // HERE (ALL) is all CAPS
Active: (order) => !order.completed,
Completed: (order) => order.completed
};
and your are trying to access (All) a property that doesn't exist in FILTER_MAP
const [filter, setFilter] = useState('All'); // not the same as (ALL) in FILTER_MAP
and that is a good example of why you should use type script, because TS would have helped you find this typo.
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>
);
}
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 rendered a list of buttons using Array.map method in a function component. When I tried to pass state to each mapped array items, the rendered results changed all array items at once, instead of one by one.
Here is my code. Am I doing something wrong? Sorry if the question has been solved in other thread or I used the wrong method. This is my first React project and I am still learning. It would be very appreciated if someone could advise. Thank you!
import React, { useState } from "react"
export default function Comp() {
const [isActive, setActive] = useState(false)
const clickHandler = () => {
setActive(!isActive)
console.log(isActive)
}
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => {
return items.map(item => (
<li key={item.id}>
<button onClick={clickHandler}>
{item.name} {isActive ? "active" : "not active"}
</button>
</li>
))
}
return (
<ul>{renderList(data)}</ul>
)
}
Put the individual item into a different component so that each has its own active state:
export default function Comp() {
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => (
items.map(item => <Item key={item.id} name={item.name} />)
);
return (
<ul>{renderList(data)}</ul>
)
}
const Item = ({ name }) => {
const [isActive, setActive] = useState(false);
const clickHandler = () => {
setActive(!isActive);
};
return (
<li>
<button onClick={clickHandler}>
{name} {isActive ? "active" : "not active"}
</button>
</li>
);
};
You need to set the active-id in handling the click-event. That will in-turn render active/non-active conditionally:
Notice the flow (1) > (2) > (3)
function Comp() {
const [activeId, setActiveId] = React.useState(null);
const clickHandler = (item) => {
setActiveId(item.id) // (2) click-handler will set the active id
}
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => {
return items.map(item => (
<li key={item.id}>
<button onClick={() => clickHandler(item)}> // (1) passing the clicked-item so that we can set the active-id
{item.name} {item.id === activeId ?
"active" : "not active" // (3) conditionally render
}
</button>
</li>
))
}
return (
<ul>{renderList(data)}</ul>
)
}
Good Luck...
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.