How to add a button and a dropdown in AgGrid Cell in react Functional Component - javascript

I have list of data. I am using AgGrid in react to display this data list. For each row i need to display a column having a delete button and a second column having a dropdown and a reset button.
When i click the delete button i need the corresponding row data and when i click reset button i need the dropdown option select as well as the corresponding row data.
I have searched but i am not able to figure out how to do it in react functional components. I have found that i need to use ICellRendererReactComp but i am not sure how as i am new to react and AgGrid
My current code looks something like this :
import React, { useState } from "react";
import toaster from "toasted-notes";
import { apiRequest, errorHandler } from "../../utilis/apiRequest";
import { columnDefsFromArr } from "../Threads/columnDefs";
import { AgGridReact, ICellRendererReactComp } from "ag-grid-react";
import { isResourcePresent } from "../../utilis/helper";
function Sessions(props) {
const [email, setEmail] = useState("");
const [reid, setReid] = useState(null);
const [sessionsResp, setSessionsResp] = useState(null);
const [columnDefs, setColumnDefs] = useState(null);
const [rowData, setRowData] = useState(null);
const defaultColDef = {
sortable: true,
filter: true,
resizable: true,
};
const colsToExlude = ["requestId"];
const resetSessionOptions = [
"None",
"ClearClientCache",
"PasswordChange",
"Suspended",
"InvalidSession",
"Expired",
];
const handleEmailChange = (e) => {
setEmail(e.target.value);
};
const handleGetSession = () => {
setReid(email);
getSessions({ reid: email, env: props.env });
};
const getSessions = (data) => {
console.log(data, reid);
let isError = validateForm(data);
if (isError.status) {
apiRequest(
props.sessionToken,
"get_session",
data,
(res) => {
if (res.status === 200) {
console.log(res.data);
setRowData(res.data.sessions);
makeGrid(res.data);
}
},
(err) => {
errorHandler(err);
}
);
} else {
toaster.notify(isError.msg, {
duration: 1500,
});
}
};
const handleDelete = (data) => {
console.log(data);
};
const handleReset = (data) => {
console.log(data);
};
const makeGrid = (data) => {
let cols = [];
data.sessions.map((ele) => {
Object.keys(ele).map((key) => cols.push(key));
});
let localCols = [];
if (isResourcePresent(props.resources, "del_session")) {
localCols.push({
headerName: "Delete Sessio",
});
}
if (isResourcePresent(props.resources, "reset_session")) {
localCols.push({
headerName: "Reset Session"
});
}
cols = [...new Set(cols)];
colsToExlude.map((key) => {
let ind = cols.indexOf(key);
if (ind > -1) {
cols.splice(ind, 1);
}
});
let finalColDefs = [...localCols, ...columnDefsFromArr(cols)];
console.log(finalColDefs);
setColumnDefs(finalColDefs);
};
const validateForm = (data) => {
if (data.reid.trim() === "") {
return { status: false, msg: "Email/Email Id is reqd" };
} else {
return { status: true };
}
};
return (
<div className="container-fluid">
<div>
<h5>Get Sessions Information</h5>
</div>
<div className="card mt-2 p-3 bg-red shadow p-3 mb-5 bg-white rounded">
<div className="row m-2">
<div className="col-sm-6">
<input
type="text"
className="form-control"
value={email || ""}
placeholder="Email / Email ID"
onChange={handleEmailChange}
/>
</div>
<div className="col-sm-2">
<button className="button btn-primary" onClick={handleGetSession}>
Get Information
</button>
</div>
</div>
</div>
{rowData == null ? null : (
<div className="card mt-2 p-3 bg-red shadow p-3 mb-5 bg-white rounded">
<div
className="ag-theme-balham"
style={{ height: "500px", width: "100%" }}
>
<AgGridReact
columnDefs={columnDefs}
rowData={rowData}
></AgGridReact>
</div>
</div>
)}
</div>
);
}
export { Sessions };
handleDelete : this is the function i want to call when that delete button is clicked for some corresponding row,
resetSessionOptions : this is the dropdown list options i need to display in second column along with Reset button.
handleReset : this the function i want to call when that Reset button besides the dropdown is clicked for some corresponding row

