react functional components - child calling parents method, can't accsess parents state inside the function - react-functional-component

I have a parent component and a list of children component that are buttons and onClick it calls a method in parents component. In that method it checks what the parents state is, and does work according to that. The problem is that when the method gets called - all of a sudden the props it gets from his parent is undefiened.
Here is the parent component
const PaymentsInfo = (props) => {
const paymentsList = useSelector((state) => state.payments.paymentsForDonor)
const [payments, setPayments] = useState()
const [showMessage, seShowMessage] = useState(
useEffect(() => {
if (paymentsList.length > 0) {
const payments = paymentsList.map(payment => {
return <div key={payment.PaymentsID}><TextButton onClick={() =>
showMessageHandler(payment)}>View Details</TextButton>
<TransactionInfo
PaymentsID={payment.PaymentsID}
campaign={payment.CampaignName}
subCampaign={subCampaign}
organization={payment.OrganizationName}
solicitor={payment.SolicitorName}
/>
</div>
})
setPayments(payments)
}
}, [paymentsList])
const showMessageHandler = (payment) => {
if(props.wasChanged){
setShowMessage(payment)
return;
}
}
return (
<Fragment>
<div>
{payments}
</div>
{showMessage && <ChangesMessageBox />}
</Fragment>
)
the method - showMessageHandler gets called, but the props.wasChanged is undefined.
If I check the props.wasChanged the last time it rendered before this function was called it was true, and in the function its undefined

I found a solution in this question: Why can't I see parent state from child component event handler?.
I added a state in the parent component that gets updated when the child component calls the parents method, and a useEffect that gets called when that state updates that does the work the method was supposed to do

Related

Change the input initial value of controlled component in React

I have this input component
const FOO = props => {
const [inputValue, setInputValue] = useState(
props.editState ? props.initialValue : ""
);
const setSearchQuery = (value) => {
setInputValue(value);
props.onSearch(value);
};
return (
<input
placeholder="Select ..."
onChange={(e) => {
setSearchQuery(e.target.value);
}}
value={inputValue}
/>
)}
I'm using it like this
const BAR = props => {
const [fetchedData, setfetchedData] = useState({
value : "" // to get rid of can't change controlled component from undefined error
});
const params = useParams();
//request here to get fetchedData
return(
<FOO
onSearch={(value) => {
searchSomethingHandler(value);
}}
initialValue={
params.ID
? fetchedData.value
: ""
}
editState={params.ID ? true : false}
/>
)}
I need to set the initial value of the fetched data into the input so the user could see the old value and edit it, if I pass the data (fetchedData) as props it works perfectly,
but if I get the data through API it wont set the value cause it's empty at the first render,
how can I solve this please?
You'll probably want to make use of the useEffect hook to run code when a value updates.
In FOO:
const FOO = props => {
// ...
useEffect(() => {
// This hook runs when props.initialValue changes
setInputValue(props.initialValue);
}, [props.initialValue]);
// ...
};
You can leave BAR the same as before, I think. Though, I would put the request to the API inside a useEffect hook with an empty dependency array so you're not querying it on every render.

Getting Proper Types for useRef with useEffect and Event Listeners

Here is the code in question, which closes a menu when the user clicks outside of it. I need to give proper types everywhere where I inserted "any":
//ANY
const ref = useRef<any>();
useEffect(() => {
//ANY
const checkIfClickedOutside = (event: any) => {
// If the menu is open and the clicked target is not within the menu,
// then close the menu
if (open && ref.current && !ref.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside);
};
}, [open]);
I have implemented ref in a div:
return (
<div className="wrapper" ref={ref}>
<button className="nav-button" onClick={handleOpen}>{menu}</button>
{open && children as React.ReactNode}
</div>
)
The useEffect references this useState hook I created:
//State for Open and Closing Menu
const [open, setOpen] = useState<boolean>(false);
const handleOpen = (event: MouseEvent ) => {
event.preventDefault();
setOpen(!open);
};
Since useRef is implemented in a div, I tried the type HTMLDivElement, like so:
const ref = useRef<HTMLDivElement>();
But the prop in the div complained that:
Types of property 'current' are incompatible.
Type 'HTMLDivElement | undefined' is not assignable to type 'HTMLDivElement | null'.
Type 'undefined' is not assignable to type 'HTMLDivElement | null'.
I then tried changing the type to HTMLDivElement | null, but then it complained about something else, and something else, and so on (this question is already long enough).
I've never implemented useRef before so I'm at a bit of a loss.
Any help is appreciated, thanks!
Seemed to have gotten it working.
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const checkIfClickedOutside = (event: globalThis.MouseEvent) => {
if (open && ref.current &&
!ref.current?.contains(event?.target as
HTMLDivElement)) {
setOpen(false);
}
};
document.addEventListener("mousedown", checkIfClickedOutside);
return () => {
document.removeEventListener("mousedown", checkIfClickedOutside);
};
}, [open]);

