react hook useState in AgGridColumn onCellClicked function - reactjs

Currently, I am using functional react components with react hook useState and an AgGridReact Component.
I am displaying an AgGridReact and put a onCellClicked function on a AgGridColumn. So far everything is working. In the onCellClicked function I want to update my state and do something depending on its current value.
Here is the problem:
if I want to use my state get/set (useState hook) inside of the onCellClicked function it is not working as expected. For some reason, I can not update my state.
In a react class component it is working.
EDIT: I experimented for a while and found out that in the onCellClicked function I have only the default value in myText. I can update it once. If I spam the onCellClicked function it will append the text again to the default value from useState("default myText");. I would expect that the string would get longer as often I click on the cell. Just as in my Class Component example.
If I use a simple button outside of the AgGridReact <button onClick={() => setMyText(myText + ", test ")}>add something to myText state</button> it is working as expected, the string gets longer every time I click on my <button>. If I change the state of myText via the <button> outside of the AgGridReact and then click on the cell function again the state previously setted through my <button> is lost.
Example react hook component:
import React, { useState } from 'react';
import { AgGridColumn, AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
function App() {
const [myText, setMyText] = useState("default myText");
const [todoListRowData, setTodoListRowData] = useState([]);
// ....fetch data and set the todoListRowData state....
const myCellClickFunction = (params, x) => {
// here is the problem:
// no matter how often I click in the cell myText is every time the default value 'default myText'
// EDIT: I found out I can update the state here but only from the initial default value once, myText is on every cell click again "default myText" and will be concatenated with "hookCellClicked". So every time I click this cell the state gets again "default myTexthookCellClicked"
console.log(myText);
setMyText(myText + "hookCellClicked");
}
return (
<div className="ag-theme-alpine" style={{ height: '600px', width: '100%' }}>
<AgGridReact rowData={todoListRowData} >
<AgGridColumn headerName="ID" field="id" maxWidth="50"></AgGridColumn>
<AgGridColumn headerName="UserId" field="userId" maxWidth="85"></AgGridColumn>
<AgGridColumn headerName="Title" field="title" minWidth="555"></AgGridColumn>
<AgGridColumn headerName="completed" field="completed"></AgGridColumn>
<AgGridColumn headerName="Testcol" onCellClicked={(params) => myCellClickFunction(params)}></AgGridColumn>
</AgGridReact>
</div>
}
export default App;
If I do the exact same thing in a class component it is working fine.
Example Class Component:
import React from 'react';
import { AgGridColumn, AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
class MyClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
myClassComponentRowData: [],
testState: "defaul testState"
};
}
// ....fetch data and set ag grid rowData state....
handleCellClick = (params) => {
// here everything works just fine and as expected
// every time I click on the cell the state testState updates and it is added ", handleCellClick" every time
console.log(this.state.testState);
this.setState({testState: this.state.testState + ", handleCellClick"});
}
render() {
return <div className="ag-theme-alpine" style={{ height: '600px', width: '100%' }}>
<AgGridReact rowData={this.state.myClassComponentRowData} >
<AgGridColumn headerName="ID" field="id" maxWidth="50"></AgGridColumn>
<AgGridColumn headerName="UserId" field="userId" maxWidth="85"></AgGridColumn>
<AgGridColumn headerName="Title" field="title" minWidth="555"></AgGridColumn>
<AgGridColumn headerName="completed" field="completed"></AgGridColumn>
<AgGridColumn headerName="Testcol" onCellClicked={(params) => this.handleCellClick(params)}></AgGridColumn>
</AgGridReact>
</div>
}
}
export default MyClassComponent;
Am I doing something wrong? I want to use a functional component with react hooks.

There is nothing wrong with the code in your question except that the callback myCellClickFunction references the old state myText which is captured in the previous render call. If you log in the render method, you can see the state is updated properly. This problem is called stale closure.
function App() {
const [myText, setMyText] = useState("default myText");
const [todoListRowData, setTodoListRowData] = useState(rowData);
console.log("render", myText); // prints the latest myText state
...
}
You can see my other answer here about how to get the latest state in callback when using React hook. Here is an example for you to try that out.
function useExtendedState(initialState) {
const [state, setState] = React.useState(initialState);
const getLatestState = () => {
return new Promise((resolve, reject) => {
setState((s) => {
resolve(s);
return s;
});
});
};
return [state, setState, getLatestState];
}
function App() {
const [myText, setMyText, getLatestMyText] = useExtendedState(
"default myText"
);
const myCellClickFunction = async (params) => {
setMyText(params.value);
const text = await getLatestMyText();
console.log("get latest state in callback", text);
};
...
}
Live Demo

Related

I want to get clientWidth of div element if it changes during render