So I searched a lot and finally came across this example : https://stackblitz.com/edit/angular-ag-grid-button-renderer?file=src%2Fapp%2Frenderer%2Fbutton-renderer.component.ts
Above example is for Angular and uses classes.
I figured out how to do it in react Functional Components. I did not used any interface or something but implemented all methods in above given example and made Renderer classes for the two columns i needed. You can see the code below.
UPDATE : My this solution works but this is causing my all other state variables to reset to initial state. I am not sure why thats happening. I am looking for a solution for that.
Session.js
import React, { useState } from "react";
import toaster from "toasted-notes";
import { apiRequest, errorHandler } from "../../utilis/apiRequest";
import { columnDefsFromArr } from "../Threads/columnDefs";
import { AgGridReact, ICellRendererReactComp } from "ag-grid-react";
import { isResourcePresent } from "../../utilis/helper";
import { ButtonRenderer } from "./ButtonRenderer";
import { DropDownRender } from "./DropDownRender";
function Sessions(props) {
const [email, setEmail] = useState("");
const [reid, setReid] = useState(null);
const [sessionsResp, setSessionsResp] = useState(null);
const [columnDefs, setColumnDefs] = useState(null);
const [rowData, setRowData] = useState(null);
const frameworkComponents = {
buttonRenderer: ButtonRenderer,
dropDownRenderer: DropDownRender,
};
const defaultColDef = {
sortable: true,
filter: true,
resizable: true,
};
const colsToExlude = ["requestId"];
const resetSessionOptions = [
"None",
"ClearClientCache",
"PasswordChange",
"Suspended",
"InvalidSession",
"Expired",
];
const handleEmailChange = (e) => {
setEmail(e.target.value);
};
const handleGetSession = () => {
setReid(email);
getSessions({ reid: email, env: props.env });
};
const getSessions = (data) => {
console.log(data, reid);
let isError = validateForm(data);
if (isError.status) {
apiRequest(
props.sessionToken,
"get_session",
data,
(res) => {
if (res.status === 200) {
console.log(res.data);
setRowData(res.data.sessions);
makeGrid(res.data);
}
},
(err) => {
errorHandler(err);
}
);
} else {
toaster.notify(isError.msg, {
duration: 1500,
});
}
};
const handleDelete = (data) => {
console.log("DEL", data);
};
const handleReset = (data) => {
console.log("RESET", data);
};
const makeGrid = (data) => {
let cols = [];
data.sessions.map((ele) => {
Object.keys(ele).map((key) => cols.push(key));
});
let localCols = [];
if (isResourcePresent(props.resources, "del_session")) {
localCols.push({
headerName: "Delete Sessio",
cellRenderer: "buttonRenderer",
cellRendererParams: {
onClick: handleDelete,
label: "Delete",
},
});
}
if (isResourcePresent(props.resources, "reset_session")) {
localCols.push({
headerName: "Reset Session",
cellRenderer: "dropDownRenderer",
cellRendererParams: {
onClick: handleReset,
label: "RESET",
dropDown: resetSessionOptions,
},
});
}
cols = [...new Set(cols)];
colsToExlude.map((key) => {
let ind = cols.indexOf(key);
if (ind > -1) {
cols.splice(ind, 1);
}
});
let finalColDefs = [...localCols, ...columnDefsFromArr(cols)];
setColumnDefs(finalColDefs);
};
const validateForm = (data) => {
if (data.reid.trim() === "") {
return { status: false, msg: "Email/Email Id is reqd" };
} else {
return { status: true };
}
};
return (
<div className="container-fluid">
<div>
<h5>Get Sessions Information</h5>
</div>
<div className="card mt-2 p-3 bg-red shadow p-3 mb-5 bg-white rounded">
<div className="row m-2">
<div className="col-sm-6">
<input
type="text"
className="form-control"
value={email || ""}
placeholder="Email / Email ID"
onChange={handleEmailChange}
/>
</div>
<div className="col-sm-2">
<button className="button btn-primary" onClick={handleGetSession}>
Get Information
</button>
</div>
</div>
</div>
{rowData == null ? null : (
<div className="card mt-2 p-3 bg-red shadow p-3 mb-5 bg-white rounded">
<div
className="ag-theme-balham"
style={{ height: "500px", width: "100%" }}
>
<AgGridReact
defaultColDef={defaultColDef}
columnDefs={columnDefs}
rowData={rowData}
frameworkComponents={frameworkComponents}
></AgGridReact>
</div>
</div>
)}
</div>
);
}
export { Sessions };
ButtonRenderer.js
import React from "react";
function ButtonRenderer(params) {
const refresh = (param) => {
return true;
};
const onClick = ($event) => {
if (params.onClick instanceof Function) {
const retParams = {
event: $event,
rowData: params.node.data,
};
params.onClick(retParams);
}
};
return (
<button className="button btn-primary" onClick={onClick}>
{params.label}
</button>
);
}
export { ButtonRenderer };
DropDownRenderer.js
import React, { useState } from "react";
function DropDownRender(params) {
const [selection, setSelection] = useState(params.dropDown[0]);
const refresh = (param) => {
return true;
};
const handleDropDown = (e) => {
setSelection(e.target.value);
};
const onClick = ($event) => {
if (params.onClick instanceof Function) {
const retParams = {
event: $event,
rowData: params.node.data,
selection: selection,
};
params.onClick(retParams);
}
};
return (
<div className="row">
<div className="col">
<select className="form-control" onChange={handleDropDown}>
{params.dropDown.map((i) => {
return (
<option key={i} value={i}>
{i}
</option>
);
})}
</select>
</div>
<div className="col">
<button className="button btn-primary" onClick={onClick}>
{params.label}
</button>
</div>
</div>
);
}
export { DropDownRender };

Related

Key={id} not working in react while mapping