Mapping from JSON Get Request. Undefined

I am trying to connect to the USDA Food Central database using an API.
let uri = encodeURI(`https://api.nal.usda.gov/fdc/v1/foods/search?api_key=${MY_API_KEY}&query=${search}`)
I want to use the API to map certain fields.
class AddFoodItemList extends Component {
static contextType = AddFoodContext;
render() {
const listItems = this.context.FoodSearch.map((foods) =>
<FoodItem
key={foods.brandOwner}
brandOwner={foods.brandOwner}
fdcId={foods.fdcId}
/>
);
return (
<div id="AddFoodItemList">
{listItems}
</div>
);
}
}
export default AddFoodItemList;
The returned JSON is this screenshot attached:
Returned JSON
I am getting an error, TypeError: Cannot read property 'map' of undefined.
Why do you think this is the case? Any sort of help or suggestions are appreciated!
You are attempting to access a property FoodSearch on the value of your AddFoodContext provider. The error tells you that this property is undefined. If the object in your screenshot is the value of your context then you want to access the property foods instead. This is an array whose elements are objects with properties brandOwner and fdcId.
On your first render this data might now be loaded yet, so you should default to an empty array if foods is undefined.
It's honestly been a long time since I've used contexts in class components the way that you are doing it. The style of code is very dated. How about using the useContext hook to access the value?
const AddFoodItemList = () => {
const contextValue = useContext(AddFoodContext);
console.log(contextValue);
const listItems = (contextValue.foods || []).map((foods) => (
<FoodItem
key={foods.fdcId} // brandOwner isn't unique
brandOwner={foods.brandOwner}
fdcId={foods.fdcId}
/>
));
return <div id="AddFoodItemList">{listItems}</div>;
};
Here's a complete code to play with - Code Sandbox Link
const MY_API_KEY = "DEMO_KEY"; // can replace with your actual key
const getUri = (search) => `https://api.nal.usda.gov/fdc/v1/foods/search?api_key=${MY_API_KEY}&query=${encodeURIComponent(search)}`;
const AddFoodContext = createContext({});
const FoodItem = ({ brandOwner, fdcId }) => {
return (
<div>
<span>{fdcId}</span> - <span>{brandOwner}</span>
</div>
);
};
const AddFoodItemList = () => {
const contextValue = useContext(AddFoodContext);
console.log(contextValue);
const listItems = (contextValue.foods || []).map((foods) => (
<FoodItem
key={foods.fdcId} // brandOwner isn't unique
brandOwner={foods.brandOwner}
fdcId={foods.fdcId}
/>
));
return <div id="AddFoodItemList">{listItems}</div>;
};
export default () => {
const [data, setData] = useState({});
useEffect(() => {
fetch(getUri("cheese"))
.then((res) => res.json())
.then(setData)
.catch(console.error);
}, []);
return (
<AddFoodContext.Provider value={data}>
<AddFoodItemList />
</AddFoodContext.Provider>
);
};

React JS component not updating on state change