I don't have access to sidebar div element because of microfrontend. I want to catch html renders during siderbar clientWidth changes. How can i access it.
Here are example code:
import { Box, BoxProps, HStack } from '#chakra-ui/react';
import React, { useEffect, useState } from 'react';
interface Props extends BoxProps {
leftActions?: React.ReactNode[];
rightActions?: React.ReactNode[];
}
export const FormFooter = ({ leftActions, rightActions, ...props }: Props) => {
const [sidebarWidth, setSidebarWidth] = useState(0);
useEffect(() => {
const sidebarWidth = document?.querySelector<any>('.css-ygpt8u')?.clientWidth;
console.log('sidebarWidth', sidebarWidth);
}, [document]);
return (
<Box
display={'flex'}
justifyContent={'space-between'}
{...props}
width={`calc(100vw - 460px)`}
right="84px"
backgroundColor="white"
>
<HStack key="form-left">{leftActions?.map((element) => element)}</HStack>
<HStack key="form-right">{rightActions?.map((element) => element)}</HStack>
</Box>
);
};
I tried to pass document inside array of useEffect but it gives me undefined.
One solution would be to use the dependency of typeof document instead of document alone.
useEffect(()=>{
Const clientWidth=document.getElementById(“someId”).clientWidth
},[typeof document])
Another solution is to use Refs on the sidebar div and its client width just as you do it with document. The difference is that refs directly change the Dom.

Can't call setState on a component that is not yet mounted. - React

I am creating a React component using an npm wysiwyg. When the page loads, I grab some user data from a context I have set up. I grab the user's name and cover letter which is some html code. When the page loads I want to change the wysiwyg's state to contain the cover letter that way it displays it to the user. It works the first time the page loads, however if I hit refresh or try to open the page again, the contents of the wysiwyg disappear. Upon inspection of the console, I am met with a
Warning: Can't call `setState` on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the n component.
I am unsure of what I am doing wrong.
Here is my component:
import React, { useContext, useEffect, useState } from 'react';
import { LoggedInContext } from './contexts/LoggedIn';
import { Editor } from 'react-draft-wysiwyg';
import { EditorState, ContentState } from 'draft-js';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import htmlToDraft from 'html-to-draftjs';
import './Profile.css';
const ProfileSettings = () => {
const { user, coverLetter } = useContext(LoggedInContext);
const blocksFromHtml = htmlToDraft(coverLetter);
const { contentBlocks, entityMap } = blocksFromHtml;
const contentState = ContentState.createFromBlockArray(
contentBlocks,
entityMap
);
const editorState = EditorState.createWithContent(contentState);
const [editorStateReact, setEditorStateReact] = useState(editorState);
const onEditorStateChange = (editorState) => {
setEditorStateReact(editorState);
};
return (
<div className="Profile">
<h2>User: {user}</h2>
<Editor
editorState={editorStateReact}
toolbarClassName="toolbarClassName"
wrapperClassName="wrapperClassName"
editorClassName="editorClassName"
onEditorStateChange={onEditorStateChange}
/>
<button className="btn btn-lg btn-primary my-3">Save Profile</button>
</div>
);
};
export default ProfileSettings;

converting react class to functional component with refs