I am making a simple messaging app, when i try to send messages the same div gets updated with new message but a new div does not get added. i was using index first while mapping the messages to display but it did not work so i added a msgId to the messages and tried to use it as the key but it's still not working
this is OpenConversation.js
import React, { useState } from 'react'
import {Button, Form, InputGroup, ListGroup} from 'react-bootstrap'
import { useConversations } from '../contexts/ConversationsProvider'
export default function OpenConversation() {
const[text,setText]=useState()
const {sendMessage,selectedConversation}=useConversations()
function handleSubmit(e) {
e.preventDefault()
console.log("index",selectedConversation)
sendMessage(
selectedConversation.recipients.map(r => r.id),
text
)
setText('')
}
return (
<div className="d-flex flex-column flex-grow-1">
<div className="flex-grow-1 overflow-auto">
<div className="d-flex flex-column align-items-start justify-content-end px-3">
{selectedConversation.messages.map((message) => {
return (
<div
key={message.msgId}
className={`my-1 d-flex flex-column ${message.fromMe ? 'align-self-end align-items-end' : 'align-items-start'}`}
>
<div
className={`rounded px-2 py-1 ${message.fromMe ? 'bg-primary text-white' : 'border'}`}>
{message.text} {message.msgId}
</div>
<div className={`text-muted small ${message.fromMe ? 'text-right' : ''}`}>
{message.fromMe ? 'You' : message.senderName}
</div>
</div>
)
})}
</div>
</div>
<Form style={{position:'absolute',bottom:'0rem'}}
onSubmit={handleSubmit}
>
<Form.Group className='m-2'>
<InputGroup >
<Form.Control as='textarea' required
value={text}
onChange={e=>setText(e.target.value)}
style={{height:'75PX',resize:'none',}}
/>
<Button type="submit" style={{background:'#7c73e6',border:'none'}}>Send</Button>
</InputGroup>
</Form.Group>
</Form>
</div>
)
}
ConversationsProvider.js
import React, { useContext, useState, useEffect, useCallback } from 'react'
import useLocalStorage from '../hooks/useLocalStorage';
import { useContacts } from './ContactsProvider';
import {v4 as uuidv4} from 'uuid'
const ConversationsContext = React.createContext()
export function useConversations() {
return useContext(ConversationsContext)
}
export default function ConversationsProvider({ id, children }) {
const [conversations, setConversations] = useLocalStorage('conversations', [])
const [selectedConversationIndex,setSelectedConversationIndex]=useState(0)
const { contacts } = useContacts()
function createConversation(recipients) {
setConversations(prevConversations => {
return [...prevConversations, { recipients, messages: [] }]
})
}
function selectConversationIndex(i)
{
setSelectedConversationIndex(i)
}
function addMessageToConversation({recipients,text,sender})
{
setConversations(prevConversations=>{
let madeChange=false
let msgId=uuidv4()
const newMessage={sender,text,msgId}
const newConversations=prevConversations.map(conversation=>{
if(arrayEquality(conversation.recipients,recipients))
{
madeChange=true
return{
...conversation,
messages:[conversation.messages,newMessage]
}
}
return conversation
})
if(madeChange){
return newConversations
}
else{
return [...prevConversations,{recipients,messages:[newMessage]}]
}
})
console.log(conversations)
}
function sendMessage(recipients,text)
{ console.log("working")
addMessageToConversation({recipients,text,sender:id})
}
const formattedConversations = conversations.map((conversation, index) => {
const recipients = conversation.recipients.map(recipient => {
const contact = contacts.find(contact => {
return contact.id === recipient
})
const name = (contact && contact.name) || recipient
return { id: recipient, name }
})
const messages = conversation.messages.map(message => {
const contact = contacts.find(contact => {
return contact.id === message.sender
})
const name = (contact && contact.name) || message.sender
const fromMe = id === message.sender
return { ...message, senderName: name, fromMe }
})
const selected = index === selectedConversationIndex
return { ...conversation, messages, recipients, selected }
})
const value = {
conversations: formattedConversations,
selectedConversation: formattedConversations[selectedConversationIndex],
sendMessage,
selectConversationIndex: setSelectedConversationIndex,
createConversation
}
return (
<ConversationsContext.Provider value={value}>
{children}
</ConversationsContext.Provider>
)
}
function arrayEquality(a, b) {
console.log("working")
if (a.length !== b.length) return false
a.sort()
b.sort()
return a.every((element, index) => {
return element === b[index]
})
}
i did not understand why index did not work, is there another way to map the messages?
Your error comes from this line
messages:[conversation.messages,newMessage]
in the addMessageToConversation function
I think you need to spread conversation.messages.
Change [conversation.messages, newMessage] to [...conversation.messages, newMessage].
With what you're doing currently, you're making the previous messages object, the first item in the array and then the new one as the second item.

Ionic Capacitor App eventlistner plugin called multiple times in a single click