I've been trying to implement a method in which you can sort a leaderboard in different ways, by toggling a select element which changes the state, causing the component to re-render.
The problem is that, it can sort the default correctly, but whenever I change the value of the select from default to "z-to-a", it does not seem to be updating.
Note: I've added a few console.log statements, which seem to be behaving weirdly.
My JSX:
import React, { useState, useEffect } from 'react';
import './Leaderboard.css';
import LbRow from '../../components/LbRow/LbRow'; /* A row in the leaderboard*/
import points from '../../data/tree-points.json';
function Leaderboard() {
// Initialize the points as the data that we passed in
const [state, setState] = useState({
points: points,
sortBy: "first-to-last"
});
// Changes the sort method used by the leaderboard
const changeSortBy = (event) => {
var newSort = event.target.value;
// Sorts the data differently depending on the select value
switch(newSort) {
case "first-to-last":
sortDescending("points","first-to-last");
break;
case "z-to-a":
sortDescending("tree_name","z-to-a");
console.log(state.points.treePoints); // Logs incorrectly, still logs the same array as in "first-to-last"
break;
default:
sortDescending("points","first-to-last");
}
// Re-renders the component with new state
setState({
points: state.points,
sortBy: newSort
});
}
/* Updates the leaderboard state to be in descending point order */
const sortDescending = (aspect, sortMethod) => {
console.log(sortMethod); // Logs correctly
// Sorts the data in descending points order
let sortedPoints = [...state.points.treePoints].sort((tree1, tree2) => {
if (tree1[aspect] > tree2[aspect]) { return -1; }
if (tree1[aspect] < tree2[aspect]) { return 1; }
return 0;
});
// Actually updates the state
setState({
points: {
...state.points,
treePoints: sortedPoints
},
sortBy: sortMethod
});
console.log(sortedPoints); // Logs correctly
};
/* Calls sortLb on component mount */
useEffect(() =>{
sortDescending("points", "first-to-last");
}
,[]);
// Attributes used for rendering the leaderboard body
var rank = 0;
const sortedData = state.points;
/* Basically all the active trees with the first tree having the top rank */
const lbBody = sortedData.treePoints.map((sortedData) => {
return (
sortedData.active &&
<LbRow rank={++rank} tree_name={sortedData.tree_name} points={sortedData.points} active={sortedData.active}/>
);
});
return (
<div>
<div className="filters">
{/* Allows user to sort by different methods */}
<label htmlFor="sortBy">Sort by:</label>
<select name="sortBy" className="sortBy" value={state.sortBy} onChange={changeSortBy}>
<option value="first-to-last">First to Last</option>
<option value="z-to-a">Z to A</option>
</select>
</div>
{/* The table with sorted content */}
<div className="table">
{lbBody}
</div>
</div>
);
}
export default Leaderboard;
I'm really confused by this behavior, especially since I have the correctly sorted value and supposedly already updated the state. What could be causing this to happen? THanks
There are 3 things you must note
State updates are batched, ie. when you call setState multiple times within a function, their result is batched together and a re-render is triggered once
State updates are bound by closures and would only reflect in the next re-render and not immediately after calling state updater
State updates with hooks are not merged to you do need to keep merging all values in state yourself
Now since you wish to call the state updater twice, you might as well use the callback approach which will guarantee that your state values from multiple setState calls are not merged, since you don't need them to. Also you must update only the fields that you want to
function Leaderboard() {
// Initialize the points as the data that we passed in
const [state, setState] = useState({
points: points,
sortBy: "first-to-last"
});
// Changes the sort method used by the leaderboard
const changeSortBy = (event) => {
var newSort = event.target.value;
// Sorts the data differently depending on the select value
switch (newSort) {
case "first-to-last":
sortDescending("points", "first-to-last");
break;
case "z-to-a":
sortDescending("tree_name", "z-to-a");
break;
default:
sortDescending("points", "first-to-last");
}
// Re-renders the component with new state
setState(prev => ({
...prev,
sortBy: newSort // overrider just sortByField
}));
}
/* Updates the leaderboard state to be in descending point order */
const sortDescending = (aspect, sortMethod) => {
console.log(sortMethod); // Logs correctly
// Sorts the data in descending points order
let sortedPoints = [...state.points.treePoints].sort((tree1, tree2) => {
if (tree1[aspect] > tree2[aspect]) {
return -1;
}
if (tree1[aspect] < tree2[aspect]) {
return 1;
}
return 0;
});
// Actually updates the state
setState(prev => ({
...prev,
points: {
...state.points,
treePoints: sortedPoints
},
}));
};
/* Calls sortLb on component mount */
useEffect(() => {
sortDescending("points", "first-to-last");
}, []);
// Attributes used for rendering the leaderboard body
var rank = 0;
const sortedData = state.points;
...
}
export default Leaderboard;
Another better way to handle this to avoid complicated is to separate out your states into two useState
function Leaderboard() {
// Initialize the points as the data that we passed in
const [points, setPoints] = useState(points);
const [sortBy, setSortBy] = useState(sortBy);
// Changes the sort method used by the leaderboard
const changeSortBy = (event) => {
var newSort = event.target.value;
// Sorts the data differently depending on the select value
switch(newSort) {
case "first-to-last":
sortDescending("points","first-to-last");
break;
case "z-to-a":
sortDescending("tree_name","z-to-a");
console.log(state.points.treePoints); // Logs incorrectly, still logs the same array as in "first-to-last"
break;
default:
sortDescending("points","first-to-last");
}
// Re-renders the component with new state
setSortBy(newSort);
}
/* Updates the leaderboard state to be in descending point order */
const sortDescending = (aspect, sortMethod) => {
console.log(sortMethod); // Logs correctly
// Sorts the data in descending points order
let sortedPoints = [...state.points.treePoints].sort((tree1, tree2) => {
if (tree1[aspect] > tree2[aspect]) { return -1; }
if (tree1[aspect] < tree2[aspect]) { return 1; }
return 0;
});
// Actually updates the state
setPoints({
...state.points,
treePoints: sortedPoints
});
console.log(sortedPoints); // Logs correctly
};
/* Calls sortLb on component mount */
useEffect(() =>{
sortDescending("points", "first-to-last");
}
,[]);
// Attributes used for rendering the leaderboard body
var rank = 0;
const sortedData = points;
/* Basically all the active trees with the first tree having the top rank */
const lbBody = sortedData.treePoints.map((sortedData) => {
return (
sortedData.active &&
<LbRow rank={++rank} tree_name={sortedData.tree_name} points={sortedData.points} active={sortedData.active}/>
);
});
return (
<div>
<div className="filters">
{/* Allows user to sort by different methods */}
<label htmlFor="sortBy">Sort by:</label>
<select name="sortBy" className="sortBy" value={sortBy} onChange={changeSortBy}>
<option value="first-to-last">First to Last</option>
<option value="z-to-a">Z to A</option>
</select>
</div>
{/* The table with sorted content */}
<div className="table">
{lbBody}
</div>
</div>
);
}
export default Leaderboard;