I'm trying to use a class example from a stackblitz file and convert it to a functional component.
I don't understand how the ref works, or where the event and args that are being used in the onTyping function are coming from. Can anyone explain where those are defined and how I'd translate this to a functional component?
import { render } from 'react-dom';
import './index.css';
import * as React from 'react';
import { AutoCompleteComponent } from '#syncfusion/ej2-react-dropdowns';
import { SampleBase } from './sample-base';
import * as data from './dataSource.json';
export class Default extends SampleBase {
constructor() {
super(...arguments);
this.temp = 'sportsData';
// define the array of string
this.sportsData = data[this.temp];
}
onTyping(args) {
console.log(event.target.value);
}
render() {
return (<div id='combodefault' className='control-pane'>
<div className='control-section'>
<div className='col-lg-12 control-wrappers'>
<div id='default'>
<AutoCompleteComponent id="games" dataSource={this.sportsData} ref={(AutoComplete) => { this.listObj = AutoComplete; }} placeholder="e.g. Basketball" actionBegin={this.onTyping}/>
</div>
</div>
</div>
</div>);
}
}
render(<Default />, document.getElementById('sample'));
anything in the constructor will need to be translated to useState:
this.temp = 'sportsData';
// define the array of string
this.sportsData = data[this.temp];
becomes:
const[temp, setTemp] = useState('sportsData');
const[sportsData, setSportsData] = useState(data[temp]);
setTemp and setSportsData are functions that you use to set the state variable temp and sportsData respectively. For example, the following will set temp to 'NFSSportsData'.
setTemp('NFLSportsData');
As for the ref, you can use the hook useRef.
const listObj = useRef(null);
for component life cycle method componentDidMount, you can use the following convention.
useEffect(()=>{
// your code
}, [])
the empty bracket [] signifies only to run the code once when the component mounts. If you want to code listen to a state variable, and runs every time the variable changes, you can do the following:
useEffect(()=>{
// your code
}, [sportsData])
This code above will run every time state variable sportsData changes.
I don't think there's a way to extend a functional component like you are doing with SampleBase. Looking at the SampleBase class, it's just running a function in the lifecycle component componentDidMount. You can do something like the following:
rendereComplete() {
/**custom render complete function */
}
useEffect(()=>{
setTimeout(() => {
this.rendereComplete();
},[]);
To tie is all together, you have something like the following:
import './index.css';
import * as React from 'react';
import { AutoCompleteComponent } from '#syncfusion/ej2-react-dropdowns';
import * as data from './dataSource.json';
export const Default = ()=> {
const [temp, setTemp] = React.useState('sportsData');
const [sportsData, setSportsData] = useState(data[this.temp]);
const listObj = useRef(null);
const onTyping = (args)=>{
console.log('arg =', args);
}
const rendereComplete() {
/**custom render complete function */
}
useEffect(()=>{
setTimeout(() => {
rendereComplete();
},[]);
return (<div id='combodefault' className='control-pane'>
<div className='control-section'>
<div className='col-lg-12 control-wrappers'>
<div id='default'>
<AutoCompleteComponent id="games" dataSource={sportsData} ref={(AutoComplete) => { listObj = AutoComplete; }} placeholder="e.g. Basketball" actionBegin={onTyping}/>
</div>
</div>
</div>
</div>);
}

React: A service returning a ui component (like toast)?

Requirement: Show toast on bottom-right corner of the screen on success/error/warning/info.
I can create a toast component and place it on any component where I want to show toasts, but this requires me to put Toast component on every component where I intend to show toasts. Alternatively I can place it on the root component and somehow manage show/hide (maintain state).
What I am wondering is having something similar to following
export class NotificationService {
public notify = ({message, notificationType, timeout=5, autoClose=true, icon=''}: Notification) => {
let show: boolean = true;
let onClose = () => {//do something};
if(autoClose) {
//set timeout
}
return show ? <Toast {...{message, notificationType, onClose, icon}} /> : </>;
}
}
And call this service where ever I need to show toasts.
Would this be the correct way to achieve the required functionality?
You can use AppContext to manage the state of your toast and a hook to trigger it whenever you want.
ToastContext:
import React, { createContext, useContext, useState } from 'react';
export const ToastContext = createContext();
export const useToastState = () => {
return useContext(ToastContext);
};
export default ({ children }) => {
const [toastState, setToastState] = useState(false);
const toastContext = { toastState, setToastState };
return <ToastContext.Provider value={toastContext}>{children}</ToastContext.Provider>;
};
App:
<ToastProvider>
<App/>
<Toast show={toastState}/>
</ToastProvider>
Then anywhere within your app you can do:
import {useToastState} from 'toastContext'
const {toastState, setToastState} = useToastState();
setToastState(!toastState);

On click returns null instead of an object

It's really basic I guess. I'm trying to add onClick callback to my script & I believe I'm missing a value that would be responsible for finding the actual item.
Main script
import React from 'react';
import { CSVLink } from 'react-csv';
import { data } from 'constants/data';
import GetAppIcon from '#material-ui/icons/GetApp';
import PropTypes from 'prop-types';
const handleClick = (callback) => {
callback(callback);
};
const DownloadData = (props) => {
const { callback } = props;
return (
<>
<CSVLink
data={data}
onClick={() => handleClick(callback)}
>
<GetAppIcon />
</CSVLink>
</>
);
};
DownloadData.propTypes = {
callback: PropTypes.func.isRequired,
};
export default DownloadData;
Storybook code
import React from 'react';
import DownloadData from 'common/components/DownloadData';
import { data } from 'constants/data';
import { action } from '#storybook/addon-actions';
export default {
title: 'DownloadData',
component: DownloadData,
};
export const download = () => (
<DownloadData
data={data}
callback={action('icon-clicked')}
/>
);
So right now with this code on click in the storybook I'd get null and I'm looking for an object.
One of the potential issues I can see is that your handleClick function is stored as it is in-memory, when you import the component. That means you're keeping reference of something that doesn't exists and expects to use it when rendering the component with the callback prop.
Each instance of a component should have its own function. To fix it, move the function declaration inside the component. Like this:
const Foo = ({ callback }) => {
// handleClick needs to be inside here
const handleClick = callback => {
console.log("clicked");
callback(callback);
};
return <div onClick={() => handleClick(callback)}>Click me!</div>;
};
Check this example.
If this doesn't fix your problem, then there is something wrong with how you're implementing Storybook. Like a missing context.

Resources