I am using Ionic capacitor to build my reactjs Web application. I have added a Capacitor App Apis to get the back event capture in my App.js file like this,
APPS.addListener("backButton", () => {
if (renderedScreenUIList.length === 0) {
// APPS.exitApp();
alert("exit app");
} else {
alert("previous screen");
}
});
But when i press back button this back event is called 3 times instead of one. my App.js file is.
// import "./StyleSeets/LightTheme.css";
import "./StyleSeets/theme.css";
import "./App.css";
import React, { useEffect, useState } from "react";
import Drawer from "#mui/material/Drawer";
import { App as APPS } from "#capacitor/app";
import Login from "./Components/Login";
import ERPUtils from "./Components/ERPUtils";
import Main from "./Components/Main";
// ************************** back fire event handling ****************
const renderedScreenUIList = [];
function App() {
// const [ stylePath, setStylePath ] = useState("./StyleSeets/LightTheme.css");
const [renderPageType, setRenderPageType] = useState("login");
const [isMobileView, setIMobileView] = useState(window.innerWidth < 920);
const isConsoleOpen = false;
window.onresize = function () {
console.log(window.outerHeight - window.innerHeight);
if ((window.outerHeight - window.innerHeight) > 100) { console.log("open"); } else console.log("close");
};
const themeList = [
{ value: "#2776ed", label: "light" },
{ value: "#45b11c", label: "nature" },
{ value: "#2AB67B", label: "evening" },
{ value: "#add8e6", label: "sky" },
{ value: "#2b035b", label: "dark" }];
const languageList = [
{ value: "ENG", label: "English" },
{ value: "MAL", label: "മലയാളം" },
{ value: "HIND", label: "हिन्दी" }];
const [language, setLanguage] = useState("ENG");
const onThemeChangeCall = () => {
const themeName = sessionStorage.getItem("theme-name");
const themeElement = document.getElementById("app-theme");
themeElement.className = "App";
document.documentElement.setAttribute("data-theme", themeName);
};
APPS.addListener("backButton", () => {
if (renderedScreenUIList.length === 0) {
// APPS.exitApp();
alert("exit app");
} else {
alert("previous screen");
}
});
const languageChanged = (type) => {
setLanguage(type);
};
useEffect(() => {
onThemeChangeCall();
}, []);
const changePage = (page) => {
setRenderPageType(page);
};
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const changeTheme = (type) => {
sessionStorage.setItem("theme-name", type);
document.documentElement.classList.add("color-theme-in-transition");
window.setTimeout(() => {
document.documentElement.classList.remove("color-theme-in-transition");
}, 1000);
onThemeChangeCall();
};
const toggleDrawer = (value) => {
setIsSettingsOpen(value);
};
const changeLanguage = (type) => {
sessionStorage.setItem("language-theme", type);
languageChanged(type);
};
useEffect(() => {
setIMobileView(window.innerWidth < 920);
}, [window.innerWidth]);
return (
<div className="App" id="app-theme">
{renderPageType === "login"
? (
<Login
onThemeChangeCall={onThemeChangeCall}
changePage={changePage}
languageChanged={languageChanged}
toggleDrawer={toggleDrawer}
/>
)
: null}
{renderPageType === "home"
? (
<Main
onThemeChangeCall={onThemeChangeCall}
changePage={changePage}
languageChanged={languageChanged}
toggleDrawer={toggleDrawer}
isMobileView={isMobileView}
/>
)
: null}
<Drawer
anchor="right"
open={isSettingsOpen}
onClose={() => toggleDrawer(false)}
>
<div className="p-2 " style={{ width: "250px", backgroundColor: "var(--primaryBackground)" }}>
<h6 className="border-bottom p-2">Themes</h6>
<div className="">
{!ERPUtils.isNullorWhiteSpace(themeList) ? themeList.map((el, index) => (
<div
key={index}
onKeyDown={(e) => {
if (ERPUtils.isKeyBoardEnterPressed(e)) {
changeTheme(el.label);
}
}}
role="button"
tabIndex={0}
className="p-2 d-flex align-items-center font-2 justify-content-start"
onClick={() => changeTheme(el.label)}
>
<span className="theme-thumbnail" style={{ backgroundColor: el.value }} />
<span>{(`${el.label}`).toLocaleUpperCase()}</span>
</div>
)) : null}
</div>
</div>
<div className="p-2" style={{ width: "250px", backgroundColor: "var(--primaryBackground)" }}>
<h6 className="border-bottom p-2">Language</h6>
<div className="">
{!ERPUtils.isNullorWhiteSpace(languageList) ? languageList.map((el, index) => (
<div
key={index}
onKeyDown={(e) => {
if (ERPUtils.isKeyBoardEnterPressed(e)) {
changeLanguage(el.value);
}
}}
role="button"
tabIndex={0}
className="p-2 d-flex align-items-center font-2 justify-content-start"
onClick={() => changeLanguage(el.value)}
>
<span className="language-thumbnail">{el?.label.split("")[0]}</span>
<span>{(`${el.label}`).toLocaleUpperCase()}</span>
</div>
)) : null}
</div>
</div>
</Drawer>
{/* <ThemeSwitcherProvider defaultTheme="light" themeMap={themes}> */}
{/* <link rel="stylesheet" type="text/css" href={stylePath} /> */}
{/* </ThemeSwitcherProvider> */}
</div>
);
}
export default App;
Kindly suggest a solution for this problem ?. If I call this event listener outside App.js file it only calls once.
With each page reload the listener is added again. To fix this you should call removeAllListeners() => Promise<void> once at the beginning:
App.removeAllListeners().then(() => {
App.addListener("backButton", () => {
if (renderedScreenUIList.length === 0) {
// APPS.exitApp();
alert("exit app");
} else {
alert("previous screen");
}
});
});

