Undefined state in parent when setting state in child - React - reactjs

I am trying to disable a button onClick based on if the button is the "current player". I am able to successfully get the correct code in the console log from my PlayerButton component, but I also get errors from my App.js that .map is not a function, meaning that my playerData (which works on initial mount) becomes undefined somehow after the onClick (i also receive undefined when trying to log playerData in App.js after the onClick).
Any ideas as to why my setPlayerData function isn't passing the data back to the parent component? I am thinking I may need to either use async await or useEffect somewhere, but I'm not sure how to implement it in this case.
App.js
import React, { useState } from 'react';
import './app.css'
import PlayerButton from './components/PlayerButton';
function App() {
const [playerData, setPlayerData] = useState([
{
name: "Player 1",
currentPlayer: true,
position: 0
},
{
name: "Player 2",
currentPlayer: false,
position: 0
},
{
name: "Player 3",
currentPlayer: false,
position: 0
}
]);
return (
<div className="container">
{playerData.map((player, index) => (
<div key={player.name}>
<PlayerButton player={player} index={index} setPlayerData={setPlayerData} />
</div>
))}
</div>
);
}
export default App;
PlayerButton.js
import React from 'react';
const PlayerButton = ({ player: { name, currentPlayer }, index, setPlayerData }) => {
const handleButtonClick = () => {
setPlayerData(playerData => {
playerData.map(player => {
if (player.name === name) {
console.log({ ...player, currentPlayer: false })
return { ...player, currentPlayer: false }
}
return player
})
})
}
return (
<button disabled={!currentPlayer} type="button" id={index + 1} className={`player${index + 1}-btn`} onClick={handleButtonClick}>{name}</button>
)
}
export default PlayerButton;
I experimented with useEffect and async await because, I'm assuming, on update the parent component is trying to re-render before the data is successfully passed from the child. But if that were the case, wouldn't it just re-render the previous data?
Somehow, I'm not successfully updating the state in the child so the parent can use it.

You've lost a return statement and returned undefined instead
Should be:
setPlayerData(playerData => {
return playerData.map(player => {
if (player.name === name) {
console.log({ ...player, currentPlayer: false })
return { ...player, currentPlayer: false }
}
return player
})
})

Ok I found a solution. I created a function in the parent component that uses the setPlayerData function inside a callback setCurrentPlayer, passed it to the child, and executed the callback inside the child.
App.js
const setCurrentPlayer = () => {
setPlayerData((playerData.map((player) => {
if (player.currentPlayer) {
return { ...player, currentPlayer: false}
}
return player
})))
}
PlayerButton.js
const handleButtonClick = () => {
setCurrentPlayer();
}

Related

How to update prop values in Child class component when Parent class component state is changed? : React Native