How to close a modal on browser back button using react-router-dom?

I'm using react-router-dom and what I want is to be able to close a Modal when I click browser back button.
Also, in my scenario, the modal component is not the part of Switch. So how can I close the modal.
Thanks in advance. :)
You could probably use something like this to detect the press of the Back button.
componentDidUpdate() {
window.onpopstate = e => {
}
}
And then, depending on your modal (Bootstrap or something else) you can call .hide() or .close().
I've made a simple hook called useAppendLocationState that does all the job:
function SomeComponent() {
const [showModal , appendShowModal ] = useAppendLocationState('showModal');
return (
<div>
<div>...some view...</div>
<button onClick={() => appendOpenModal(true)}>open modal</button>
{showModal && <SomeModal closeHandler={() => window.history.back()} />}
</div>
)
}
useAppendLocationState returns a array with two entries just like useState, The first entry is the state prop value coming from browser location state and the second entry is a method that pushes a new item to browser history with new state prop appended to current location state.
here is our useAppendLocationState definition:
import { useHistory } from 'react-router';
export function useAppendLocationState(key) {
if (!key) throw new Error("key cannot be null or empty value")
const history = useHistory()
const currentLocationState = history.location.state;
const appendStateItemValue = (value) => {
const newLocationState = { ...currentLocationState }
newLocationState[key] = value;
history.push(history.location.pathname, newLocationState)
}
const stateItemValue = history.location.state && history.location.state[key]
return [stateItemValue, appendStateItemValue]
}
export default useAppendLocationState
Have you tried: ComponentWillUnmount?