How to display a page from a list of items in React

I'm trying to display a dynamic page from a list according to the content of the list. Here are my codes:
My DB is from a firebase firestore, don't know if that will help.
The Data page
import './datatable.scss'
import { DataGrid } from '#mui/x-data-grid'
import { userColumns } from '../../datatablesource'
import { Link } from 'react-router-dom'
import { useEffect, useState } from 'react'
import {
collection,
//getDocs,
//deleteDoc,
//doc,
onSnapshot,
} from 'firebase/firestore'
import { db } from '../../firebase'
const Datatable = () => {
const [data, setData] = useState([])
useEffect(() => {
// LISTEN (REALTIME)
const unsub = onSnapshot(
collection(db, 'users'),
(snapShot) => {
let list = []
snapShot.docs.forEach((doc) => {
list.push({ id: doc.id, ...doc.data() })
})
setData(list)
},
(error) => {
console.log(error)
}
)
return () => {
unsub()
}
}, [])
/* const handleDelete = async (id) => {
try {
await deleteDoc(doc(db, "users", id));
setData(data.filter((item) => item.id !== id));
} catch (err) {
console.log(err);
}
}; */
const actionColumn = [
{
field: 'action',
headerName: 'Action',
width: 200,
renderCell: (params) => {
return (
<div className='cellAction' >
<Link to={params.row.id} style={{ textDecoration: 'none' }}>
<div className='viewButton'>View</div>
</Link>
<div
className='deleteButton'
/* onClick={() => handleDelete(params.row.id)} */
>
Delete
</div>
</div>
)
},
},
]
return (
<div className='datatable'>
<div className='datatableTitle'>
View Users
<Link to='/users/new' className='link'>
Add New
</Link>
</div>
<DataGrid
className='datagrid'
rows={data}
columns={userColumns.concat(actionColumn)}
pageSize={9}
rowsPerPageOptions={[9]}
checkboxSelection
/>
</div>
)
}
export default Datatable
The page I'm trying to display the data in
import './single.scss'
import Sidebar from '../../components/sidebar/Sidebar'
import Navbar from '../../components/navbar/Navbar'
import Chart from '../../components/chart/Chart'
import List from '../../components/table/Table'
//import Datatable from '../../components/datatable/Datatable'
//import { useParams } from 'react-router-dom'
//import { userColumns } from '../../datatablesource'
import { useEffect, useState } from 'react'
import {
collection,
//getDocs,
//deleteDoc,
//doc,
onSnapshot,
} from 'firebase/firestore'
import { db } from '../../firebase'
//import { list } from "firebase/storage";
const Single = () => {
const [data, setData] = useState([])
useEffect(() => {
// const fetchData = async () => {
// let list = [];
// try {
// const querySnapshot = await getDocs(collection(db, "users"));
// querySnapshot.forEach((doc) => {
// list.push({ id: doc.id, ...doc.data() });
// });
// setData(list);
// console.log(list);
// } catch (err) {
// console.log(err);
// }
// };
// fetchData();
// LISTEN (REALTIME)
const unsub = onSnapshot(
collection(db, 'users'),
(snapShot) => {
let list = []
snapShot.docs.forEach((doc) => {
list.push({ _id: doc.id, ...doc.data() })
})
setData(list)
},
(error) => {
console.log(error)
}
)
return () => {
unsub()
}
}, [])
return (
<div className='single'>
<Sidebar />
<div className='singleContainer'>
<Navbar />
{data.map((item) => {
return (
<div className='top' key={item.id}>
<div className='left'>
<div className='editButton'>Edit</div>
<h1 className='title'>Information</h1>
<div className='item'>
<img
src='https://pbs.twimg.com/profile_images/1362431344953982981/5I26nhqK_400x400.jpg'
alt=''
className='itemImg'
/>
<div className='details' key={item.id} >
<h1 className='itemTitle'>{item.businessName}</h1>
<div className='detailItem'>
<span className='itemKey'>Email:</span>
<span className='itemValue'>{item.email}</span>
</div>
<div className='detailItem'>
<span className='itemKey'>Phone:</span>
<span className='itemValue'>{item.phoneNumber}</span>
</div>
<div className='detailItem'>
<span className='itemKey'>Wallet Balance:</span>
<span className='itemValue'>
{item.walletBalance ? item.walletBalance : '0'}
</span>
<br />
<br />
<span className='itemKey'>Business Category:</span>
<span className='itemValue'>
{item.businessCategory}
</span>
</div>
<div className='detailItem'>
<span className='itemKey'>Country:</span>
<span className='itemValue'>Nigeria</span>
</div>
</div>
</div>
</div>
<div className='right'>
<Chart aspect={3 / 1} title='User Spending ( Last 6 Months)' />
</div>
</div>
)
})}
<div className='bottom'>
<h1 className='title'>Last Transactions</h1>
<List />
</div>
</div>
</div>
)
}
export default Single
The map function above is a temporary fix. It doesn't return a page with dynamic data.