I have a parent class component called CardView.js which contains a child class component called Tab.js (which contains a FlatList).
When a button is pressed in CardView.js, a modal appears with various options. A user chooses an option and presses 'OK' on the modal. At this point the onOKHandler method in the parent component updates the parent state (tabOrderBy: orderBy and orderSetByModal: true). NOTE: These two pieces of state are passed to the child component as props.
Here is what I need:
When the onOKHandler is pressed in the parent, I need the child component to re-render with it's props values reflecting the new state values in the parent state. NOTE: I do not want the Parent Component to re-render as well.
At the moment when onOKHandler is pressed, the child component reloads, but it's props are still showing the OLD state from the parent.
Here is what I have tried:
When the onOKHandler is pressed, I use setState to update the parent state and then I use the setState callback to call a method in the child to reload the child. The child reloads but its props are not updated.
I have tried using componentDidUpdate in the child which checks when the prop orderSetByModal is changed. This does not work at all.
I have tried many of the recommendations in other posts like this - nothing works! Where am I going wrong please? Code is below:
Parent Component: CardView.js
import React from "react";
import { View } from "react-native";
import { Windows} from "../stores";
import { TabView, SceneMap } from "react-native-tab-view";
import { Tab, TabBar, Sortby } from "../components";
class CardView extends React.Component {
state = {
level: 0,
tabIndex: 0,
tabRoutes: [],
recordId: null,
renderScene: () => {},
showSortby: false,
orderSetByModal: false,
tabOrderBy: ''
};
tabRefs = {};
componentDidMount = () => {
this.reload(this.props.windowId, null, this.state.level, this.state.tabIndex);
};
reload = (windowId, recordId, level, tabIndex) => {
this.setState({ recordId, level, tabIndex });
const tabRoutes = Windows.getTabRoutes(windowId, level);
this.setState({ tabRoutes });
const sceneMap = {};
this.setState({ renderScene: SceneMap(sceneMap)});
for (let i = 0; i < tabRoutes.length; i++) {
const tabRoute = tabRoutes[i];
sceneMap[tabRoute.key] = () => {
return (
<Tab
onRef={(ref) => (this.child = ref)}
ref={(tab) => (this.tabRefs[i] = tab)}
windowId={windowId}
tabSequence={tabRoute.key}
tabLevel={level}
tabKey={tabRoute.key}
recordId={recordId}
orderSetByModal={this.state.orderSetByModal}
tabOrderBy={this.state.tabOrderBy}
></Tab>
);
};
}
};
startSortByHandler = () => {
this.setState({showSortby: true});
};
endSortByHandler = () => {
this.setState({ showSortby: false});
};
orderByFromModal = () => {
return 'creationDate asc'
}
refreshTab = () => {
this.orderByFromModal();
this.child.refresh()
}
onOKHandler = () => {
this.endSortByHandler();
const orderBy = this.orderByFromModal();
this.setState({
tabOrderBy: orderBy,
orderSetByModal: true}, () => {
this.refreshTab()
});
}
render() {
return (
<View>
<TabView
navigationState={{index: this.state.tabIndex, routes: this.state.tabRoutes}}
renderScene={this.state.renderScene}
onIndexChange={(index) => {
this.setState({ tabIndex: index });
}}
lazy
swipeEnabled={false}
renderTabBar={(props) => <TabBar {...props} />}
/>
<Sortby
visible={this.state.showSortby}
onCancel={this.endSortByHandler}
onOK={this.onOKHandler}
></Sortby>
</View>
);
}
}
export default CardView;
Child Component: Tab.js
import React from "react";
import { FlatList } from "react-native";
import { Windows } from "../stores";
import SwipeableCard from "./SwipeableCard";
class Tab extends React.Component {
constructor(props) {
super(props);
this.state = {
currentTab: null,
records: [],
refreshing: false,
};
this.listRef = null;
}
async componentDidMount() {
this.props.onRef(this);
await this.reload(this.props.recordId, this.props.tabLevel, this.props.tabSequence);
}
componentWillUnmount() {
this.props.onRef(null);
}
//I tried adding componentDidUpdate, but it did not work at all
componentDidUpdate(prevProps) {
if (this.props.orderSetByModal !== prevProps.orderSetByModal) {
this.refresh();
}
}
getOrderBy = () => {
let orderByClause;
if (this.props.orderSetByModal) {
orderByClause = this.props.tabOrderBy;
} else {
orderByClause = "organization desc";
}
return orderByClause;
};
async reload() {
const currentTab = Windows.getTab(this.props.windowId, this.props.tabSequence, this.props.tabLevel);
this.setState({ currentTab });
let response = null;
const orderBy = this.getOrderBy();
response = await this.props.entity.api.obtainRange(orderBy);
this.setState({ records: response.dataList })
}
refresh = () => {
this.setState({ refreshing: true }, () => {
this.reload(this.props.recordId, this.props.tabLevel, this.props.tabSequence)
.then(() => this.setState({ refreshing: false }));
});
};
renderTabItem = ({ item, index }) => (
<SwipeableCard
title={"Card"}
/>
);
render() {
if (!this.state.currentTab) {
return null;
}
return (
<>
<FlatList
ref={(ref) => (this.listRef = ref)}
style={{ paddingTop: 8 }}
refreshing={this.state.refreshing}
onRefresh={this.refresh}
data={this.state.records}
keyExtractor={(item) => (item.isNew ? "new" : item.id)}
/>
</>
);
}
}
export default Tab;

if else statement not working in react component

