I am using a table to display data. I would like to get the row data into a div when the user clicks the table row:
Currently I am getting the data onclick which is working:
const handleClick = (rowData) => {
console.log(rowData);
}
Although currently not sure how I should pass and change the data into the div, here's the full code:
const Start = ({rowData}) => {
const options = {
onRowClick: rowData => handleClick(rowData),
}; // MUI Datatables onRowClick, works fine
const handleClick = (rowData) => {
console.log(rowData); // shows data in console, works
}
return (
<div className="tablehead">
<h2>({rowData})</h2> // no data here, how to pass the onclick data here?
</div>
)
}
export default Start;
So what's happened here is that you have mixed up passing row data props with a variable that is the same name. Instead you need to look into managing state through hooks.
const Start = ({rowData}) => {
const [dataForDisplay, setDataForDisplay] = useState(rowData);
const handleClick = (rowData) => {
setDataForDisplay(rowData);
}
return (
<div className="tablehead">
<h2>({dataForDisplay})</h2>
</div>
)
}
export default Start;
However using props to seed state is most of the time an anti-pattern, so you might want to reconsider your component hierarchy. Either way however you'll need to look into state management and hooks.
https://reactjs.org/docs/state-and-lifecycle.html
Related
, Using props I was able to effectively pass state upwards from my child component to its parent, but a change in the state does not cause a re-render of the page.
import React, { useState } from "react";
export default function App() {
const AddToList = (item) => {
setText([...text, item]);
};
const removeFromList = (item) => {
const index = text.indexOf(item);
setText(text.splice(index, 1));
};
const [text, setText] = React.useState(["default", "default1", "default2"]);
return (
<div className="App">
<div>
<button
onClick={() => {
AddToList("hello");
}}
>
Add
</button>
</div>
{text.map((item) => {
return <ChildComponent text={item} removeText={removeFromList} />;
})}
</div>
);
}
const ChildComponent = ({ text, removeText }) => {
return (
<div>
<p>{text}</p>
<button
onClick={() => {
removeText(text);
}}
>
Remove
</button>
</div>
);
};
In the snippet, each time AddToList is called, a new child component is created and the page is re-rendered reflecting that. However, when i call removeFromList on the child component, nothing happens. The page stays the same, even though I thought this would reduce the number of childComponents present on the page. This is the problem I'm facing.
Updated Answer (Following Edits To Original Question)
In light of your edits, the problem is that you are mutating and passing the original array in state back into itself-- React sees that it is receiving a reference to the same object, and does not update. Instead, spread text into a new duplicate array, splice the duplicate array, and pass that into setText:
const removeFromList = (item) => {
const index = text.indexOf(item);
const dupeArray = [...text];
dupeArray.splice(index, 1);
setText(dupeArray);
};
You can see this working in this fiddle
Original Answer
The reason React has things like state hooks is that you leverage them in order to plug into and trigger the React lifecycle. Your problem does not actually have anything to do with a child attempting to update state at a parent. It is that while your AddToList function is properly leveraging React state management:
const AddToList = (item) => {
setText([...text, item]);
};
Your removeFromList function does not use any state hooks:
const removeFromList = (item) => {
const index = text.indexOf(item);
text.splice(index, 1); // you never actually setText...
};
...so React has no idea that state has updated. You should rewrite it as something like:
const removeFromList = (item) => {
const index = text.indexOf(item);
const newList = text.splice(index, 1);
setText(newList);
};
(Also, for what it is worth, you are being a little loose with styles-- your AddToList is all caps using PascalCase while removeFromCase is using camelCase. Typically in JS we reserve PascalCase for classes, and in React we also might leverage it for components and services; we generally would not use it for a method or a variable.)
I currently have the following useState and function:
const [displayTraits, setDisplayTraits] = useState(false);
const feelingsFilled = () => {
const keysToCheck = ["anxiety", "mood", "cognition"];
function validate(obj) {
try {
if (
Object.keys(obj)
.filter((key) => keysToCheck.includes(key))
.every((key) => obj[key].counter > 0)
) {
setDisplayTraits(true);
} else {
setDisplayTraits(false);
}
} catch (err) {
console.log("error");
}
}
validate(daily);
};
feelingsFilled();
which I then try to hook up with a modal so that when my function feelingsFilled() return true and changes the displayTraits to true, then it will open.
<Modal isVisible={displayTraits} />
I am trying to run this but get the following error
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
I would need to make some assumptions on where the daily data comes from. If it is a changing prop or some other state, you can compute stuff based on that.
Using some state:
useEffect(() => {
//computeYourStuffAndSetState
const result = validate(daily);
setDisplayTraits(result);
}, [daily]);
// The array param is a change-listener
Then you can bind your <Modal visible directly to the state variable, instead of a function.
Another example is to not use state at all and compute it everytime daily changes.
const showModal = useMemo(() => {
return validate(daily);
}, [daily]);
useMemo and useEffect is a part of the built in react hooks.
Or you can just do something like:
const showModal = validate(daily);
This will also work, but will be less performant as it will recompute on every render
I got a react functional component:
const DataGrid = (props) =>
{
const [containerName, setContainerName] = useState("");
const [frameworkComponents, setFrameworkComponents] = useState(
{customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
useEffect(async () =>
{
if(props.containerName && props.containerName !== "")
{
setContainerName(props.containerName);
}
},[props.containerName]);
.
.
.
const onDeleteSetting = async (settingKey) =>
{
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName); //HERE THE CONTAINER NAME IS EMPTY
...
}
return (
<UxDataGrid
frameworkComponents={frameworkComponents}/>
);
The container name inside useEffect exists and is not empty. As you can see in the comment in onDeleteSetting, the containerName is empty when this callback is invoked. I tried adding this to the useEffect after setContainerName:
setFrameworkComponents({customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
That didn't work.
How can I get the name inside the callback? There is no special need to leave that frameworkComponents struct in the state.. it can also be moved to somewhere else if you think its better
Try this in your useEffect, update the onDeleteSetting function with the new containerName when it's updated
.....
useEffect(async() => {
if (props.containerName && props.containerName !== "") {
setContainerName(props.containerName);
// move this function here
const onDeleteSetting = async(settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
// use props.containerName since the state update is async
console.log(props.containerName);
...
}
// update your components with the updated functions
setFrameworkComponents(prevComponents => ({
...prevComponents,
editButton: params =>
<ViewAndDeleteSetting
{...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}
/>,
}));
}
}, [props.containerName]);
.....
This should provide the updated state with the updated function, if it works, I can add more details.
You almost certainly shouldn't be storing it in state. Props are essentially state controlled by the parent. Just use it from props. Copying props to state is usually not best practice.
If you're looking at one of the very rare situations where it makes sense to set derived state based on props, this page in the documentation tells you how to do that with hooks. Basically, you don't use useEffect, you do your state update right away.
Here's a full quote from the linked documentation:
How do I implement getDerivedStateFromProps?
While you probably don’t need it, in rare cases that you do (such as implementing a <Transition> component), you can update the state right during rendering. React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.
Here, we store the previous value of the row prop in a state variable so that we can compare:
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
This might look strange at first, but an update during rendering is exactly what getDerivedStateFromProps has always been like conceptually.
If you did it the same way they did in that example, your component would still render with containerName set to the default state (""), it's just that it will then almost immediately re-render with the updated containerName. That makes sense for their example of a transition, but you could avoid that by making the prop's initial value the state's initial value, like this:
const DataGrid = (props) => {
const [containerName, setContainerName] = useState(props.containerName); // *** ONLY USES THE INITIAL PROP VALUE
const [frameworkComponents, setFrameworkComponents] = useState(
// ...
});
// *** Updates the state value (on the next render) if the prop changes
if (containerName !== props.containerName) {
setContainerName(props.containerName);
}
// ...
};
Every time the containerName prop changes, though, your component will render twice, which brings us back full circle to: Don't store it in state, just use it from props. :-)
Stepping back and looking at the component as a whole, I don't think you need any state information at all, but if your goal is to avoid having the frameworkComponents you pass UxDataGrid change unnecessarily, you probably want useMemo or React.memo rather than state.
For instance, with useMemo (but keep reading):
const DataGrid = ({containerName}) => {
const frameworkComponents = useMemo(() => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return {
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
};
}, [containerName]);
return (
<UxDataGrid frameworkComponents={frameworkComponents} />
);
};
But if componentName is your only prop, it may well be even simpler with React.memo:
const DataGrid = React.memo(({containerName}) => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return (
<UxDataGrid frameworkComponents={{
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
}} />
);
});
React.memo memoizes your component, so that your component function is only ever called again when the props change. Since everything in the component needs to update based on the componentName prop changing, that looks like a good match (but I don't know what UxDataGrid is).
The problem was with how I tried passing props to ViewAndDeleteSetting. If you want to pass prop to a cell rendered component, you shouldn't be doing it in frameworkComponents, but rather you need to do it in the column definition like this:
useEffect(() =>
{
let columns = [{headerName: '', cellRenderer: 'editButton', width: 90, editable: false,
cellRendererParams: {
openAddConfigurationsWindow: openAddConfigurationsWindow,
onDeleteSetting: onDeleteSetting
}},
.. other columns
]
setColumnDefinition(columns);
},[props.containerName]);
The columns with the cellRendererParams do gets recreated in the useEffect when the name changes, and then the component can access this params regularly via its props
I have a todo list app which users can read and save items to. Here, Todo is a functional component that queries an API for the users current items on their list using the useEffect() hook. When a successful response is received the data is added to the component's state using useState() and rendered as part of the ItemList component.
When a user submits the form within the AddItemForm component a call back is fired that updates the state of newItem, a dependency of useEffect, which triggers another call to the API and a re-render of the component.
Logically, everything above works. However, it seems wrong to make an extra request to the API simply to receive the data that is already available but I can't find the correct pattern that would allow me to push the item available in useCallback to the items array without causing useEffect to loop infinitely yet still update the ItemList component.
Is there away for my app to push new date from the form submission to items array whilst updating the view and only calling the API once when the page loads?
function Todo() {
const [items, setItems] = useState();
const [newItem, setNewItem] = useState();
useEffect(() => {
fetch('https://example.com/items').then(
(response) => {
setItems(response.data.items);
}, (error) => {
console.log(error);
},
);
}, [newItem]);
const updateItemList = useCallback((item) => {
setNewItem(item);
});
return (
<>
<AddItemForm callback={updateItemList} />
<ItemList items={items} />
</>
);
}
function ItemList(props) {
const { items } = props;
return (
<div>
{ items
&& items.map((item) => <p>{item.description}</p>)}
</div>
);
}
Call API only on start by removing newItem from useEffect(...,[]).
Then add item to the items by destructuring in setItems:
const updateItemList = (item) => {
setItems([...items, item]);
}
I am trying to have a user be able to click an item from a list of all possible items and have a modal open to display data about that item (including the current quantity they have) and buttons to increment/decrement that amount.
To my understanding since I am just showing data that is being passed in and then dispatching an action to update the store I should be using a functional component to display the data and useDispatch to call the store action.
Currently when I update the store I see the change in Redux debugging tools but the change is not reflected in the modal until I reopen it. While I have been looking for answers to this I see many similar questions but they all use Class Components and mapStateToProps (such as this post). I thought best practices was to use functional components unless needed. Am I wrong to think that if I am getting a value from the store in a functional component it should update on change?
Code Snippets
Dialog
export default function ItemDialog({
...
selectedItem,
}) {
const dispatch = useDispatch()
const inventory = useSelector(
state => state.user.inventory
)
let userItem = inventory.find(
userItem => userItem.name === selectedItem.name
)
const changeItemCount = (item, change) => {
item.change = change
dispatch({
type: "USER_INVENTORY_UPDATED",
payload: item
})
}
const showQuantity = userItem => {
return userItem.quantity > 0 ? `(${userItem.quantity})` : ""
}
...
render(
<p className="text-xl text-center font-semibold">
{selectedItem.name}
</p>
<p className="text-center font-light">
{showQuantity(userItem)}
</p>
...
<AddBoxIcon
onClick={() => changeItemCount(selectedItem, 1)}
/>
)
Store
const userReducer = (state = InitialUserState, action) => {
let inventoryCopy = { ...state.inventory }
switch (action.type) {
case "USER_INVENTORY_UPDATED":
let category = action.payload.category
let updatedItemIndex = inventoryCopy[category].findIndex(
item => item.name === action.payload.name.toUpperCase()
)
// If item is already there
if (updatedItemIndex >= 0) {
inventoryCopy[category][updatedItemIndex].quantity +=
action.payload.change
} else {
// If item needs to be added to inventory category
let newItem = {
name: action.payload.name,
quantity: action.payload.change
}
inventoryCopy[category].push(newItem)
}
return {
...state,
inventory: inventoryCopy
}
...
default:
return state
}
}
Check your spread operator when you return your updated state. You may need to deep clone the old state depending on how many nested objects it has.
The docs have more information on shallow cloning objects.
Deeply cloning your state object will help you get rid of:
let inventoryCopy = { ...state.inventory }