useState defaults appear to rerun after running function on state change, defaults shouldnt run twice

I have the following issue with website where the settings state resets after running more searches. The settings component is show below in the picture, it usually works but if you uncheck a box and then run a few more searches at some point the showAllDividends setting will be set to false, the All dividends component won't be on the screen, but for some reason the checkbox itself is checked (true). This is my first time really working with checkboxes in React, and I think I'm using the onChange feature wrong. Right now I get the event.target.checked boolean, but only onChange.
If that isn't the issue then the most likely cause is the default statements being run again on another render:
const [showMainInfo, setShowMainInfo] = useState(true);
const [showYieldChange, setShowYieldChange] = useState(true);
const [showAllDividends, setShowAllDividends] = useState(true);
the thing is I don't see why the default statements would run more than once, the component isn't being destroyed there's no react router usage. I expected it to keep its current state after the page is first loaded. I think the settings defaults are being rerun, but just don't understand why they would.
I unchecked, checked, unchecked the 'All dividends' checkbox, and it was unchecked when I ran 2 more searches. After the second search the checkbox was checked but the component was gone, because showAllDividends was false
main component SearchPage.js:
import React, {useState, useEffect} from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import SearchBar from './SearchBar';
import AllDividendsDisplay from './dividend_results_display/AllDividendsDisplay';
import DividendResultsDisplay from './dividend_results_display/DividendResultsDisplay';
import SettingsView from './settings/SettingsView';
const HOST = process.env.REACT_APP_HOSTNAME
const PROTOCOL = process.env.REACT_APP_PROTOCOL
const PORT = process.env.REACT_APP_PORT
const BASE_URL = PROTOCOL + '://' + HOST + ':' + PORT
const SearchPage = ({userId}) => {
const DEFAULT_STOCK = 'ibm';
const [term, setTerm] = useState(DEFAULT_STOCK);
const [debouncedTerm, setDebouncedTerm] = useState(DEFAULT_STOCK);
const [loading, setLoading] = useState(false);
const [recentSearches, setRecentSearches] = useState([DEFAULT_STOCK]);
const [dividendsYearsBack, setDividendsYearsBack] = useState('3');
const [debouncedDividendYearsBack, setDebouncedDividendYearsBack] = useState('3');
const [errorMessage, setErrorMessage] = useState('');
const [dividendsData, setDividendsData] = useState(
{
current_price: '',
recent_dividend_rate: '',
current_yield: '',
dividend_change_1_year: '',
dividend_change_3_year: '',
dividend_change_5_year: '',
dividend_change_10_year: '',
all_dividends: [],
name: '',
description: '',
}
)
const [settingsViewVisible, setSettingsViewVisible] = useState(false);
const [showMainInfo, setShowMainInfo] = useState(true);
const [showYieldChange, setShowYieldChange] = useState(true);
const [showAllDividends, setShowAllDividends] = useState(true);
const onTermUpdate = (term) => {
setTerm(term)
}
// TODO: write a custom hook that debounces taking the term and the set debounced term callback
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedTerm(term);
}, 1500);
return () => {
clearTimeout(timerId);
};
}, [term]);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedDividendYearsBack(dividendsYearsBack);
}, 1500);
return () => {
clearTimeout(timerId);
};
}, [dividendsYearsBack]);
useEffect(() => {runSearch()}, [debouncedTerm]);
useEffect(() => {
// alert(dividendsYearsBack)
if (dividendsYearsBack !== '') {
runSearch();
}
}, [debouncedDividendYearsBack])
useEffect(() => {
console.log("user id changed")
if (userId) {
const user_profile_api_url = BASE_URL + '/users/' + userId
axios.get(user_profile_api_url, {})
.then(response => {
const recent_searches_response = response.data.searches;
const new_recent_searches = [];
recent_searches_response.map(dict => {
new_recent_searches.push(dict.search_term)
})
setRecentSearches(new_recent_searches);
})
.catch((error) => {
console.log("error in getting user profile: ", error.message)
})
}
}, [userId])
useEffect(() => {
const user_profile_api_url = BASE_URL + '/users/' + userId
const request_data = {searches: recentSearches}
axios.post(user_profile_api_url, request_data)
// .then(response => {
// console.log(response)
// })
}, [recentSearches])
const makeSearchApiRequest = () => {
let dividends_api_url = BASE_URL + '/dividends/' + term + '/' + dividendsYearsBack
if (!recentSearches.includes(term)) {
setRecentSearches([...recentSearches, term])
}
axios.get(dividends_api_url, {})
.then(response => {
// console.log(response)
setLoading(false);
setDividendsData(response.data);
})
.catch((error) => {
console.log(error.message);
setLoading(false);
setErrorMessage(error.message);
})
}
const runSearch = () => {
console.log("running search: ", term);
setErrorMessage('');
if (term) {
setLoading(true);
if (!dividendsYearsBack) {
setDividendsYearsBack('3', () => {
makeSearchApiRequest()
});
} else {
makeSearchApiRequest()
}
}
}
const recentSearchOnClick = (term) => {
setTerm(term);
setDebouncedTerm(term);
}
const removeRecentSearchOnClick = (term) => {
const searchesWithoutThisOne = recentSearches.filter(search => search !== term)
setRecentSearches(searchesWithoutThisOne);
}
const dividendsYearsBackOnChange = (text) => {
setDividendsYearsBack(text);
}
const renderMainContent = () => {
if (!debouncedTerm) {
return (
<div className="ui active">
<div className="ui text">Search for info about a stock</div>
</div>
)
}
if (loading === true) {
return (
<div className="ui active dimmer">
<div className="ui big text loader">Loading</div>
</div>
)
}
if (errorMessage) {
return (
<div className="ui active">
<div className="ui text">{errorMessage}</div>
</div>
)
} else {
return (
<DividendResultsDisplay
data={dividendsData}
dividends_years_back={dividendsYearsBack}
dividendsYearsBackOnChange={dividendsYearsBackOnChange}
showMainInfo={showMainInfo}
showYieldChange={showYieldChange}
showAllDividends={showAllDividends}/>
)
}
}
// https://stackoverflow.com/questions/38619981/how-can-i-prevent-event-bubbling-in-nested-react-components-on-click
const renderRecentSearches = () => {
return recentSearches.map((term) => {
return (
<div key={term}>
<button
onClick={() => recentSearchOnClick(term)}
style={{marginRight: '10px'}}
>
<div>{term} </div>
</button>
<button
onClick={(event) => {event.stopPropagation(); removeRecentSearchOnClick(term)}}>
X
</button>
<br/><br/>
</div>
)
})
}
const renderSettingsView = (data) => {
if (settingsViewVisible) {
return (
<SettingsView data={data} />
)
} else {
return null;
}
}
const toggleSettingsView = () => {
setSettingsViewVisible(!settingsViewVisible);
}
const toggleDisplay = (e, setter) => {
setter(e.target.checked)
}
const SETTINGS_DATA = [
{
label: 'Main info',
id: 'main_info',
toggler: toggleDisplay,
setter: setShowMainInfo
},
{
label: 'Yield change',
id: 'yield_change',
toggler: toggleDisplay,
setter: setShowYieldChange
},
{
label: 'Dividends list',
id: 'all_dividends',
toggler: toggleDisplay,
setter: setShowAllDividends
},
]
console.log("showMainInfo: ", showMainInfo);
console.log("showYieldChange: ", showYieldChange);
console.log("showAllDividends: ", showAllDividends);
return (
<div className="ui container" style={{marginTop: '10px'}}>
<SearchBar term={term} onTermUpdate={onTermUpdate} />
{renderRecentSearches()}
<br/><br/>
<button onClick={toggleSettingsView}>Display settings</button>
{renderSettingsView(SETTINGS_DATA)}
<div className="ui segment">
{renderMainContent()}
</div>
</div>
)
}
const mapStateToProps = state => {
return { userId: state.auth.userId };
};
export default connect(
mapStateToProps
)(SearchPage);
// export default SearchPage;
the settingsView component:
import React from 'react';
import SettingsCheckbox from './SettingsCheckbox';
const SettingsView = ({data}) => {
const checkboxes = data.map((checkbox_info) => {
return (
<div key={checkbox_info.id}>
<SettingsCheckbox
id={checkbox_info.id}
label={checkbox_info.label}
toggler={checkbox_info.toggler}
setter={checkbox_info.setter}/>
<br/>
</div>
)
});
return (
<div className="ui segment">
{checkboxes}
</div>
);
}
export default SettingsView;
SettingsCheckbox.js:
import React, {useState} from 'react';
const SettingsCheckbox = ({id, label, toggler, setter}) => {
const [checked, setChecked] = useState(true)
return (
<div style={{width: '200px'}}>
<input
type="checkbox"
checked={checked}
id={id}
name={id}
value={id}
onChange={(e) => {
setChecked(!checked);
toggler(e, setter);
}} />
<label htmlFor="main_info">{label}</label><br/>
</div>
);
}
export default SettingsCheckbox;