I am trying to implement a condition in my react component . When the user triggers the onClick the state updates allStakes creating one array of 4 values. The problem is that I do not want the user to input more than 4 values so tried to give the limit by doing an if else statement. I tried to add a console.log in both statements.The weird fact is that setState get updated but the csonole.log is never displayed.The component keeps rendering all the values that I insert even if the array is longer than 4. Thanks in advance
import React, { Component } from 'react';
import Stake from './stake';
class FetchRandomBet extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
bet: null,
value: this.props.value,
allStakes: []
};
}
async componentDidMount() {
const url = "http://localhost:4000/";
const response = await fetch(url);
const data = await response.json();
this.setState({
loading: false,
bet: data.bets,
});
}
render() {
const { valueProp: value } = this.props;
const { bet, loading } = this.state;
const { allStakes } = this.state;
if (loading) {
return <div>loading..</div>;
}
if (!bet) {
return <div>did not get data</div>;
}
return (
< div >
{
loading || !bet ? (
<div>loading..</div>
) : value === 0 ? (
<div className="bet-list">
<ol>
<p>NAME</p>
{
bet.map(post => (
<li key={post.id}>
{post.name}
</li>
))
}
</ol>
<ul>
<p>ODDS</p>
{
bet.map(post => (
<li key={post.id}>
{post.odds[4].oddsDecimal}
<div className="stake-margin">
<Stake
onClick={(newStake) => {
if (allStakes.length <= 3) {
this.setState({ allStakes: [allStakes, ...newStake] })
console.log('stop')
} else if (allStakes.length == 4) {
console.log('more than 3')
}
}}
/>
</div>
</li>
))
}
</ul>
</div>
May be it happens because of incorrect array destructuring. Try to change this code:
this.setState({ allStakes: [allStakes, ...newStake] })
by the next one:
this.setState({ allStakes: [newStake, ...allStakes] })
Your state belongs to your FetchRandomBet component and you are trying to update that from your imported component. There are 2 solutions to that.
1> Wrap your Stake component to a separate component with onClick handler something like this
<div onClick={(newStake) => {
if (allStakes.length <= 3) {
this.setState({
allStakes: [allStakes, ...newStake
]
})
console.log('stop')
} else if (allStakes.length == 4) {
console.log('more than 3')
}
}}><Stake /></div>
Or
2> Pass the state as a prop to the Stake component which will be responsible to update the state for FetchRandomBet. something like this
<Stake parentState={this}/>
And inside the Stake component change the parentState on click of wherever you want.
I solved the problem. I transfered the onClick method in stake component and I handled the upload of the common array with an array useState. I add the value to newStake and when I click ok I retrieve newStake and spread it into a new array and then I check that array. If there is a value should not keep adding otherwise it can add values. It works fine. Thanks anyway
import React, { useState } from 'react';
import CurrencyInput from 'react-currency-input-field';
function Stake(props) {
const [newStake, setStake] = useState(null);
const [allStakes, setStakes] = useState(null);
const changeStake = (e) => {
setStake([e.target.value])
}
const mySubmit = () => {
if (!allStakes) {
setStakes([...newStake, allStakes])
props.onClick(newStake);
} else if (allStakes) {
console.log('stop')
}
}
return (
<>
<CurrencyInput
onChange={changeStake}
style={{
marginLeft: "40px",
width: "50px"
}}
placeholder="Stake"
decimalScale={2}
prefix="£"
/>
<button onClick={mySubmit}>yes</button>
<button>no</button>
{newStake}
</>
);
}
export default Stake;

Custom react hook triggers api call multiple times

I cannot figure out how to handle my function components calling my api repeatedly. I have two components which retrieve data, one of them calls the api twice. Once before the second component once after.
I am using a custom react hook and axios get method to retrieve the data. My two components are are nested. The first component when loads and fetches data. Inside this component is a child component which when renders it fetches data right before passing the first set of data as props to another child component. When it completes loading it reloads the first child component which again calls the api for data. I understand the function components reload on state change. I would be happy for it to not call the api a second time. Is there a way to check if it already has data and bypass the api call?
Custom hook to retrieve data
import React, { useState, useEffect, useReducer } from "react";
import axios from "axios";
const dataFetchReducer = (state, action) => {
switch (action.type) {
case "FETCH_INIT":
return { ...state, isLoading: true, hasErrored: false };
case "FETCH_SUCCESS":
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: action.payload
};
case "FETCH_FAILURE":
return {
...state,
isLoading: false,
hasErrored: true,
errorMessage: "Data Retrieve Failure"
};
case "REPLACE_DATA":
// The record passed (state.data) must have the attribute "id"
const newData = state.data.map(rec => {
return rec.id === action.replacerecord.id ? action.replacerecord : rec;
});
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: newData
};
default:
throw new Error();
}
};
const useAxiosFetch = (initialUrl, initialData) => {
const [url] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
hasErrored: false,
errorMessage: "",
data: initialData
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: "FETCH_INIT" });
try {
let result = await axios.get(url);
if (!didCancel) {
dispatch({ type: "FETCH_SUCCESS", payload: result.data });
}
} catch (err) {
if (!didCancel) {
dispatch({ type: "FETCH_FAILURE" });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
const updateDataRecord = record => {
dispatch({
type: "REPLACE_DATA",
replacerecord: record
});
};
return { ...state, updateDataRecord };
};
export default useAxiosFetch;
Main component which renders the "CompaniesDropdown" twice inside
CompaniesDropdown is one of three dropdowns within the ListFilterContainer component but the only one which calls the api more than once. The other two dropdowns load by selection of the CompaniesDropdown.
import React, { useMemo, useEffect, useContext } from "react";
import InvoiceList from "../src/Components/Lists/InvoiceList";
import useAxiosFetch from "../src/useAxiosFetch";
import { ConfigContext } from "./_app";
import ListFilterContainer from "../src/Components/Filters/InvoiceFilters";
// import "../css/ListView.css";
const Invoices = props => {
const context = useContext(ConfigContext);
useEffect(() => {
document.title = "Captive Billing :: Invoices";
});
const {
data,
isLoading,
hasErrored,
errorMessage,
updateDataRecord
} = useAxiosFetch("https://localhost:44394/Invoice/GetInvoices/false", []);
const newInvoicesList = useMemo(
() => data
// .filter(
// ({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
// )
// .sort(function(a, b) {
// if (a.firstName < b.firstName) {
// return -1;
// }
// if (a.firstName > b.firstName) {
// return 1;
// }
// return 0;
// }),
// [speakingSaturday, speakingSunday, data]
);
const invoices = isLoading ? [] : newInvoicesList;
if (hasErrored)
return (
<div>
{errorMessage} "Make sure you have launched "npm run json-server"
</div>
);
if (isLoading) return <div>Loading...</div>;
const dataProps = {
data: invoices,
titlefield: "invoiceNumber",
titleHeader: "Invoice Number:",
childPathRoot: "invoiceDetail",
childIdField: "invoiceId",
childDataCollection: "invoiceData"
};
var divStyle = {
height: context.windowHeight - 100 + "px"
};
return (
<main>
<ListFilterContainer />
<section style={divStyle} id="invoices" className="card-container">
<InvoiceList data={dataProps} />
</section>
</main>
);
};
Invoices.getInitialProps = async ({ req }) => {
const isServer = !!req;
return { isServer };
};
export default Invoices;
Actual result is described above. My main concern is to not have the api calls more than once.
Here is some additional code to help. It is the filter control mentioned above. It, as you will notice really just contains dropdowns and a text box. The first dropdown is the one that calls the api twice. The second two are not visible until that one is selected.
import React, { useState, useMemo } from "react";
import CompaniesDropdown from "../Dropdowns/CompaniesDropdown";
import LocationsDropdown from "../Dropdowns/LocationsDropdown";
import AccountsDropdown from "../Dropdowns/AccountsDropdown";
import Search from "./SearchFilter/SearchFilter";
const InvoiceFilters = props => {
const [company, setCompany] = useState("");
const [location, setLocation] = useState(undefined);
const [account, setAccount] = useState(undefined);
const handleClientChange = clientValue => {
setCompany(clientValue);
};
const handleLocationsChange = locationValue => {
setLocation(locationValue);
};
const handleAccountsChange = AccountValue => {
setAccount(AccountValue);
};
return (
<section className="filter-container mb-3">
<div className="form-row">
<div className="col-auto">
<CompaniesDropdown change={e => handleClientChange(e)} />
</div>
<div className="col-auto">
<LocationsDropdown
selectedCompany={company}
change={e => handleLocationsChange(e)}
/>
</div>
<div className="col-auto">
<AccountsDropdown
selectedCompany={company}
change={e => handleAccountsChange(e)}
/>
</div>
<div className="col-auto">
<Search />
</div>
</div>
</section>
);
};
InvoiceFilters.getInitialProps = async ({ req }) => {
const isServer = !!req;
return { isServer };
};
export default InvoiceFilters;
Also the datalist
import React from "react";
import Link from "next/link";
import InvoiceListRecord from "./InvoiceListRecord";
const InvoiceList = props => {
let dataCollection = props.data.data;
return dataCollection.length == 0 ? "" : dataCollection.map((item, index) => {
return (
<section key={"item-" + index} className="card text-left mb-3">
<header className="card-header">
<span className="pr-1">{props.data.titleHeader}</span>
<Link
href={
"/" +
props.data.childPathRoot +
"?invoiceId=" +
item[props.data.childIdField]
}
as={
"/" +
props.data.childPathRoot +
"/" +
item[props.data.childIdField]
}
>
<a>{item[props.data.titlefield]}</a>
</Link>{" "}
</header>
<div className="card-body">
<div className="row">
<InvoiceListRecord
data={item}
childDataCollection={props.data.childDataCollection}
/>
</div>
</div>
</section>
);
});
};
InvoiceList.getInitialProps = async ({ req }) => {
console.log("Get Intitial Props works: Invoices Page!");
const isServer = !!req;
return { isServer };
};
export default InvoiceList;
and the list items component.
import React from "react";
const InvoiceListRecord = props => {
var invoiceData = JSON.parse(props.data[props.childDataCollection]);
return invoiceData.map((invKey, index) => {
return (
<div className="col-3 mb-1" key={"item-data-" + index}>
<strong>{invKey.MappedFieldName}</strong>
<br />
{invKey.Value}
</div>
);
});
};
export default InvoiceListRecord;
The API is not called more than once if the url is the same. It just gets the value from data variable. The api call is not made again, unless the url changes.
I created an example from your code, changing all the unknown components to div. I added a console.log in the useEffect of the useAxiosFetch hook. And to re-render the component, I added a button to increment the count.
You'll see that the console.log from the hook is printed only once, even though the component re-renders on every button click. The value just comes from the data variable from the hook and the api call is not made again and again.

React JS: Unable to display array data onClick

In the console i can see the object inside the array on click every time.
But I am unable to display it
I tried map() instead of forEach, and it shows the item just once.
I need it to show each time i click the button
Can someone tell me what I am doing Wrong?
import React, { Component } from 'react'
export default class App extends Component {
state = {
isClicked : false
}
handleClick = () => {
this.setState({
isClicked : true
})
}
render() {
let myarray = [ ]
let myobject = {
color: 'blue',
shape: 'round'
}
myarray.push(myobject)
let looped = myarray.forEach((data) => <><h1>{data.color}</h1> <h1>{data.shape}</h1></>)
console.log(myarray)
return (
<div>
<button onClick={this.handleClick}>Submit</button>
{ this.state.isClicked ? looped : null}
</div>
)
}
}
I want the object to display on the page when I click the button
There are some issues in your snippet:
forEach() has no return value.
on handleClick you change state to isClicked: true once.
In order to fix your code and display your data on every click you should:
Use map() to map each object to React.Element.
Change the state on every button click, so in your case, on every click, you should reverse the boolean value of this.state.isClicked.
Here is a working example:
import React from 'react';
const myArray = [
{
color: 'blue',
shape: 'round'
}
];
const looped = myArray.map((data, i) => (
<div key={i}>
<h1>{data.color}</h1> <h1>{data.shape}</h1>
</div>
));
export default class App extends React.Component {
state = {
isClicked: false
};
handleClick = () => {
this.setState(prevState => {
console.log(prevState);
return { isClicked: !prevState.isClicked };
});
};
render() {
return (
<div>
<button onClick={this.handleClick}>Submit</button>
{this.state.isClicked && looped}
</div>
);
}
}

React: this.setState() working in some functions, not in others within the same component

I am trying to develop a simple little survey component with its own local state. I've already successfully used this.setState() in my handleChange function to update which radio button is selected. I can see that it's working in console logs and on the live page. However, I am now trying to use the state to display a modal and highlight incomplete questions if the user tries to submit the survey without completing it. The functions I have to do this execute, but the state does not update. I am having trouble seeing any difference between where I updated state successfully and where I didn't.
Here's my container where I'm controlling state and defining the functions. I don't think any other code is relevant, but let me know if I need to include something else:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Redirect } from 'react-router'
import { history } from '../history'
import { connect } from 'react-redux'
import _ from 'lodash'
import { postPhq, sumPhq } from '../store'
import { Phq } from '.'
import { UnfinishedModal } from '.'
/**
* CONTAINER
*/
class PhqContainer extends Component {
constructor(props) {
super(props)
this.state = {
phq: {
q1: false, q2: false, q3: false, q4: false, q5: false,
q6: false, q7: false, q8: false, q9: false, q10: false
},
hilitRows: [],
modalOpen: false
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.makeButtons = this.makeButtons.bind(this)
this.toggleModal = this.toggleModal.bind(this)
}
//handles clicks on radio buttons
handleChange(question, event) {
var newPhq = this.state.phq
newPhq[question] = event.target.value
this.setState({phq: newPhq})
console.log("radio button", this.state)
}
// on submit, first check that PHQ-9 is entirely completed
// then score the PHQ-9 and dispatch the result to the store
// as well as posting the results to the database and
// dispatching them to the store
// if the PHQ-9 is not complete, open a modal to alert the user
handleSubmit(event) {
event.preventDefault()
if (this.checkCompletion()) {
this.props.sumPhq(this.sum())
this.props.postPhq(this.state.phq)
this.props.history.push('/results')
} else {
console.log(this.unfinishedRows())
this.toggleModal()
this.setState({hilitRows: this.unfinishedRows()})
}
}
// returns true if user has completed the PHQ-9 with no missing values
checkCompletion() {
return !this.unfinishedRows().length || this.oneToNine().every(val => val === 0)
}
unfinishedRows() {
return Object.keys(_.pickBy(this.state.phq, (val) => val === false))
}
oneToNine() {
var qs1to9 = Object.assign({}, this.state.phq)
delete qs1to9.q10
return Object.values(qs1to9)
}
toggleModal() {
console.log("I'm about to toggle the modal", this.state)
this.setState({modalOpen: !this.state.modalOpen})
console.log("I toggled the modal", this.state)
}
// scores the PHQ-9
sum() {
return this.oneToNine
.map(val => parseInt(val))
.reduce((total, num) => total + num)
}
makeButtons = (num, labels) => {
var question = 'q' + (num + 1).toString()
return (
<div style={rowStyle}>
{labels.map((label, i) => {
return (
<td style={{border: 'none'}}>
<div className="col radio center-text" >
<input type="radio"
value={i}
onChange={(event) => {this.handleChange(question, event)}}
checked={this.state.phq[question] && parseInt(this.state.phq[question]) == i} />
<label>{label}</label>
</div>
</td>
)
})}
</div>
)
}
render() {
return (
<div>
<Phq {...this.state} {...this.props}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
makeButtons={this.makeButtons} />
<UnfinishedModal show={this.state.modalOpen} toggleModal={this.toggleModal} />
</div>
)
}
}
const mapDispatch = (dispatch) => {
return {
postPhq: (qs) => {
dispatch(postPhq(qs))
},
sumPhq: (total) => {
dispatch(sumPhq(total))
}
}
}
export default connect(() => { return {} }, mapDispatch)(PhqContainer)
const rowStyle = {
paddingRight: 15,
borderWidth: 0,
margin: 0,
border: 'none',
borderTop: 0
}
This line this.setState({phq: newPhq}) updates the state. This line this.setState({hilitRows: this.unfinishedRows()}) and this line this.setState({modalOpen: !this.state.modalOpen}) do not.
I had the same issue happen in my project like setState is not working in some functions but its perfect for other functions in the same component. First of all you should understand, As mentioned in the React documentation, the setState being fired synchronously. So you can check like below value being updated.
this.setState({mykey: myValue}, () => {
console.log(this.state.mykey);
});
In my case, I was tried to update the value of an event onChange with an input, in every keyUp it's trying to update, Its causes the problem for me. So I have tried to used a debounce(delay) for the function so its working fine for me. Hope this hint will help you or someone else.
this.myFunction = debounce(this.myFunction,750);
<Input onChange={this.myFunction} />

Resources