Why isn't this button showing when the state is false?

I have created a component to function as a "Like/Unlike" button. When the state is true, the "Unlike" button successfully displays, but when I click "Unlike", and it DOES unlike successfully, the state should be set to false as (liked: false). However, I don't see the button.
One thing I noticed is, when I click "Unlike", the "Unlike" button disappears and the "Like" button does appear, for a millisecond, and then it vanishes in thin air. I cannot figure it out why.
Here are all the codes for my like button component:
import React from "react";
import { API, graphqlOperation } from "aws-amplify";
import { Button } from "element-react";
import { createLike, deleteLike } from "../graphql/mutations";
import { UserContext } from "../App";
class Like extends React.Component {
state = {
liked: "",
};
componentDidMount() {
this.setLiked();
}
setLiked() {
console.log(this.props);
const { user } = this.props;
const { post } = this.props;
if (post.likes.items.find((items) => items.liker === user.username)) {
this.setState({ liked: true });
console.log("liked: true");
} else {
this.setState({ liked: false });
console.log("liked: false");
}
}
handleLike = async (user) => {
try {
const input = {
liker: user.username,
likePostId: this.props.postId,
};
await API.graphql(graphqlOperation(createLike, { input }));
this.setState({
liked: true,
});
console.log("Liked!");
} catch (err) {
console.log("Failed to like", err);
}
};
handleUnlike = async (likeId) => {
try {
const input = {
id: likeId,
};
await API.graphql(graphqlOperation(deleteLike, { input }));
this.setState({
liked: false,
});
console.log("Unliked!");
} catch (err) {
console.log("Failed to unlike", err);
}
};
render() {
const { like } = this.props;
const { liked } = this.state;
return (
<UserContext.Consumer>
{({ user }) => (
<React.Fragment>
{liked ? (
<Button type="primary" onClick={() => this.handleUnlike(like.id)}>
Unlike
</Button>
) : (
<Button
type="primary"
onClick={() => this.handleLike(user, like.id)}
>
Like
</Button>
)}
</React.Fragment>
)}
</UserContext.Consumer>
);
}
}
export default Like;
The code of the parent component:
import React from "react";
import { API, graphqlOperation } from "aws-amplify";
import {
onCreateComment,
onCreateLike,
onDeleteLike,
} from "../graphql/subscriptions";
import { getPost } from "../graphql/queries";
import Comment from "../components/Comment";
import Like from "../components/Like";
import LikeButton from "../components/LikeButton";
import { Loading, Tabs, Icon } from "element-react";
import { Link } from "react-router-dom";
import { S3Image } from "aws-amplify-react";
import NewComment from "../components/NewComment";
class PostDetailPage extends React.Component {
state = {
post: null,
isLoading: true,
isAuthor: false,
};
componentDidMount() {
this.handleGetPost();
this.createCommentListener = API.graphql(
graphqlOperation(onCreateComment)
).subscribe({
next: (commentData) => {
const createdComment = commentData.value.data.onCreateComment;
const prevComments = this.state.post.comments.items.filter(
(item) => item.id !== createdComment.id
);
const updatedComments = [createdComment, ...prevComments];
const post = { ...this.state.post };
post.comments.items = updatedComments;
this.setState({ post });
},
});
this.createLikeListener = API.graphql(
graphqlOperation(onCreateLike)
).subscribe({
next: (likeData) => {
const createdLike = likeData.value.data.onCreateLike;
const prevLikes = this.state.post.likes.items.filter(
(item) => item.id !== createdLike.id
);
const updatedLikes = [createdLike, ...prevLikes];
const post = { ...this.state.post };
post.likes.items = updatedLikes;
this.setState({ post });
},
});
this.deleteLikeListener = API.graphql(
graphqlOperation(onDeleteLike)
).subscribe({
next: (likeData) => {
const deletedLike = likeData.value.data.onDeleteLike;
const updatedLikes = this.state.post.likes.items.filter(
(item) => item.id !== deletedLike.id
);
const post = { ...this.state.post };
post.likes.items = updatedLikes;
this.setState({ post });
},
});
}
componentWillUnmount() {
this.createCommentListener.unsubscribe();
}
handleGetPost = async () => {
const input = {
id: this.props.postId,
};
const result = await API.graphql(graphqlOperation(getPost, input));
console.log({ result });
this.setState({ post: result.data.getPost, isLoading: false }, () => {});
};
checkPostAuthor = () => {
const { user } = this.props;
const { post } = this.state;
if (user) {
this.setState({ isAuthor: user.username === post.author });
}
};
render() {
const { post, isLoading } = this.state;
return isLoading ? (
<Loading fullscreen={true} />
) : (
<React.Fragment>
{/*Back Button */}
<Link className="link" to="/">
Back to Home Page
</Link>
{/*Post MetaData*/}
<span className="items-center pt-2">
<h2 className="mb-mr">{post.title}</h2>
</span>
<span className="items-center pt-2">{post.content}</span>
<S3Image imgKey={post.file.key} />
<div className="items-center pt-2">
<span style={{ color: "var(--lightSquidInk)", paddingBottom: "1em" }}>
<Icon name="date" className="icon" />
{post.createdAt}
</span>
</div>
<div className="items-center pt-2">
{post.likes.items.map((like) => (
<Like
user={this.props.user}
like={like}
post={post}
postId={this.props.postId}
/>
))}
</div>
<div className="items-center pt-2">
{post.likes.items.length}people liked this.
</div>
<div>
Add Comment
<NewComment postId={this.props.postId} />
</div>
{/* Comments */}
Comments: ({post.comments.items.length})
<div className="comment-list">
{post.comments.items.map((comment) => (
<Comment comment={comment} />
))}
</div>
</React.Fragment>
);
}
}
export default PostDetailPage;
I think I know why it doesn't show up. It's because at first when the user hasn't liked it, there is no "like" object, so there is nothing to be shown, as it is only shown when there is a "like" mapped to it. I don't know how to fix it though.

Categories

Resources